This is the abridged developer documentation for Uptimizr
# Uptimizr Documentation
> Analytics for 3D web. Capture what users look at, click, and miss inside any 3D scene — then replay it.
## What is Uptimizr? [Section titled “What is Uptimizr?”](#what-is-uptimizr) Uptimizr is an open-source, self-hostable analytics platform for 3D scenes. Drop a connector into your Babylon.js, three.js, PlayCanvas, react-three-fiber, or A-Frame app and it captures the spatial signals flat web analytics can’t — gaze, clicks, mesh interactions, performance — then aggregates them into heatmaps, rankings, and full session replay. Instrument in minutes Install one connector, call `trackScene`, and heatmaps start filling in. See the [Quickstart](/docs/quickstart/). Every major engine One event schema across Babylon.js, three.js, PlayCanvas, R3F, and A-Frame. See [Connectors](/docs/connectors/overview/). Privacy-first Cookieless, no client persistent IDs, no PII by default. See [Privacy & configuration](/docs/deploy/privacy/). Self-host anywhere A single collector with an embedded store — zero infrastructure to start. See [Deploy](/docs/deploy/collector/). Try it in your browser The [live demo](https://demo.uptimizr.com) runs the playground and dashboard side-by-side with the analytics database in your browser — no account, no server, nothing uploaded. HTTP API Ingest events and query heatmaps, sessions, and performance over a simple, key-authenticated REST API. See the [API reference](/docs/api/overview/). Built for agents Every read is declared in a semantic metric registry — with units, limits and caveats — and generated into MCP tools, an OpenAPI 3.1 document and bounded `summary` results. See [Building agents on Uptimizr](/docs/guides/agents/).
# Project context
> GET /api/v1/context — the one read that tells an agent what your project's scenes, regions, custom events and capture channels actually are.
An agent that has never seen your project knows the *shape* of this API — every metric, every parameter, every row schema, from the [metric registry](/docs/api/query/). It knows nothing about *your* project: what the scenes are called, which regions have names, which capture channels are on, what your application calls its own events, whether raw retention is enabled, or how fresh the data is. `GET /api/v1/context` answers all of it in one read. It takes a `query`-capable `x-api-key`, it is bounded (well under 16 KB for a typical project), and the collector caches it per project for about 30 seconds.
```bash
curl -H "x-api-key: $KEY" "https://collect.example.com/api/v1/context"
```
## The document [Section titled “The document”](#the-document)
```jsonc
{
"project": {
"id": "…",
"store": "duckdb", // the COLLECTOR_STORE engine behind this collector
"schemaVersion": "1.0", // event wire format
"collectorVersion": "2.1.0",
},
"dataQuality": {
"lastEventAt": 1757600000000, // epoch ms, or null for an empty project
"sessions24h": 91,
"events24h": 18023,
"retention": { "rawSessions": false },
},
"capture": {
"channels": {
"camera_sample": { "seen": true, "events28d": 41203 },
"mesh_visibility": { "seen": false },
},
},
"scenes": [
{
"id": "lobby",
"label": "Main Lobby",
"regions": [{ "id": "counter", "label": "Checkout counter" }],
"proxy": true,
"events28d": 12043,
},
],
"vocabulary": {
"customEvents": [
{
"name": "add_to_cart",
"count28d": 311,
"sessions28d": 180,
"props": { "sku": "string", "qty": "number" },
},
],
"meshes": { "count": 63, "top": ["checkout_button", "door_left"] },
"inputActions": ["jump", "sprint"],
},
"definitions": { "funnels": [], "segments": [], "glossary": [] },
"annotations": { "recent": [] },
"metrics": {
"available": ["top_meshes", "…"],
"disabledByCapture": ["mesh_dwell"],
},
"window": { "since": 1755008000000, "until": 1757600000000 },
"generatedAt": 1757600000000,
}
```
## How to read it [Section titled “How to read it”](#how-to-read-it) | Field | What to do with it | | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `scenes[].id` | The exact value for the `scene=` filter. Never infer one. | | `scenes[].regions[].id` | The exact value for the `region=` filter — a named place instead of a hand-drawn box. | | `vocabulary.customEvents[].name` | The exact name for a funnel step or a variant predicate. | | `vocabulary.customEvents[].props` | Observed prop keys with a coarse type. Prop **values** are never reported. | | `capture.channels` | One entry per canonical event type, so an absence is never ambiguous. | | `metrics.disabledByCapture` | These metrics return empty because every capture channel that feeds them is off. Say the channel is off — do not report the zero as a finding. | | `dataQuality.retention` | `rawSessions: false` means session timelines and replay are unavailable **by design** (privacy, opt-in), not broken. | | `dataQuality.lastEventAt` | How stale the answer you are about to give is. `null` means the project has recorded nothing. | Everything in the document is aggregate and project-scoped — nothing a `query` key could not already read through the query API. ## Bounds [Section titled “Bounds”](#bounds) The document is capped so it always fits in a model’s context window: at most 40 scenes (20 regions each), 25 custom events (40 prop keys each), 10 top meshes, 25 input actions, 10 annotations and 50 glossary entries. `definitions.funnels` / `definitions.segments` and `annotations.recent` are present and empty until the metadata store holds any. The 28-day figures are computed over `window`. ## Custom-event vocabulary [Section titled “Custom-event vocabulary”](#custom-event-vocabulary) The `vocabulary.customEvents` block is also a metric in its own right, so it is a generated agent tool (`custom_event_vocabulary`) and takes the usual `since` / `until` / `scene` / `limit` / [`format`](/docs/api/query/) parameters:
```bash
curl -H "x-api-key: $KEY" \
"https://collect.example.com/api/v1/vocabulary/custom-events?since=$SINCE&limit=50"
```
```json
[
{
"name": "add_to_cart",
"count": 311,
"sessions": 180,
"props": { "sku": "string", "qty": "number" }
}
]
```
`count` and `sessions` are exact over the range. `props` is discovered from the **20 most recent events per name**, so treat it as a vocabulary hint rather than a schema: a key that stopped being emitted long ago will be absent, and a rarely-sent optional key can be missed. A key seen with more than one JSON kind is reported as `"mixed"`; `"null"` means every sampled value was null. ## Clients [Section titled “Clients”](#clients) * **MCP** — the same document is the `uptimizr://context` resource. Every curated prompt tells the agent to read it first. * **The in-browser assistant** — `useAssistant` fetches it when the collector connection is established and injects a compact rendering into the system prompt. A collector too old to serve the endpoint is not an error: the assistant simply runs without it. * **Anything else** — `renderContextForPrompt()` in `@uptimizr/agent-core` turns the document into the same short prompt block, if you are building your own loop.
# Ingestion
> The collector's batched event ingestion endpoint.
The SDK delivers events to a single ingestion endpoint. You normally never call this directly — a connector’s `trackScene` (via `@uptimizr/sdk-core`) batches events and sends them for you — but the contract is documented here for custom transports and self-hosting. ## `POST /api/v1/collect` [Section titled “POST /api/v1/collect”](#post-apiv1collect) Batched events. The SDK uses `navigator.sendBeacon` (credentialed) and falls back to `fetch` with `keepalive`. Authenticate with the project API key. | Property | Value | | ------------ | ------------------------------------------------------------ | | Method | `POST` | | Path | `/api/v1/collect` | | Auth | `x-api-key: ` | | Content-Type | `application/json` | | Body | A batch of events, each validated against `@uptimizr/schema` | Events are validated against the Zod schemas in `@uptimizr/schema` at the edge; invalid batches are rejected. Every event carries the shared envelope (ordered, timestamped, keyed by `sessionId`). ## Delivery behavior [Section titled “Delivery behavior”](#delivery-behavior) * **Batching.** `@uptimizr/sdk-core` flushes when the batch reaches `batchSize` (default `20`) or after `flushIntervalMs` (default `5000`). * **Beacon first.** Sends use `navigator.sendBeacon` so events survive page unload, falling back to `fetch({ keepalive: true })`. * **Privacy.** No cookies and no persistent client id; the `sessionId` is in-memory only. ## Scene registry (write) [Section titled “Scene registry (write)”](#scene-registry-write) A scene can register a **proxy** of its geometry (per-mesh bounding boxes) so the dashboard’s 3D heatmaps draw against a recognizable backdrop. Writes use the same project API key. | Method | Path | Purpose | Body | | ------ | ---------------------------------------- | -------------------------------------------------------------------- | ------------------- | | `PUT` | `/api/v1/scenes/:sceneId/representation` | Register/replace a scene proxy. `proxy.sceneId` must match the path. | `{ proxy, label? }` |
```bash
curl -X PUT -H "x-api-key: $KEY" -H "content-type: application/json" \
-d '{"proxy": , "label": "Main Lobby"}' \
"https://collect.example.com/api/v1/scenes/lobby/representation"
```
The proxy is produced client-side by `scanSceneProxy(scene, { sceneId })` in `@uptimizr/babylon`.
# Metadata endpoints
> Annotations, the project glossary and saved analyses — the collector's only write path besides ingestion, behind the `annotate` capability.
Numbers alone do not carry what a team knows. These three endpoints are where that knowledge goes: a note on a spike, a definition of a name only your project uses, a question worth re-asking with the answer it got. | Table | What it holds | | ---------------- | --------------------------------------------------------------------------------------------- | | `annotations` | Notes pinned to the project, a scene, a mesh, a region, a metric, or a period of time. | | `glossary` | What a name means **in this project** — a mesh name, a scene id, a custom event, a shorthand. | | `saved_analyses` | A titled question plus the conclusion drawn from it, so it need not be re-derived. | ## Endpoints [Section titled “Endpoints”](#endpoints) | Method | Path | Purpose | Capability | | -------- | ------------------------- | -------------------------------------------------- | ---------- | | `GET` | `/api/v1/annotations` | The project’s notes, newest first. | `query` | | `POST` | `/api/v1/annotations` | Leave a note. Answers `201` with the stored row. | `annotate` | | `DELETE` | `/api/v1/annotations/:id` | Remove a note. `204`, or `404` if it is not there. | `annotate` | | `GET` | `/api/v1/glossary` | The whole glossary, ordered by term. | `query` | | `PUT` | `/api/v1/glossary/:term` | Define (or redefine) a term — idempotent. | `annotate` | | `DELETE` | `/api/v1/glossary/:term` | Undefine a term. | `annotate` | | `GET` | `/api/v1/analyses` | Saved analyses, newest first. | `query` | | `POST` | `/api/v1/analyses` | Save an analysis. Answers `201`. | `annotate` | | `DELETE` | `/api/v1/analyses/:id` | Remove a saved analysis. | `annotate` |
```bash
# A note about a period of time
curl -X POST -H "x-api-key: $KEY" -H "content-type: application/json" \
-d '{"targetKind":"window","since":1757000000000,"until":1757003600000,
"text":"CDN incident — ignore the load-time spike here."}' \
"https://collect.example.com/api/v1/annotations"
# What a mesh name means in this project
curl -X PUT -H "x-api-key: $KEY" -H "content-type: application/json" \
-d '{"meaning":"the till cluster by the exit"}' \
"https://collect.example.com/api/v1/glossary/checkout%20counter"
# A question worth keeping
curl -X POST -H "x-api-key: $KEY" -H "content-type: application/json" \
-d '{"title":"Lobby FPS after the lighting change",
"query":{"metric":"perf_summary","scene":"lobby"},
"conclusion":"p50 fell from 58 to 41 on integrated GPUs."}' \
"https://collect.example.com/api/v1/analyses"
```
## An annotation’s target [Section titled “An annotation’s target”](#an-annotations-target) `targetKind` is one of `project`, `scene`, `mesh`, `region`, `metric` or `window`. The four middle ones name something and need a `targetId` (the scene id, mesh name, region id or metric id); `window` needs a `since`; `project` is a standing note about everything. `since`/`until` are epoch milliseconds. On a read they are an **overlap** filter — a note matches when its period intersects the requested window, and a standing note always matches, because it is about the project rather than about a moment. ## Who wrote it [Section titled “Who wrote it”](#who-wrote-it) Every stored row carries `authorKind` (`user` or `agent`) and `authorKeyId` (the key’s row id, never the key itself). The collector decides `authorKind` from the calling client, never from the payload: a request carrying `x-uptimizr-client: dashboard` — a person clicking in a first-party UI — is `user`; everything else holding an `annotate` key, including the in-browser assistant writing up its own answer, is `agent`. ## Bounds [Section titled “Bounds”](#bounds) Free text is capped at the boundary — `text` ≤ 2 000 characters, `meaning` ≤ 500, `title` ≤ 120, `conclusion` ≤ 4 000, and a saved `query` document ≤ 8 000 serialized characters. Each project holds at most 500 annotations, 200 glossary terms and 200 saved analyses. A write past a cap answers `409`: the payload was fine, the project is full — delete something and retry. `query` on a saved analysis is an **opaque JSON object**: the collector stores it and does not interpret it. Put the endpoint and filters that produced the answer in it, so the next reader can re-run what you actually ran. ## Privacy [Section titled “Privacy”](#privacy) These rows are written by your own operators and agents, not captured from visitors, so they are the one place in the collector that is not machine-bounded to non-PII. They are project-scoped, readable only with that project’s key, bounded in size and count, and audited — but do not paste personal data into them. See [Privacy & configuration](/docs/deploy/privacy/). ## From an agent [Section titled “From an agent”](#from-an-agent) `@uptimizr/mcp` exposes the same three writes as the MCP tools `annotate`, `define_term` and `save_analysis`, plus `list_annotations`, `list_glossary` and `list_analyses`. They are registered **only** when the configured key holds `annotate` — the server asks `GET /api/v1/whoami` once at start-up — so a read-only key yields a read-only server. See [MCP server](/docs/guides/mcp/) and [Building agents on Uptimizr](/docs/guides/agents/).
# HTTP API — overview & auth
> How to authenticate against the collector's ingestion and query API, and the shared parameters.
The collector exposes one **ingestion** endpoint and a set of **read** endpoints. All requests authenticate with a **project API key** in the `x-api-key` header; the project is resolved from the key server-side, so a client can only ever read or write its own data.
```bash
KEY=… # a project API key
BASE=https://collect.example.com # or http://localhost:8787 locally
curl -s -H "x-api-key: $KEY" "$BASE/api/v1/sessions?limit=20"
```
* No key / unknown key → `401`. * There is no cross-project query — a caller can only ever read its own data. * Do **not** add a `projectId` query param to “widen” a query; it is ignored. ## API keys and capabilities [Section titled “API keys and capabilities”](#api-keys-and-capabilities) A key is not “all or nothing”: it carries a **set of capabilities**, so a key you hand to an agent can be scoped to exactly what that agent needs (ADR 0051 §7). | Capability | Grants | | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `query` | The aggregate analytics API — every endpoint on the [query reference](/docs/api/query/) — plus the scene registry reads, the live token exchange, `whoami` and the audit trail. | | `query:raw` | Raw **per-session** streams only: `GET /api/v1/sessions/:id/events` and the live per-session follow `GET /api/v1/live/sessions/:id`. | | `annotate` | The project **metadata** write path (today: [`PUT /api/v1/scenes/:sceneId/regions`](/docs/api/query/#scene-regions)). Never events — events stay append-only. | | `ingest` | Reserved for server-side write paths. Public ingestion is keyless by design, so issued keys are normally read keys. | `uptimizr init` and `uptimizr new-project` mint one **owner** key — `query`, `query:raw` and `annotate` — because it is the operator’s own key, driving the dashboard, replay, live follow and scene regions. Every other key comes from `uptimizr new-key`, which defaults to `query` (read-only) — the key to hand an agent or MCP client:
```bash
npx -p @uptimizr/collector-server uptimizr new-key \
--capabilities query --label "mcp-agent"
```
* A key that is authenticated but **lacks** the capability a route requires → `403`, not `401`. The body names the missing permission (`api key not permitted to read raw session data`, …). * `query:raw` is honoured only when the collector also runs with `ENABLE_RAW_SESSION_RETENTION`; both halves are required, and either one missing is a `403`. ### `GET /api/v1/whoami` [Section titled “GET /api/v1/whoami”](#get-apiv1whoami) Ask what the calling key actually holds, so an agent can register only the tools its capabilities permit instead of discovering a `403` mid-conversation. It needs `query` and reports the key’s **id**, never the key itself:
```jsonc
{
"projectId": "3f2a…",
"keyId": "9c41…",
"capabilities": ["query", "query:raw"],
"label": "replay",
"rateLimit": { "max": 600, "windowMs": 60000 },
"rateLimitSource": "default", // or "key" when the key carries its own budget
}
```
### Per-key rate limits [Section titled “Per-key rate limits”](#per-key-rate-limits) `uptimizr new-key … --rate-limit-max 120 --rate-limit-window-ms 60000` gives a key its own request budget, bucketed on the **key id** rather than the client IP — so one busy agent cannot spend another’s allowance. Keys without one fall back to the collector’s `COLLECTOR_RATE_LIMIT_MAX` / `COLLECTOR_RATE_LIMIT_WINDOW_MS` defaults (600 requests / 60 s). `whoami` always reports the budget actually in force. Ingestion is untouched: it is keyless and keeps its own `COLLECTOR_INGEST_RATE_LIMIT_*` budget. ### Agent audit log [Section titled “Agent audit log”](#agent-audit-log) Every authenticated request made with a key that is not the dashboard’s own session is recorded — `{ id, projectId, keyId, at, surface, toolOrPath, params, rowCount, durationMs, status }` — and `GET /api/v1/audit` (a `query` key, newest first, `since` / `until` / `limit`) reads it back. Refusals are recorded too, which is precisely what a project owner wants to see. Rows carry the route **pattern** rather than the URL, bounded and credential-redacted params, and never any key material. Writes happen after the response is flushed, so the log can never block or fail a request. Rows older than `AUDIT_RETENTION_DAYS` (default `30`) are swept. The dashboard identifies itself with `x-uptimizr-client: dashboard` and is skipped by default so its panel refreshes do not bury agent activity; `AUDIT_DASHBOARD_REQUESTS=1` records everything. That is a volume filter, not a security boundary. ### Machine-readable description [Section titled “Machine-readable description”](#machine-readable-description)
```bash
curl https://collect.example.com/api/v1/openapi.json
```
`GET /api/v1/openapi.json` serves an **OpenAPI 3.1** document for the whole read API — no key required, because it is documentation and contains no project data. It is generated from the same semantic metric registry the endpoints are, so it lists exactly the aggregations *that* collector can compute, with the schema that actually validates each parameter and the `apiKey` security scheme declared. See the [MCP guide](/docs/guides/mcp/#openapi). ## Shared parameters [Section titled “Shared parameters”](#shared-parameters) Binned and aggregate endpoints accept these (coerced and bounded by Zod at the edge — out-of-range values are rejected with `400`, not clamped): | Param | Unit / type | Notes | | ------------ | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- | | `since` | epoch **milliseconds** | Range start. Omit for the server default window. | | `until` | epoch **milliseconds** | Range end. | | `bins` | `1`–`500` | Bin count for binned heatmaps. | | `limit` | `1`–`1000` | Result cap on list/top endpoints (no cursors beyond this). | | `cellSize` | `> 0`, `≤ 1000` | Voxel size in world units for world / click-ray heatmaps. | | `interval` | **seconds** | Bucket width for `timeseries`. | | `scene` | id | Scope to one scene/area/level (the value passed to `setScene`). | | `source` | enum | Input-source filter: `mouse`, `touch`, `stylus`, `pen`, `xr-controller`, `hand`, `gaze`, `transient`, `other`. | | `session` | id | Scope an aggregate to a single session. | | `cameraMode` | `viewer` \| `first-person` | Restrict to orbit (`viewer`) or free/walkable (`first-person`) camera sessions. | | `format` | `full` \| `table` \| `summary` | Result envelope, not a filter. `full` (the default) is the bare rows; see [result formats](/docs/api/query/#result-formats). | Common pitfalls * **`since`/`until` are milliseconds; `interval` is seconds.** Mixing units is the most common “empty result” cause. * **Aggregate columns are JSON numbers on every store** — no client-side coercion needed. But a `null` means “no samples”, not `0`: a one-row summary is still returned over a range that matched nothing, with its aggregate columns `null`. * **Empty (200 + no rows) is not an error** — usually the time window or a filter excluded everything. Widen `since`/`until` and drop filters to confirm data exists, then narrow. ## Prefer the MCP server for agents [Section titled “Prefer the MCP server for agents”](#prefer-the-mcp-server-for-agents) For an AI agent (or any MCP client) that should read analytics, use the read-only [`@uptimizr/mcp`](https://github.com/RaananW/Uptimizr/tree/main/oss/packages/mcp) server instead of hand-rolling HTTP calls. It wraps each read endpoint as a typed, GET-only tool, configured with `UPTIMIZR_COLLECTOR_URL` + `UPTIMIZR_API_KEY`. It exposes no ingestion, mutation, or raw-event tools.
# Concepts
> The core ideas behind Uptimizr — events, the envelope, scenes, heatmaps, sessions, and replay.
A short tour of the vocabulary you’ll meet throughout these docs. ## Events & the envelope [Section titled “Events & the envelope”](#events--the-envelope) Everything Uptimizr captures is an **event**. Every event shares a common **envelope** — it is ordered, timestamped, and keyed by `sessionId` — and a typed payload. All event shapes live once, as Zod schemas, in `@uptimizr/schema`; connectors and the collector import them rather than redefining them. This is what makes a session **replay-complete**: the ordered event stream is enough to reconstruct what happened. Events fall into two groups: * **Continuous channels** — `camera_sample`, `pointer_move`, `frame_perf`. Sampled at a configurable rate (Hz) and idle-suppressed. * **Discrete events** — `pointer_click`, `mesh_interaction`, `scene_change`, `custom`, `session_start`/`session_end`, and more. Always captured at 100%; cannot be rate-limited. ## Sessions & visitors [Section titled “Sessions & visitors”](#sessions--visitors) A **session** is one continuous visit, identified by an in-memory `sessionId` that never touches disk on the client. A **visitor** is counted with a hash computed **server-side** that rotates every day — so visits can be de-duplicated within a day without tracking anyone across days. There are no cookies and no persistent client identifiers (see [Privacy](/docs/deploy/privacy/)). ## Scenes [Section titled “Scenes”](#scenes) A **scene** is a named area, level, or view within your app. Call `tracker.setScene("level-2")` (or pass `meta.sceneId`) to segment analytics. Most query endpoints accept a `scene` filter so you can scope a heatmap to one area. Optionally, a scene can register a lightweight **proxy** of its geometry (per-mesh bounding boxes) so the dashboard can draw 3D heatmaps against a recognizable backdrop without your real assets. ## Heatmaps [Section titled “Heatmaps”](#heatmaps) Uptimizr aggregates raw spatial events into several heatmap types: * **Pointer heatmap** — 2D screen-space hover/click density. * **World heatmap** — 3D world-space pointer density, voxel-binned. * **Camera (view-direction) heatmap** — spherical bins of where the camera looked. * **Gaze heatmap** — world-space points where the audience actually looked on the geometry (opt-in). * **Click rays / flow** — clicks ray-cast into the scene, and gaze→mesh flow links. Pose inputs (XR controllers, hands, gaze) use their own pointing-ray origin; flat pointers (mouse, touch, stylus) reconstruct the ray origin on the camera’s near plane from the click’s screen position (ADR 0043), so the rays fan out the way the clicks were actually made. In **first-person / walkable** scenes that use the browser Pointer Lock API, the cursor is hidden and the aim point is the fixed crosshair at the viewport centre. The connectors detect pointer lock and report pointer/click events from screen centre (ADR 0034), so the 2D pointer heatmap naturally clusters at the centre — for those scenes read the cursor-independent **gaze heatmap**, the floor-plan position heatmap, and session trajectories instead. ## Performance [Section titled “Performance”](#performance) `frame_perf` samples carry FPS, frame time, long-frame counts, and draw metrics. The query API returns percentile aggregates (median/p50, min, avg) so a “p95 frame time” is one request away. ## Replay [Section titled “Replay”](#replay) Because the event stream is replay-complete, a recorded session can be **re-driven** inside your own scene — re-applying the visitor’s camera, pointer, and interactions (and, opt-in, scene-actor motion). Replay reads the raw, ordered event stream, which requires the collector to run with `ENABLE_RAW_SESSION_RETENTION` enabled.
# Connectors overview
> How Uptimizr connectors work, which engines are supported, and what they all capture.
A **connector** is the engine-specific adapter that observes your 3D scene and emits events. Every connector registers as a collector on `@uptimizr/sdk-core`, captures the same channels, and emits the same versioned event schema — so heatmaps, rankings, and replay behave identically no matter which renderer you use. ## Supported engines [Section titled “Supported engines”](#supported-engines) | Engine | Package | Status | Entry point | | ----------------- | ------------------------ | ------ | ---------------------------------------- | | Babylon.js | `@uptimizr/babylon` | Stable | `trackScene(scene, …)` | | Babylon Lite | `@uptimizr/babylon-lite` | Stable | `trackScene(scene, camera, canvas, …)` | | three.js | `@uptimizr/three` | Stable | `trackScene(scene, camera, renderer, …)` | | PlayCanvas | `@uptimizr/playcanvas` | Stable | `trackScene(app, camera, …)` | | react-three-fiber | `@uptimizr/r3f` | Stable | `` / `useUptimizr()` | | A-Frame | `@uptimizr/aframe` | Stable | `uptimizr` HTML component | | Unity (WebGL) | `@uptimizr/unity` | Beta | `trackUnity(…)` | | Godot (web) | `@uptimizr/godot` | Beta | `trackGodot(…)` | | Unreal (web) | `@uptimizr/unreal` | Alpha | `trackUnreal(…)` | > **Web-export engines** (Unity, Godot, Unreal) compile to WebAssembly and render into a `