From 6ca63130548ae859de09eecd5c0495cd8d452e39 Mon Sep 17 00:00:00 2001 From: andresilva-guardian Date: Wed, 20 May 2026 12:09:39 +0100 Subject: [PATCH 1/5] Fix Braze session handling and banner eligibility refresh logic --- .../src/lib/braze/buildBrazeMessaging.ts | 34 +++++++++++++++++ .../src/lib/braze/initialiseBraze.ts | 37 ++++++++++++++++++- 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/dotcom-rendering/src/lib/braze/buildBrazeMessaging.ts b/dotcom-rendering/src/lib/braze/buildBrazeMessaging.ts index d5341e41154..d5a83631f25 100644 --- a/dotcom-rendering/src/lib/braze/buildBrazeMessaging.ts +++ b/dotcom-rendering/src/lib/braze/buildBrazeMessaging.ts @@ -150,6 +150,40 @@ export const buildBrazeMessaging = async ( braze.openSession(); + /** + * Re-request banner eligibility whenever the user returns to this tab. + * + * After the initial requestBannersRefresh above, the Braze SDK manages + * subsequent session starts silently: once the user has been inactive for + * sessionTimeoutInSeconds (1800s / 30 minutes), the SDK internally starts a + * new session and fires a "Start Session" event to the Braze backend. This + * can trigger Canvas re-entry and make a banner available again, but the SDK + * does not expose a session lifecycle hook (there is no subscribeToSessionUpdates + * in SDK v6.5.0). This means even if the Canvas has re-entered the user and a + * new banner is ready, the SDK keeps serving its locally cached null because it + * was never instructed to fetch new data. + * + * We use the document visibilitychange event as a proxy for "user has returned + * after a potentially session-creating absence". This is the same signal the + * SDK itself uses internally to detect inactivity and start new sessions, so it + * is the correct moment to ask Braze for updated eligibility. + * + * The Braze SDK's built-in token bucket rate limiting (5 tokens per session, + * 1 refill every 3 minutes) ensures that rapid or accidental tab switches + * do not trigger unnecessary network requests. If no tokens are available, the + * SDK fires the error callback in refreshBanners, which resolves gracefully + * without blocking the page or throwing. + * + * This fix was validated with Braze support, who confirmed that + * requestBannersRefresh must be called at the start of each new session for + * Canvas re-eligibility to work as expected. + */ + document.addEventListener('visibilitychange', () => { + if (document.visibilityState === 'visible') { + void refreshBanners(braze); + } + }); + const brazeCards = window.guardian.config.switches.brazeContentCards ? new BrazeCards(braze, errorHandler) : new NullBrazeCards(); diff --git a/dotcom-rendering/src/lib/braze/initialiseBraze.ts b/dotcom-rendering/src/lib/braze/initialiseBraze.ts index ab56e38d366..168c6898d0c 100644 --- a/dotcom-rendering/src/lib/braze/initialiseBraze.ts +++ b/dotcom-rendering/src/lib/braze/initialiseBraze.ts @@ -4,11 +4,46 @@ import { isUndefined, log } from '@guardian/libs'; // Define the type alias export type BrazeInstance = typeof braze; +/** + * Braze SDK initialisation options. + * + * IMPORTANT: sessionTimeoutInSeconds is set to 1800 (30 minutes, the SDK default). + * + * We previously had this set to 1 second, which caused two critical problems with + * the Braze Banners System when used with a Braze Canvas: + * + * 1. Canvas entry chaos: + * Braze Canvas campaigns that use "Start Session" as their entry trigger rely on + * a meaningful definition of a session. With a 1-second timeout, the SDK was + * firing hundreds of "Start Session" events per user per day (one per second of + * inactivity). A Canvas configured with "re-entry after 10 seconds" was never + * designed to cycle every 10-11 seconds, so Canvas step advancement became + * chaotic and banner availability was unpredictable. + * Any Canvas using session-based delay steps (e.g., "wait 2 sessions") would + * also behave incorrectly, since "2 sessions" was effectively "2 seconds". + * + * 2. Silent requestBannersRefresh drops: + * The Braze SDK enforces a "once per session" constraint on requestBannersRefresh. + * With a 1-second timeout, a fast page reload (under 1 second since the previous + * page unload) was seen by the SDK as still within the previous session, causing + * the requestBannersRefresh call at init to be silently dropped. getBanner() would + * then return the stale cached null from the previous session, and the banner would + * not appear. This produced the intermittent "sometimes I see it, sometimes I do not" + * behaviour observed in production. + * + * Setting this to 1800 aligns with the SDK default and ensures that "Start Session" + * events map to genuine user visits rather than sub-second inactivity timeouts. + * + * NOTE ON ANALYTICS IMPACT: Changing from 1 to 1800 will dramatically reduce the + * session count reported in Braze. The previous count was heavily inflated (every + * 1 second of inactivity = new session). Any dashboards or audience segments built + * on session counts will need to be reviewed by CRM after this change. + */ const SDK_OPTIONS: braze.InitializationOptions = { enableLogging: true, noCookies: true, baseUrl: 'https://sdk.fra-01.braze.eu/api/v3', - sessionTimeoutInSeconds: 1, + sessionTimeoutInSeconds: 1800, // 30 minutes, the Braze SDK default. See JSDoc above for rationale. minimumIntervalBetweenTriggerActionsInSeconds: 0, devicePropertyAllowlist: [], allowUserSuppliedJavascript: true, // Supplied javascript is required for Braze Banners System integration From b2ed781fa90135b6d31021116481dfb85c0ed724 Mon Sep 17 00:00:00 2001 From: andresilva-guardian Date: Fri, 22 May 2026 09:43:22 +0100 Subject: [PATCH 2/5] Temp disabled the refresh banners on visibilitychange for testing purposes --- .../src/lib/braze/buildBrazeMessaging.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/dotcom-rendering/src/lib/braze/buildBrazeMessaging.ts b/dotcom-rendering/src/lib/braze/buildBrazeMessaging.ts index d5a83631f25..58fbd33ac20 100644 --- a/dotcom-rendering/src/lib/braze/buildBrazeMessaging.ts +++ b/dotcom-rendering/src/lib/braze/buildBrazeMessaging.ts @@ -174,13 +174,23 @@ export const buildBrazeMessaging = async ( * SDK fires the error callback in refreshBanners, which resolves gracefully * without blocking the page or throwing. * + * We also keep a local timestamp of the last refresh. The initial page-load + * requestBannersRefresh above counts as the first refresh, so lastRefreshAt + * starts at Date.now(). Inside the listener we skip the call if fewer than + * 5 seconds have elapsed — this prevents burning two tokens on a trivial + * background tab open (e.g. the user Command+clicks a link and instantly + * switches back), while still refreshing on any meaningful return to the tab. + * * This fix was validated with Braze support, who confirmed that * requestBannersRefresh must be called at the start of each new session for * Canvas re-eligibility to work as expected. */ + let lastRefreshAt = Date.now(); document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') { - void refreshBanners(braze); + if (Date.now() - lastRefreshAt < 5000) return; + lastRefreshAt = Date.now(); + // void refreshBanners(braze); } }); From be10516773da0d25a444d9c9d46547c8f8519047 Mon Sep 17 00:00:00 2001 From: andresilva-guardian Date: Fri, 22 May 2026 12:07:38 +0100 Subject: [PATCH 3/5] Temp disabled braze open session --- dotcom-rendering/src/lib/braze/buildBrazeMessaging.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/dotcom-rendering/src/lib/braze/buildBrazeMessaging.ts b/dotcom-rendering/src/lib/braze/buildBrazeMessaging.ts index 58fbd33ac20..f32c57077d1 100644 --- a/dotcom-rendering/src/lib/braze/buildBrazeMessaging.ts +++ b/dotcom-rendering/src/lib/braze/buildBrazeMessaging.ts @@ -141,14 +141,10 @@ export const buildBrazeMessaging = async ( ); } - // Trigger the Braze Banners System refresh (Ask Braze to fetch data) - // (Requests banners by a list of placement IDs from the Braze backend.) - // (Note that this method can only be called once per session.) - // Since we want to suppress In-App Messages if a banner exists, we must - // call requestBannersRefresh before openSession. - await refreshBanners(braze); + // braze.openSession(); - braze.openSession(); + // Trigger the Braze Banners System refresh (fetch banner content for all placement IDs). + await refreshBanners(braze); /** * Re-request banner eligibility whenever the user returns to this tab. From aec9e75b7b21adfabe7acf6144e6cbbaadd971fe Mon Sep 17 00:00:00 2001 From: andresilva-guardian Date: Fri, 22 May 2026 14:40:21 +0100 Subject: [PATCH 4/5] Enable Braze session opening in buildBrazeMessaging function --- dotcom-rendering/src/lib/braze/buildBrazeMessaging.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dotcom-rendering/src/lib/braze/buildBrazeMessaging.ts b/dotcom-rendering/src/lib/braze/buildBrazeMessaging.ts index f32c57077d1..ee31258beca 100644 --- a/dotcom-rendering/src/lib/braze/buildBrazeMessaging.ts +++ b/dotcom-rendering/src/lib/braze/buildBrazeMessaging.ts @@ -141,7 +141,8 @@ export const buildBrazeMessaging = async ( ); } - // braze.openSession(); + // Open the Braze session + braze.openSession(); // Trigger the Braze Banners System refresh (fetch banner content for all placement IDs). await refreshBanners(braze); From 8815dc9e1cbd7524e8891945307e8bdc1e2acae9 Mon Sep 17 00:00:00 2001 From: andresilva-guardian Date: Fri, 22 May 2026 16:17:35 +0100 Subject: [PATCH 5/5] Update session timeout for Braze SDK to 1 second for testing purposes --- dotcom-rendering/src/lib/braze/initialiseBraze.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotcom-rendering/src/lib/braze/initialiseBraze.ts b/dotcom-rendering/src/lib/braze/initialiseBraze.ts index 168c6898d0c..8b348547a85 100644 --- a/dotcom-rendering/src/lib/braze/initialiseBraze.ts +++ b/dotcom-rendering/src/lib/braze/initialiseBraze.ts @@ -43,7 +43,7 @@ const SDK_OPTIONS: braze.InitializationOptions = { enableLogging: true, noCookies: true, baseUrl: 'https://sdk.fra-01.braze.eu/api/v3', - sessionTimeoutInSeconds: 1800, // 30 minutes, the Braze SDK default. See JSDoc above for rationale. + sessionTimeoutInSeconds: 1, minimumIntervalBetweenTriggerActionsInSeconds: 0, devicePropertyAllowlist: [], allowUserSuppliedJavascript: true, // Supplied javascript is required for Braze Banners System integration