Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[NASAA-205] - Send top stories experiment analytics events #12075

Open
wants to merge 42 commits into
base: latest
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
7625d2d
Add amp-analytics to AmpExperiment
hotinglok Oct 21, 2024
ac4cd70
Add tests
hotinglok Oct 21, 2024
ef8f7f1
Remove commented out snapshot
hotinglok Oct 21, 2024
a7cd6af
Pass down & use producerId
hotinglok Oct 21, 2024
6905317
Make selectors for event triggers more robust
hotinglok Oct 22, 2024
5607655
Update selectors to account for secondary column section
hotinglok Oct 22, 2024
30f746f
Refactor scripts in AmpExperiment to use same component
hotinglok Oct 22, 2024
558bb5e
Cleanup of tests/types
hotinglok Oct 22, 2024
fc995db
Add conditions to account for short articles
hotinglok Oct 23, 2024
9c42c11
Refactor block insertion logic, exclude articles too short
hotinglok Oct 24, 2024
34973c6
Add variant to pageview only for pages impacted by experiment
hotinglok Oct 24, 2024
150f137
Refactor ATIData to accept ampExperimentName, add tests
hotinglok Oct 27, 2024
b723306
Fix test
hotinglok Oct 28, 2024
2b79825
Add more test assets
hotinglok Oct 28, 2024
cae1f67
Add CPS asset
hotinglok Oct 28, 2024
4766c64
Remove unnecessary undefined type from ampExperimentName
hotinglok Oct 28, 2024
1792eb4
Minor cleanup
hotinglok Oct 28, 2024
c24cdec
Update analytics config type
hotinglok Oct 28, 2024
d97435f
Update data attribute name
hotinglok Oct 28, 2024
6a14fe1
Add position of top stories section to view events, add cymrufyw asse…
hotinglok Oct 28, 2024
5e92f4c
Add position to click events
hotinglok Oct 29, 2024
4cd546e
Fix click event selectors
hotinglok Oct 29, 2024
85e6a69
Fix page view event, renamed variables
hotinglok Oct 29, 2024
5eac132
Add buildATIPageTrackPath() test
hotinglok Oct 30, 2024
3295412
Add MVT test property
hotinglok Oct 30, 2024
2f64abe
Update snapshots
hotinglok Oct 30, 2024
f585e09
Fix missed prop name update
hotinglok Oct 30, 2024
f3d828a
Add valid sport asset
hotinglok Oct 30, 2024
42f3c53
Merge branch 'latest' into nasaa-amp-experiment-analytics
hotinglok Oct 30, 2024
4439bf6
Refactor block insertion to add more blocks
hotinglok Oct 30, 2024
a2040e3
Add other variants & update tests
hotinglok Oct 31, 2024
a6a7619
Add sick typescript refactor
hotinglok Nov 4, 2024
8c67eba
Remove comments in insertBlockAtPosition()
hotinglok Nov 4, 2024
c49dc8d
Add types, refactor css
hotinglok Nov 4, 2024
745e9dd
Refactor types in helpers
hotinglok Nov 4, 2024
e0d0b7a
Fix conflicts
hotinglok Nov 5, 2024
ff62b71
Update src/app/pages/ArticlePage/experimentTopStories/helpers.tsx
hotinglok Nov 5, 2024
5819495
Linting
hotinglok Nov 5, 2024
b04d1ab
Merge branch 'nasaa-amp-experiment-analytics' into nasaa-amp-experime…
hotinglok Nov 6, 2024
b5e725d
Merge pull request #12119 from bbc/nasaa-amp-experiment-more-variants
hotinglok Nov 12, 2024
82475ce
Merge branch 'latest' into nasaa-amp-experiment-analytics
hotinglok Nov 12, 2024
be4d6cb
Update bundle size limits
hotinglok Nov 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/app/components/ATIAnalytics/atiUrl/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ describe('buildATIEventTrackUrl', () => {
format: 'format',
url: 'url',
detailedPlacement: 'detailedPlacement',
variant: 'variant_1',
});

