Skip to content

feat(core): pair server previews with browser commits - #524

Closed
Charles Hudson (phobetron) wants to merge 1 commit into
mainfrom
NT-4307_paired-replay
Closed

Charles Hudson (phobetron) wants to merge 1 commit into
mainfrom
NT-4307_paired-replay

Conversation

@phobetron

@phobetron Charles Hudson (phobetron) commented Sep 30, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Tracking: NT-4307

Private SSR and Edge personalization previously committed events on the server and suppressed the browser page. This PR evaluates one server preflight for rendering, then commits the retained events from the browser so hybrid profile mutations and durable continuity have one browser owner.

  • Build and intercept events once on the server. A private handoff carries provisional state plus experience and insights wire-event arrays, retaining IDs, timestamps, channel, library metadata, and request context.
  • Preflight Personalization once in input order. Deliver Analytics afterward from the browser using the returned or available initial profile; do not reconstruct cross-transport interleaving, intermediate profiles, or locale-based request boundaries.
  • previewInitialExperience() appends the initial page; previewExperience() also supports journals without a page and Analytics-only journals with an existing profile. Ordinary Node event methods continue to commit on the server.
  • hydrateAndTrackCurrentPage() owns hydration, replay admission, and ordinary-page fallback. Live consent, ordinary browser interceptors, validation, queue capacity, and retries still apply; browser rebuilding is removed.
  • Preview rendering and state readiness do not wait for event delivery. New handoffs preserve earlier admitted journals; repeated use of the same handoff shares completion. Existing route tracking handles concurrent callers and StrictMode.
  • Attempt an ordinary page only if replay accepted no page. A later Analytics failure cannot duplicate an accepted page. Recoverable hydration errors permit browser delivery; unsafe public/static replay handoffs are rejected.
  • Hydrate preview state in memory. Persist continuity only after a successful live Experience response when persistence consent permits it; offline queue acceptance is not a durable commit.

System implemented by this PR

The diagram shows a private, page-bearing SSR request. Public/static handoffs carry selections without replay. Analytics-only rendering uses the same delivery owner without installing content-resolution context.

sequenceDiagram
    participant App as Application
    participant Server as Node / Next.js / Angular SSR
    participant Experience as Experience API
    participant Browser as Browser SDK
    participant Insights as Insights API

    App->>Server: Request context, consent, existing profile ID, event inputs
    Server->>Server: Build wire events and run server interceptors once
    Server->>Experience: One Personalization batch, type=preflight
    Experience-->>Server: Provisional profile, selections, and changes
    Server-->>App: Personalized HTML + private handoff with state and wire arrays
    App-->>Browser: Deliver handoff through existing SSR data channel
    Browser->>Browser: Hydrate memory and publish state readiness
    Note over App,Browser: Preview rendering does not wait for event delivery
    Browser->>Browser: Match route, check live consent, run ordinary queue interceptors
    alt Online and accepted replay page
        Browser->>Experience: Commit retained Personalization events, normal POST
        Experience-->>Browser: Live profile, selections, and changes
        Browser->>Browser: Apply live state and persist when persistence consent permits
    else Offline and queue admits replay
        Browser->>Browser: Queue events under existing capacity/retry policy
        Note over Browser,Experience: Durable continuity waits for a successful live response
    else No accepted replay page
        Browser->>Browser: Run browser prerequisite work
        Browser->>Experience: Ordinary page attempt when consent permits
        Experience-->>Browser: Live response
        Browser->>Browser: Apply state and persist when permitted
    end
    Browser->>Insights: Queue/flush retained Analytics with available profile
    Note over Server,Browser: Event IDs and server-origin metadata are retained
    Note over Browser: New handoffs preserve earlier admitted journals
Loading

Integration and compatibility

Next.js App Router request resources, Pages Router helpers, Edge handoffs, Angular, and Node/Web integrations use the paired flow. The Next.js request-context handler only forwards sanitized request context. Framework roots use the combined initial operation and ordinary later-route tracking, without a separate event-completion presentation gate.

