Skip to content

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

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

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

Conversation

@phobetron

Copy link
Copy Markdown
Collaborator

Summary

Tracking: NT-4307

This changes private SSR and ESR personalization from server commit + browser skip to server preview + browser commit.

  • Request-scoped server helpers preflight an ordered initial Experience batch.
  • Optional identify and track commands retain application order; the SDK appends page.
  • Preview state personalizes the server render without durably mutating or relocating the profile.
  • A private handoff carries the preview state and a route-bound, one-shot replay.
  • The browser rebuilds the commands with live consent, context, and interceptors, then commits them through the normal stateful queue.
  • Durable profile continuity is written only from the browser commit response.
  • Operational preview or hydration failures degrade to baseline rendering and ordinary page tracking; cache-safety violations remain fail-closed.

The change also confines low-level preflight to supported single-profile mutations, removes Experience work from the Next.js request-context proxy, and retains superseded options as deprecated inert compatibility inputs.

Why

The previous flow committed on the server and suppressed the corresponding browser page event. In hybrid integrations, later browser mutations could move the profile between regions after each server render. It also made preflight unsafe as a global option because some runtimes had no browser owner available to perform the commit.

Paired replay gives each phase one responsibility:

  1. The server evaluates a preview for the first render.
  2. The private handoff carries preview state and browser-safe command inputs.
  3. The browser is the only durable mutation and relocation authority for hybrid flows.
  4. Server-only Node usage continues to use normal committing event methods.

Request lifecycle

sequenceDiagram
    participant Request as Incoming request
    participant Server as Node / Next.js / Edge
    participant API as Experience API
    participant Handoff as Private handoff
    participant Browser as Web SDK

    Request->>Server: context, consent, existing profile ID
    Server->>API: POST profile?type=preflight<br/>identify*, track*, page
    API-->>Server: preview profile and selections
    Server->>Handoff: preview state + route-bound commands
    Handoff-->>Browser: hydrate preview state in memory
    Browser->>Browser: match route and re-check live consent
    Browser->>API: normal POST profile<br/>identify*, track*, page
    API-->>Browser: committed profile and selections
    Browser->>Browser: publish state and persist continuity
Loading

Failure ownership

flowchart TD
    A[Prepare server preview] -->|accepted| B[Private handoff with preview state]
    A -->|operational failure| F[Profileless private fallback]
    B --> C[Hydrate state in memory]
    C -->|matching route| D[Attempt one-shot browser replay]
    C -->|stale, mismatched, or hydration failure| E[Discard replay]
    D -->|committed or atomically queued| G[Accept route and persist committed state]
    D -->|blocked or delivery failure| E
    E --> H[Ordinary current-page attempt]
    F --> H
    B -->|unsafe public or static cache| I[Reject handoff]
Loading

Main changes

Core and API client

  • Add CoreStatelessRequest.previewInitialExperience() and browser-safe initial-command types.
  • Preserve flat identify / track order and append the SDK-built page command.
  • Build the server evaluation as one forced-preflight single-profile mutation.
  • Add Core-owned private replay handoffs and stateful ordered batch delivery.
  • Re-run browser consent, builders, interceptors, and validation before commit.
  • Keep offline replay batches atomic when queue capacity is insufficient.
  • Stop forwarding global stateful api.preflight.
  • Prevent preflight from affecting profile reads or batch /events ingestion.

Web and React Web

  • Apply preview state in memory without writing durable continuity.
  • Stage replay only after the latest handoff hydration succeeds.
  • Consume a matching replay once through ordinary route tracking.
  • Preserve same-route and StrictMode deduplication with the existing accepted-route tracker.
  • Fall back to the ordinary page path when a safe private handoff cannot hydrate or replay.
  • Persist profile identity and selections only after a committed browser response.
  • Make legacy initialPageEvent inputs inert while keeping them source-compatible and deprecated.

Next.js and maintained integrations

  • Make createNextjsOptimizationContextHandler() a sanitized context-forwarding helper only.
  • Move preview ownership into App Router request resources, Pages Router helpers, and Edge handoffs.
  • Add optional initial identify / track command resolvers to framework request helpers.
  • Return a profileless private fallback on operational preview failure.
  • Do not invent a route when App Router forwarding context is missing; the browser tracker derives it.
  • Keep invalid cache ownership outside recoverable failure boundaries.
  • Update Angular and Node + Web examples to preview on the server and commit in the browser.
  • Keep Node-only event methods commit-by-default.

Consumer DX

Custom Node SSR

Before, the server committed events and encoded browser suppression in the handoff:

await requestOptimization.identify({ userId, traits })
const page = await requestOptimization.page({ properties: { path } })

const handoff = {
  ...createRequestHandoffFromData({ data: page.data }),
  hydration: 'preserve-server',
  initialPageEvent: 'skip',
}

Now, the server previews one ordered batch and the browser-owned route commits it:

const preview = await requestOptimization.previewInitialExperience({
  events: [
    { type: 'identify', userId, traits },
    { type: 'track', event: 'product-opened', properties: { sku } },
  ],
  page: { properties: { path } },
})

const handoff = preview.accepted
  ? {
      ...createRequestHandoffFromPreview({ preview, routeKey: path }),
      hydration: 'preserve-server',
    }
  : undefined

Browser application code does not manually replay the commands. Its existing current-page call owns both the replay and ordinary fallback:

if (handoff) await optimization.hydrateOptimizationHandoff(handoff)
await optimization.trackCurrentPage({ routeKey: location.pathname })

Next.js App Router

Before, the proxy could own Experience work and persistence configuration:

export const proxy = createNextjsOptimizationContextHandler({
  sdk: serverOptimization,
  consent: resolveConsent,
  locale: 'en-US',
})

Now, the proxy only forwards sanitized request context and the request binding owns preview/replay:

export const proxy = createNextjsOptimizationContextHandler()

export const optimization = bindNextjsAppRouterServerOptimization({
  spaceId,
  environment,
  consent: { server: resolveConsent },
  request: {
    initialExperienceEvents: ({ routeKey }) => [
      {
        type: 'track',
        event: 'server-route-preview',
        properties: { routeKey },
      },
    ],
  },
})

Compatibility and release impact

The following inputs remain accepted but are deprecated and inert:

  • stateful/global api.preflight;
  • browser and page-tracking initialPageEvent;
  • App Router trustedRequestHandoff;
  • configured SDK, consent, locale, and cookie inputs on the Next.js context handler;
  • Edge handoff persist().

This is intentionally a non-breaking correction: obsolete inputs remain source-compatible, and consumers do not need a mode to preserve the previous server-commit/browser-skip behavior. The additive replay APIs imply minor releases for Core, Node, Web, React Web, and Next.js. API Client and native dependency releases remain patch-level where Release Please propagation allows it. The generated Release Please PR remains authoritative for the exact coordinated version set.

Direct Node + Web consumers have one deployment requirement: enable request-scoped server preview before, or atomically with, the browser version that consumes the private replay. Node-only consumers should continue using ordinary committing event methods.

No-JavaScript requests can render previewed personalized HTML, but cannot perform the browser commit or establish new durable profile continuity.

Bundle budgets

Only entries that failed the fresh aggregate report were changed. Values are rounded and retain modest headroom rather than matching the current artifact byte-for-byte.

Package / entry Previous Measured gzip New budget Headroom
Core index.cjs 23,200 24,234 24,500 266 B
Core index.mjs 23,100 23,323 23,500 177 B
Web UMD 36,600 37,807 38,000 193 B
Web index.cjs 11,800 12,202 12,300 98 B
Web index.mjs 12,400 12,434 12,600 166 B
Web Components UMD 42,100 43,257 43,500 243 B
Web Components web-components.cjs 17,200 17,493 17,600 107 B
Next.js app-router-server.cjs 4,900 5,120 5,300 180 B
Next.js edge.cjs 3,100 3,185 3,300 115 B
Next.js server.cjs 2,700 2,979 3,100 121 B

Validation

Passed on the final commit or during its push gate:

  • pnpm build
  • pnpm size:report
  • Core, Web, and Next.js package size:check
  • staged ESLint and Prettier across all 133 changed files
  • commitlint for feat(core): pair server previews with browser commits
  • clean package rebuild and pnpm build:pkgs
  • local-tarball reinstall for Next App, Next Pages, Next Edge, Node + Web, and Angular
  • typechecks for all eight changed workspaces and all five changed implementations
  • changed-workspace unit suites: 1,152 tests passed
  • Node + Web implementation unit suite: 1 test passed; other changed implementations declare no unit suite
  • pnpm knowledge:check
  • pnpm guides:check
  • focused graceful-fallback lint, type, and unit checks

Targeted browser E2E completed during implementation:

  • Node + Web: 30 passed
  • Angular: 66 passed, 123 skipped by scenario selection
  • Next App Router: 3 passed
  • Next Pages Router: 3 passed
  • Next App Router Edge runtime: 3 passed

Those browser suites were not repeated after the final graceful-fallback refinements. The affected final paths were covered by focused and changed-workspace unit/type gates instead. Per project direction, no full mobile E2E cycle was run.

CI remains responsible for the path-triggered aggregate lint, documentation generation, platform checks, and maintained browser scenarios on the published branch.

@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 5 Low
Software Management Finding Software Management Findings -
Total 5 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.

Preview ordered identify and track commands with the initial page during request-scoped server rendering, then carry browser-safe commands and preview state in a private handoff. The browser rebuilds and commits the batch through the normal consent, interceptor, queue, state, and persistence pipeline.

Make global preflight and initialPageEvent inputs deprecated inert compatibility shells, restrict preflight transport to single-profile mutations, and move Next.js, Angular, and Node plus Web integrations to the paired replay path. Operational preview and private hydration failures now fall back to baseline rendering and an ordinary browser page attempt while cache-safety violations remain fail-closed.

Update maintained implementations, documentation, focused coverage, install tooling, and gzip budgets for the new ownership model.

This branch has not been deployed

No deployments
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