From 65a6307f84988cc2bc657f12a7b299e8a010f3d0 Mon Sep 17 00:00:00 2001 From: andresilva-guardian Date: Tue, 3 Mar 2026 10:33:50 +0000 Subject: [PATCH 01/10] Fixed Braze Banner overflow issus on portrait tablet + Changed overscroll behavior --- dotcom-rendering/src/lib/braze/BrazeBannersSystem.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dotcom-rendering/src/lib/braze/BrazeBannersSystem.tsx b/dotcom-rendering/src/lib/braze/BrazeBannersSystem.tsx index 4c910aa2022..6b78062a0cd 100644 --- a/dotcom-rendering/src/lib/braze/BrazeBannersSystem.tsx +++ b/dotcom-rendering/src/lib/braze/BrazeBannersSystem.tsx @@ -841,6 +841,8 @@ export const BrazeBannersSystemDisplay = ({ max-height: 65svh; border-top: 1px solid rgb(0, 0, 0); background-color: ${wrapperModeBackgroundColor}; + overflow: auto; + overscroll-behavior: none; ${until.phablet} { border: none; } From e2415c981eb8beaeec59c1e9b9c3f2dee516d607 Mon Sep 17 00:00:00 2001 From: andresilva-guardian Date: Wed, 4 Mar 2026 10:50:54 +0000 Subject: [PATCH 02/10] Removed no border styling for phablet in Braze Banners System display --- dotcom-rendering/src/lib/braze/BrazeBannersSystem.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/dotcom-rendering/src/lib/braze/BrazeBannersSystem.tsx b/dotcom-rendering/src/lib/braze/BrazeBannersSystem.tsx index 6b78062a0cd..63937181393 100644 --- a/dotcom-rendering/src/lib/braze/BrazeBannersSystem.tsx +++ b/dotcom-rendering/src/lib/braze/BrazeBannersSystem.tsx @@ -843,9 +843,6 @@ export const BrazeBannersSystemDisplay = ({ background-color: ${wrapperModeBackgroundColor}; overflow: auto; overscroll-behavior: none; - ${until.phablet} { - border: none; - } ` : undefined } From ff819f64b6ae1f37639fc392aebbb90d07e1927d Mon Sep 17 00:00:00 2001 From: andresilva-guardian Date: Wed, 4 Mar 2026 12:36:17 +0000 Subject: [PATCH 03/10] Update Braze Banners System display to use full width for copy-container and close-button --- dotcom-rendering/src/lib/braze/BrazeBannersSystem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotcom-rendering/src/lib/braze/BrazeBannersSystem.tsx b/dotcom-rendering/src/lib/braze/BrazeBannersSystem.tsx index 63937181393..87599cea2d6 100644 --- a/dotcom-rendering/src/lib/braze/BrazeBannersSystem.tsx +++ b/dotcom-rendering/src/lib/braze/BrazeBannersSystem.tsx @@ -889,7 +889,7 @@ export const BrazeBannersSystemDisplay = ({ max-width: 660px; grid-template: '. .' - 'copy-container close-button' / auto 0px; + 'copy-container close-button' / 100% 0px; } ` : undefined From a70af4767dcb98d628d233751c9cd908663ff1fa Mon Sep 17 00:00:00 2001 From: andresilva-guardian Date: Thu, 5 Mar 2026 11:45:58 +0000 Subject: [PATCH 04/10] Enhance Braze Banners System logging for CSS validation and user interactions --- .../src/lib/braze/BrazeBannersSystem.tsx | 63 ++++++++++++++++++- 1 file changed, 60 insertions(+), 3 deletions(-) diff --git a/dotcom-rendering/src/lib/braze/BrazeBannersSystem.tsx b/dotcom-rendering/src/lib/braze/BrazeBannersSystem.tsx index 87599cea2d6..a8ec59dc800 100644 --- a/dotcom-rendering/src/lib/braze/BrazeBannersSystem.tsx +++ b/dotcom-rendering/src/lib/braze/BrazeBannersSystem.tsx @@ -657,7 +657,22 @@ export const BrazeBannersSystemDisplay = ({ meta.braze.insertBanner(meta.banner, containerRef.current); // CSS Checker - runCssCheckerOnBrazeBanner(meta); + const cssValidationResult = runCssCheckerOnBrazeBanner(meta); + if (!cssValidationResult) { + brazeBannersSystemLogger.warn( + 'CSS validation failed for the Braze Banner. This may indicate broken styles. Check UI/UX ASAP!', + ); + meta.braze.logCustomEvent( + 'braze_banner_css_validation_failed', + { + placementId: meta.banner.placementId, + }, + ); + // We don't want to block the display of the banner if the CSS is broken, but we log it for awareness and action. + } + + // Log the impression with Braze + meta.braze.logBannerImpressions([meta.banner.placementId]); } }, [showBanner, meta, meta.banner, meta.braze, setWrapperModeColors]); @@ -729,6 +744,10 @@ export const BrazeBannersSystemDisplay = ({ }); break; case BrazeBannersSystemMessageType.NewsletterSubscribe: + meta.braze.logBannerClick( + meta.banner, + 'newsletter_subscribe_button', + ); const { newsletterId } = event.data; if (newsletterId) { void subscribeToNewsletter(newsletterId).then( @@ -739,11 +758,23 @@ export const BrazeBannersSystemDisplay = ({ success, }, ); + meta.braze.logCustomEvent( + 'braze_banner_newsletter_subscribe', + { + placementId: meta.banner.placementId, + newsletterId, + success, + }, + ); }, ); } break; case BrazeBannersSystemMessageType.ReminderSubscribe: + meta.braze.logBannerClick( + meta.banner, + 'reminder_subscribe_button', + ); const { reminderPeriod, reminderComponent, @@ -761,6 +792,16 @@ export const BrazeBannersSystemDisplay = ({ success, }, ); + meta.braze.logCustomEvent( + 'braze_banner_reminder_subscribe', + { + placementId: meta.banner.placementId, + reminderPeriod, + reminderComponent, + reminderOption, + success, + }, + ); }); } break; @@ -771,8 +812,20 @@ export const BrazeBannersSystemDisplay = ({ ); break; case BrazeBannersSystemMessageType.NavigateToUrl: + meta.braze.logBannerClick( + meta.banner, + 'navigate_to_url_button', + ); const { url, target } = event.data; if (url) { + meta.braze.logCustomEvent( + 'braze_banner_navigate_to_url', + { + placementId: meta.banner.placementId, + url, + target, + }, + ); if (target === 'blank') { window.open(url, '_blank'); } else { @@ -797,8 +850,11 @@ export const BrazeBannersSystemDisplay = ({ } break; case BrazeBannersSystemMessageType.DismissBanner: - // Remove the banner from the DOM + meta.braze.logBannerClick(meta.banner, 'dismiss_button'); dismissBanner(); + meta.braze.logCustomEvent('braze_banner_dismissed', { + placementId: meta.banner.placementId, + }); break; } }; @@ -841,7 +897,8 @@ export const BrazeBannersSystemDisplay = ({ max-height: 65svh; border-top: 1px solid rgb(0, 0, 0); background-color: ${wrapperModeBackgroundColor}; - overflow: auto; + overflow-y: auto; + overflow-x: hidden; overscroll-behavior: none; ` : undefined From 39b249e66c3f7bcc3bbb869ad4fca8b245900c60 Mon Sep 17 00:00:00 2001 From: andresilva-guardian Date: Thu, 5 Mar 2026 11:50:08 +0000 Subject: [PATCH 05/10] Log Braze banner impressions conditionally based on placement ID --- dotcom-rendering/src/lib/braze/BrazeBannersSystem.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/dotcom-rendering/src/lib/braze/BrazeBannersSystem.tsx b/dotcom-rendering/src/lib/braze/BrazeBannersSystem.tsx index a8ec59dc800..07b24d27804 100644 --- a/dotcom-rendering/src/lib/braze/BrazeBannersSystem.tsx +++ b/dotcom-rendering/src/lib/braze/BrazeBannersSystem.tsx @@ -672,7 +672,12 @@ export const BrazeBannersSystemDisplay = ({ } // Log the impression with Braze - meta.braze.logBannerImpressions([meta.banner.placementId]); + if ( + meta.banner.placementId === BrazeBannersSystemPlacementId.Banner + ) { + // We know for sure that the banner was displayed, so we can log the impression immediately. + meta.braze.logBannerImpressions([meta.banner.placementId]); + } } }, [showBanner, meta, meta.banner, meta.braze, setWrapperModeColors]); From d29de973855a832abde29872c54cda69fe13c070 Mon Sep 17 00:00:00 2001 From: andresilva-guardian Date: Thu, 5 Mar 2026 12:04:22 +0000 Subject: [PATCH 06/10] Add visibility tracking for Braze banners and log impressions on view --- .../src/lib/braze/BrazeBannersSystem.tsx | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/dotcom-rendering/src/lib/braze/BrazeBannersSystem.tsx b/dotcom-rendering/src/lib/braze/BrazeBannersSystem.tsx index 07b24d27804..5f245fe2314 100644 --- a/dotcom-rendering/src/lib/braze/BrazeBannersSystem.tsx +++ b/dotcom-rendering/src/lib/braze/BrazeBannersSystem.tsx @@ -15,6 +15,7 @@ import type { CandidateConfig, CanShowResult } from '../messagePicker'; import { useAuthStatus } from '../useAuthStatus'; import type { BrazeInstance } from './initialiseBraze'; import { suppressForTaylorReport } from './taylorReport'; +import { useIsInView } from '../useIsInView'; /** * Determines the best mix color (black or white) @@ -150,6 +151,7 @@ export function refreshBanners(braze: BrazeInstance): Promise { * Meta information required to display a Braze Banner. */ export type BrazeBannersSystemMeta = { + id: string; braze: BrazeInstance; banner: Banner; }; @@ -455,6 +457,10 @@ export const BrazeBannersSystemDisplay = ({ useState('#ffffff'); const [wrapperModeForegroundColor, setWrapperModeForegroundColor] = useState('#000000'); + const [hasBeenSeen, setNode] = useIsInView({ + debounce: true, + threshold: 0, + }); /** * Subscribes the user to a newsletter via the Identity API. @@ -670,14 +676,6 @@ export const BrazeBannersSystemDisplay = ({ ); // We don't want to block the display of the banner if the CSS is broken, but we log it for awareness and action. } - - // Log the impression with Braze - if ( - meta.banner.placementId === BrazeBannersSystemPlacementId.Banner - ) { - // We know for sure that the banner was displayed, so we can log the impression immediately. - meta.braze.logBannerImpressions([meta.banner.placementId]); - } } }, [showBanner, meta, meta.banner, meta.braze, setWrapperModeColors]); @@ -879,8 +877,17 @@ export const BrazeBannersSystemDisplay = ({ postMessageToBrazeBanner, ]); - // Log Impressions with Braze and Button Clicks with Ophan - // TODO + // Log Impressions when the banner is seen, using the hasBeenSeen value from the useIsInView hook + useEffect(() => { + if (hasBeenSeen) { + document.dispatchEvent( + new CustomEvent('banner:open', { + detail: { bannerId: meta.id }, + }), + ); + meta.braze.logBannerImpressions([meta.banner.placementId]); + } + }, [hasBeenSeen]); /** * If showBanner is false, we return null to unmount the component and remove the banner from the DOM. @@ -892,6 +899,7 @@ export const BrazeBannersSystemDisplay = ({ return (
Date: Thu, 5 Mar 2026 13:19:05 +0000 Subject: [PATCH 07/10] Log Braze banner impressions and dispatch custom event on view --- .../src/lib/braze/BrazeBannersSystem.tsx | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/dotcom-rendering/src/lib/braze/BrazeBannersSystem.tsx b/dotcom-rendering/src/lib/braze/BrazeBannersSystem.tsx index 5f245fe2314..ed1c58a9b3d 100644 --- a/dotcom-rendering/src/lib/braze/BrazeBannersSystem.tsx +++ b/dotcom-rendering/src/lib/braze/BrazeBannersSystem.tsx @@ -16,6 +16,8 @@ import { useAuthStatus } from '../useAuthStatus'; import type { BrazeInstance } from './initialiseBraze'; import { suppressForTaylorReport } from './taylorReport'; import { useIsInView } from '../useIsInView'; +import { submitComponentEvent } from 'src/client/ophan/ophan'; +import { useConfig } from 'src/components/ConfigContext'; /** * Determines the best mix color (black or white) @@ -457,6 +459,7 @@ export const BrazeBannersSystemDisplay = ({ useState('#ffffff'); const [wrapperModeForegroundColor, setWrapperModeForegroundColor] = useState('#000000'); + const { renderingTarget } = useConfig(); const [hasBeenSeen, setNode] = useIsInView({ debounce: true, threshold: 0, @@ -880,12 +883,29 @@ export const BrazeBannersSystemDisplay = ({ // Log Impressions when the banner is seen, using the hasBeenSeen value from the useIsInView hook useEffect(() => { if (hasBeenSeen) { + // Dispatch a custom event document.dispatchEvent( new CustomEvent('banner:open', { detail: { bannerId: meta.id }, }), ); + + // Log the impression with Braze meta.braze.logBannerImpressions([meta.banner.placementId]); + + // Log VIEW event with Ophan + void submitComponentEvent( + { + component: { + componentType: 'RETENTION_ENGAGEMENT_BANNER', + id: + meta.banner.getStringProperty('ophanComponentId') ?? + meta.banner.placementId, + }, + action: 'VIEW', + }, + renderingTarget, + ); } }, [hasBeenSeen]); From ea994619f2f9b29ae6f689c7b4889f435ead2f4f Mon Sep 17 00:00:00 2001 From: andresilva-guardian Date: Thu, 5 Mar 2026 13:26:19 +0000 Subject: [PATCH 08/10] Log Braze banner impressions with detailed info on visibility --- .../src/lib/braze/BrazeBannersSystem.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/dotcom-rendering/src/lib/braze/BrazeBannersSystem.tsx b/dotcom-rendering/src/lib/braze/BrazeBannersSystem.tsx index ed1c58a9b3d..4c886853c39 100644 --- a/dotcom-rendering/src/lib/braze/BrazeBannersSystem.tsx +++ b/dotcom-rendering/src/lib/braze/BrazeBannersSystem.tsx @@ -7,17 +7,17 @@ import type { ReminderComponent, } from '@guardian/support-dotcom-components/dist/shared/types/reminders'; import { useCallback, useEffect, useRef, useState } from 'react'; +import { submitComponentEvent } from '../../client/ophan/ophan'; +import { useConfig } from '../../components/ConfigContext'; import { isProd } from '../../components/marketing/lib/stage'; import type { StageType } from '../../types/config'; import type { TagType } from '../../types/tag'; import { getAuthState, getOptionsHeaders } from '../identity'; import type { CandidateConfig, CanShowResult } from '../messagePicker'; import { useAuthStatus } from '../useAuthStatus'; +import { useIsInView } from '../useIsInView'; import type { BrazeInstance } from './initialiseBraze'; import { suppressForTaylorReport } from './taylorReport'; -import { useIsInView } from '../useIsInView'; -import { submitComponentEvent } from 'src/client/ophan/ophan'; -import { useConfig } from 'src/components/ConfigContext'; /** * Determines the best mix color (black or white) @@ -883,6 +883,10 @@ export const BrazeBannersSystemDisplay = ({ // Log Impressions when the banner is seen, using the hasBeenSeen value from the useIsInView hook useEffect(() => { if (hasBeenSeen) { + brazeBannersSystemLogger.info( + `Banner with placement ID "${meta.banner.placementId}" has been seen. Logging impression.`, + ); + // Dispatch a custom event document.dispatchEvent( new CustomEvent('banner:open', { @@ -907,7 +911,7 @@ export const BrazeBannersSystemDisplay = ({ renderingTarget, ); } - }, [hasBeenSeen]); + }, [hasBeenSeen, meta.banner, meta.id, meta.braze, renderingTarget]); /** * If showBanner is false, we return null to unmount the component and remove the banner from the DOM. From 331947df1e56533829eca55d0bf13a2ae9df112b Mon Sep 17 00:00:00 2001 From: andresilva-guardian Date: Thu, 5 Mar 2026 13:30:16 +0000 Subject: [PATCH 09/10] Add logging parameter to canShowBrazeBannersSystem for improved debugging --- dotcom-rendering/src/lib/braze/BrazeBannersSystem.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dotcom-rendering/src/lib/braze/BrazeBannersSystem.tsx b/dotcom-rendering/src/lib/braze/BrazeBannersSystem.tsx index 4c886853c39..18020832cd2 100644 --- a/dotcom-rendering/src/lib/braze/BrazeBannersSystem.tsx +++ b/dotcom-rendering/src/lib/braze/BrazeBannersSystem.tsx @@ -160,6 +160,7 @@ export type BrazeBannersSystemMeta = { /** * Checks if a Braze Banner for the given placement ID can be shown. + * @param id Unique ID for the message candidate, used for logging and debugging * @param braze Braze instance * @param placementId Placement ID to check for a banner * @param contentType Content type of the article @@ -168,6 +169,7 @@ export type BrazeBannersSystemMeta = { * @returns CanShowResult with the Banner meta if it can be shown */ export const canShowBrazeBannersSystem = async ( + id: string, braze: BrazeInstance | null, placementId: BrazeBannersSystemPlacementId, contentType: string, @@ -232,6 +234,7 @@ export const canShowBrazeBannersSystem = async ( return { show: true, meta: { + id, braze, banner, }, @@ -270,6 +273,7 @@ export const buildBrazeBannersSystemConfig = ( id, canShow: () => { return canShowBrazeBannersSystem( + id, braze, placementId, contentType, From df23b65656d069adda487c393ee69b4cd7cdb16f Mon Sep 17 00:00:00 2001 From: andresilva-guardian Date: Thu, 5 Mar 2026 13:42:36 +0000 Subject: [PATCH 10/10] Enhance Braze Banners README with detailed logging and impression tracking updates --- dotcom-rendering/src/lib/braze/README.md | 54 ++++++++++++++++++++---- 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/dotcom-rendering/src/lib/braze/README.md b/dotcom-rendering/src/lib/braze/README.md index 03e5f2f0e13..cea43a1336d 100644 --- a/dotcom-rendering/src/lib/braze/README.md +++ b/dotcom-rendering/src/lib/braze/README.md @@ -77,8 +77,10 @@ Because marketing teams have styling flexibility, there is a risk of campaigns t - Parses the banner's HTML/CSS. - Validates that every CSS selector matches at least one element. -- Logs warnings (in development) if "dead" selectors are found. - This ensures broken creatives are caught during the QA process before launch. +- Logs warnings if "dead" selectors are found. +- On failure: emits a `brazeBannersSystemLogger.warn` and logs a Braze custom event `braze_banner_css_validation_failed` (with `placementId`) for Braze-side alerting. The banner is **not** blocked from rendering — it is shown regardless, but the failure is recorded for awareness and action. + +This ensures broken creatives are caught during the QA process before launch. #### B. JavaScript Isolation @@ -103,7 +105,9 @@ To support more complex designs while maintaining consistency, the system suppor - **Enabled via**: `wrapperModeEnabled` (Boolean) Key-Value pair. - **Behavior**: When enabled, DCR applies specific styles to the **container** holding the Braze iframe, including: - `max-height: 65svh` (prevents banners from taking over the full screen). - - `border-top: 1px solid black` (provides visual separation). + - `border-top: 1px solid black` (provides visual separation at all breakpoints — the previous phablet-level `border: none` override has been removed for consistency). + - `overflow-y: auto` and `overflow-x: hidden` (allow vertical scrolling within the banner container on small/portrait screens). + - `overscroll-behavior: none` (prevents scroll from propagating to the host page once the banner reaches its scroll boundary). - Dynamic Background Color (see below). #### Automatic Color Contrast @@ -118,14 +122,46 @@ When providing a background color in Wrapper Mode, DCR automatically calculates The system automatically reads specific Key-Value pairs from the Braze Campaign to configure the banner wrapper. -| Key | Type | Description | -| :--------------------------- | :------ | :----------------------------------------------------------------------------------------- | -| `minHeight` | String | Sets the CSS `min-height` of the container (e.g., "300px") to minimize layout shift (CLS). | -| `wrapperModeEnabled` | Boolean | Activates Wrapper Mode (see above). | -| `wrapperModeBackgroundColor` | String | Sets the background color of the wrapper and triggers the auto-contrast calculation. | +| Key | Type | Description | +| :--------------------------- | :------ | :-------------------------------------------------------------------------------------------------------------------- | +| `minHeight` | String | Sets the CSS `min-height` of the container (e.g., "300px") to minimize layout shift (CLS). | +| `wrapperModeEnabled` | Boolean | Activates Wrapper Mode (see above). | +| `wrapperModeBackgroundColor` | String | Sets the background color of the wrapper and triggers the auto-contrast calculation. | +| `ophanComponentId` | String | Overrides the Ophan component ID used when logging the `VIEW` impression event. Defaults to `placementId` if not set. | _Custom keys can also be retrieved by the banner creative using the `BRAZE_BANNERS_SYSTEM:GET_SETTINGS_PROPERTY_VALUE` message._ +### 8. Impression Tracking & Analytics + +The system tracks when the banner enters the viewport and logs impressions to multiple destinations. + +#### Visibility Detection + +`BrazeBannersSystemDisplay` uses the `useIsInView` hook (backed by `IntersectionObserver`) to detect when the banner first becomes visible. Impression logging fires **once** per mount when `hasBeenSeen` becomes `true`. + +#### On First View + +When the banner enters the viewport for the first time, the following actions are performed in sequence: + +1. **Logger**: `brazeBannersSystemLogger.info` records the event with the banner's placement ID. +2. **DOM Event**: A `banner:open` custom event is dispatched on `document`, carrying `{ bannerId: meta.id }`. This allows other parts of the page to react to the banner becoming visible. +3. **Braze Impression**: `meta.braze.logBannerImpressions([placementId])` notifies Braze that the banner was shown (used for frequency capping and campaign reporting). +4. **Ophan VIEW**: A `VIEW` component event is submitted via `submitComponentEvent` with `componentType: 'RETENTION_ENGAGEMENT_BANNER'`. The component ID is read from the `ophanComponentId` Key-Value pair, falling back to `placementId` if not set. + +### 9. User Interaction Logging + +All user interactions with the banner are tracked with both a Braze banner click (`logBannerClick`) and a Braze custom event (`logCustomEvent`) containing contextual metadata. + +| Interaction | Braze Click Label | Braze Custom Event | Custom Event Properties | +| :------------------- | :---------------------------- | :----------------------------------- | :-------------------------------------------------------------------------------- | +| Newsletter Subscribe | `newsletter_subscribe_button` | `braze_banner_newsletter_subscribe` | `placementId`, `newsletterId`, `success` | +| Reminder Subscribe | `reminder_subscribe_button` | `braze_banner_reminder_subscribe` | `placementId`, `reminderPeriod`, `reminderComponent`, `reminderOption`, `success` | +| Navigate to URL | `navigate_to_url_button` | `braze_banner_navigate_to_url` | `placementId`, `url`, `target` | +| Dismiss Banner | `dismiss_button` | `braze_banner_dismissed` | `placementId` | +| CSS Validation Fail | — | `braze_banner_css_validation_failed` | `placementId` | + +> The Braze click (`logBannerClick`) feeds native Braze campaign reports, while the custom event (`logCustomEvent`) provides granular analytics available in Braze Data Export and BigQuery. + ## Communication Protocol The banner uses a `postMessage` protocol to interact with the host DCR page. @@ -663,4 +699,4 @@ const brazeCandidate = buildBrazeBannersSystemConfig( --- **Value Team** -_Last Updated: February 2026_ +_Last Updated: March 2026_