Existing server input APIs remain usable; input dispatch stays on the server. The unmerged serialized replay contract uses wire arrays instead of semantic commands. SDK adapters, maintained consumers, package guidance, guides, and verified SDK facts are updated together.

Global/stateful api.preflight, initialPageEvent, trustedRequestHandoff, obsolete context-handler inputs, and Edge persist() remain deprecated and inert. Low-level preflight applies only to supported single-profile mutations. Direct Node/Web deployments must coordinate server preview with the browser SDK that consumes the handoff. Without JavaScript, previewed HTML can render but no browser commit occurs.

Consumer migration: before → after

Each red/green diff is a visual comparison: removed lines show the base branch's pattern, added lines show the new pattern. These are adaptation fragments using app-owned configuration, renderers, consent policy, and request values. Runtime source compatibility does not imply unchanged behavior. The migration requirement is stated for each path.

The focused handoff-factory typing fix is included in this branch. Its after examples select hydration directly in Core/Node factories and receive the corresponding literal return type, without a spread wrapper or assertion. Existing Pages Router and Edge helper APIs are unchanged; their broad result unions can still require a content/analytics type guard. Compile contracts reject invalid hydration strings.

1. Manual Node SSR + vanilla Web — migration required for server-commit/browser-skip flows

Move the initial mutation to browser ownership. Transfer an accepted preview, or a typed empty private fallback, through the app's existing SSR channel. Use the preview data for server rendering; persist identity from live browser responses rather than preview data.

Server:

 import {
   createRequestHandoffFromData,
+  createRequestHandoffFromPreview,
 } from '@contentful/optimization-node'

-await requestOptimization.identify({ userId, traits })
-const result = await requestOptimization.page({ properties: { url: pageUrl } })
-const handoff = result.accepted
-  ? {
-      ...createRequestHandoffFromData({ data: result.data }),
-      hydration: 'preserve-server',
-      initialPageEvent: 'skip',
-    }
-  : undefined
+const preview = await requestOptimization.previewInitialExperience({
+  events: [{ type: 'identify', userId, traits }],
+  page: { properties: { url: pageUrl } },
+})
+const handoff = preview.accepted
+  ? createRequestHandoffFromPreview({ preview, routeKey, hydration: 'preserve-server' })
+  : createRequestHandoffFromData({ hydration: 'preserve-server' })

Browser:

-await hydrateOptimizationHandoff(optimization, handoff)
-await optimization.trackCurrentPage({
-  routeKey,
-  initialPageEvent: handoff.initialPageEvent,
-  buildPayload,
-})
+const delivery = optimization.hydrateAndTrackCurrentPage(handoff, {
+  routeKey,
+  buildPayload,
+  onHydrated: () => renderCurrentRoute(),
+})
+void delivery.catch(reportDeliveryError)
 // Later router events use the existing ordinary page tracker:
 void optimization.trackCurrentPage({ routeKey: nextRouteKey, buildPayload: buildNextPage })

2. React Web with a custom SSR handoff — change the server flow; remove suppression ownership

The browser root remains the delivery owner. The handoff must come from the preview/fallback factories above; deleting a skip flag while continuing server commits can double-count events.

 <OptimizationRoot
   {...clientConfig}
-  handoff={committedServerHandoff}
+  handoff={previewHandoff}
   routeKey={routeKey}
   buildPagePayload={() => currentPagePayload}
   onStatesReady={subscribeToEvents}
 >
-  <AutoPageTracker initialPageEvent="skip" />
   <PersonalizedPage />
 </OptimizationRoot>

AutoPageTracker above denotes the app's separately mounted router tracker, not a new SDK export. If a separate tracker was not mounted, omit that removal. One handoff/callback-owning root handles initial and later pages. beforeInitialPage gates the fallback page attempt, while preview content can render independently.

3. Angular SSR — transfer preview handoff; publish the live runtime before delivery finishes

Update the service's server transfer object and browser setup. App-owned consent and entry rendering remain in place.

