diff --git a/dotcom-rendering/src/lib/braze/BrazeBannersSystem.tsx b/dotcom-rendering/src/lib/braze/BrazeBannersSystem.tsx index 4c910aa2022..18020832cd2 100644 --- a/dotcom-rendering/src/lib/braze/BrazeBannersSystem.tsx +++ b/dotcom-rendering/src/lib/braze/BrazeBannersSystem.tsx @@ -7,12 +7,15 @@ 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'; @@ -150,12 +153,14 @@ export function refreshBanners(braze: BrazeInstance): Promise { * Meta information required to display a Braze Banner. */ export type BrazeBannersSystemMeta = { + id: string; braze: BrazeInstance; banner: Banner; }; /** * 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 @@ -164,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, @@ -228,6 +234,7 @@ export const canShowBrazeBannersSystem = async ( return { show: true, meta: { + id, braze, banner, }, @@ -266,6 +273,7 @@ export const buildBrazeBannersSystemConfig = ( id, canShow: () => { return canShowBrazeBannersSystem( + id, braze, placementId, contentType, @@ -455,6 +463,11 @@ export const BrazeBannersSystemDisplay = ({ useState('#ffffff'); const [wrapperModeForegroundColor, setWrapperModeForegroundColor] = useState('#000000'); + const { renderingTarget } = useConfig(); + const [hasBeenSeen, setNode] = useIsInView({ + debounce: true, + threshold: 0, + }); /** * Subscribes the user to a newsletter via the Identity API. @@ -657,7 +670,19 @@ 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. + } } }, [showBanner, meta, meta.banner, meta.braze, setWrapperModeColors]); @@ -729,6 +754,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 +768,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 +802,16 @@ export const BrazeBannersSystemDisplay = ({ success, }, ); + meta.braze.logCustomEvent( + 'braze_banner_reminder_subscribe', + { + placementId: meta.banner.placementId, + reminderPeriod, + reminderComponent, + reminderOption, + success, + }, + ); }); } break; @@ -771,8 +822,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 +860,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; } }; @@ -818,8 +884,38 @@ 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) { + brazeBannersSystemLogger.info( + `Banner with placement ID "${meta.banner.placementId}" has been seen. Logging impression.`, + ); + + // 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, 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. @@ -831,6 +927,7 @@ export const BrazeBannersSystemDisplay = ({ return (
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_