Skip to content

Feast Contextual Nudge Braze Banners - #16254

Merged
andresilva-guardian merged 24 commits into
mainfrom
afs/feast-contextual-nudge-braze-banners
Jul 27, 2026
Merged

andresilva-guardian merged 24 commits into
mainfrom
afs/feast-contextual-nudge-braze-banners

Conversation

@andresilva-guardian

@andresilva-guardian andresilva-guardian commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

What does this change?

We integrate Braze Banners into the FeastContextualNudge component, 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:

  • Add five new BrazeBannersSystemPlacementId entries (FeastContextualNudge1FeastContextualNudge5) so we can address up to five distinct nudge positions per article.
  • Update ArticleRenderer to distribute up to five placement slots evenly across all recipe sections. We calculate an interval = ceil(sections.length / 5) and assign a nudgeIndex (1–5) to every section that falls on a multiple of that interval, passing nudgeIndex and idApiUrl down to the component.
  • Update FeastContextualNudge to accept nudgeIndex and idApiUrl as props, call useBraze, and, if Braze returns a banner for the computed placement ID, render BrazeBannersSystemDisplay instead of the default nudge card.
  • Add a GetContext message type to BrazeBannersSystem and a context prop to BrazeBannersSystemDisplay, so the Braze banner iframe can request page, recipe and user context (recipe details, pageId, nudgeIndex, darkMode, etc.) to personalise its content.
  • Reserve a min-height for 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 own minHeight custom property to match or exceed these values if their content is taller.
  • Consolidate --feast-nudge-heading and --feast-nudge-subtext CSS custom properties into a single --feast-nudge-text variable, as requested by the Design team.
  • Update the Storybook story args to include the new nudgeIndex and idApiUrl props.
  • Remove the feast-recipe-nudge-v2 AB test gating from FeastContextualNudge - the nudge is now fully rolled out to all recipe article readers rather than only a test variant. This removes the useAB/isUserInTestGroup check (including the temporary dev-only override) from the component, drops the corresponding feast-recipe-nudge-v2 test definition from the AB testing config, and removes the now-unneeded AB test mocks/Control story from FeastContextualNudge.stories.tsx and DecideLayout.stories.tsx -> this was authorized by Sprint priority on this chat message.

Page-aware placement loading

  • Add ISLAND_PLACEMENT_MAP - a map from gu-island component 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.
  • Update buildBrazeMessaging to call getPagePlacements() before refreshBanners and skip the refresh entirely when no relevant placements are on the page.