-const result = await requestOptimization.page({ properties: { url: request.url } })
-transferState.set(serverStateKey, { defaults: serverDefaults, data: result.data })
-persistAnonymousIdCookie(response, result.data?.profile.id)
+const preview = await requestOptimization.previewInitialExperience({
+  page: { properties: { url: request.url } },
+})
+const handoff = preview.accepted
+  ? createRequestHandoffFromPreview({ preview, routeKey, hydration: 'preserve-server' })
+  : createRequestHandoffFromData({ hydration: 'preserve-server' })
+transferState.set(serverStateKey, { defaults: serverDefaults, handoff })

-await hydrateOptimizationHandoffState(sdk, transfer.data)
-runtimeSignal.set(sdk)
-await sdk.trackCurrentPage({ routeKey, initialPageEvent: 'skip' })
+void sdk.hydrateAndTrackCurrentPage(transfer.handoff, {
+  routeKey,
+  buildPayload: () => ({ properties: { url: location.href } }),
+  onHydrated: (error) => {
+    runtimeSignal.set(sdk)
+    if (error !== undefined) reportHydrationError(error)
+  },
+}).catch(reportDeliveryError)

The cookie writer shown on the removed line is app code. The existing Angular router subscription continues calling trackCurrentPage().

4. Next.js App Router — move proxy policy into the request binding

If consent/SDK work was configured on the context handler, move it into the server request binding. Existing request-root injection and request-family components remain in place. A binding already configured this way may need no source edits, but server preview/cookie semantics still change.

 // Imports are from /request-handler and /app-router/server respectively.
-export const proxy = createNextjsOptimizationContextHandler({
-  sdk: serverSdk,
-  consent: resolveServerConsent,
-  locale,
-})
+export const proxy = createNextjsOptimizationContextHandler()

 export const optimization = bindNextjsAppRouterServerOptimization(
   {
     ...serverConfig,
+    consent: { server: resolveServerConsent },
     request: {
-      trustedRequestHandoff: true,
       hydration: 'preserve-server',
+      initialExperienceEvents: ({ routeKey }) => [
+        { type: 'track', event: 'route-opened', properties: { routeKey } },
+      ],
     },
   },
   { request: { OptimizationRoot: ClientRequestOptimizationRoot } },
 )

The initialExperienceEvents addition is optional; use it only for events that belong in the initial batch. The client root is still obtained from bindNextjsAppRouterClientOptimization(clientConfig).RequestOptimizationRoot, and the server layout still mounts optimization.request.OptimizationRoot and request OptimizedEntry components. Do not add a second request tracker.

5. Next.js Pages Router — remove server cookie-write options and migrate extra initial events

The helper name and basic root wiring remain usable. Its operation changes from server commit to preflight; it no longer writes the server profile cookie. Removed cookie-write options are a source migration for callers that supplied them.

 // Server helper bound from /pages-router/server:
 const handoff = await createRequestHandoff(context, {
   hydration: 'preserve-server',
   cache: { scope: 'private-request' },
   pagePayload: currentPagePayload,
-  cookieOptions: { path: '/', sameSite: 'lax' },
-  deleteWhenProfileCannotPersist: true,
+  initialExperienceEvents: [{ type: 'identify', userId, traits }],
 })
+if (handoff.hydration === 'analytics-only') throw new Error('Expected content handoff')
 return { props: { contentfulOptimization: { handoff } } }

 // pages/_app.tsx: the bound root remains the route owner.
 <OptimizationRoot
   handoff={pageProps.contentfulOptimization?.handoff}
   routeKey={router.asPath || router.pathname}
   buildPagePayload={() => currentPagePayload}
 >
-  <NextPagesAutoPageTracker initialPageEvent="skip" />
   <Component {...pageProps} />
 </OptimizationRoot>

The identify input is optional and replaces a separate initial server identify when browser commit is intended. Remove the tracker only if it was mounted beside the handoff/callback-owning root. Configure browser continuity through the existing browser/root policy rather than removed per-request server-cookie options.

6. Next.js Edge — stop persisting preview identity from the response

