Replace src query parameter with UTM params (controlled by a feature flag) (#9548)

This commit is contained in:
William Durand 2020-07-31 14:25:25 +02:00 коммит произвёл GitHub
Родитель 2ac03c8bb0
Коммит b81d862cb0
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
30 изменённых файлов: 750 добавлений и 104 удалений

Просмотреть файл

@ -23,6 +23,7 @@ module.exports = {
'discoParamsToUse',
'enableDevTools',
'enableFeatureDiscoTaar',
'enableFeatureUseUtmParams',
'enableRequestID',
'enableStrictMode',
'experiments',

Просмотреть файл

@ -125,10 +125,11 @@ module.exports = {
'cookieSecure',
'defaultLang',
'dismissedExperienceSurveyCookieName',
'enableFeatureBlockPage',
'enableDevTools',
'enableFeatureBlockPage',
'enableFeatureDiscoTaar',
'enableFeatureExperienceSurvey',
'enableFeatureUseUtmParams',
'enableRequestID',
'enableStrictMode',
'experiments',
@ -381,6 +382,8 @@ module.exports = {
enableFeatureExperienceSurvey: false,
dismissedExperienceSurveyCookieName: 'dismissedExperienceSurvey',
enableFeatureUseUtmParams: false,
extensionWorkshopUrl: 'https://extensionworkshop.com',
// This defines experiments for use with the withExperiment HOC, but no

Просмотреть файл

@ -31,4 +31,6 @@ module.exports = {
},
extensionWorkshopUrl: 'https://extensionworkshop-dev.allizom.org',
enableFeatureUseUtmParams: true,
};

Просмотреть файл

@ -31,4 +31,6 @@ module.exports = {
},
extensionWorkshopUrl: 'https://extensionworkshop-dev.allizom.org',
enableFeatureUseUtmParams: true,
};

Просмотреть файл

@ -1,4 +1,5 @@
/* @flow */
import config from 'config';
import * as React from 'react';
import { compose } from 'redux';
import { withRouter } from 'react-router-dom';
@ -6,10 +7,10 @@ import { withRouter } from 'react-router-dom';
import Link from 'amo/components/Link';
import { reviewListURL } from 'amo/reducers/reviews';
import translate from 'core/i18n/translate';
import type { AddonType } from 'core/types/addons';
import MetadataCard from 'ui/components/MetadataCard';
import Rating from 'ui/components/Rating';
import RatingsByStar from 'amo/components/RatingsByStar';
import type { AddonType } from 'core/types/addons';
import type { I18nType } from 'core/types/i18n';
import type { ReactRouterLocationType } from 'core/types/router';
@ -21,6 +22,7 @@ type Props = {|
type InternalProps = {|
...Props,
_config: typeof config,
i18n: I18nType,
location: ReactRouterLocationType,
|};
@ -30,8 +32,12 @@ export const roundToOneDigit = (value: number | null): number => {
};
export class AddonMetaBase extends React.Component<InternalProps> {
static defaultProps = {
_config: config,
};
render() {
const { addon, i18n, location } = this.props;
const { _config, addon, i18n, location } = this.props;
let averageRating;
if (addon) {
@ -68,7 +74,7 @@ export class AddonMetaBase extends React.Component<InternalProps> {
const reviewsLink =
addon && reviewCount
? reviewListURL({ addonSlug: addon.slug, src: location.query.src })
? reviewListURL({ _config, addonSlug: addon.slug, location })
: null;
const reviewsContent = reviewsLink ? (

Просмотреть файл

@ -1,4 +1,5 @@
/* @flow */
import config from 'config';
import * as React from 'react';
import { connect } from 'react-redux';
import { compose } from 'redux';
@ -16,7 +17,10 @@ import { isAddonAuthor, trimAndAddProtocolToUrl } from 'core/utils';
import Card from 'ui/components/Card';
import DefinitionList, { Definition } from 'ui/components/DefinitionList';
import LoadingText from 'ui/components/LoadingText';
import { addQueryParams } from 'core/utils/url';
import {
addQueryParams,
getQueryParametersForAttribution,
} from 'core/utils/url';
import type { AppState } from 'amo/store';
import type { AddonVersionType, VersionInfoType } from 'core/reducers/versions';
import type { I18nType } from 'core/types/i18n';
@ -29,6 +33,7 @@ type Props = {|
type InternalProps = {|
...Props,
_config: typeof config,
hasStatsPermission: boolean,
userId: number | null,
currentVersion: AddonVersionType | null,
@ -37,8 +42,13 @@ type InternalProps = {|
|};
export class AddonMoreInfoBase extends React.Component<InternalProps> {
static defaultProps = {
_config: config,
};
listContent() {
const {
_config,
addon,
currentVersion,
hasStatsPermission,
@ -98,9 +108,11 @@ export class AddonMoreInfoBase extends React.Component<InternalProps> {
statsLink = (
<Link
className="AddonMoreInfo-stats-link"
href={addQueryParams(`/addon/${addon.slug}/statistics/`, {
src: location.query.src,
})}
href={addQueryParams(
`/addon/${addon.slug}/statistics/`,
getQueryParametersForAttribution(location, _config),
_config,
)}
>
{i18n.gettext('Visit stats dashboard')}
</Link>
@ -115,9 +127,11 @@ export class AddonMoreInfoBase extends React.Component<InternalProps> {
if (license) {
const linkProps = license.isCustom
? {
to: addQueryParams(`/addon/${addon.slug}/license/`, {
src: location.query.src,
}),
to: addQueryParams(
`/addon/${addon.slug}/license/`,
getQueryParametersForAttribution(location, _config),
_config,
),
}
: { href: license.url, prependClientApp: false, prependLang: false };
const licenseName = license.name || i18n.gettext('Custom License');
@ -153,9 +167,11 @@ export class AddonMoreInfoBase extends React.Component<InternalProps> {
privacyPolicyLink: addon.has_privacy_policy ? (
<Link
className="AddonMoreInfo-privacy-policy-link"
to={addQueryParams(`/addon/${addon.slug}/privacy/`, {
src: location.query.src,
})}
to={addQueryParams(
`/addon/${addon.slug}/privacy/`,
getQueryParametersForAttribution(location, _config),
_config,
)}
>
{i18n.gettext('Read the privacy policy for this add-on')}
</Link>
@ -163,9 +179,11 @@ export class AddonMoreInfoBase extends React.Component<InternalProps> {
eulaLink: addon.has_eula ? (
<Link
className="AddonMoreInfo-eula-link"
to={addQueryParams(`/addon/${addon.slug}/eula/`, {
src: location.query.src,
})}
to={addQueryParams(
`/addon/${addon.slug}/eula/`,
getQueryParametersForAttribution(location, _config),
_config,
)}
>
{i18n.gettext('Read the license agreement for this add-on')}
</Link>
@ -174,9 +192,11 @@ export class AddonMoreInfoBase extends React.Component<InternalProps> {
<li>
<Link
className="AddonMoreInfo-version-history-link"
to={addQueryParams(`/addon/${addon.slug}/versions/`, {
src: location.query.src,
})}
to={addQueryParams(
`/addon/${addon.slug}/versions/`,
getQueryParametersForAttribution(location, _config),
_config,
)}
>
{i18n.gettext('See all versions')}
</Link>

Просмотреть файл

@ -1,4 +1,5 @@
/* @flow */
import config from 'config';
import makeClassName from 'classnames';
import invariant from 'invariant';
import * as React from 'react';
@ -66,6 +67,7 @@ type Props = {|
type InternalProps = {|
...Props,
_config: typeof config,
beginningToDeleteReview: boolean,
deletingReview: boolean,
dispatch: DispatchFunc,
@ -82,6 +84,7 @@ type InternalProps = {|
export class AddonReviewCardBase extends React.Component<InternalProps> {
static defaultProps = {
_config: config,
flaggable: true,
shortByLine: false,
showControls: true,
@ -327,6 +330,7 @@ export class AddonReviewCardBase extends React.Component<InternalProps> {
render() {
const {
_config,
beginningToDeleteReview,
className,
deletingReview,
@ -389,9 +393,10 @@ export class AddonReviewCardBase extends React.Component<InternalProps> {
title={i18n.moment(review.created).format('lll')}
key={review.id}
to={reviewListURL({
_config,
addonSlug: String(slugForReviewLink),
id: review.id,
src: location.query.src,
location,
})}
>
{text}

Просмотреть файл

@ -1,4 +1,5 @@
/* @flow */
import config from 'config';
import * as React from 'react';
import { compose } from 'redux';
import { withRouter } from 'react-router-dom';
@ -8,7 +9,10 @@ import Link from 'amo/components/Link';
import RatingsByStar from 'amo/components/RatingsByStar';
import translate from 'core/i18n/translate';
import { getAddonIconUrl } from 'core/imageUtils';
import { addQueryParams } from 'core/utils/url';
import {
addQueryParams,
getQueryParametersForAttribution,
} from 'core/utils/url';
import Card from 'ui/components/Card';
import LoadingText from 'ui/components/LoadingText';
import Rating from 'ui/components/Rating';
@ -27,18 +31,28 @@ type Props = {|
type InternalProps = {|
...Props,
_config: typeof config,
i18n: I18nType,
location: ReactRouterLocationType,
|};
export const AddonSummaryCardBase = ({
_config = config,
addon,
headerText,
i18n,
location,
}: InternalProps) => {
const queryParamsForAttribution = getQueryParametersForAttribution(
location,
_config,
);
const addonUrl = addon
? addQueryParams(getAddonURL(addon.slug), { src: location.query.src })
? addQueryParams(
getAddonURL(addon.slug),
queryParamsForAttribution,
_config,
)
: '';
const iconUrl = getAddonIconUrl(addon);
const iconImage = (
@ -56,7 +70,11 @@ export const AddonSummaryCardBase = ({
</div>
<div className="AddonSummaryCard-header-text">
<h1 className="visually-hidden">{headerText}</h1>
<AddonTitle addon={addon} linkToAddon linkSource={location.query.src} />
<AddonTitle
addon={addon}
linkToAddon
queryParamsForAttribution={queryParamsForAttribution}
/>
</div>
</div>
);

Просмотреть файл

@ -16,10 +16,10 @@ import type { I18nType } from 'core/types/i18n';
import './styles.scss';
type Props = {|
as?: string,
addon: AddonType | null,
as?: string,
linkToAddon?: boolean,
linkSource?: string,
queryParamsForAttribution?: { [name: string]: ?string | number },
|};
type InternalProps = {|
@ -29,12 +29,12 @@ type InternalProps = {|
|};
export const AddonTitleBase = ({
as: Component = 'h1',
addon,
as: Component = 'h1',
i18n,
isRTL,
linkToAddon = false,
linkSource,
queryParamsForAttribution = {},
}: InternalProps) => {
const authors = [];
@ -68,7 +68,10 @@ export const AddonTitleBase = ({
<>
{linkToAddon ? (
<Link
to={addQueryParams(getAddonURL(addon.slug), { src: linkSource })}
to={addQueryParams(
getAddonURL(addon.slug),
queryParamsForAttribution,
)}
>
{addon.name}
</Link>

Просмотреть файл

@ -173,8 +173,6 @@ export class FooterBase extends React.Component {
className="Footer-lockwise-link"
href={`https://www.mozilla.org/firefox/lockwise/${makeQueryStringWithUTM(
{
utm_source: 'addons.mozilla.org',
utm_medium: 'referral',
utm_content: 'footer-link',
utm_campaign: null,
},
@ -187,8 +185,6 @@ export class FooterBase extends React.Component {
<a
className="Footer-monitor-link"
href={`https://monitor.firefox.com/${makeQueryStringWithUTM({
utm_source: 'addons.mozilla.org',
utm_medium: 'referral',
utm_content: 'footer-link',
utm_campaign: null,
})}`}
@ -200,8 +196,6 @@ export class FooterBase extends React.Component {
<a
className="Footer-send-link"
href={`https://send.firefox.com${makeQueryStringWithUTM({
utm_source: 'addons.mozilla.org',
utm_medium: 'referral',
utm_content: 'footer-link',
utm_campaign: null,
})}`}
@ -214,8 +208,6 @@ export class FooterBase extends React.Component {
className="Footer-browsers-link"
href={`https://www.mozilla.org/firefox/browsers/${makeQueryStringWithUTM(
{
utm_source: 'addons.mozilla.org',
utm_medium: 'referral',
utm_content: 'footer-link',
utm_campaign: null,
},
@ -228,8 +220,6 @@ export class FooterBase extends React.Component {
<a
className="Footer-pocket-link"
href={`https://getpocket.com${makeQueryStringWithUTM({
utm_source: 'addons.mozilla.org',
utm_medium: 'referral',
utm_content: 'footer-link',
utm_campaign: null,
})}`}

Просмотреть файл

@ -1,4 +1,5 @@
/* @flow */
import config from 'config';
import makeClassName from 'classnames';
import invariant from 'invariant';
import * as React from 'react';
@ -29,6 +30,7 @@ type Props = {|
type InternalProps = {|
...Props,
_config: typeof config,
dispatch: DispatchFunc,
errorHandler: ErrorHandlerType,
groupedRatings?: GroupedRatingsType,
@ -37,6 +39,10 @@ type InternalProps = {|
|};
export class RatingsByStarBase extends React.Component<InternalProps> {
static defaultProps = {
_config: config,
};
constructor(props: InternalProps) {
super(props);
@ -87,7 +93,14 @@ export class RatingsByStarBase extends React.Component<InternalProps> {
}
render() {
const { addon, errorHandler, i18n, groupedRatings, location } = this.props;
const {
_config,
addon,
errorHandler,
i18n,
groupedRatings,
location,
} = this.props;
const loading = (!addon || !groupedRatings) && !errorHandler.hasError();
const linkTitles = {
@ -115,9 +128,10 @@ export class RatingsByStarBase extends React.Component<InternalProps> {
<Link
title={linkTitles[star] || ''}
to={reviewListURL({
_config,
addonSlug: addon.slug,
score: star,
src: location.query.src,
location,
})}
>
{text}

Просмотреть файл

@ -1,4 +1,5 @@
/* @flow */
import config from 'config';
import makeClassName from 'classnames';
import * as React from 'react';
import { connect } from 'react-redux';
@ -36,6 +37,7 @@ type Props = {|
type InternalProps = {|
...Props,
_config: typeof config,
clientApp: string,
history: ReactRouterHistoryType,
i18n: I18nType,
@ -44,6 +46,7 @@ type InternalProps = {|
export class SearchResultBase extends React.Component<InternalProps> {
static defaultProps = {
_config: config,
showMetadata: true,
showRecommendedBadge: true,
showSummary: true,
@ -55,9 +58,15 @@ export class SearchResultBase extends React.Component<InternalProps> {
addonInstallSource?: string,
) {
let linkTo = getAddonURL(addon.slug);
if (addonInstallSource) {
linkTo = addQueryParams(linkTo, { src: addonInstallSource });
linkTo = addQueryParams(
linkTo,
{ src: addonInstallSource },
this.props._config,
);
}
return linkTo;
}

Просмотреть файл

@ -185,7 +185,14 @@ export class AddonBase extends React.Component {
}
renderRatingsCard() {
const { RatingManager, addon, i18n, location, currentVersion } = this.props;
const {
RatingManager,
addon,
config,
i18n,
location,
currentVersion,
} = this.props;
let content;
let footerPropName = 'footerText';
@ -224,7 +231,11 @@ export class AddonBase extends React.Component {
content = (
<Link
className="Addon-all-reviews-link"
to={reviewListURL({ addonSlug: addon.slug, src: location.query.src })}
to={reviewListURL({
_config: config,
addonSlug: addon.slug,
location,
})}
>
{linkText}
</Link>

Просмотреть файл

@ -1,5 +1,6 @@
/* @flow */
/* eslint-disable react/no-unused-prop-types */
import config from 'config';
import invariant from 'invariant';
import makeClassName from 'classnames';
import * as React from 'react';
@ -73,6 +74,7 @@ type Props = {|
type InternalProps = {|
...Props,
_config: typeof config,
addon: AddonType | null,
addonIsLoading: boolean,
areReviewsLoading: boolean,
@ -91,6 +93,10 @@ type InternalProps = {|
|};
export class AddonReviewListBase extends React.Component<InternalProps> {
static defaultProps = {
_config: config,
};
constructor(props: InternalProps) {
super(props);
@ -202,16 +208,17 @@ export class AddonReviewListBase extends React.Component<InternalProps> {
}
onSelectOption = (event: ElementEvent<HTMLSelectElement>) => {
const { addon, clientApp, history, lang, location } = this.props;
const { _config, addon, clientApp, history, lang, location } = this.props;
invariant(addon, 'addon is required');
event.preventDefault();
const { value } = event.target;
const listURL = reviewListURL({
_config,
addonSlug: addon.slug,
score: value === SHOW_ALL_REVIEWS ? undefined : value,
src: location.query.src,
location,
});
history.push(`/${lang || ''}/${clientApp || ''}${listURL}`);
@ -243,6 +250,7 @@ export class AddonReviewListBase extends React.Component<InternalProps> {
render() {
const {
_config,
addon,
errorHandler,
i18n,
@ -326,9 +334,10 @@ export class AddonReviewListBase extends React.Component<InternalProps> {
count={reviewCount}
currentPage={getCurrentPage(location)}
pathname={reviewListURL({
_config,
addonSlug: addon.slug,
score: location.query.score,
src: location.query.src,
location,
})}
perPage={Number(pageSize)}
/>

Просмотреть файл

@ -1,4 +1,5 @@
/* @flow */
import config from 'config';
import { oneLine } from 'common-tags';
import deepcopy from 'deepcopy';
import invariant from 'invariant';
@ -32,7 +33,10 @@ import {
UPDATE_RATING_COUNTS,
createInternalReview,
} from 'amo/actions/reviews';
import { addQueryParams } from 'core/utils/url';
import {
addQueryParams,
getQueryParametersForAttribution,
} from 'core/utils/url';
import type {
BeginDeleteAddonReviewAction,
CancelDeleteAddonReviewAction,
@ -65,22 +69,34 @@ import type {
import type { GroupedRatingsType } from 'amo/api/reviews';
import type { FlagReviewReasonType } from 'amo/constants';
import type { AppState } from 'amo/store';
import type { ReactRouterLocationType } from 'core/types/router';
export function reviewListURL({
_config = config,
addonSlug,
id,
location,
score,
src,
}: {|
_config?: typeof config,
addonSlug: string,
id?: number,
location?: ReactRouterLocationType,
score?: number | string,
src?: string,
|}) {
invariant(addonSlug, 'addonSlug is required');
const path = `/addon/${addonSlug}/reviews/${id ? `${id}/` : ''}`;
return addQueryParams(path, { src, score });
let queryParams = { score };
if (location) {
queryParams = {
...queryParams,
...getQueryParametersForAttribution(location, _config),
};
}
return addQueryParams(path, queryParams, _config);
}
type ReviewsById = {

Просмотреть файл

@ -7,6 +7,7 @@ import config from 'config';
import { makeQueryString } from 'core/api';
import { addQueryParams } from 'core/utils/url';
import { DEFAULT_UTM_SOURCE, DEFAULT_UTM_MEDIUM } from 'core/constants';
/*
* Return a base62 object that encodes/decodes just like how Django does it
@ -28,8 +29,8 @@ export function getAddonURL(slug: string) {
}
export const makeQueryStringWithUTM = ({
utm_source = 'addons.mozilla.org',
utm_medium = 'referral',
utm_source = DEFAULT_UTM_SOURCE,
utm_medium = DEFAULT_UTM_MEDIUM,
utm_campaign = 'non-fx-button',
utm_content,
}: {|

Просмотреть файл

@ -306,3 +306,6 @@ export const LTR = 'ltr';
export const AMO_REQUEST_ID_HEADER = 'amo-request-id';
export const DISCO_TAAR_CLIENT_ID_HEADER = 'moz-client-id';
export const DEFAULT_UTM_SOURCE = 'addons.mozilla.org';
export const DEFAULT_UTM_MEDIUM = 'referral';

Просмотреть файл

@ -1,6 +1,11 @@
/* @flow */
import url from 'url';
import config from 'config';
import { DEFAULT_UTM_SOURCE, DEFAULT_UTM_MEDIUM } from 'core/constants';
import type { ReactRouterLocationType } from 'core/types/router';
// TODO: move this function in `index.js` if possible. It was moved from
// `core/utils/addons` to here in order to avoid a weird import error, but it
// does not really belong to `core/utils/addons` or `core/utils/url` either. It
@ -23,11 +28,48 @@ export function removeUndefinedProps(object: Object): Object {
export function addQueryParams(
urlString: string,
queryParams: { [key: string]: ?string | number } = {},
_config: typeof config = config,
): string {
let adjustedQueryParams = { ...queryParams };
if (
_config.get('enableFeatureUseUtmParams') &&
typeof queryParams.src !== 'undefined'
) {
adjustedQueryParams = {
...queryParams,
// Use UTM parameters instead of `src`, according to the PRD.
utm_source: DEFAULT_UTM_SOURCE,
utm_medium: DEFAULT_UTM_MEDIUM,
utm_content: queryParams.src,
src: undefined,
};
}
const urlObj = url.parse(urlString, true);
// Clear search, since query object will only be used if search property
// doesn't exist.
urlObj.search = undefined;
urlObj.query = removeUndefinedProps({ ...urlObj.query, ...queryParams });
urlObj.query = removeUndefinedProps({
...urlObj.query,
...adjustedQueryParams,
});
return url.format(urlObj);
}
export function getQueryParametersForAttribution(
location: ReactRouterLocationType,
_config: typeof config = config,
): Object {
if (_config.get('enableFeatureUseUtmParams')) {
return {
utm_campaign: location.query.utm_campaign,
utm_content: location.query.utm_content,
utm_medium: location.query.utm_medium,
utm_source: location.query.utm_source,
};
}
return { src: location.query.src };
}

Просмотреть файл

@ -15,6 +15,7 @@ import {
dispatchClientMetadata,
fakeAddon,
fakeI18n,
getFakeConfig,
shallowUntilTarget,
} from 'tests/unit/helpers';
import MetadataCard from 'ui/components/MetadataCard';
@ -167,7 +168,88 @@ describe(__filename, () => {
const reviewTitleLink = getReviewTitle(root).find(Link);
const reviewCountLink = getReviewCount(root).find(Link);
const listURL = reviewListURL({ addonSlug: slug, src });
const listURL = reviewListURL({ addonSlug: slug, src, location });
expect(reviewTitleLink).toHaveProp('to', listURL);
expect(reviewCountLink).toHaveProp('to', listURL);
});
it('renders links with `src` when the location has a `src` param but the UTM flag is disabled', () => {
const _config = getFakeConfig({ enableFeatureUseUtmParams: false });
const slug = 'some-slug';
const src = 'some-value-for-src';
const location = createFakeLocation({ query: { src } });
const root = render({
_config,
addon: createInternalAddon({
...fakeAddon,
ratings: { text_count: 3, count: 123 },
slug,
}),
location,
});
const reviewTitleLink = getReviewTitle(root).find(Link);
const reviewCountLink = getReviewCount(root).find(Link);
const listURL = `/addon/${slug}/reviews/?src=${src}`;
expect(reviewTitleLink).toHaveProp('to', listURL);
expect(reviewCountLink).toHaveProp('to', listURL);
});
it('renders links with UTM query parameters when the location has some and the UTM flag is enabled', () => {
const _config = getFakeConfig({ enableFeatureUseUtmParams: true });
const slug = 'some-slug';
const utm_source = 'some-src';
const utm_medium = 'some-medium';
const location = createFakeLocation({
query: { utm_source, utm_medium },
});
const root = render({
_config,
addon: createInternalAddon({
...fakeAddon,
ratings: { text_count: 3, count: 123 },
slug,
}),
location,
});
const reviewTitleLink = getReviewTitle(root).find(Link);
const reviewCountLink = getReviewCount(root).find(Link);
const listURL = `/addon/${slug}/reviews/?utm_medium=some-medium&utm_source=some-src`;
expect(reviewTitleLink).toHaveProp('to', listURL);
expect(reviewCountLink).toHaveProp('to', listURL);
});
it('renders links without UTM query parameters when the location has some and the UTM flag is disabled', () => {
const _config = getFakeConfig({ enableFeatureUseUtmParams: false });
const slug = 'some-slug';
const utm_source = 'some-src';
const utm_medium = 'some-medium';
const location = createFakeLocation({
query: { utm_source, utm_medium },
});
const root = render({
_config,
addon: createInternalAddon({
...fakeAddon,
ratings: { text_count: 3, count: 123 },
slug,
}),
location,
});
const reviewTitleLink = getReviewTitle(root).find(Link);
const reviewCountLink = getReviewCount(root).find(Link);
const listURL = `/addon/${slug}/reviews/`;
expect(reviewTitleLink).toHaveProp('to', listURL);
expect(reviewCountLink).toHaveProp('to', listURL);

Просмотреть файл

@ -19,10 +19,11 @@ import {
dispatchClientMetadata,
dispatchSignInActions,
fakeAddon,
fakeTheme,
fakeI18n,
fakeVersion,
fakePlatformFile,
fakeTheme,
fakeVersion,
getFakeConfig,
shallowUntilTarget,
} from 'tests/unit/helpers';
import LoadingText from 'ui/components/LoadingText';
@ -610,4 +611,108 @@ describe(__filename, () => {
expect(root.find(AddonAuthorLinks)).toHaveProp('addon', addon);
});
describe('enableFeatureUseUtmParams', () => {
const authorUserId = 11;
const addon = createInternalAddon({
...fakeAddon,
has_privacy_policy: true,
has_eula: true,
authors: [
{
...fakeAddon.authors[0],
id: authorUserId,
name: 'tofumatt',
picture_url: 'http://cdn.a.m.o/myphoto.jpg',
url: 'http://a.m.o/en-GB/firefox/user/tofumatt/',
username: 'tofumatt',
},
],
});
beforeEach(() => {
store = dispatchSignInActions({ userId: authorUserId }).store;
_loadVersions({
license: {
is_custom: true,
name: 'tofulicense',
url: 'www.license.com',
},
});
});
describe('with enableFeatureUseUtmParams = true', () => {
const _config = getFakeConfig({ enableFeatureUseUtmParams: true });
it('renders links with UTM query params when there are some', () => {
const utm_medium = 'referral';
const root = render({
_config,
addon,
location: createFakeLocation({ query: { utm_medium } }),
});
const expectedQueryString = `utm_medium=${utm_medium}`;
expect(root.find('.AddonMoreInfo-stats-link')).toHaveProp(
'href',
`/addon/${addon.slug}/statistics/?${expectedQueryString}`,
);
expect(root.find('.AddonMoreInfo-license-link')).toHaveProp(
'to',
`/addon/${addon.slug}/license/?${expectedQueryString}`,
);
expect(
root.find('.AddonMoreInfo-privacy-policy').find(Link),
).toHaveProp(
'to',
`/addon/${addon.slug}/privacy/?${expectedQueryString}`,
);
expect(root.find('.AddonMoreInfo-eula').find(Link)).toHaveProp(
'to',
`/addon/${addon.slug}/eula/?${expectedQueryString}`,
);
expect(
root.find('.AddonMoreInfo-version-history-link').find(Link),
).toHaveProp(
'to',
`/addon/${addon.slug}/versions/?${expectedQueryString}`,
);
});
});
describe('with enableFeatureUseUtmParams = false', () => {
const _config = getFakeConfig({ enableFeatureUseUtmParams: false });
it('renders links without UTM query params when there are some', () => {
const utm_medium = 'referral';
const root = render({
_config,
addon,
location: createFakeLocation({ query: { utm_medium } }),
});
expect(root.find('.AddonMoreInfo-stats-link')).toHaveProp(
'href',
`/addon/${addon.slug}/statistics/`,
);
expect(root.find('.AddonMoreInfo-license-link')).toHaveProp(
'to',
`/addon/${addon.slug}/license/`,
);
expect(
root.find('.AddonMoreInfo-privacy-policy').find(Link),
).toHaveProp('to', `/addon/${addon.slug}/privacy/`);
expect(root.find('.AddonMoreInfo-eula').find(Link)).toHaveProp(
'to',
`/addon/${addon.slug}/eula/`,
);
expect(
root.find('.AddonMoreInfo-version-history-link').find(Link),
).toHaveProp('to', `/addon/${addon.slug}/versions/`);
});
});
});
});

Просмотреть файл

@ -34,6 +34,7 @@ import {
fakeAddon,
fakeI18n,
fakeReview,
getFakeConfig,
shallowUntilTarget,
} from 'tests/unit/helpers';
import DismissibleTextForm from 'ui/components/DismissibleTextForm';
@ -1070,23 +1071,24 @@ describe(__filename, () => {
});
});
it('adds a `src` query parameter to the link in the byLine if available in the location', () => {
it('adds a `src` query parameter to the link in the byLine if available in the location when the UTM flag is disabled', () => {
const _config = getFakeConfig({ enableFeatureUseUtmParams: false });
const slug = 'some-slug';
const review = signInAndDispatchSavedReview({
externalReview: { ...fakeReview, addon: { ...fakeReview.addon, slug } },
});
const src = 'some-src';
const location = createFakeLocation({ query: { src } });
const root = render({
review,
store,
location: createFakeLocation({ query: { src } }),
});
const root = render({ _config, review, store, location });
expect(renderByLine(root).find(Link)).toHaveProp(
'to',
reviewListURL({ addonSlug: slug, id: review.id, src }),
);
// Use hardcoded value to ensure that expectations are correct. We don't
// want to test that `reviewListURL()` was called but that the URLs are
// correct. This is why we use static values in the test cases involving
// `enableFeatureUseUtmParams`.
const expectedURL = `/addon/${slug}/reviews/${review.id}/?src=${src}`;
expect(renderByLine(root).find(Link)).toHaveProp('to', expectedURL);
});
it('uses the addonId for the byLine link when the reviewAddon has an empty slug', () => {

Просмотреть файл

@ -14,6 +14,7 @@ import {
createFakeLocation,
fakeAddon,
fakeI18n,
getFakeConfig,
shallowUntilTarget,
} from 'tests/unit/helpers';
import { getAddonURL } from 'amo/utils';
@ -21,12 +22,13 @@ import LoadingText from 'ui/components/LoadingText';
import Rating from 'ui/components/Rating';
describe(__filename, () => {
const render = ({ addon, headerText, location }) => {
const render = ({ addon, headerText, location, ...props }) => {
return shallowUntilTarget(
<AddonSummaryCard
addon={addon ? createInternalAddon(addon) : addon}
headerText={headerText}
i18n={fakeI18n()}
{...props}
/>,
AddonSummaryCardBase,
{
@ -72,11 +74,13 @@ describe(__filename, () => {
expect(header.find(Link)).toHaveProp('to', getAddonURL(addon.slug));
});
it('adds a `src` query parameter to the link on the icon when there is an add-on', () => {
it('adds a `src` query parameter to the link on the icon when there is a `src` query param', () => {
const _config = getFakeConfig({ enableFeatureUseUtmParams: false });
const src = 'some-src';
const addon = fakeAddon;
const header = renderAddonHeader({
_config,
addon,
location: createFakeLocation({ query: { src } }),
});
@ -87,6 +91,37 @@ describe(__filename, () => {
);
});
it('adds UTM query parameters to the link on the icon when there are some and UTM flag is enabled', () => {
const _config = getFakeConfig({ enableFeatureUseUtmParams: true });
const addon = fakeAddon;
const utm_medium = 'some-utm-medium';
const header = renderAddonHeader({
_config,
addon,
location: createFakeLocation({ query: { utm_medium } }),
});
expect(header.find(Link)).toHaveProp(
'to',
`${getAddonURL(addon.slug)}?utm_medium=${utm_medium}`,
);
});
it('does not add UTM query parameters to the link on the icon when there are some but UTM flag is disabled', () => {
const _config = getFakeConfig({ enableFeatureUseUtmParams: false });
const addon = fakeAddon;
const utm_medium = 'some-utm-medium';
const header = renderAddonHeader({
_config,
addon,
location: createFakeLocation({ query: { utm_medium } }),
});
expect(header.find(Link)).toHaveProp('to', getAddonURL(addon.slug));
});
it('renders the fallback icon if the origin is not allowed', () => {
const addon = {
...fakeAddon,
@ -115,18 +150,56 @@ describe(__filename, () => {
'addon',
createInternalAddon(addon),
);
expect(header.find(AddonTitle)).toHaveProp('linkSource', undefined);
expect(header.find(AddonTitle)).toHaveProp(
'queryParamsForAttribution',
{},
);
});
it('sets the linkSource to the value of `location.query.src`', () => {
it('passes queryParamsForAttribution with the value of `location.query.src` when UTM flag is disabled', () => {
const _config = getFakeConfig({ enableFeatureUseUtmParams: false });
const src = 'some-src';
const header = renderAddonHeader({
_config,
addon: fakeAddon,
location: createFakeLocation({ query: { src } }),
});
expect(header.find(AddonTitle)).toHaveProp('linkSource', src);
expect(header.find(AddonTitle)).toHaveProp('queryParamsForAttribution', {
src,
});
});
it('passes an empty queryParamsForAttribution when UTM flag is enabled and there is no UTM parameter', () => {
const _config = getFakeConfig({ enableFeatureUseUtmParams: true });
const src = 'some-src';
const header = renderAddonHeader({
_config,
addon: fakeAddon,
location: createFakeLocation({ query: { src } }),
});
expect(header.find(AddonTitle)).toHaveProp(
'queryParamsForAttribution',
{},
);
});
it('passes queryParamsForAttribution with the value of `utm_content` if available when UTM flag is enabled', () => {
const _config = getFakeConfig({ enableFeatureUseUtmParams: true });
const utm_content = 'some-src';
const header = renderAddonHeader({
_config,
addon: fakeAddon,
location: createFakeLocation({ query: { utm_content } }),
});
expect(header.find(AddonTitle)).toHaveProp('queryParamsForAttribution', {
utm_content,
});
});
});

Просмотреть файл

@ -201,15 +201,19 @@ describe(__filename, () => {
expect(root.find('span.AddonTitle')).toHaveLength(1);
});
it('accepts a linkSource prop to append to the add-on URL', () => {
const linkSource = 'some-src';
it('accepts some query params for attribution to append to the add-on URL', () => {
const queryParamsForAttribution = { some: 'value' };
const addon = createInternalAddon(fakeAddon);
const root = render({ addon, linkToAddon: true, linkSource });
const root = render({
addon,
linkToAddon: true,
queryParamsForAttribution,
});
expect(root.find(Link).at(0)).toHaveProp(
'to',
`${getAddonURL(addon.slug)}?src=${linkSource}`,
`${getAddonURL(addon.slug)}?some=value`,
);
});
});

Просмотреть файл

@ -65,8 +65,6 @@ describe(__filename, () => {
expect(root.find('.Footer-lockwise-link')).toHaveProp(
'href',
`https://www.mozilla.org/firefox/lockwise/${makeQueryStringWithUTM({
utm_source: 'addons.mozilla.org',
utm_medium: 'referral',
utm_content: 'footer-link',
utm_campaign: null,
})}`,
@ -76,8 +74,6 @@ describe(__filename, () => {
expect(root.find('.Footer-monitor-link')).toHaveProp(
'href',
`https://monitor.firefox.com/${makeQueryStringWithUTM({
utm_source: 'addons.mozilla.org',
utm_medium: 'referral',
utm_content: 'footer-link',
utm_campaign: null,
})}`,
@ -87,8 +83,6 @@ describe(__filename, () => {
expect(root.find('.Footer-send-link')).toHaveProp(
'href',
`https://send.firefox.com${makeQueryStringWithUTM({
utm_source: 'addons.mozilla.org',
utm_medium: 'referral',
utm_content: 'footer-link',
utm_campaign: null,
})}`,
@ -98,8 +92,6 @@ describe(__filename, () => {
expect(root.find('.Footer-browsers-link')).toHaveProp(
'href',
`https://www.mozilla.org/firefox/browsers/${makeQueryStringWithUTM({
utm_source: 'addons.mozilla.org',
utm_medium: 'referral',
utm_content: 'footer-link',
utm_campaign: null,
})}`,
@ -109,8 +101,6 @@ describe(__filename, () => {
expect(root.find('.Footer-pocket-link')).toHaveProp(
'href',
`https://getpocket.com${makeQueryStringWithUTM({
utm_source: 'addons.mozilla.org',
utm_medium: 'referral',
utm_content: 'footer-link',
utm_campaign: null,
})}`,

Просмотреть файл

@ -15,6 +15,7 @@ import {
dispatchClientMetadata,
fakeAddon,
fakeI18n,
getFakeConfig,
shallowUntilTarget,
} from 'tests/unit/helpers';
import ErrorList from 'ui/components/ErrorList';
@ -204,7 +205,9 @@ describe(__filename, () => {
});
it('adds a `src` query parameter to the review links when available in the location', () => {
const _config = getFakeConfig({ enableFeatureUseUtmParams: false });
const src = 'some-src';
const location = createFakeLocation({ query: { src } });
const grouping = {
5: 964,
4: 821,
@ -214,16 +217,78 @@ describe(__filename, () => {
};
const addon = addonForGrouping(grouping);
store.dispatch(setGroupedRatings({ addonId: addon.id, grouping }));
const root = render({
addon,
location: createFakeLocation({ query: { src } }),
});
const root = render({ _config, addon, location });
const counts = root.find('.RatingsByStar-count').find(Link);
function validateLink(link, score) {
expect(link).toHaveProp(
'to',
reviewListURL({ addonSlug: addon.slug, score, src }),
`/addon/${addon.slug}/reviews/?score=${score}&src=${src}`,
);
}
validateLink(counts.at(0), '5');
validateLink(counts.at(1), '4');
validateLink(counts.at(2), '3');
validateLink(counts.at(3), '2');
validateLink(counts.at(4), '1');
});
it('adds UTM query parameters to the review links when there are some and UTM flag is enabled', () => {
const _config = getFakeConfig({ enableFeatureUseUtmParams: true });
const utm_medium = 'some-utm-medium';
const location = createFakeLocation({ query: { utm_medium } });
const grouping = {
5: 964,
4: 821,
3: 543,
2: 22,
1: 0,
};
const addon = addonForGrouping(grouping);
store.dispatch(setGroupedRatings({ addonId: addon.id, grouping }));
const root = render({ _config, addon, location });
const counts = root.find('.RatingsByStar-count').find(Link);
function validateLink(link, score) {
const expectedQueryString = [
`score=${score}`,
`utm_medium=${utm_medium}`,
].join('&');
expect(link).toHaveProp(
'to',
`/addon/${addon.slug}/reviews/?${expectedQueryString}`,
);
}
validateLink(counts.at(0), '5');
validateLink(counts.at(1), '4');
validateLink(counts.at(2), '3');
validateLink(counts.at(3), '2');
validateLink(counts.at(4), '1');
});
it('does not add UTM query parameters to the review links when there are some but UTM flag is disabled', () => {
const _config = getFakeConfig({ enableFeatureUseUtmParams: false });
const utm_medium = 'some-utm-medium';
const location = createFakeLocation({ query: { utm_medium } });
const grouping = {
5: 964,
4: 821,
3: 543,
2: 22,
1: 0,
};
const addon = addonForGrouping(grouping);
store.dispatch(setGroupedRatings({ addonId: addon.id, grouping }));
const root = render({ _config, addon, location });
const counts = root.find('.RatingsByStar-count').find(Link);
function validateLink(link, score) {
const expectedQueryString = `score=${score}`;
expect(link).toHaveProp(
'to',
`/addon/${addon.slug}/reviews/?${expectedQueryString}`,
);
}

Просмотреть файл

@ -8,6 +8,7 @@ import {
ADDON_TYPE_STATIC_THEME,
CLIENT_APP_ANDROID,
CLIENT_APP_FIREFOX,
DEFAULT_UTM_SOURCE,
} from 'core/constants';
import { createInternalAddon } from 'core/reducers/addons';
import {
@ -19,6 +20,7 @@ import {
fakeI18n,
fakePreview,
fakeTheme,
getFakeConfig,
normalizeSpaces,
shallowUntilTarget,
} from 'tests/unit/helpers';
@ -98,6 +100,19 @@ describe(__filename, () => {
});
});
it('links the heading to the detail page with UTM params when UTM flag is enabled', () => {
const _config = getFakeConfig({ enableFeatureUseUtmParams: true });
const addonInstallSource = 'home-page-featured';
const root = render({ _config, addonInstallSource });
const link = root.find('.SearchResult-link');
expect(url.parse(link.prop('to'), true).query).toMatchObject({
utm_source: DEFAULT_UTM_SOURCE,
utm_content: addonInstallSource,
});
});
it('renders the author', () => {
const root = render();

Просмотреть файл

@ -63,6 +63,7 @@ import {
fakeI18n,
fakeTheme,
fakeVersion,
getFakeConfig,
sampleUserAgentParsed,
shallowUntilTarget,
} from 'tests/unit/helpers';
@ -1144,18 +1145,64 @@ describe(__filename, () => {
});
it('adds a `src` query parameter to the all reviews link when available in the location', () => {
const config = getFakeConfig({ enableFeatureUseUtmParams: false });
const src = 'some-src';
const location = createFakeLocation({ query: { src } });
const addonSlug = 'adblock-plus';
const card = readReviewsCard({
config,
addonSlug,
ratingsCount: 2,
location,
});
const link = allReviewsLink(card);
// Use hardcoded value to ensure that expectations are correct. We don't
// want to test that `reviewListURL()` was called but that the URLs are
// correct. This is why we use static values in the test cases involving
// `enableFeatureUseUtmParams`.
expect(allReviewsLink(card)).toHaveProp(
'to',
`${getAddonURL(addonSlug)}reviews/?src=${src}`,
);
});
expect(link).toHaveProp('to', reviewListURL({ addonSlug, src }));
it('does not add UTM query parameters to the all reviews link when there are some but UTM flag is disabled', () => {
const config = getFakeConfig({ enableFeatureUseUtmParams: false });
const utm_campaign = 'some-utm-campaign';
const location = createFakeLocation({ query: { utm_campaign } });
const addonSlug = 'adblock-plus';
const card = readReviewsCard({
config,
addonSlug,
ratingsCount: 2,
location,
});
expect(allReviewsLink(card)).toHaveProp(
'to',
`${getAddonURL(addonSlug)}reviews/`,
);
});
it('adds UTM query parameters to the all reviews link when there are some and UTM flag is enabled', () => {
const config = getFakeConfig({ enableFeatureUseUtmParams: true });
const utm_campaign = 'some-utm-campaign';
const location = createFakeLocation({ query: { utm_campaign } });
const addonSlug = 'adblock-plus';
const card = readReviewsCard({
config,
addonSlug,
ratingsCount: 2,
location,
});
expect(allReviewsLink(card)).toHaveProp(
'to',
`${getAddonURL(addonSlug)}reviews/?utm_campaign=${utm_campaign}`,
);
});
});

Просмотреть файл

@ -47,6 +47,7 @@ import {
fakeAddon,
fakeI18n,
fakeReview,
getFakeConfig,
shallowUntilTarget,
} from 'tests/unit/helpers';
@ -831,25 +832,71 @@ describe(__filename, () => {
});
it('adds a `src` query parameter to the reviews URL when available in the location', () => {
const _config = getFakeConfig({ enableFeatureUseUtmParams: false });
const src = 'some-src';
const location = createFakeLocation({ query: { src } });
const addonSlug = 'adblock-plus';
const addon = { ...fakeAddon, id: 8765, slug: addonSlug };
loadAddon(addon);
const root = renderWithPagination({
_config,
addon,
params: { addonSlug },
location,
});
const paginator = renderFooter(root);
expect(paginator).toHaveProp(
// Use hardcoded value to ensure that expectations are correct. We
// don't want to test that `reviewListURL()` was called but that the
// URLs are correct. This is why we use static values in the test cases
// involving `enableFeatureUseUtmParams`.
expect(renderFooter(root)).toHaveProp(
'pathname',
reviewListURL({ addonSlug, src }),
`${getAddonURL(addonSlug)}reviews/?src=${src}`,
);
});
it('adds UTM query parameters to the reviews URL when there are some and UTM flag is enabled', () => {
const _config = getFakeConfig({ enableFeatureUseUtmParams: true });
const utm_campaign = 'some-utm-campaign';
const location = createFakeLocation({ query: { utm_campaign } });
const addonSlug = 'adblock-plus';
const addon = { ...fakeAddon, id: 8765, slug: addonSlug };
loadAddon(addon);
const root = renderWithPagination({
_config,
addon,
params: { addonSlug },
location,
});
expect(renderFooter(root)).toHaveProp(
'pathname',
`${getAddonURL(addonSlug)}reviews/?utm_campaign=${utm_campaign}`,
);
});
it('does not add UTM query parameters to the reviews URL when there are some but UTM flag is disabled', () => {
const _config = getFakeConfig({ enableFeatureUseUtmParams: false });
const utm_campaign = 'some-utm-campaign';
const location = createFakeLocation({ query: { utm_campaign } });
const addonSlug = 'adblock-plus';
const addon = { ...fakeAddon, id: 8765, slug: addonSlug };
loadAddon(addon);
const root = renderWithPagination({
_config,
addon,
params: { addonSlug },
location,
});
expect(renderFooter(root)).toHaveProp(
'pathname',
`${getAddonURL(addonSlug)}reviews/`,
);
});
it('configures a paginator with the right Link', () => {
const root = renderWithPagination();

Просмотреть файл

@ -45,7 +45,12 @@ import reviewsReducer, {
storeReviewObjects,
} from 'amo/reducers/reviews';
import { DEFAULT_API_PAGE_SIZE } from 'core/api';
import { fakeAddon, fakeReview } from 'tests/unit/helpers';
import {
createFakeLocation,
fakeAddon,
fakeReview,
getFakeConfig,
} from 'tests/unit/helpers';
describe(__filename, () => {
function setFakeReview({
@ -1905,20 +1910,26 @@ describe(__filename, () => {
);
});
it('returns a URL with a src query parameter', () => {
it('returns a URL with a src query parameter when UTM flag is disabled', () => {
const _config = getFakeConfig({ enableFeatureUseUtmParams: false });
const addonSlug = 'adblock-plus';
const src = 'some-src';
expect(reviewListURL({ addonSlug, src })).toEqual(
const location = createFakeLocation({ query: { src } });
expect(reviewListURL({ _config, addonSlug, location })).toEqual(
`/addon/${addonSlug}/reviews/?src=${src}`,
);
});
it('returns a URL with score and src in the query string', () => {
it('returns a URL with score and src in the query string when UTM flag is disabled', () => {
const _config = getFakeConfig({ enableFeatureUseUtmParams: false });
const addonSlug = 'adblock-plus';
const score = 5;
const src = 'some-src';
expect(reviewListURL({ addonSlug, score, src })).toEqual(
`/addon/${addonSlug}/reviews/?src=${src}&score=${score}`,
const location = createFakeLocation({ query: { src } });
expect(reviewListURL({ _config, addonSlug, score, location })).toEqual(
`/addon/${addonSlug}/reviews/?score=${score}&src=${src}`,
);
});
});

Просмотреть файл

@ -1,6 +1,12 @@
import url from 'url';
import { addQueryParams, removeUndefinedProps } from 'core/utils/url';
import {
addQueryParams,
removeUndefinedProps,
getQueryParametersForAttribution,
} from 'core/utils/url';
import { DEFAULT_UTM_SOURCE, DEFAULT_UTM_MEDIUM } from 'core/constants';
import { createFakeLocation, getFakeConfig } from 'tests/unit/helpers';
describe(__filename, () => {
describe('removeUndefinedProps', () => {
@ -76,5 +82,49 @@ describe(__filename, () => {
});
expect(url.parse(output, true).query).toEqual({});
});
it('replaces `src` with UTM parameters when UTM flag is enabled', () => {
const _config = getFakeConfig({ enableFeatureUseUtmParams: true });
const src = 'some-src';
const output = addQueryParams('http://whatever.com/', { src }, _config);
expect(url.parse(output, true).query).toEqual({
utm_source: DEFAULT_UTM_SOURCE,
utm_medium: DEFAULT_UTM_MEDIUM,
utm_content: src,
});
});
it('does not replace `src` with UTM parameters when UTM flag is disabled', () => {
const _config = getFakeConfig({ enableFeatureUseUtmParams: false });
const src = 'some-src';
const output = addQueryParams('http://whatever.com/', { src }, _config);
expect(url.parse(output, true).query).toEqual({ src });
});
});
describe('getQueryParametersForAttribution', () => {
it('returns the `src` query param when UTM flag is disabled', () => {
const _config = getFakeConfig({ enableFeatureUseUtmParams: false });
const src = 'some-src';
const location = createFakeLocation({ query: { src } });
expect(getQueryParametersForAttribution(location, _config)).toEqual({
src,
});
});
it('returns the UTM parameters in the location when UTM flag is enabled', () => {
const _config = getFakeConfig({ enableFeatureUseUtmParams: true });
const utm_campaign = 'some-utm-campaign';
const location = createFakeLocation({ query: { utm_campaign } });
expect(getQueryParametersForAttribution(location, _config)).toEqual({
utm_campaign,
});
});
});
});