Skip to content

Commit 10ff756

Browse files
Dispatch custom events on marketing components (#14875)
* Dispatch custom events on marketing components appearing and disappearing in the viewport. * eventTypes structured * remove eventEmitter from epicContent.apps
1 parent 5ef0f04 commit 10ff756

File tree

9 files changed

+78
-32
lines changed

9 files changed

+78
-32
lines changed

dotcom-rendering/src/components/SignInGate/README.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
## Quick Setup Guide
44

55
1. Create AB test switch in [guardian/frontend](https://github.com/guardian/frontend/blob/main/common/app/conf/switches/ABTestSwitches.scala), [docs](https://github.com/guardian/frontend/blob/main/docs/03-dev-howtos/01-ab-testing.md#adding-a-switch)
6-
2. Add test definition to `src/web/experiments/tests` folder, and import test in the `src/web/experiments/ab-tests.ts` file. Variant name in test definition must be unique.
6+
2. Add test definition to `src/experiments/tests` folder, and import test in the `src/experiments/ab-tests.ts` file. Variant name in test definition must be unique.
77
3. Import test definition in the `signInGateTests` array in the `signInGate.ts`
88
4. If the test needs a design, make it in the `gateDesigns` folder
99
5. Set up individual variants in the `gates` folder, exports the `SignInGateComponent` type. Display rules defined here in the `canShow` method. Helpers in `displayRule.ts`.
@@ -78,7 +78,7 @@ When running dotcom-rendering locally for development, it is useful to have fron
7878

7979
#### AB Test Definition
8080

81-
Once the switch is set up, you'll need to add a test definition to the `src/web/experiments/tests` folder. Here's an example of a test definition.
81+
Once the switch is set up, you'll need to add a test definition to the `src/experiments/tests` folder. Here's an example of a test definition.
8282

8383
```ts
8484
import { ABTest } from '@guardian/ab-core';
@@ -124,13 +124,13 @@ The most important properties are:
124124
- `canRun` - A function to determine if the test is allowed to run (Eg, so you can target individual pages, segments etc.). For the sign in gate, this is likely always just returning true, since the display rules for the gate are determined in the component itself.
125125
- `variants` - An array of objects representing the groups (variants) in the test. In terms of the object properties, the main one that's required for the sign in gate is the `id` property. This is the `id` of the variant, and should be unique across all sign in gate tests. The `run` property should just be void, since we display the test elsewhere.
126126

127-
Once you've made the test definition, you'll need to import it into the `tests` array in the `src/web/experiments/ab-tests.ts` file.
127+
Once you've made the test definition, you'll need to import it into the `tests` array in the `src/experiments/ab-tests.ts` file.
128128

129129
The test definition should also be replicated in `frontend` too if the same sign in gate tests is required on both `DCR` and `frontend`. Use the existing documentation in [`frontend`](https://github.com/guardian/frontend/blob/main/static/src/javascripts/projects/common/modules/identity/sign-in-gate/README.md) to set up the tests there. Tests should be mirrored is as far as possible.
130130

131131
### Sign In Gate Design
132132

133-
The designs for the gate live as React components in the `src/web/components/SignInGate/gateDesigns` folder.
133+
The designs for the gate live as React components in the `src/components/SignInGate/gateDesigns` folder.
134134

135135
#### Gate Component
136136

@@ -144,10 +144,10 @@ The ideal way to view/develop the design is to use Storybook. See "Storybook" in
144144

145145
### Sign In Gate Component
146146

147-
In the `src/web/components/SignInGate/gates` folder, we have a file for each unique variant. Each file exports a `SignInGateComponent` type which is defined as:
147+
In the `src/components/SignInGate/gates` folder, we have a file for each unique variant. Each file exports a `SignInGateComponent` type which is defined as:
148148

149149
```ts
150-
// src/web/components/SignInGate/types.ts
150+
// src/components/SignInGate/types.ts
151151
type SignInGateComponent = {
152152
gate?: (props: SignInGateProps) => JSX.Element;
153153
canShow: (isSignedIn: boolean, currentTest: CurrentABTest) => boolean;
@@ -223,7 +223,7 @@ export const signInGateComponent: SignInGateComponent = {
223223

224224
Now that the required components have been set up, the AB tests must be matched to the SignInGateComponent that we've created.
225225

226-
All of the following section happens in the `src/web/components/SignInGate/signInGate.ts` file.
226+
All of the following section happens in the `src/components/SignInGate/signInGate.ts` file.
227227

228228
#### AB Tests
229229

@@ -398,7 +398,7 @@ To run the cypress tests interactively, make sure the development server is runn
398398
399399
Since the Cypress tests rely on a production article, it would normally get the AB switch state from there. In some cases this switch may not be on, or may not be defined yet, in turn meaning that the Cypress tests will fail.
400400
401-
To decouple the switch state from production, we define the state of the switch in DCR that will be set only when running within Cypress. In `src/web/experiments/cypress-switches.ts` update the `cypressSwitches` object to add the switch and state for your new test.
401+
To decouple the switch state from production, we define the state of the switch in DCR that will be set only when running within Cypress. In `src/experiments/cypress-switches.ts` update the `cypressSwitches` object to add the switch and state for your new test.
402402
403403
```ts
404404
...
@@ -413,6 +413,6 @@ Now the AB test will be picked up even if the switch does not exist yet in Front
413413
414414
#### Unit Tests
415415
416-
Finally for specific code which can be unit tested too, there are some tests for those too. For display rules, you'll find them in `src/web/components/SignInGate/displayRule.test.ts` and for the setting/checking if the gate has been dismissed in `src/web/components/SignInGate/dismissGate.test.ts`. Ideally unit tests should not be a replacement for the integration tests.
416+
Finally for specific code which can be unit tested too, there are some tests for those too. For display rules, you'll find them in `src/components/SignInGate/displayRule.test.ts` and for the setting/checking if the gate has been dismissed in `src/components/SignInGate/dismissGate.test.ts`. Ideally unit tests should not be a replacement for the integration tests.
417417
418-
You can run tests using `pnpm test`, if you want to run for a specific test file use `pnpm test ./path/to/file` e.g. `pnpm test ./src/web/components/SignInGate/displayRule.test.ts`
418+
You can run tests using `pnpm test`, if you want to run for a specific test file use `pnpm test ./path/to/file` e.g. `pnpm test ./src/components/SignInGate/displayRule.test.ts`

dotcom-rendering/src/components/SignInGateSelector.importable.tsx

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -297,16 +297,14 @@ const SignInGateSelectorAuxia = ({
297297
new CustomEvent('article:sign-in-gate-dismissed'),
298298
);
299299

300-
if (signInGateVersion === 'v2') {
301-
// Emit modal dismiss event
302-
document.dispatchEvent(
303-
new CustomEvent('modal:close', {
304-
detail: {
305-
modalType: `sign-in-gate-${signInGateVersion}`,
306-
},
307-
}),
308-
);
309-
}
300+
// Emit modal dismiss event
301+
document.dispatchEvent(
302+
new CustomEvent('modal:close', {
303+
detail: {
304+
modalType: `sign-in-gate-${signInGateVersion}`,
305+
},
306+
}),
307+
);
310308
}
311309
}, [isGateDismissed]);
312310

@@ -455,16 +453,14 @@ const ShowSignInGateAuxia = ({
455453
// the tracking of the number of times the gate has been displayed
456454
incrementGateDisplayCount();
457455

458-
if (signInGateVersion === 'v2') {
459-
// Emit modal view event
460-
document.dispatchEvent(
461-
new CustomEvent('modal:open', {
462-
detail: {
463-
modalType: `sign-in-gate-${signInGateVersion}`,
464-
},
465-
}),
466-
);
467-
}
456+
// Emit modal view event
457+
document.dispatchEvent(
458+
new CustomEvent('modal:open', {
459+
detail: {
460+
modalType: `sign-in-gate-${signInGateVersion}`,
461+
},
462+
}),
463+
);
468464
}
469465
}, [
470466
hasBeenSeen,

dotcom-rendering/src/components/SlotBodyEnd/BrazeEpic.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,14 @@ const BrazeEpicWithSatisfiedDependencies = ({
139139
},
140140
renderingTarget,
141141
);
142+
143+
document.dispatchEvent(
144+
new CustomEvent('epic:in-view', {
145+
detail: {
146+
epicType: 'braze',
147+
},
148+
}),
149+
);
142150
}
143151
}, [hasBeenSeen, meta, renderingTarget]);
144152

dotcom-rendering/src/components/StickyBottomBanner/BrazeBanner.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { getZIndex } from '../../lib/getZIndex';
1313
import { getOptionsHeaders } from '../../lib/identity';
1414
import type { CanShowResult } from '../../lib/messagePicker';
1515
import { useAuthStatus } from '../../lib/useAuthStatus';
16+
import { useIsInView } from '../../lib/useIsInView';
1617
import type { TagType } from '../../types/tag';
1718
import { useConfig } from '../ConfigContext';
1819

@@ -109,6 +110,10 @@ const BrazeBannerWithSatisfiedDependencies = ({
109110
}: InnerProps) => {
110111
const authStatus = useAuthStatus();
111112
const { renderingTarget } = useConfig();
113+
const [hasBeenSeen, setNode] = useIsInView({
114+
debounce: true,
115+
threshold: 0,
116+
});
112117

113118
useEffect(() => {
114119
// Log the impression with Braze
@@ -130,6 +135,16 @@ const BrazeBannerWithSatisfiedDependencies = ({
130135
// eslint-disable-next-line react-hooks/exhaustive-deps
131136
}, []);
132137

138+
useEffect(() => {
139+
if (hasBeenSeen) {
140+
document.dispatchEvent(
141+
new CustomEvent('banner:open', {
142+
detail: { bannerId: 'braze-banner' },
143+
}),
144+
);
145+
}
146+
}, [hasBeenSeen]);
147+
133148
const componentName = meta.dataFromBraze.componentName;
134149
if (!componentName) return null;
135150

@@ -152,7 +167,7 @@ const BrazeBannerWithSatisfiedDependencies = ({
152167
lazyFetchEmailWithTimeout();
153168

154169
return (
155-
<div css={containerStyles}>
170+
<div css={containerStyles} ref={setNode}>
156171
<BrazeComponent
157172
logButtonClickWithBraze={meta.logButtonClickWithBraze}
158173
submitComponentEvent={(event) =>

dotcom-rendering/src/components/marketing/banners/common/BannerWrapper.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,14 @@ const withBannerData =
112112
}
113113
}, [hasBeenSeen, submitComponentEvent, tracking]);
114114

115+
useEffect(() => {
116+
if (hasBeenSeen) {
117+
document.dispatchEvent(
118+
new CustomEvent('banner:open', { detail: { bannerId } }),
119+
);
120+
}
121+
}, [hasBeenSeen]);
122+
115123
useEffect(() => {
116124
if (submitComponentEvent) {
117125
void submitComponentEvent(
@@ -358,7 +366,7 @@ export const bannerWrapper = (
358366
Banner: ReactComponent<BannerRenderProps>,
359367
bannerId: BannerId,
360368
): ReactComponent<BannerProps> =>
361-
withCloseable(withBannerData(Banner, bannerId));
369+
withCloseable(withBannerData(Banner, bannerId), bannerId);
362370

363371
const validate = (props: unknown): props is BannerProps => {
364372
const result = bannerSchema.safeParse(props);

dotcom-rendering/src/components/marketing/banners/utils/withCloseable.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export interface CloseableBannerProps extends BannerProps {
1515

1616
const withCloseable = (
1717
CloseableBanner: ReactComponent<CloseableBannerProps>,
18+
bannerId?: string,
1819
): ReactComponent<BannerProps> => {
1920
const Banner: ReactComponent<BannerProps> = (bannerProps: BannerProps) => {
2021
const [isOpen, setIsOpen] = useState(true);
@@ -23,6 +24,9 @@ const withCloseable = (
2324
setChannelClosedTimestamp(bannerProps.bannerChannel);
2425
setIsOpen(false);
2526
document.body.focus();
27+
document.dispatchEvent(
28+
new CustomEvent('banner:close', { detail: { bannerId } }),
29+
);
2630
};
2731

2832
useEscapeShortcut(onClose);

dotcom-rendering/src/components/marketing/epics/ContributionsEpic.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,13 @@ const ContributionsEpic: ReactComponent<EpicProps> = ({
285285
if (hasBeenSeen) {
286286
// For epic view count
287287
logEpicView(tracking.abTestName);
288+
document.dispatchEvent(
289+
new CustomEvent('epic:in-view', {
290+
detail: {
291+
epicType: 'contributions',
292+
},
293+
}),
294+
);
288295

289296
// For ophan
290297
if (submitComponentEvent) {

dotcom-rendering/src/components/marketing/epics/ContributionsLiveblogEpic.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,13 @@ export const ContributionsLiveblogEpic: ReactComponent<EpicProps> = ({
158158
if (hasBeenSeen) {
159159
// For epic view count
160160
logEpicView(tracking.abTestName);
161+
document.dispatchEvent(
162+
new CustomEvent('epic:in-view', {
163+
detail: {
164+
epicType: 'liveblog-contributions',
165+
},
166+
}),
167+
);
161168

162169
// For ophan
163170
if (submitComponentEvent) {

dotcom-rendering/src/components/marketing/gutters/GutterAskWrapper.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ export const GutterAskWrapper: ReactComponent<GutterProps> = (
7171
useEffect(() => {
7272
if (hasBeenSeen) {
7373
sendOphanEvent('VIEW');
74+
document.dispatchEvent(new CustomEvent('gutter:in-view'));
7475
}
7576
}, [hasBeenSeen, sendOphanEvent]);
7677

0 commit comments

Comments
 (0)