Skip to content

Multi-scene experiences

Real 3D apps rarely show one fixed scene. A game moves the player between levels; a configurator or product viewer swaps models in and out; an architectural walkthrough crosses rooms or floors. Uptimizr treats all of these as scenes within one session — you keep a single tracker running and tell it when the active scene changes.

Stopping and restarting tracking on every level/model change would fragment the visit into disconnected sessions and break replay continuity (the timeline must stay ordered and sessionId-keyed). Instead, keep one trackScene(...) client alive for the whole visit and mark the transitions. Each scene becomes a segment of the same session, so you can:

  • compare heatmaps, dwell, and performance per level / per model, and
  • still replay the entire visit end to end as one ordered stream.

Set the initial scene id when you start, then call setScene(id) every time the active scene changes.

const client = trackScene(scene, {
projectId,
endpoint,
meta: { sceneId: "level-1" }, // initial scene / area / model
});
// Later, when the next scene loads:
client.setScene("level-2");

setScene emits an ordered scene_change marker and stamps the new sceneId onto every subsequent event, so gaze/click heatmaps, mesh interactions, performance, and custom events are all attributable to the scene that was active when they happened. Filter any read endpoint by the scene query param to get per-scene results.

Call setScene from your level-loaded hook, after the new level is live:

async function loadLevel(id: string) {
await engine.loadLevel(id);
client.setScene(id); // e.g. "level-3", "boss-arena"
}

Use stable, low-cardinality ids (the level’s slug), not a per-playthrough guid — that keeps aggregates meaningful across visitors.

Most read endpoints accept a scene filter, so you can scope an aggregate to a single level/model/area. Reads are scoped to the project behind your x-api-key — there is no projectId query param:

Terminal window
curl "$ENDPOINT/api/v1/meshes/dwell?scene=level-2" \
-H "x-api-key: $KEY"

Omit the filter to see the whole session across all scenes, and use /api/v1/scenes to list the distinct scenes that have activity. See the query endpoints for the full parameter list.

A sceneId names a whole area; a region names a place inside one — “the entrance”, “the checkout counter”. A region is a labelled axis-aligned box in the scene’s world space, stored next to the scene proxy, and it does two things: it gives dashboards, summaries and agents a shared vocabulary instead of raw coordinates, and it lets any spatial endpoint be drilled into that place by name with ?region=<id>.

Declare a scene’s regions once (they replace the stored set, so leaving one out removes it):

import { registerRegions } from "@uptimizr/sdk-core";
await registerRegions(
"lobby",
[
{ id: "entrance", label: "Entrance", bounds: [-5, 0, -5, 5, 3, 0] },
{ id: "counter", label: "Checkout counter", bounds: [-1, 0, 1, 1, 2, 3] },
],
{ endpoint: ENDPOINT, apiKey: process.env.UPTIMIZR_API_KEY! },
);

Then read any spatial aggregate for just that place:

Terminal window
curl "$ENDPOINT/api/v1/heatmaps/world?scene=lobby&region=counter" \
-H "x-api-key: $KEY"

Regions may overlap (an enclosing hall and a counter within it), so a point can belong to several. Where they do, membership is every containing region and the smallest by volume is the one reported — a click on the till is “in the counter”, not “in the hall”.

Regions are the vocabulary an agent answers in. Ask for a summarised spatial query (format=summary) on a scene that has regions and a proxy, and every hotspot comes back labelled with the place it is in and the object it sits on, instead of a voxel index:

// GET /api/v1/heatmaps/world?scene=lobby&format=summary — one cluster
{
"centroid": [7, 2, 11],
"region": "counter",
"regions": ["counter", "shop-floor"],
"nearestMesh": "checkout_button",
"distance": 0,
"drill": { "region": "counter" },
}

The reading sentence uses them too — “the densest spans 3x2x3 voxels on checkout_button in region counter — and drill.region hands back the region id, so the follow-up query (“show me just that place”) is one parameter away. Without regions the same answer is a coordinate; without a proxy nearestMesh is null and the summary says so in its caveats. The dashboard’s 3D panels use the same pair to name a voxel on hover. See Labelled spatial clusters.

Registering regions is an authenticated write — like the scene-proxy upload it uses your project API key, which must hold the annotate capability (uptimizr new-key <projectId> --capabilities annotate), so do it from a build script, a server-side route, or a developer-only path, never from a public bundle. There is also a CLI, which writes straight to the store and needs no key: uptimizr regions set lobby --file regions.json. See Scene regions.

The 3D panels (world heatmap, gaze heatmap, click rays) draw your scene geometry as a backdrop. By default — with the Scene filter on “All scenes” — the dashboard shows the whole building: it merges every active section’s scoped proxy into one backdrop, so every level and far-flung area is present at once. Pick the Scene dropdown in the filter bar to focus a single section — that anchors the backdrop to just that area’s geometry and scopes the heatmap data to it.

Showing the whole building is deterministic: the full multi-level world renders immediately, rather than sections appearing one at a time as a live visitor crosses boundaries. When the live layer is on, crossing into a newly-active section simply refreshes the merge so the new area joins the backdrop promptly; the heatmap data stays across all scenes, so the aggregate isn’t re-scoped underneath you. Session replay follows the same rule — it renders the whole building for the session’s time window so the entire walkthrough plays against full geometry. A mesh that bridges two areas (a ramp or stairway) belongs to one section’s proxy but is always visible in the merged whole-building view.

Because the transitions are recorded as ordered scene_change markers in the same stream, session replay re-drives the visit through every scene change in order — your replay target can switch levels/models in lockstep by reacting to the scene_change events.