Session replay
Replay re-drives a captured session in your own scene: camera pose is applied directly, and pointer / mesh / custom / input events are surfaced to callbacks so you can draw a cursor, highlight a mesh, or annotate a timeline.
Option A — npm
Section titled “Option A — npm”npm install @uptimizr/replayimport { fetchSessionEvents, ReplayPlayer } from "@uptimizr/replay";import { createBabylonReplayDriver } from "@uptimizr/replay/babylon";
const events = await fetchSessionEvents({ endpoint, apiKey, sessionId });const driver = createBabylonReplayDriver({ scene, onPointer: (screen, hitPoint, hitMesh, type) => { /* draw cursor / flash */ }, onMeshInteraction: (mesh, kind) => { /* highlight */ }, onCustom: (name, props) => { /* timeline marker */ }, onInputAction: (input, ts) => { /* input.action / input.source / input.code / input.button */ }, onLifecycle: (event, ts) => { /* viewport_resize / focus_change / context_lost / … */ }, onError: (error, ts) => { /* mark a runtime_error (only if captureErrors was on) */ },});
const player = new ReplayPlayer(events, driver, { speed: 1 });player.play();// player.pause(); player.seek(ms); player.stop();import { fetchSessionEvents, ReplayPlayer } from "@uptimizr/replay";import { createThreeReplayDriver } from "@uptimizr/replay/three";
const events = await fetchSessionEvents({ endpoint, apiKey, sessionId });// three has no scene.activeCamera, so `camera` is required.const driver = createThreeReplayDriver({ scene, camera, // required onPointer: (screen, hitPoint, hitMesh, type) => { /* draw cursor / flash */ },});
const player = new ReplayPlayer(events, driver, { speed: 1 });player.play();ReplayPlayer is deterministic — seeking backward resets the driver and replays from the start.
player.durationMs gives the total length.
Replaying scene actors
Section titled “Replaying scene actors”To replay scene actors
(node_transform), pass a nodes map from each recorded nodeId to the engine node to
drive, and/or an onNodeTransform callback to observe every sample:
const driver = createBabylonReplayDriver({ scene, nodes: { "npc-guard": () => scene.getMeshByName("Guard_root"), // resolver, name, or ref }, onNodeTransform: (sample, ts) => { /* sample.nodeId / sample.boneId? / sample.position / sample.rotation / sample.scale? */ },});The Babylon, three, and PlayCanvas drivers re-apply Tier-1 root transforms and Tier-2 skeleton bones
(matching each bone by name on the node’s skeleton). The babylon-lite driver drives the Tier-1 root
and forwards bone samples to the callback only. Unknown nodeId / boneId are skipped without error
(forward/back-compatible).
Loading a scene backdrop
Section titled “Loading a scene backdrop”Replay normally re-drives into the scene you already have. When you only have the captured stream and
no scene to host it — a hosted drag-and-drop viewer, say — load an arbitrary asset as a backdrop
first, then replay over it. The Babylon helper accepts a URL or a dropped File:
import { loadSceneBackdrop } from "@uptimizr/replay/babylon";
const backdrop = await loadSceneBackdrop(scene, urlOrFile); // ".glb" / ".gltf"console.log(`${backdrop.meshes.length} meshes added`);
// swap one dropped model for the next:backdrop.dispose();It returns a handle ({ rootNodes, meshes, container, dispose() }) whose dispose() removes
everything it added and releases the GPU resources. The default loader lazily imports Babylon’s
glTF SceneLoader, so the lean replay path never pulls it in unless a backdrop is requested; pass
options.load to supply your own loader or options.pluginExtension to force a parser. Actor /
subtree nodes from the loaded model re-drive exactly like any other scene node (see above).
Option B — <script> tag
Section titled “Option B — <script> tag”The global build exposes window.UptimizrReplay, with a one-call replayInScene convenience that
fetches and plays a session:
const r = document.createElement("script");r.src = "https://cdn.jsdelivr.net/npm/@uptimizr/replay/dist/uptimizr-replay.global.js";r.onload = () => { UptimizrReplay.replayInScene({ scene, endpoint: "https://collect.example.com", apiKey: "your-project-api-key", sessionId: "<copy from the dashboard Sessions table>", backdropUrl: "https://example.com/room.glb", // optional — load a model first debug: true, // log fetch/play progress to the console });};document.head.appendChild(r);replayInScene starts playback immediately — it does not wait for the scene to be “ready”, so call it
once scene exists and has an activeCamera. It always logs a concise summary and warns about the
common “nothing happens” causes: an empty session, a session with no camera_sample events (camera
won’t move), or a scene with no active camera. (pnpm playground prints this snippet pre-filled and
serves the bundle at /uptimizr-replay.global.js.)
backdropUrl loads a .glb/.gltf into the scene before replay. To keep the global bundle from
shipping a second copy of Babylon’s SceneLoader, it reuses the host page’s loader: expose Babylon
as window.BABYLON (with LoadAssetContainerAsync and a glTF loader registered) or pass an explicit
loadBackdrop callback. When no loader is found it warns and replays without a backdrop.
Troubleshooting
Section titled “Troubleshooting”403from the events endpoint — raw-session retention is off (ENABLE_RAW_SESSION_RETENTION).200with 0 events — usually an API-key / project mismatch: reads are scoped to the key’s project, so a valid session id looked up with another project’s key returns nothing. Copy the session id and the API key from the same dashboard project.- Camera doesn’t move — the session has no
camera_sampleevents.
In the dashboard: replay + live follow
Section titled “In the dashboard: replay + live follow”The dashboard’s Session replay birdview is the same engine, no code required. Open a session from the Sessions table to scrub its camera path, interaction rays, and moving scene actors, with a colour-coded event timeline you can click to seek. Each tracked actor is drawn as one labelled marker per root — a subtree actor (an articulated character) shows a single dot for “where it is” rather than a marker per joint; its full per-joint motion still reconstructs when you replay it in your own scene.
When the session is live right now (it has produced an event within the active-now window), the same window switches to live follow: new camera moves and interactions stream in and the timeline grows in real time. A ● LIVE control pins the playhead to the live edge; scrub back (or press Play) to review what already happened, then press ● LIVE to jump back to the edge. Live follow uses the same raw-retention gate as replay — with retention off, the window shows nothing to follow.
Load your model into the birdview
Section titled “Load your model into the birdview”By default the birdview draws a wireframe box per registered proxy
mesh so the recorded motion reads against your scene’s rough shape. To
see the session re-driven over the real geometry, use Load model (.glb) under the timeline and
pick a .glb/.gltf. The model loads as a backdrop and replaces the
wireframe boxes; the camera path, interaction rays, and actor markers keep re-driving over it.
Replace model swaps in a different file and Remove model restores the boxes. The model is held
only in your browser for the current view — nothing is uploaded.