Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
53 changes: 47 additions & 6 deletions dotcom-rendering/src/lib/braze/buildBrazeMessaging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,14 +141,55 @@ 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.
// Open the Braze session
braze.openSession();

// Trigger the Braze Banners System refresh (fetch banner content for all placement IDs).
await refreshBanners(braze);

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.
*
* 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') {
if (Date.now() - lastRefreshAt < 5000) return;
lastRefreshAt = Date.now();
// void refreshBanners(braze);
}
});

const brazeCards = window.guardian.config.switches.brazeContentCards
? new BrazeCards(braze, errorHandler)
Expand Down
35 changes: 35 additions & 0 deletions dotcom-rendering/src/lib/braze/initialiseBraze.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,41 @@ 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,
Expand Down
Loading