Replace src query parameter with UTM params (controlled by a feature flag) (#9548)
This commit is contained in:
Родитель
2ac03c8bb0
Коммит
b81d862cb0
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Загрузка…
Ссылка в новой задаче