Rate-limit observability, and a stopgap for the placement-request ceiling

  • refreshBanners now 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.
  • New: while the total number of known BrazeBannersSystemPlacementId values fits within that 10-per-call cap, refreshBanners requests all of them on every call, rather than just the placements getPagePlacements() 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.
    • This is explicitly a stopgap: once we add an 11th placement, it automatically falls back to requesting only what the caller passed in (today's per-page-filtered behaviour), and we'll need a proper prioritisation strategy (e.g. session-level token budgeting by placement priority) at that point.

Stale-cache handling

  • Add a module-level stalePlacements Set that tracks placement IDs whose last requestBannersRefresh was 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.
  • Export isPlacementStale(id) for island components to query the stale state of a specific placement before calling getBanner().
  • Add PLACEMENT_SUPPRESS_ON_STALE - a flat Partial<Record<BrazeBannersSystemPlacementId, boolean>> that controls per-placement suppression behaviour when stale. Banner and EndOfArticle are set to true (suppress - avoids showing outdated MRR campaigns); the five FeastContextualNudge placements are set to false (fall through to the native card). The ISLAND_PLACEMENT_MAP remains a pure DOM-detection structure and carries no suppression logic.
  • canShowBrazeBannersSystem checks stalePlacements before calling getBanner(), and FeastContextualNudge performs the same check before attempting to render a Braze banner.

Ophan click tracking

We added two new CLICK component events, both under componentType: 'RETENTION_ENGAGEMENT_BANNER' (the same componentType already used by the existing VIEW event recorded when a Braze banner is scrolled into view):

  • When the reader taps the native (non-Braze) "Download the app" button on the fallback nudge card.
  • When a Braze banner's NavigateToUrl postMessage 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's value field carries the destination URL.

IDs to search for in Ophan (componentType is RETENTION_ENGAGEMENT_BANNER for all three):

Event action id
Braze banner viewed VIEW The banner's ophanComponentId custom property if the campaign sets one, otherwise the placement ID (dotcom-rendering_feast-contextual-nudge-1-5)
Native "Download the app" button clicked CLICK feast-contextual-nudge-1feast-contextual-nudge-5
Braze banner "navigate to URL" button clicked CLICK Same as View: ophanComponentId if set, otherwise the placement ID

⚠️ Note the native button's id (feast-contextual-nudge-1) intentionally doesn't match the Braze-driven events' id format (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 componentType so 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

Before After
Screenshot 2026-06-29 at 09 30 54 Screenshot 2026-06-29 at 09 32 30

@andresilva-guardian andresilva-guardian added the feature Departmental tracking: work on a new feature label Jun 24, 2026
@github-actions

github-actions Bot commented Jun 24, 2026

Copy link
Copy Markdown

@github-actions

github-actions Bot commented Jun 24, 2026

Copy link
Copy Markdown

@andresilva-guardian andresilva-guardian added the run_chromatic Runs chromatic when label is applied label Jun 29, 2026
@github-actions github-actions Bot removed the run_chromatic Runs chromatic when label is applied label Jun 29, 2026
@andresilva-guardian
andresilva-guardian marked this pull request as ready for review June 29, 2026 08:34
@andresilva-guardian
andresilva-guardian requested a review from a team as a code owner June 29, 2026 08:34
@github-actions

Copy link
Copy Markdown

Hello 👋! When you're ready to run Chromatic, please apply the run_chromatic label to this PR.

You will need to reapply the label each time you want to run Chromatic.

Click here to see the Chromatic project.

@akash1810

akash1810 commented Jul 9, 2026

Copy link
Copy Markdown
Member

Please rebase this branch against main before deploying to CODE. #16321 made some changes to CI and infrastructure. Deploying this branch without these changes present will either:

  • Fail when using Riff-Raff's default update strategy
  • OR delete the new infrastructure if using Riff-Raff's "dangerous" mode

Please rebase this branch against main before deploying to CODE.

@andresilva-guardian andresilva-guardian added the run_chromatic Runs chromatic when label is applied label Jul 9, 2026
@github-actions github-actions Bot removed the run_chromatic Runs chromatic when label is applied label Jul 9, 2026
@andresilva-guardian andresilva-guardian added the run_chromatic Runs chromatic when label is applied label Jul 17, 2026
@github-actions github-actions Bot removed the run_chromatic Runs chromatic when label is applied label Jul 17, 2026
@andresilva-guardian
andresilva-guardian requested a review from a team as a code owner July 24, 2026 11:20
@andresilva-guardian andresilva-guardian added the run_chromatic Runs chromatic when label is applied label Jul 27, 2026
@github-actions github-actions Bot removed the run_chromatic Runs chromatic when label is applied label Jul 27, 2026
@andresilva-guardian andresilva-guardian added the run_chromatic Runs chromatic when label is applied label Jul 27, 2026
@github-actions github-actions Bot removed the run_chromatic Runs chromatic when label is applied label Jul 27, 2026
@andresilva-guardian
andresilva-guardian added this pull request to the merge queue Jul 27, 2026
Merged via the queue into main with commit 3edbc34 Jul 27, 2026
37 checks passed
@andresilva-guardian
andresilva-guardian deleted the afs/feast-contextual-nudge-braze-banners branch July 27, 2026 10:44
@gu-prout

gu-prout Bot commented Jul 27, 2026

Copy link
Copy Markdown

Seen on PROD (merged by @andresilva-guardian 11 minutes and 6 seconds ago) Please check your changes!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature Departmental tracking: work on a new feature Seen-on-PROD

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants