Repository navigation
feat(runtime)!: preview on server and commit in browser - #535
Merged
Merged
Conversation
Remove constructor-level preflight configuration and retain preview only on per-request single-profile mutations. Evaluate preview responses without persisting mock profile state.
Prepare request handoffs with API-issued profile IDs and retain atomic replay batches in the existing Experience queue. Share reset invalidation across stateful queues and preserve events appended during delivery. Remove ordinary preflight controls, record the accepted phase plan, and apply the approved measured-size budget formula.
|
Bito Automatic Review Skipped - Large PR |
Wiz Scan Summary
To detect these findings earlier in the dev lifecycle, try the Wiz Code extension for VS Code, JetBrains, or Visual Studio. |
Charles Hudson (phobetron)
requested a review
from François (Lp-Francois)
as a code owner
October 9, 2026 08:20
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.
Why and behavior
Previously, the server could commit the first page event while rendering and tell the browser to emit or skip it. This change makes the server responsible for preparation and preview, and the browser responsible for event commitment.
identify/track/pageevents and previews them with per-requestpreflight: true. Preview data can select server-rendered content; a recoverable preview failure keeps the replay available while the page can render baseline content.Delivery sequence and failure boundaries
sequenceDiagram participant Server as Server request participant API as Experience API participant Browser as Browser SDK participant Queue as Experience queue Note over Server,Browser: routeKey = visible pathname + search Server->>Server: Prepare identify/track + page alt Effective page gate permits preview Server->>API: Upsert prepared events, preflight=true alt Preview succeeds API-->>Server: Selected state + API-issued profile ID Server->>Server: Render personalized HTML else Preview fails API-->>Server: Preview error Server->>Server: Render baseline and retain prepared replay end else Page event blocked Server->>Server: Render baseline with no replay end Server-->>Browser: HTML + private handoff (state and/or replay) Browser->>Browser: Validate handoff and publish continuation identity Browser->>Browser: Hydrate current state and compare current route alt Matching prepared replay Browser->>Queue: Recheck consent and admit whole batch alt Queue admits batch Queue-->>Browser: accepted = local admission Queue->>API: Deliver batch, preflight=false API-->>Queue: Delivery response Queue-->>Browser: Publish state if lifecycle and profile revision remain current else Validation or admission rejects batch Queue-->>Browser: Not admitted Browser->>Browser: Verify initialization is still current alt Initialization remains current Browser->>Queue: Attempt ordinary current page through normal guards Queue-->>Browser: Ordinary page result else Superseded, reset, or destroyed Browser->>Browser: Return accepted=false without stale fallback end end else No replay or route mismatch Browser->>Queue: Track ordinary current page end Note over Browser,Queue: Accepted replay admission is not HTTP completion. Delivery failure never starts fallbackPreview determines first paint without committing the paired events. The browser is the paired batch's submitter; a preview failure can still leave useful baseline HTML and a replay for later delivery. A lost delivery response leaves backend commitment ambiguous, so retries do not guarantee exactly-once delivery.
Queue admission, flush completion, and identity
flush()calls join the active drain for each queue, including forced calls. PublicCoreStateful.flush()joins both Experience and Insights attempts. Completion means the attempt settled; it does not guarantee delivery or wait for scheduled retries. Force bypasses connectivity/backoff/circuit gates, never the concurrency guard.{ accepted: false }; a prepared batch is admitted whole or rejected whole. Capacity defaults to 100 events and remains configurable.onOfflineDrop, its context type, and native callback declarations remain as deprecated, inactive compatibility interfaces; rejection is not reported as eviction.{ accepted: false }admission results as an ordinary current-page fallback. It retains adopted identity and applicable hydrated state, uses the caller’sbuildPayloador the default payload, and returns the ordinary attempt’s result. Deduplication, consent, queue capacity, and lifecycle guards remain authoritative; no capacity is reserved and no accepted work is evicted. Repeated current initialization joins the fallback. Supersession, reset, or destroy suppresses stale fallback; an accepted replay never falls back after an HTTP failure. Envelope/cache-safety failures and unrelated programming or ordinary transport errors retain their existing behavior.Attempt ownership and overlapping response publication
/acompletion cannot accept or clear a new/aattempt; refusal or failure of the new attempt leaves a subsequent attempt possible.Destroy-time best-effort delivery
CoreStateful.destroy()can continue an active Experience flush or force one flush of queued prepared batches. The drain attempts batches sequentially with their original events, target, locale, and order. It can carry an API-returned profile ID to a later batch without publishing late profile or request state. Destroy remains synchronous, rejects new submissions, and cancels retry timers;reset()still cancels immediately. A failed request may stop the drain, so this remains best-effort delivery rather than durable or exactly-once delivery.The measured gzip sizes required four budget updates under the repository rule (measured bytes + 100, rounded up to the next 100): Core
index.cjs25,300 andindex.mjs24,500; Web UMD 38,900 and Web Components UMD 44,400. No other bundle budgets changed.Public API and option changes
prepareRequestHandoff({ routeKey, initialEvents?, page? })routeKeyis the visible pathname plus search, starts with/, and excludes origin and hash. It must match the browser route exactly for replay.initialEventsaccepts orderedidentifyandtrackinputs only; consent-allowed inputs precede the generated page. The prepared batch permits at most 100 events including the page and at most 50 identifies; invalid or oversized input is rejected before preflight.pageoptionally supplies page builder args, with request event context as the default.{ handoff, data? }handoffis private-request state with optionalprofileIdandreplay(routeKey, preparedevents,locale).dataexists on a successful preview. A preview error retains replay withoutdata; a blocked page or invalid input has no replay. IDs come from an existing profile or the API, not SDK generation.hydrateAndTrackCurrentPage(handoff, { routeKey, buildPayload? })buildPayloador the default payload. Accepted replay settles at local queue admission; fallback returns the ordinary page result, which can await online HTTP delivery. If both attempts are refused, the result is{ accepted: false }. An accepted fallback completes shared initialization. UsetrackCurrentPage({ routeKey, buildPayload })for later navigation.flush(),queuePolicy.offlineMaxEvents,EventEmissionResult.acceptedonOfflineDropis deprecated and inactive.consent, SDKallowedEventTypes, browser consent/persistenceidentifyandpagebefore consent, so strict opt-in requiresallowedEventTypes: []. Browser replay rechecks current event policy; profile-cookie persistence is a separate decision.preflight?preflightbelongs to single-profile mutation request options, not constructor or SDK-wideapiconfig.truepreviews; omitted orfalseis normal mutation delivery. Preparation sendstrueand browser replay sendsfalse. A preview can return an API-issued ID without committing profile state.request.initialEvents?,request.pagePayload?{ requestUrl, routeKey, cookies, headers }. The request family resolves inputs once and derives a visible pathname/search key (excluding Next.js transport_rsc); the page is appended after initial events.hydrationremains a separate rendering choice.createRequestHandoff(context, { initialEvents?, pagePayload?, hydration, ... })resolvedUrland previews instead of committing. Pass its returned handoff to the browser root and tracker; no app-suppliedrouteKeyoption is needed here.createNextjsRequestHandoff()result{ data?, handoff, requestOptimization }instead ofpageResult;hydrationis required and request cache scope is private. StandalonegetNextjsServerOptimizationData()remains the direct server-event path.createEdgeRequestHandoff({ request, initialEvents?, pagePayload?, hydration, ... })request.urlpathname/search. The result exposesdata(possibly undefined),handoff,requestOptimization, andpersist(response);pageResultis removed. Request handoffs remainprivate-request; callpersist(response)to apply the cookie write or clear under the configured persistence policy.requestHandler/createNextjsOptimizationContextHandler()trustedRequestHandoffand forwarded server-commit data are removed.handoff?; provider/rootrouteKey?; rootbeforeInitialPageinitialPageEventis removed. A root with prepared replay bypassesbeforeInitialPage; a browser-owned callback root without replay requiresrouteKeyand lazybuildPagePayload, with no separate tracker.hydrateAndTrackCurrentPage/hydrateOptimizationAnalyticsHandoffrouteKeyand lazybuildPagePayload, with optionalisCurrentto stop stale hydration; it has no browser content-resolution APIs.initialPageEventis removed. Public/static handoffs carry caller-supplied selections, not requestprofileIdorreplay; cache safety rejects private state in shared scopes. Hydration mode controls presentation, not event ownership.cookie; nativeOptimizationApiConfig.preflight/ bridgeapi.preflightdomain,expires,path,sameSite,secure); the profile cookie stays browser-readable when persisted. Globalpreflightis removed from the Swift, Android, and React Native bridge configs.Consumer DX: before and after
The examples show the integration paths changed by this branch. App-owned helpers and values are abbreviated.
Manual Node + Web SSR
Before: The server sent the page event and passed its resulting state to the browser.
After: The server previews a prepared batch for rendering; the browser commits that batch for the matching route.
Angular SSR
Before: Browser hydration consumed state plus an emit/skip instruction.
After: Angular transfers the prepared handoff alongside the SSR snapshot and admits its events through Web SDK delivery.
React Web router integration
Before: The root had a handoff, but the router tracker had no handoff input.
After: Root and tracker share the handoff, so the matching first-route replay is admitted together and later routes use ordinary tracking.
Next.js App Router
Before: Before-page browser work required a callback-bound root injected into the server request family.
After: Request-scoped initial events are prepared with the server page preview; the request tracker receives the handoff automatically.
Next.js Pages Router
Before: The server committed the first page and the browser tracker received an emit/skip instruction.
After: The helper previews prepared events, and root and tracker share the handoff for browser queue commitment.
Next.js Edge request
Before: Consumers observed a server page result and its browser skip signal.
After: Edge previews render data and returns prepared replay in the private handoff; the browser commits it after hydration.
Static and ISR public permutations
Before: A public selection handoff needed an explicit first-page emit instruction.
After: Public handoffs carry selection state without visitor replay; normal browser tracking owns the page.
React Native, iOS, and Android configuration
React Native's maintained integration moves SDK ownership into its root:
Global preflight configuration is removed from native APIs; server request APIs own preview instead:
Lines changed
Counts cover the full PR using
git diff origin/main...HEADatec4ef8ba. Churn includes blank lines. Markdown is documentation;.github/is CI; remaining files underimplementations/are reference implementations. Full-line comment markers (//,/*,*,#,<!--) elsewhere are comments. Remaining E2E paths and test files are counted separately; other source, configuration, and tooling are production code.Validation
Validation of the current changes:
pnpm --filter './packages/**' typecheckandpnpm --filter './packages/**' test:unit: passed; 1,341 tests across API Client, Core, API Schemas, Node, Web, React Native, the native JS bridge, React Web, and Next.js. Core has 422 tests and Web has 353, including the 11 new regressions.pnpm lint,pnpm format:check, andpnpm knowledge:check: passed. Lint reports two existing deprecation warnings. Commit hooks passed; push hooks reran Core/Web typechecks and unit suites successfully.pnpm build:pkgs,pnpm size:report, andpnpm size:check: passed across the package graph. No bundle budget changes were needed for the runtime correctness fixes.pnpm test:e2e:web-sdk navigation.spec.ts live-updates.spec.ts offline-queue-recovery.spec.ts: 30 passed, 6 skipped across Chromium, Firefox, and WebKit. The skips are preview-panel cases disabled by the build configuration.pnpm test:e2e:node-sdk+web-sdk displays-identified-user-variants.spec.ts offline-admission.spec.ts: 24 passed across the three browsers, covering prepared replay, cookie/profile continuity, delayed/failed delivery, capacity rejection, FIFO order, and concurrent flush completion.