Feast Contextual Nudge Braze Banners - #16254
Merged
andresilva-guardian merged 24 commits intoJul 27, 2026
Merged
Conversation
…with text variables
…essibility and a margin
…ssion configuration
juabara
approved these changes
Jun 26, 2026
andresilva-guardian
marked this pull request as ready for review
June 29, 2026 08:34
|
Hello 👋! When you're ready to run Chromatic, please apply the You will need to reapply the label each time you want to run Chromatic. |
juabara
approved these changes
Jul 8, 2026
Member
|
Please rebase this branch against
Please rebase this branch against |
…thub.com/guardian/dotcom-rendering into afs/feast-contextual-nudge-braze-banners
…eastContextualNudge" This reverts commit e279321.
|
Seen on PROD (merged by @andresilva-guardian 11 minutes and 6 seconds ago) Please check your changes! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this change?
We integrate Braze Banners into the
FeastContextualNudgecomponent, so that when a user is signed in and Braze has a banner ready for the corresponding placement slot, we render the Braze banner in place of the native static nudge.Concretely, we:
BrazeBannersSystemPlacementIdentries (FeastContextualNudge1–FeastContextualNudge5) so we can address up to five distinct nudge positions per article.ArticleRendererto distribute up to five placement slots evenly across all recipe sections. We calculate aninterval = ceil(sections.length / 5)and assign anudgeIndex(1–5) to every section that falls on a multiple of that interval, passingnudgeIndexandidApiUrldown to the component.FeastContextualNudgeto acceptnudgeIndexandidApiUrlas props, calluseBraze, and, if Braze returns a banner for the computed placement ID, renderBrazeBannersSystemDisplayinstead of the default nudge card.GetContextmessage type toBrazeBannersSystemand acontextprop toBrazeBannersSystemDisplay, so the Braze banner iframe can request page, recipe and user context (recipe details,pageId,nudgeIndex,darkMode, etc.) to personalise its content.min-heightfor the nudge (nudgeMinHeightStyles), sized to the native fallback card's measured rendered height at each breakpoint (< mobileMedium: 112px, mobileMedium–phablet: 92px, ≥ phablet: 72px, each with a small buffer for webfont/fallback-font metric differences). This prevents a layout shift (CLS) during the async swap between the native fallback card and a Braze banner. Braze banners can set their ownminHeightcustom property to match or exceed these values if their content is taller.--feast-nudge-headingand--feast-nudge-subtextCSS custom properties into a single--feast-nudge-textvariable, as requested by the Design team.nudgeIndexandidApiUrlprops.feast-recipe-nudge-v2AB test gating fromFeastContextualNudge- the nudge is now fully rolled out to all recipe article readers rather than only a test variant. This removes theuseAB/isUserInTestGroupcheck (including the temporary dev-only override) from the component, drops the correspondingfeast-recipe-nudge-v2test definition from the AB testing config, and removes the now-unneeded AB test mocks/Controlstory from FeastContextualNudge.stories.tsx and DecideLayout.stories.tsx -> this was authorized by Sprint priority on this chat message.Page-aware placement loading
ISLAND_PLACEMENT_MAP- a map fromgu-islandcomponent names to their Braze placement IDs.getPagePlacements()queries the server-rendered DOM at runtime and returns only the placements whose island element is present, so we never request placements for components that cannot appear on the current page.buildBrazeMessagingto callgetPagePlacements()beforerefreshBannersand skip the refresh entirely when no relevant placements are on the page.Rate-limit observability, and a stopgap for the placement-request ceiling
refreshBannersnow logs an info message listing every placement ID and count before the request, and emits a warning when the caller exceeds Braze's limit of 10 placements per refresh.BrazeBannersSystemPlacementIdvalues fits within that 10-per-call cap,refreshBannersrequests all of them on every call, rather than just the placementsgetPagePlacements()found on the current page. A refresh call consumes the same one rate-limit token regardless of how many placement IDs it contains, so this costs nothing extra, and it keeps every placement's eligibility data fresh for whichever page happens to render it - rather than only ever refreshing placements the moment their specific island is server-rendered.Stale-cache handling
stalePlacementsSetthat tracks placement IDs whose lastrequestBannersRefreshwas rejected by Braze's rate-limiter (5 tokens per session, refilling every 3 minutes). The error callback marks the affected placements as stale and logs which ones were added; the success callback clears them.isPlacementStale(id)for island components to query the stale state of a specific placement before callinggetBanner().PLACEMENT_SUPPRESS_ON_STALE- a flatPartial<Record<BrazeBannersSystemPlacementId, boolean>>that controls per-placement suppression behaviour when stale.BannerandEndOfArticleare set totrue(suppress - avoids showing outdated MRR campaigns); the fiveFeastContextualNudgeplacements are set tofalse(fall through to the native card). TheISLAND_PLACEMENT_MAPremains a pure DOM-detection structure and carries no suppression logic.canShowBrazeBannersSystemchecksstalePlacementsbefore callinggetBanner(), andFeastContextualNudgeperforms the same check before attempting to render a Braze banner.Ophan click tracking
We added two new
CLICKcomponent events, both undercomponentType: 'RETENTION_ENGAGEMENT_BANNER'(the same componentType already used by the existingVIEWevent recorded when a Braze banner is scrolled into view):NavigateToUrlpostMessage is handled - i.e. the reader taps a "navigate to URL" button inside a Braze-rendered banner - fired immediately before the actual navigation/window.open. The event'svaluefield carries the destination URL.IDs to search for in Ophan (
componentTypeisRETENTION_ENGAGEMENT_BANNERfor all three):actionidVIEWophanComponentIdcustom property if the campaign sets one, otherwise the placement ID (dotcom-rendering_feast-contextual-nudge-1…-5)CLICKfeast-contextual-nudge-1…feast-contextual-nudge-5CLICKophanComponentIdif set, otherwise the placement IDid(feast-contextual-nudge-1) intentionally doesn't match the Braze-driven events'idformat (dotcom-rendering_feast-contextual-nudge-1), since the latter is derived from the placement ID. Worth keeping in mind when building dashboards/queries that need to cover both the native and Braze paths for the same nudge slot.Why?
The MRR team needs to be able to personalise the copywriting and CTA of the Feast contextual nudge based on the reader's context - for example whether they already have the Feast app installed, whether they hold a Feast subscription, and what country they are browsing from. The native static nudge cannot account for these variables. By delegating rendering to Braze when a banner is available for the signed-in user, we enable a richer and more relevant signed-in experience.
We cap the number of Braze placement slots at five because the Braze Banners API allows up to ten placements per refresh request, and we want to leave headroom for other placements on the page. Rather than placing a nudge at every recipe section, we distribute the five slots evenly so they are spread across the full length of the article.
Page-aware loading avoids burning rate-limit tokens on placements that cannot appear on the current page, and requesting all known placements while we're still under Braze's 10-per-call cap gives every placement the best chance of having fresh data by the time a reader reaches the page that needs it - without any additional token cost. Stale-cache handling ensures that, when rate-limit tokens are exhausted, components either suppress the cached banner (for time-sensitive MRR placements) or fall through to a native fallback (for Feast nudges), rather than silently showing potentially outdated content or hiding the slot entirely. The new click-tracking gives the analytics/MRR teams visibility into engagement with both the native and Braze-driven versions of the nudge, using a consistent
componentTypeso both paths can be queried together in Ophan.The Feast contextual nudge AB test (
feast-recipe-nudge-v2) has concluded, and the nudge is now moving to a full rollout for all recipe article readers, so the AB test gating is no longer needed.Screenshots