expect(splitUrl(atiEventTrackUrl)).toEqual([
Expand All @@ -255,7 +256,7 @@ describe('buildATIEventTrackUrl', () => {
're=getBrowserViewPort',
'hl=getCurrentTime',
'lng=getDeviceLanguage',
'atc=PUB-[campaignID]-[component]-[]-[format]-[pageIdentifier]-[detailedPlacement]-[]-[url]',
'atc=PUB-[campaignID]-[component]-[variant_1]-[format]-[pageIdentifier]-[detailedPlacement]-[]-[url]',
'type=AT',
]);
});
Expand Down
2 changes: 2 additions & 0 deletions src/app/components/ATIAnalytics/atiUrl/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ export const buildATIEventTrackUrl = ({
advertiserID,
url,
detailedPlacement,
variant,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confusingly, we use the term variant for services with dual scripts e.g. serbian with cyrillic script has a variant = cyr

If this is not the intention of the term variant here, can we please rename it to something else, to avoid confusion - perhaps experimentVariant or something similar?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I saw this as well, but had a look at the utility and apparently it was set for A/B testing 3 years ago

variant = '', // not a service variant - used for A/B testing

Copy link
Collaborator Author

@hotinglok hotinglok Oct 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've added this here to use as the experiment variant in our use-case because getEventInfo() uses this as a variant for A/B testing, it just wasn't previously passed in here.
As the value just needs to be in the right place in the string, I'll rename it in there too and remove the comment to make it clearer

}: ATIEventTrackingProps) => {
// on AMP, variable substitutions are used in the value and they cannot be
// encoded: https://github.com/ampproject/amphtml/blob/master/spec/amp-var-substitutions.md
Expand Down Expand Up @@ -323,6 +324,7 @@ export const buildATIEventTrackUrl = ({
advertiserID,
url,
detailedPlacement,
variant,
}),
wrap: false,
disableEncoding: true,
Expand Down
1 change: 1 addition & 0 deletions src/app/components/ATIAnalytics/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export interface ATIEventTrackingProps {
advertiserID?: string;
url?: string;
detailedPlacement?: string;
variant?: string;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As per comment above - is this an experiment variant or a dual script variant?

}

export interface ATIPageTrackingProps {
Expand Down
46 changes: 45 additions & 1 deletion src/app/components/AmpExperiment/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,22 @@ const multipleExperimentConfig = {
},
};

const analyticsConfig = {
requests: {
base: 'somehost.com',
clicks: 'somehost.com?somequeryparam=somevalue',
},
triggers: {
trackClicks: {
on: 'click',
request: 'clicks',
selector: 'a',
},
},
};

describe('Amp experiment container on Amp pages', () => {
it('should render an amp-experiment with the expected config', async () => {
it('should render an amp-experiment with the expected experiment config', async () => {
const { container } = render(
<AmpExperiment experimentConfig={experimentConfig} />,
);
Expand Down Expand Up @@ -66,6 +80,36 @@ describe('Amp experiment container on Amp pages', () => {
`);
});

it('should render an amp-analytics with the expected analytics config if present', async () => {
const { container } = render(
<AmpExperiment
experimentConfig={experimentConfig}
analyticsConfig={analyticsConfig}
/>,
);
expect(container.querySelector('amp-experiment')).toBeInTheDocument();
expect(container).toMatchInlineSnapshot(`
<div>
<amp-experiment>
<script
type="application/json"
>
{"someExperiment":{"variants":{"control":33,"variant_1":33,"variant_2":33}}}
</script>
</amp-experiment>
<amp-analytics
type="piano"
>
<script
type="application/json"
>
{"requests":{"base":"somehost.com","clicks":"somehost.com?somequeryparam=somevalue"},"triggers":{"trackClicks":{"on":"click","request":"clicks","selector":"a"}}}
</script>
</amp-analytics>
</div>
`);
});

it(`should add amp-experiment extension script to page head`, async () => {
render(<AmpExperiment experimentConfig={experimentConfig} />);

Expand Down
34 changes: 27 additions & 7 deletions src/app/components/AmpExperiment/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,14 @@ type AmpExperimentConfig = {
};
};

type AmpAnalyticsConfig = {
requests: Record<string, unknown>;
triggers: Record<string, unknown>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question (non-blocking) Can this be typed any more strictly? I can see that we set base, clicks, trackClicks which we could encode here too.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated this a bit with just the things that we've used for the experiment. There's a lot of other optional properties that I've not included, but those can be added whenever they're needed.

};

type AmpExperimentProps = {
[key: Experiment]: AmpExperimentConfig;
experimentConfig: AmpExperimentConfig;
analyticsConfig?: AmpAnalyticsConfig;
};

const AmpHead = () => (
Expand All @@ -32,17 +38,31 @@ const AmpHead = () => (
</Helmet>
);

const AmpExperiment = ({ experimentConfig }: AmpExperimentProps) => {
const AmpScript = ({ config }: { config: Record<string, unknown> }) => {
hotinglok marked this conversation as resolved.
Show resolved Hide resolved
return (
<script
type="application/json"
/* eslint-disable-next-line react/no-danger */
dangerouslySetInnerHTML={{ __html: JSON.stringify(config) }}
/>
);
};

const AmpExperiment = ({
experimentConfig,
analyticsConfig,
}: AmpExperimentProps) => {
return (
<>
<AmpHead />
<amp-experiment>
<script
type="application/json"
/* eslint-disable-next-line react/no-danger */
dangerouslySetInnerHTML={{ __html: JSON.stringify(experimentConfig) }}
/>
<AmpScript config={experimentConfig} />
</amp-experiment>
{analyticsConfig && (
<amp-analytics type="piano">
<AmpScript config={analyticsConfig} />
</amp-analytics>
)}
</>
);
};
Expand Down
14 changes: 12 additions & 2 deletions src/app/pages/ArticlePage/ArticlePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,18 +65,21 @@ import { ComponentToRenderProps, TimeStampProps } from './types';
import AmpExperiment from '../../components/AmpExperiment';
import {
experimentTopStoriesConfig,
getExperimentAnalyticsConfig,
getExperimentTopStories,
ExperimentTopStories,
} from './experimentTopStories/helpers';

const ArticlePage = ({ pageData }: { pageData: Article }) => {
const { isApp, pageType, service, isAmp, id } = useContext(RequestContext);
const { isApp, pageType, service, isAmp, id, env } =
useContext(RequestContext);

const {
articleAuthor,
isTrustProjectParticipant,
showRelatedTopics,
brandName,
atiAnalyticsProducerId,
} = useContext(ServiceContext);

const { enabled: preloadLeadImageToggle } = useToggle('preloadLeadImage');
Expand Down Expand Up @@ -227,7 +230,14 @@ const ArticlePage = ({ pageData }: { pageData: Article }) => {
return (
<div css={styles.pageWrapper}>
{shouldEnableExperimentTopStories && (
<AmpExperiment experimentConfig={experimentTopStoriesConfig} />
<AmpExperiment
experimentConfig={experimentTopStoriesConfig}
analyticsConfig={getExperimentAnalyticsConfig({
env,
service,
atiAnalyticsProducerId,
})}
/>
)}
<ATIAnalytics atiData={atiData} />
<ChartbeatAnalytics
Expand Down
6 changes: 5 additions & 1 deletion src/app/pages/ArticlePage/SecondaryColumn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ const SecondaryColumn = ({ pageData }: { pageData: Article }) => {
return (
<div css={styles.secondaryColumn}>
{topStoriesContent && (
<div css={styles.topStoriesSection} data-testid="top-stories">
<div
css={styles.topStoriesSection}
data-testid="top-stories"
data-experiment="position:secondaryColumn"
>
<TopStoriesSection content={topStoriesContent} />
</div>
)}
Expand Down
132 changes: 123 additions & 9 deletions src/app/pages/ArticlePage/experimentTopStories/helpers.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import { getExperimentTopStories } from './helpers';
import {
getExperimentAnalyticsConfig,
getExperimentTopStories,
} from './helpers';
import { topStoriesList } from '../PagePromoSections/TopStoriesSection/fixture/index';

jest.mock('../../../lib/analyticsUtils', () => ({
...jest.requireActual('../../../lib/analyticsUtils'),
getAtUserId: jest.fn().mockReturnValue('123-456-789'),
}));

Comment on lines +7 to +10
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This block is needed because getAtUserId() always generates a random ID.

describe('AMP top stories experiment', () => {
const mockTextBlock = {
type: 'text',
Expand All @@ -16,8 +24,6 @@ describe('AMP top stories experiment', () => {
};
};

const blocksShortLength = [mockTextBlock];

const blocksEvenLength = [
mockTextBlock,
mockTextBlock,
Expand Down Expand Up @@ -63,26 +69,67 @@ describe('AMP top stories experiment', () => {
},
);

const blocksShortLength = [mockTextBlock];
const blocksLongEvenLength = [
mockTextBlock,
mockTextBlock,
mockTextBlock,
mockTextBlock,
mockTextBlock,
mockTextBlock,
];
const blocksLongOddLength = [
mockTextBlock,
mockTextBlock,
mockTextBlock,
mockTextBlock,
mockTextBlock,
mockTextBlock,
mockTextBlock,
];

const expectedBlocksEvenLength = [
mockTextBlock,
mockTextBlock,
expectedExperimentTopStoriesBlock(2),
mockTextBlock,
expectedExperimentTopStoriesBlock(3),
mockTextBlock,
];
const expectedBlocksOddLength = [
mockTextBlock,
expectedExperimentTopStoriesBlock(1),
mockTextBlock,
mockTextBlock,
expectedExperimentTopStoriesBlock(3),
];
const expectedBlocksLongEvenLength = [
mockTextBlock,
mockTextBlock,
expectedExperimentTopStoriesBlock(2),
mockTextBlock,
mockTextBlock,
mockTextBlock,
mockTextBlock,
];

const expectedBlocksLongOddLength = [
mockTextBlock,
mockTextBlock,
mockTextBlock,
expectedExperimentTopStoriesBlock(3),
mockTextBlock,
mockTextBlock,
mockTextBlock,
mockTextBlock,
];

it.each`
testType | inputBlocks | expectedOutput
${'even'} | ${blocksEvenLength} | ${expectedBlocksEvenLength}
${'odd'} | ${blocksOddLength} | ${expectedBlocksOddLength}
testType | inputBlocks | expectedOutput
${'4'} | ${blocksEvenLength} | ${expectedBlocksEvenLength}
${'3'} | ${blocksOddLength} | ${expectedBlocksOddLength}
${'even'} | ${blocksLongEvenLength} | ${expectedBlocksLongEvenLength}
${'odd'} | ${blocksLongOddLength} | ${expectedBlocksLongOddLength}
`(
'should insert experimentTopStories block into blocks array in the correct position when blocks.length is $testType',
'should insert experimentTopStories block into blocks array in the correct position when blocks.length is $testType.',
({ inputBlocks, expectedOutput }) => {
const { transformedBlocks } = getExperimentTopStories({
blocks: inputBlocks,
Expand All @@ -106,4 +153,71 @@ describe('AMP top stories experiment', () => {
expect(transformedBlocks).toBe(blocksShortLength);
});
});

describe('getExperimentAnalyticsConfig()', () => {
process.env.SIMORGH_ATI_BASE_URL = 'http://foobar.com?';

const PS_NEWS_DESTINATION_ID = 598285;
const PS_NEWS_TEST_DESTINATION_ID = 598286;
const PS_NEWS_GNL_DESTINATION_ID = 598287;
const PS_NEWS_GNL_TEST_DESINTATION_ID = 598288;
const PS_SPORT_DESTINATION_ID = 598310;
const PS_SPORT_TEST_DESTINATION_ID = 598311;
const PS_SPORT_GNL_DESTINATION_ID = 598308;
const PS_SPORT_GNL_TEST_DESTINATION_ID = 598309;
const NEWS_PRODUCER_ID = 64;
const SPORT_PRODUCER_ID = 85;

it.each`
service | env | destinationId | gnlId | producerId
${'news'} | ${'live'} | ${PS_NEWS_DESTINATION_ID} | ${PS_NEWS_GNL_DESTINATION_ID} | ${NEWS_PRODUCER_ID}
${'news'} | ${'test'} | ${PS_NEWS_TEST_DESTINATION_ID} | ${PS_NEWS_GNL_TEST_DESINTATION_ID} | ${NEWS_PRODUCER_ID}
${'sport'} | ${'live'} | ${PS_SPORT_DESTINATION_ID} | ${PS_SPORT_GNL_DESTINATION_ID} | ${SPORT_PRODUCER_ID}
${'sport'} | ${'test'} | ${PS_SPORT_TEST_DESTINATION_ID} | ${PS_SPORT_GNL_TEST_DESTINATION_ID} | ${SPORT_PRODUCER_ID}
`(
'should create the analytics config with the correct parameters for $service on $env.',
({ env, service, destinationId, gnlId, producerId }) => {
const analyticsConfig = getExperimentAnalyticsConfig({
env,
service,
atiAnalyticsProducerId: producerId,
});
expect(analyticsConfig).toMatchInlineSnapshot(`
{
"requests": {
"topStoriesClick": "http://foobar.com?idclient=123-456-789&s=$IF($EQUALS($MATCH(\${ampGeo}, gbOrUnknown, 0), gbOrUnknown), ${destinationId}, ${gnlId})&s2=${producerId}&p=SOURCE_URL&r=\${screenWidth}x\${screenHeight}x\${screenColorDepth}&re=\${availableScreenWidth}x\${availableScreenHeight}&hl=\${timestamp}&lng=\${browserLanguage}&atc=PUB-[article]-[top-stories-promo]-[topStoriesExperiment:VARIANT(topStoriesExperiment)]-[]-[SOURCE_URL]-[]-[]-[]&type=AT",
"topStoriesView": "http://foobar.com?idclient=123-456-789&s=$IF($EQUALS($MATCH(\${ampGeo}, gbOrUnknown, 0), gbOrUnknown), ${destinationId}, ${gnlId})&s2=${producerId}&p=SOURCE_URL&r=\${screenWidth}x\${screenHeight}x\${screenColorDepth}&re=\${availableScreenWidth}x\${availableScreenHeight}&hl=\${timestamp}&lng=\${browserLanguage}&ati=PUB-[article]-[top-stories-section]-[topStoriesExperiment:VARIANT(topStoriesExperiment)]-[]-[SOURCE_URL]-[]-[]-[]&type=AT",
},
"triggers": {
"trackTopStoriesClick": {
"on": "click",
"request": "topStoriesClick",
"selector": "a[aria-labelledby*='top-stories-promo']",
},
"trackTopStoriesDesktopView": {
"on": "visible",
"request": "topStoriesView",
"visibilitySpec": {
"continuousTimeMin": 200,
"selector": "div[data-experiment='position:secondaryColumn'] > section[aria-labelledby='top-stories-heading']",
"totalTimeMin": 500,
"visiblePercentageMin": 20,
},
},
"trackTopStoriesView": {
"on": "visible",
"request": "topStoriesView",
"visibilitySpec": {
"continuousTimeMin": 200,
"selector": "div[data-experiment='position:articleBody'] > section[aria-labelledby='top-stories-heading']",
"totalTimeMin": 500,
"visiblePercentageMin": 20,
},
},
},
}
`);
},
);
});
});
Loading
Loading