Single-Page Apps

Episode 6: when the page never reloads, the SDK re-runs the whole pipeline on every URL change and keeps every invariant true

Guide~3 min read

Copy this guide and your current settings as a prompt for your AI agent.

Preview agent promptSee the guide, code and settings included when you copy.

Every episode so far assumed a page load. In a single-page app there is only ever one. The URL changes, the view changes, the visitor feels like they navigated, and none of the browser events that drive episodes 1 to 5 ever fire again. So the SDK has to notice, and re-run everything itself.

Watch: 3m 31s — Runtime Walkthrough, episode 6: two watchers, catching the navigation, rebinding metrics, the loop guard, one frame then abort, the frame cache, and re-running the pipeline
On this page
  1. 01Two watchers
  2. 02Catching the navigation
  3. 03Rebinding metrics
  4. 04The loop guard
  5. 05One frame, then abort
  6. 06The frame cache
  7. 07Re-running the pipeline
  8. 08What stays true
  9. 09Where in the code

01Two watchers

The SDK installs two watchers, both only when the project enables them (project.settings.mutation_observer_active): a MutationObserver on document.body watching the whole subtree, which keeps metrics bound to DOM that your framework just replaced, and a URL observer, which is the real navigation signal.

02Catching the navigation

There is no navigation event in a single-page app, so the SDK makes one (sdk-js/src/router.ts). It patches history.pushState and replaceState, calling the original first and checking the URL on the next tick. It listens for popstate (the back button) and hashchange. Four hooks, one question: did the URL actually change? Even a query parameter counts: ?step=1 to ?step=2 is a navigation.

03Rebinding metrics

When your framework swaps the DOM, the elements your metrics were bound to are gone. Every child-list change schedules rebindMetrics, debounced to 100 ms because a render can fire hundreds of mutations. Rebinding is idempotent: it re-stamps coframe-metric-ids on whatever is on the page now and is safe to run as often as it likes.

04The loop guard

The SDK changes the DOM, the observer sees the change, the SDK changes the DOM again. So there is a counter: after 5,000 mutation callbacks between navigations the observer disconnects itself and logs mutation_observer_loop_protection. It resets on every URL change. A runaway page degrades to no rebinding, never to a frozen tab.

05One frame, then abort

When the URL changes, the SDK waits exactly one animation frame, long enough for the new view to settle. Then it aborts whatever it was fetching. A visitor clicking through three tabs quickly fires three navigations and only the last one is allowed to finish; the abort signal is checked twice more before anything is applied.

06The frame cache

Cache first. Every page’s frames are cached by normalized URL for 60 seconds (sdk-js/src/frameCache.ts). Back and forward hit that cache and apply instantly with no network. A miss fetches from the edge and caches the result. If the fetch fails, the SDK falls back to the project baked into the bundle, so the page still gets variants.

07Re-running the pipeline

Then it re-runs everything: assignment for this URL (episodes 1 and 4), a fresh session id and the pair written again (episode 5), variants applied, the page revealed. The only thing that does not repeat is the personalization headers: device, country, bot. Those were computed at the edge on the first load and ride along unchanged.

08What stays true

  • One pageview, one session, one exposure, even without a page load.
  • The same token, so the same bins, so the same seats.
  • Consent still gates only sending.
  • The reveal still cannot be skipped.

The page never reloaded, and the measurement never noticed.

09Where in the code

File What to look for
sdk/sdk-js/src/init/modifyPage.ts setupSPANavigation, the 100 ms debounce, the 5,000-callback guard, observeUrlChanges handling with AbortController
sdk/sdk-js/src/router.ts observeUrlChanges, the history patches, urlsMatch
sdk/sdk-js/src/frameCache.ts CACHE_TTL_MS, getCachedFrames, cacheFrames
sdk/sdk-js/src/metrics.ts rebindMetrics

Content and video lessons from Coframe documentation. Code examples are not executed on this page.

BACK TOHow Coframe Works

Documentation

Open Coframe App

Search documentation

Search all guides and references↑ ↓ Navigate · Esc Close