The helper continues returning a handoff, but persist() becomes inert. Selecting the mode inline needs no literal assertion. The existing helper still has a broad return type, so narrow the result before passing it to a content-only root.

-const { handoff, persist } = await createEdgeRequestHandoff({
+const { handoff } = await createEdgeRequestHandoff({
   request,
   hydration: 'preserve-server',
   cache: { scope: 'private-request' },
   pagePayload: { properties: { url: request.url } },
+  initialExperienceEvents: [{ type: 'track', event: 'edge-route-opened' }],
 })
+if (handoff.hydration === 'analytics-only') throw new Error('Expected content handoff')
 const response = renderPrivateResponse({ handoff })
-persist(response)
 return response

 // Client Component, using the App Router client binding:
-<optimization.OptimizationRoot handoff={committedServerHandoff} routeKey={routeKey}>
+<optimization.OptimizationRoot handoff={handoff} routeKey={routeKey}>
   {children}
 </optimization.OptimizationRoot>

The initial track input is optional. renderPrivateResponse() denotes the app's renderer/serialization layer. Preview state can render with JavaScript disabled, but the browser cannot commit events or establish new continuity in that case.

7. Static/public permutation and analytics-only pages — review first-page policy

An old emit setting can be removed with the same page-delivery intent. An old skip setting is a behavior migration: the update emits an ordinary browser page when consent and deduplication admit it; there is no equivalent skip policy in the new flow.

 const handoff = optimization.createPublicPermutationHandoff({
   selectedOptimizations,
   permutationKey: segmentKey,
   cacheVersion,
   locale,
   entryIds,
   hydration: 'preserve-server',
-  initialPageEvent: 'emit',
 })
 <optimization.OptimizationRoot handoff={handoff} routeKey={routeKey}>
   <optimization.OptimizedEntry baselineEntry={entry} liveUpdates={false}>
     {renderEntry}
   </optimization.OptimizedEntry>
 </optimization.OptimizationRoot>
 // Analytics-only describes rendering ownership; page delivery still uses Personalization.
 const analyticsHandoff = optimization.createHandoffFromSelections({
   cache: { scope: 'static' },
   selectedOptimizations,
   hydration: 'analytics-only',
-  initialPageEvent: 'emit',
 })
 <optimization.OptimizationAnalyticsRoot handoff={analyticsHandoff} routeKey={routeKey}>
   {serverRenderedMarkup}
 </optimization.OptimizationAnalyticsRoot>

Existing selections and server-rendered markup remain app-owned; public/static handoffs never acquire private replay. Non-React analytics owners still use initializeOptimizationAnalyticsRuntime() and hydrateOptimizationAnalyticsHandoff().

8. Client-only Web, React Native, iOS, and Android — review use of global preflight

If live committing events were intended, remove the obsolete flag as shown. If preview-only behavior was intended, merely removing the flag is not an equivalent migration: stateful SDK events now commit, and those consumers need an explicit supported preview integration or the old behavior retained for compatibility. Callers that never enabled preflight need no source migration for this setting.

 // Vanilla Web, custom browser framework, or Web Component host configuration:
-const sdk = new ContentfulOptimization({ spaceId, api: { preflight: true } })
+const sdk = new ContentfulOptimization({ spaceId })
 await sdk.page({ properties: { url: location.href } })

 // React Native:
-const sdk = await ContentfulOptimization.initialize({ spaceId, api: { preflight: true } })
+const sdk = await ContentfulOptimization.initialize({ spaceId })
 await sdk.screen({ name: 'Home' })
 // SwiftUI and UIKit configuration:
-let config = OptimizationConfig(spaceId: spaceId, api: OptimizationApiConfig(preflight: true))
+let config = OptimizationConfig(spaceId: spaceId)
 try client.initialize(config: config)

 // Compose and Views configuration:
-val config = OptimizationConfig(spaceId = spaceId, api = OptimizationApiConfig(preflight = true))
+val config = OptimizationConfig(spaceId = spaceId)

These are alternatives in their respective applications. Mobile consumers do not acquire a browser replay API; ordinary consent, persistence, offline behavior, and screen ownership remain with their runtime.

9. Server-only Node and low-level API Client — unchanged commit APIs; scoped preview where supported

Server-only Node's ordinary request event methods remain committing calls, so no migration is required for that flow. The comparison makes that unchanged path explicit.

Before After
await scoped.page({ properties: { url: requestUrl } }) await scoped.page({ properties: { url: requestUrl } })

For custom API integrations, constructor-level preflight still applies to supported single-profile mutations, but it no longer leaks into profile reads or batch ingestion. Use request-scoped preview to make ownership explicit:

-const api = new ExperienceApiClient({ spaceId, environment, preflight: true })
-const preview = await api.upsertProfile({ profileId, events: wireEvents })
+const api = new ExperienceApiClient({ spaceId, environment })
+const preview = await api.upsertProfile(
+  { profileId, events: wireEvents },
+  { preflight: true },
+ )
 // Normal single-profile mutations commit when the integration owns that step:
 const committed = await api.upsertProfile({ profileId, events: wireEvents }, { preflight: false })

The explicit request-scoped form is recommended, not required for valid single-profile constructor-preflight callers. Profile reads and batch ingestion are not supported preview operations. Custom adapters implementing WebOptimizationRuntime must add the new hydrateAndTrackCurrentPage member. The API Schemas facade and preview panel have no new consumer initialization API in this PR.

Breaking-change and migration assessment

Comparison baseline: the PR's base branch, which includes published SDK v2.0.1. This assessment concerns the complete branch, including the additive handoff-factory typing fix.

The current branch is behaviorally breaking for supported consumers, and it includes a source-breaking options change. Keeping old field/method names as inert shells does not make those contracts non-breaking. Under normal semantic versioning, the affected published contracts require a major release unless their earlier behavior is explicitly established as unsupported, or the implementation preserves that behavior during a deprecation window. The base release is published, so this cannot be dismissed as replacing only unmerged APIs.

Existing consumer pattern What changes Migration action
Manual server commit + browser initialPageEvent: 'skip' Skip is inert; continuing server commits can cause both server and browser page delivery. Replace the hybrid initial server commit with preview and handoff; give one browser owner the initial decision.
Stateful Web/mobile api.preflight: true Events commit instead of being preview-only. Review mutation/persistence intent. Omitting the flag is appropriate for committing flows; preserving preview-only intent needs an explicit supported preview path or compatibility behavior.
Configured Next.js context handler SDK, consent, locale, and cookie work on the handler become inert. Move request policy into the server request binding; keep the handler context-only.
Pages Router request cookie options cookieOptions and deleteWhenProfileCannotPersist are removed from the helper options; automatic server cookie writes stop. Remove those options and configure browser continuity ownership. Inline TypeScript option objects can require source edits.
Edge persist(response) The method remains callable but no longer persists identity. Move durable continuity to browser commit; do not depend on a preview response cookie.
Framework helper callers relying on server commits, server cookies, or no-JavaScript mutations Helpers now preflight; preview HTML can render without JavaScript, but commits/new continuity cannot occur. Review the event/persistence contract even if existing helper/root calls still compile. Use committing Node methods for genuine server-only work.
Custom structural WebOptimizationRuntime implementations The runtime interface requires hydrateAndTrackCurrentPage. Implement the new member or use a supplied SDK/snapshot runtime.
Node-only ordinary event methods; client-only SDKs without affected flags; basic bindings already owning request policy Main call patterns remain usable. No source migration for those unchanged paths; review any hybrid cookie/event ownership assumptions.

Request-level preflight on valid single-profile API Client mutations remains supported. Preventing constructor preflight from affecting reads and unsupported batch ingestion is a transport correction; it does not remove valid single-profile preview use.

The focused SDK typing fix is additive: optional hydration on request handoff factories, literal-preserving return types, and shared metadata type exports. Calls without the new option preserve their old payload shape. The fix leaves Pages Router/Edge helper return types and runtime behavior unchanged.

Consumer DX assessment

DX improves materially for hybrid/framework integrations, but not uniformly across the suite. The improvement is clearer ownership and fewer coordination responsibilities, not simply shorter code or lower payload size.

Consumer Improvement Cost or limitation
Next.js request families The request binding owns consent, preview, route context, handoff, initial replay/fallback, and later routing; proxy configuration loses hidden mutation/persistence work. Applications that relied on proxy policy must relocate it. Existing correctly configured bindings may see little source-code reduction.
Manual Node/Web and Angular One browser operation handles memory hydration, replay, ordinary-page fallback, and deduplication. Callback readiness can publish usable preview state without awaiting delivery. Server preview, serialization, route identity, and consent policy remain app-owned; setup can be more verbose than the old commit-and-skip path.
Initial identify/track/page sequences One preflight batch avoids separate sequential initial server mutations; the framework input seam centralizes initial work. Consumers express batch inputs instead of receiving intermediate per-event profile transitions.
Handoff construction The small factory fix selects hydration in the SDK call and returns the right literal type, eliminating consumer spread/assertion boilerplate. Existing broad Next.js helper result types can still need a content/analytics guard; unrelated configuration storage uses ordinary TypeScript contextual types.
Browser/native client-only and server-only Node Ordinary committing APIs remain familiar. No new workflow benefit for unaffected consumers; preflight-dependent consumers incur a migration cost.
Build-once wire delivery Event identity and server interceptor output survive preflight-to-commit; browser reconstruction disappears. Serialized handoffs grow, and server-origin metadata is an intentional semantics change.

These are source/ownership conclusions, not results of a consumer usability study. Source-line savings, bundle-policy reductions, and the number of examples are not evidence by themselves that DX improved. The strongest supported benefit is the single browser delivery owner and predictable framework request ownership; the main unresolved release concern is preserving or versioning the changed public behavior.

Validation and measured results

Earlier paired-replay implementation validation:

  • 1,172 SDK unit tests passed; workspace typechecking, package and implementation lint, full package build/packing, formatting, SDK knowledge, guide, and Fern checks passed.
  • 35 focused Chromium E2E tests passed across App Router, Edge runtime, Pages Router, React Web, Angular hydration, and Node/Web continuity. Scenario flags skip cases belonging to other targets. Mobile/native E2E is deferred to CI.
  • Build-once delivery removes 35 production-source lines from the preceding implementation, leaving the complete simplification 11 production-source lines below the branch's original paired-replay revision.
  • Core CJS/ESM save 34/27 gzip bytes; Web/Web Components UMD save 158/172 bytes. Modular Web entries save 1–2 bytes. A representative identify/track/page replay grows from 840 to 1,810 JSON bytes because generated metadata is retained.

The budget normalization used a fresh pnpm build and aggregate pnpm size:report; its amend/push skipped validation and hooks.

Before the final amend/push, the narrow handoff-factory typing fix passed workspace typechecking and lint (four existing deprecation warnings), Core/Node/Next.js unit suites, full pnpm build:pkgs, aggregate pnpm size:report and pnpm size:check, SDK knowledge, guides, Fern, and formatting checks. Refreshed Node/Web and Angular implementation installs passed their typechecks and implementation lint; the final Next.js typecheck also passed. No E2E was rerun for this typing fix. Mobile/native E2E remains deferred to CI.

The latest amend/push ran no validation and bypassed commit/push hooks at the requester's direction.

Bundle budgets

At the requester's direction, every configured release bundle budget was normalized to its gzip measurement at normalization time, rounded to the nearest 100 bytes, plus 100 bytes:

Math.round(gzipBytes / 100) * 100 + 100

The normalization covered 81 configured bundles across 9 SDK packages: 14 budgets increased, 40 decreased, and 27 remained unchanged. Headroom at normalization was 52–149 bytes. The report below records that measurement snapshot and the applied thresholds. The subsequent aggregate pnpm size:check passed with the handoff-factory typing fix, without further budget changes. Its Core CJS/ESM measurements were 24,597/23,759 gzip bytes against budgets of 24,700/23,800 bytes. No runtime source, dependencies, bundle entries, aliases, or exports changed as part of the budget normalization.

Complete bundle report at budget normalization and applied budgets

@contentful/optimization-api-client

Bundle Raw Gzip Previous budget New budget Headroom
guards.cjs 1,443 525 550 600 75
guards.mjs 185 197 200 300 103
index.cjs 26,122 4,998 5,000 5,100 102
index.mjs 20,683 4,626 4,700 4,700 74

@contentful/optimization-core

Bundle Raw Gzip Previous budget New budget Headroom
index.cjs 137,153 24,578 24,500 24,700 122
index.mjs 115,075 23,741 23,500 23,800 59
bridge-support.cjs 2,646 852 1,000 1,000 148
bridge-support.mjs 1,194 622 800 700 78
runtime.cjs 15,800 3,502 3,700 3,600 98
runtime.mjs 16,889 3,954 4,900 4,100 146
entry-source.cjs 7,896 1,983 2,100 2,100 117
entry-source.mjs 6,222 1,724 1,900 1,800 76
preview-support.cjs 25,565 5,158 5,200 5,300 142
preview-support.mjs 22,332 4,551 5,100 4,700 149

@contentful/optimization-node

Bundle Raw Gzip Previous budget New budget Headroom
index.cjs 4,318 1,176 1,500 1,300 124
index.mjs 1,639 702 1,300 800 98

@contentful/optimization-api-schemas

Bundle Raw Gzip Previous budget New budget Headroom
index.cjs 3,115 792 5,100 900 108
index.mjs 124 92 3,400 200 108

@contentful/optimization-web

Bundle Raw Gzip Previous budget New budget Headroom
contentful-optimization-web.umd.js 130,692 38,147 38,000 38,200 53
index.cjs 43,780 12,476 12,300 12,600 124
index.mjs 40,937 12,687 12,600 12,800 113
bridge-support.cjs 1,483 594 700 700 106
bridge-support.mjs 104 106 300 200 94
runtime.cjs 2,226 910 1,000 1,000 90
runtime.mjs 651 346 500 400 54
handoff.cjs 4,219 1,500 1,700 1,600 100
handoff.mjs 2,300 1,063 1,600 1,200 137
analytics.cjs 43,615 12,587 12,900 12,700 113
analytics.mjs 41,563 12,928 14,100 13,000 72
contentful-optimization-web-components.umd.js 154,686 43,586 43,500 43,700 114
presentation.cjs 12,313 3,482 3,600 3,600 118
presentation.mjs 9,679 3,126 3,500 3,200 74
tracking-attributes.cjs 2,015 826 1,000 900 74
tracking-attributes.mjs 810 492 700 600 108
web-components.cjs 65,585 17,775 17,600 17,900 125
web-components.mjs 63,785 18,867 18,700 19,000 133

@contentful/optimization-react-native

Bundle Raw Gzip Previous budget New budget Headroom
index.cjs 77,114 12,911 13,000 13,000 89
index.mjs 61,581 12,520 12,700 12,600 80
preview-support.cjs 2,383 744 800 800 56
preview-support.mjs 63 81 200 200 119
preview.cjs 108,899 18,975 19,100 19,100 125
preview.mjs 89,924 17,802 18,000 17,900 98

@contentful/optimization-react-web

Bundle Raw Gzip Previous budget New budget Headroom
index.cjs 25,554 6,302 7,100 6,400 98
index.mjs 15,211 5,401 6,200 5,500 99
router/next-app.cjs 5,672 1,841 2,000 1,900 59
router/next-app.mjs 3,388 1,735 2,000 1,800 65
router/next-pages.cjs 4,781 1,692 1,900 1,800 108
router/next-pages.mjs 2,896 1,584 1,900 1,700 116
router/react-router.cjs 4,911 1,664 1,900 1,800 136
router/react-router.mjs 2,897 1,543 1,800 1,600 57
router/tanstack-router.cjs 5,001 1,721 1,900 1,800 79
router/tanstack-router.mjs 2,965 1,596 1,900 1,700 104
handoff.cjs 1,501 651 800 800 149
handoff.mjs 50 70 200 200 130
analytics.cjs 1,505 654 800 800 146
analytics.mjs 52 72 200 200 128
tracking-attributes.cjs 1,527 660 800 800 140
tracking-attributes.mjs 62 81 200 200 119

@contentful/optimization-web-preview-panel

Bundle Raw Gzip Previous budget New budget Headroom
contentful-optimization-web-preview-panel.umd.js 131,822 37,046 37,100 37,100 54
index.cjs 99,777 24,257 24,300 24,400 143
index.mjs 89,991 22,644 22,700 22,700 56

@contentful/optimization-nextjs

Bundle Raw Gzip Previous budget New budget Headroom
app-router.cjs 6,312 1,667 1,700 1,800 133
app-router.mjs 3,014 1,258 1,600 1,400 142
app-router-server.cjs 15,943 5,120 5,300 5,200 80
app-router-server.mjs 15,727 6,555 6,700 6,700 145
cache-middleware.cjs 5,099 1,811 1,900 1,900 89
cache-middleware.mjs 2,662 1,566 1,700 1,700 134
client.cjs 4,526 984 1,100 1,100 116
client.mjs 457 233 600 300 67
edge.cjs 10,059 3,185 3,300 3,300 115
edge.mjs 6,260 2,948 3,100 3,000 52
pages-router.cjs 6,235 1,661 1,800 1,800 139
pages-router.mjs 2,977 1,244 1,600 1,300 56
pages-router/server.cjs 10,629 3,813 3,900 3,900 87
pages-router/server.mjs 10,917 5,188 5,600 5,300 112
server.cjs 8,619 2,979 3,100 3,100 121
server.mjs 7,576 3,586 3,900 3,700 114
request-handler.cjs 3,592 1,266 2,900 1,400 134
request-handler.mjs 1,791 1,124 5,100 1,200 76
tracking-attributes.cjs 1,457 612 700 700 88
tracking-attributes.mjs 240 184 400 300 116

Internal native bridge outputs

These private build outputs have no configured release budgets and are reported separately.

Bundle Raw Gzip
optimization-ios-bridge.umd.js 113,033 33,341
optimization-android-bridge.umd.js 113,041 33,346

@bito-code-review

bito-code-review Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Bito Automatic Review Skipped - Large PR

Bito didn't auto-review this change because the pull request exceeded the line limit. No action is needed if you didn't intend for the agent to review it. Otherwise, to manually trigger a review, type /review in a comment and save.

@wiz-inc-38d59fb8d7

wiz-inc-38d59fb8d7 Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Wiz Scan Summary

Scanner Findings
Vulnerability Finding Vulnerabilities -
Data Finding Sensitive Data -
Secret Finding Secrets -
IaC Misconfiguration IaC Misconfigurations -
SAST Finding SAST Findings 7 Low
Software Management Finding Software Management Findings -
Total 7 Low

View scan details in Wiz

To detect these findings earlier in the dev lifecycle, try the Wiz Code extension for VS Code, JetBrains, or Visual Studio.

@phobetron
Charles Hudson (phobetron) force-pushed the NT-4307_paired-replay branch 6 times, most recently from 07d2826 to 2b3d47d Compare October 1, 2026 09:09
Build Personalization and Analytics events once on the server, preflight one Personalization batch, and carry the wire events and provisional state in a private handoff. Commit from the browser through normal consent, interceptor, queue, state, and persistence policy without regenerating event identity or metadata.

Combine hydration and the initial replay/page decision, preserve admitted journals when newer handoffs arrive, and render preview content independently of delivery. Keep superseded options and methods deprecated and inert, restrict preflight transport to single-profile mutations, and update Next.js, Angular, Node plus Web integrations, documentation, and coverage together.

Normalize all configured release bundle gzip budgets to the fresh measured size rounded to the nearest hundred bytes, plus one hundred bytes.

Synchronize the Pages Router watchdog E2E with the timeout diagnostic before releasing identify, since newer handoff page delivery is independent.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant