# Glitchless: the full docs
> Glitchless (glitchless.gg) is open-source game observability: it shows how a game runs and plays for real players, and gives coding agents the same data over MCP so they can fix what players hit. This file holds the site's guides and the docs from https://github.com/glitchless-gg/glitchless in one place. Built 2026-10-23.
---
Source: https://www.glitchless.gg/index.md
# Glitchless: game observability and analytics for web, Unity, Godot, Unreal and GameMaker games
> Vibe debugging for your games. Glitchless shows where your game lags, crashes and loses players, then gives your coding agent the data to fix it.
Website: https://www.glitchless.gg · Source: https://github.com/glitchless-gg/glitchless (MIT) · Pricing: https://www.glitchless.gg/pricing.md · Start free: https://app.glitchless.gg · Privacy: https://www.glitchless.gg/privacy.md · Terms: https://www.glitchless.gg/terms.md
## What it does
- **Frame time.** Every frame is timed on every player's device, by level and release. Each hitch comes with its likely cause: a long script, a shader compile, garbage collection or an asset load.
- **Crashes.** Errors are grouped into issues, each with what the player did just before it.
- **Where players give up.** Level funnels, death heatmaps and rage-quit detection.
- **Findings.** Problems ranked worst first, each with its evidence and a suggested fix.
- **Vibe analytics: a dashboard built for your game.** Your coding agent asks what you want to know, adds the events that answer it with `track()` and lays out the game's own pages: lap times for a racer, weapons for a shooter, the paywall for a mobile game. Each event gets counts, breakdowns by attribute, funnels and a comparison by release.
- **Your coding agent.** The MCP server (`glitchless-mcp`) gives Claude Code, Codex, Cursor or any MCP client the same data you see. WebMCP tools in the running game let the agent load a level, reproduce a bug and measure it.
- **Bug reports from players.** A player presses F8 (or the game calls `reportBug`) and sends a note with a screenshot. Each report links to the session behind it, and several on one level become a finding.
- **Share links.** A read-only link to one board, for a dev log or a teammate without an account. It can show the last days or fixed dates, such as a jam week.
- **Source maps.** Upload a web build's source maps (`glitchless-sourcemaps`, a CLI and a Vite plugin) and errors from minified builds show the original function, file and line.
- **Retention and players now.** D1, D7 and D30 retention by the day players first came, and how many are playing right now. Retention needs install mode, which keeps a random ID on the device.
- **Revenue.** Send `track('purchase', { amount, currency, item })` and boards show revenue, revenue per player and conversion.
- **Alerts.** Discord, Slack or email when a release brings a new error, more crashes or slower frames, with the finding for your coding agent.
- **CSV export.** Every panel and table downloads as CSV.
- **Playtest bots.** `glitchless-playtest` plays the old build and the new one with the same seeds, then compares frame time, draw calls, errors and deaths. A regression exits with code 1, so it can block a pull request. Agents can run it with the `glitchless_playtest` tool.
- **Privacy.** Anonymous by default: no player IDs, no device fingerprints and no positions. Install mode (for retention) and full mode (for heatmaps and replays) are opt-in.
- **Steam Deck.** Sessions on a Deck are tagged, and `npx glitchless-deck` sends a build to your Deck, starts it and measures it there.
- **Overhead.** About 30 ns of work a frame, and one small gzipped request every 15 seconds.
Engines: Three.js (and React Three Fiber or plain WebGL), Phaser 3 and 4, Babylon.js and PlayCanvas in the browser, Unity 2022.3 LTS and Unity 6, Godot 4.5+ on desktop and mobile, Unreal Engine 5.8, and GameMaker 2024.2+. It can also send the same data to Datadog or any OpenTelemetry backend.
## A bug it found and fixed
Zoomies is a Three.js platformer. Every tennis ball the player picked up fired 800 confetti meshes, each drawn twice for its shadow, and the game never removed them, so it slowed down with every pickup.
1. Glitchless flagged it: frame-time spikes on level 2, p95 51 ms against a 16.7 ms budget, with 74% of hitches within 1.5 s of a powerup and the particle count stuck at 1,600.
2. Claude Code read that finding over MCP, reproduced it with WebMCP (3,494 draw calls a frame) and replaced the confetti with one reusable InstancedMesh.
3. Playtest bots played both builds, three runs each with the same seeds: draw calls went from 4,652 to 286 a frame, render time from 12.3 ms to 1.3 ms, and p95 frame time from 33.1 ms to 16.8 ms, which is 60 fps.
## Set it up
Paste the setup prompt into your coding agent and it does the rest: https://github.com/glitchless-gg/glitchless/blob/main/docs/agent-setup-prompt.md
Or by hand, for a web game:
```bash
npm install glitchless # the SDK
npx -y glitchless-mcp # the MCP server, added to your agent's MCP settings
npx glitchless-playtest --save baseline # save a baseline, then run `npx glitchless-playtest` after a change
```
For Phaser 3 or 4, install the same package and call `instrumentPhaser(glitchless, { game, Phaser })` from `glitchless/phaser` after creating the game.
For Babylon.js or PlayCanvas, install the same package and call `instrumentBabylon(glitchless, { engine, scene })` from `glitchless/babylon` or `instrumentPlayCanvas(glitchless, { app })` from `glitchless/playcanvas`.
For Unity 2022.3 or 6, open Package Manager, choose **Add package from git URL** and paste `https://github.com/glitchless-gg/glitchless.git?path=unity/Packages/gg.glitchless.sdk#gg.glitchless.sdk/v0.2.0`.
For Godot 4.5+, copy `godot/addons/glitchless` from the repository into your project.
For Unreal Engine 5.8, download `glitchless-unreal-0.1.0.zip` from the [glitchless-unreal 0.1.0 release](https://github.com/glitchless-gg/glitchless/releases/tag/glitchless-unreal/v0.1.0) and unzip it into your project's `Plugins` folder, then fill in Project Settings → Plugins → Glitchless.
For GameMaker 2024.2+, make the local package with `node gamemaker/tools/project.mjs package` in the repository, then import it with **Tools → Import Local Package** and call `glitchless_init` at startup.
## Questions
### How do I find performance problems in my Three.js game?
Add the Glitchless SDK. It times every frame on your players' machines, groups slow frames by level and release, and names the likely cause of each hitch. The findings page ranks the worst problems with their evidence.
### Can my coding agent see how real players experience my game?
Yes. Add the `glitchless-mcp` server to Claude Code, Codex, Cursor or any MCP client. Your agent can then triage findings, read errors with the gameplay before them, compare releases and run playtest bots.
### Can I track my game's own metrics?
Yes. Send any moment with `track(name, attributes)`: plays by mode, purchases, lap times, waves survived. Your coding agent picks the events with you, then saves pages that fit your game with the `glitchless_set_dashboard` tool. Nothing on the dashboard assumes a genre.
### How do I check that a fix didn't make the game worse?
Run `npx glitchless-playtest --save baseline` before the change and `npx glitchless-playtest` after it. Bots play both builds with the same seeds and the run fails if anything got worse.
### Does it work with Unreal Engine?
Yes, with Unreal Engine 5.8. The plugin times the game, render, RHI and GPU threads, names hitches from shader and pipeline compiles, garbage collection and level streaming, and catches errors and ensures from the log. It works beside Epic's Unreal MCP editor plugin: that one edits the project, Glitchless tells the agent how the game runs.
### Does it work with GameMaker?
Yes, with GameMaker 2024.2 and later (tested on the LTS 2026 runtime on macOS). The SDK is GML: frame times from `delta_time`, rooms as levels, garbage collection and instance spikes as hitch causes, errors and crashes from `exception_unhandled_handler`, sent on the next launch.
### Does it work with Babylon.js or PlayCanvas?
Yes. One call hooks the engine's own loop and counters: draw calls, shader compiles, texture uploads and scenes as levels, with fix advice in each engine's terms.
### Does it work with Godot?
Yes, with Godot 4.5 and later. The addon sends the same events as the web SDK, including errors from Godot's loggers.
### Does it work with Unity?
Yes, with Unity 2022.3 LTS and Unity 6. The package times update, physics and render for every frame, reads Unity's profiler counters for draw calls and memory, and catches errors from Unity's log.
### Does it work with Phaser?
Yes, with Phaser 3 and 4. One call hooks Phaser's game loop, treats each scene as a level and reports a game that freezes after an uncaught error.
### Is it free?
The open-source version is free (MIT) and runs on a laptop or your own server. The hosted version is free for 5,000 play sessions a month; paid plans start at $19 a month. Teams in the Epic AI MegaJam get 50,000 play sessions a month free until January 31, 2027 with code MEGAJAM26 ([details](https://www.glitchless.gg/megajam.md)).
---
Source: https://www.glitchless.gg/docs.md
# Glitchless docs
> Glitchless (glitchless.gg) documentation: the setup prompt for coding agents, guides for Unreal, Unity, Godot, GameMaker and web games, the MCP server, playtest bots, bug reports, share links, source maps, alerts, exporters, self-hosting with Docker and the API spec.
Website: https://www.glitchless.gg/docs.html
Most of the time your coding agent reads these for you: paste the setup prompt and it does the rest. They're here for when you want to know how something works.
## Start here
- [The setup prompt](https://www.glitchless.gg/index.md): Paste it into Claude Code, Codex, Cursor or any agent. It adds Glitchless to your game, connects the MCP server and builds a dashboard.
- [Glitchless Cloud](https://app.glitchless.gg): Sign in with Google or GitHub. Get started gives you the same prompt with your project's keys. Free for 5,000 play sessions a month.
- [Run it yourself](https://github.com/glitchless-gg/glitchless#run-it-on-a-server): One Docker container, or `npm run server`: the server, the dashboard and a file store, no other services.
## Engines
- [Unreal Engine](https://www.glitchless.gg/unreal.md): The C++ plugin for UE 5.7 and 5.8: settings, Blueprint nodes, crashes, `-glitchless-measure` and Unreal MCP.
- [Unity](https://www.glitchless.gg/unity.md): The package for 2022.3 LTS and Unity 6: setup, what it measures in each build type, bug reports.
- [Godot](https://www.glitchless.gg/godot.md): The addon for Godot 4.5 and later: autoload, hitch causes, errors, phones.
- [Web games](https://www.glitchless.gg/web.md): One npm package for Three.js, Phaser, Babylon.js and PlayCanvas, with WebMCP and an overlay.
- [GameMaker](https://github.com/glitchless-gg/glitchless/blob/main/gamemaker/README.md): GML scripts for GameMaker 2024.2 and later, as a local package or installed by a script.
## Coding agents
- [MCP server](https://github.com/glitchless-gg/glitchless/blob/main/packages/mcp/README.md): `npx -y glitchless-mcp`: 16 tools for findings, frame times, errors, death hotspots, bug reports, versions and playtests.
- [Claude Code plugin and skills](https://github.com/glitchless-gg/glitchless/tree/main/plugin/glitchless): The instrument, dashboard, triage and Steam Deck skills, which any agent can also read as plain files.
- [WebMCP](https://github.com/glitchless-gg/glitchless/blob/main/packages/sdk/README.md#webmcp): Tools a running web game gives your agent: measure frames, inspect the scene, take a screenshot.
- [Playtest bots](https://github.com/glitchless-gg/glitchless/blob/main/packages/playtest/README.md): `glitchless-playtest` plays fixed-seed runs and compares a change with a saved baseline.
- [Steam Deck](https://github.com/glitchless-gg/glitchless-deck): `glitchless-deck` sends a build to a Deck on your network, starts it and measures it there.
## Features
- [Dashboards and boards](https://github.com/glitchless-gg/glitchless/blob/main/docs/SPEC.md#3b-dashboards-and-boards): Every game's own pages, which your agent designs from the events the game sends.
- [Bug reports](https://github.com/glitchless-gg/glitchless/blob/main/packages/sdk/README.md#bug-reports): F8 or your own button: the player's note, a screenshot and the moments before it.
- [Share links](https://github.com/glitchless-gg/glitchless/blob/main/docs/SPEC.md#3c-share-links): One board, read only and signed out, optionally pinned to fixed dates, for dev logs and publishers.
- [Source maps](https://github.com/glitchless-gg/glitchless/blob/main/packages/sourcemaps/README.md): A CLI and a Vite plugin, so a minified web build's errors show the original file and line.
- [Alerts](https://github.com/glitchless-gg/glitchless#alerts-in-discord-or-slack): New errors, crash-free drops, frame-time regressions and new findings, to Discord, Slack or email.
- [Datadog and OpenTelemetry](https://github.com/glitchless-gg/glitchless#exporters-datadog-or-any-opentelemetry-backend): Send the same metrics and logs on to Datadog or any OpenTelemetry backend.
- [Privacy modes and retention](https://github.com/glitchless-gg/glitchless/blob/main/docs/SPEC.md#4b-privacy-levels): Anonymous by default; install mode adds a random player ID for D1, D7 and D30 retention.
## Reference
- [Technical spec](https://github.com/glitchless-gg/glitchless/blob/main/docs/SPEC.md): Every event and field, the query API, how the numbers are computed, keys and storage.
- [Wire format](https://github.com/glitchless-gg/glitchless/blob/main/docs/SPEC.md#2-wire-format): What an SDK sends to `/v1/intake`, for writing your own.
- [What the SDKs send](https://github.com/glitchless-gg/glitchless/blob/main/packages/sdk/PRIVACY.md): Every field in each privacy mode, with lines to adapt for your game's privacy policy.
- [Releases](https://github.com/glitchless-gg/glitchless/releases): The Unreal plugin and GameMaker package downloads, and what changed in each.
## For AI agents
- [llms.txt](https://www.glitchless.gg/llms.txt): An index of this site and the docs, for assistants and crawlers.
- [llms-full.txt](https://www.glitchless.gg/llms-full.txt): All the docs above in one plain-text file: guides, READMEs, skills and the spec.
- [Markdown copies](https://www.glitchless.gg/docs.md): Every page on this site has one: add `.md` to its address.
---
Source: https://www.glitchless.gg/godot.md
# Glitchless for Godot
> Glitchless (glitchless.gg) is a free, open-source addon for Godot 4.5 and later. It records frame times with each hitch's cause (pipeline compiles, scene changes, node bursts), errors with script backtraces, player bug reports and where players quit, and gives coding agents the same data over MCP.
Website: https://www.glitchless.gg/godot.html · Read the addon guide: https://github.com/glitchless-gg/glitchless/blob/main/godot/addons/glitchless/README.md
An addon that records how your Godot game runs for the people playing it: every stutter with its cause, every error with its backtrace, and where players quit. Your coding agent reads the same data over MCP and fixes what they hit.
## What it records in a Godot game
- **Stutters, with a cause.** Every frame is timed from wall-clock time, so delta smoothing can't hide a stutter. Each hitch names its likely cause: a pipeline (shader) compile, a scene change, a burst of new nodes, slow `_process` code, physics or rendering.
- **Errors.** Through Godot 4.5's custom loggers, with script backtraces and the last game events as breadcrumbs.
- **Leaks.** Draw calls, objects, nodes and orphan nodes in every frame window. A level where orphan nodes only climb is the commonest Godot leak, and the findings point it out.
- **Bug reports from playtesters.** F8, or your own button, opens a short form. The report carries the player's note, a screenshot, where they were and what had just happened.
- **Levels, deaths and quits.** The level funnel, deaths, rage quits, your own events and gauges.
- **Phones too.** Built for the Mobile renderer: sessions pause in the background, resume on return, and batches wait offline until there's a network.
## Set it up
Copy the addon, add the autoload, call `init` once.
1. Copy `addons/glitchless/` into your project's `addons/`.
2. Turn the plugin on (or add the autoload) and call `Glitchless.init` in your first scene.
3. Run the game: sessions show on your dashboard.
project.godot, then your first scene:
```gdscript
# project.godot, under [autoload]
Glitchless="*res://addons/glitchless/glitchless.gd"
# your first scene
func _ready() -> void:
Glitchless.init("my-game", "", "prod", "https://api.glitchless.gg", {"client_token": "gl_pub_..."})
```
## Built for working with a coding agent
Glitchless's MCP server gives Claude Code, Codex, Cursor or any other agent the findings, frame times, errors and bug reports from your players, so it can find what to fix and check the result.
A build measures itself for an agent: `--glitchless-measure=10` prints frame stats as JSON and quits, for a before-and-after comparison. In GDScript, `await Glitchless.measure(5.0)` does the same.
The addon adds no global class names and works in a project that has never been opened in the editor, so an agent can install it from the command line.
## Tested
- The end-to-end test runs the demo game headless against a local server and checks offline saving, privacy modes, errors, background and resume, measuring and bug reports (58 checks).
- Its frame-time sketch matches the JavaScript SDK's bucket for bucket.
## Not yet
- Godot 4.5 or later: the Logger class came in 4.5.
- Web exports are untested. A web game is better served by the [browser SDK](https://www.glitchless.gg/web.md).
## Frequently asked questions
### Which Godot versions does it support?
Godot 4.5 and later. It's built for 4.7 and the Mobile renderer.
### Is it free?
The addon and the server are open source under the MIT licence. Glitchless Cloud is free for 5,000 play sessions a month, or you can run the server yourself with Docker. See [pricing](https://www.glitchless.gg/pricing.md).
### How is it different from Talo?
Talo is a game backend (players, saves, leaderboards) with analytics. Glitchless measures how the game runs and plays, and gives that to your agent. Many games could use both: [Glitchless vs Talo](https://www.glitchless.gg/glitchless-vs-talo.md).
### What does it send about players?
Nothing that identifies them by default. Install mode adds a saved random player ID for retention, and [PRIVACY.md](https://github.com/glitchless-gg/glitchless/blob/main/godot/addons/glitchless/PRIVACY.md) lists every field.
---
Source: https://www.glitchless.gg/unity.md
# Glitchless for Unity
> Glitchless (glitchless.gg) is a free, open-source package for Unity 2022.3 LTS and Unity 6. It records frame times with each hitch's cause (GC, shader compiles, scene loads), errors, player bug reports and where players quit, and gives coding agents the same data over MCP.
Website: https://www.glitchless.gg/unity.html · Read the package guide: https://github.com/glitchless-gg/glitchless/blob/main/unity/Packages/gg.glitchless.sdk/README.md
A package that records how your Unity game runs for the people playing it: every stutter with its cause, every error with what led to it, and where players quit. Your coding agent reads the same data over MCP and fixes what they hit.
## What it records in a Unity game
- **Stutters, with a cause.** Every frame is timed, with where the time went: physics, your `Update` code, rendering, the GPU. Each hitch names its likely cause: a garbage collection, a shader compile, a scene load, a burst of new objects or long script work.
- **Errors.** Uncaught exceptions, `Debug.LogError`, assertions and engine errors, grouped into issues with the game events just before each one.
- **Bug reports from playtesters.** F8, or your own button, opens a short form. The report carries the player's note, a screenshot, where they were and what had just happened.
- **Levels, deaths and quits.** Scenes are levels on their own. Deaths, fails and quits feed the level funnel, death heatmaps and rage-quit detection.
- **Leaks.** Mesh, material, texture and object counts at each level load in development builds, so a count that only climbs stands out.
- **Retention and revenue.** Install mode adds a saved random player ID for D1, D7 and D30 retention. Purchases add up to revenue and conversion.
## Set it up
One call in the first scene, or a Bootstrap component with the same three fields.
1. Package Manager → + → Add package from git URL, and paste the package's URL.
2. Call `Glitchless.Init` in the `Awake` of your first scene.
3. Press Play: Editor and development sessions show on your dashboard as `dev`.
Package URL, then Init:
```csharp
// Package Manager → Add package from git URL:
// https://github.com/glitchless-gg/glitchless.git?path=unity/Packages/gg.glitchless.sdk
using GlitchlessSdk;
Glitchless.Init(new GlitchlessOptions
{
Service = "my-game",
Intake = "https://api.glitchless.gg",
ClientToken = "gl_pub_...",
});
```
## Built for working with a coding agent
Glitchless's MCP server gives Claude Code, Codex, Cursor or any other agent the findings, frame times, errors and bug reports from your players, so it can find what to fix and check the result.
A built player measures itself for an agent: `--glitchless-measure=10` prints frame-time percentiles and hitches as JSON and quits, for a before-and-after comparison. In the Editor, `Glitchless.Measure` does the same from code.
The Fetch! sample (Package Manager → Samples) is a small runner with an autoplay bot and four planted bugs to try it on.
## Tested
- EditMode tests check the frame-time sketch and error fingerprints bucket for bucket against the JavaScript SDK.
- PlayMode tests cover the runner and the bug report form.
- An end-to-end test builds the sample and checks what arrives at a local server.
## Not yet
- GPU time needs Player Settings → Frame Timing Stats.
- Mesh, material and texture counts come from Unity's profiler counters, which only development builds fill in.
- IL2CPP stacks show method names unless IL2CPP Stacktrace Information includes file and line.
## Frequently asked questions
### Which Unity versions does it support?
Unity 2022.3 LTS and Unity 6.
### Is it free?
The package and the server are open source under the MIT licence. Glitchless Cloud is free for 5,000 play sessions a month, or you can run the server yourself with Docker. See [pricing](https://www.glitchless.gg/pricing.md).
### Does it replace Unity Analytics?
It covers custom events, funnels, retention and revenue, with performance, errors and bug reports in the same place. [Glitchless vs GameAnalytics](https://www.glitchless.gg/glitchless-vs-gameanalytics.md) compares it with a dedicated game-analytics tool.
### What does it send about players?
Nothing that identifies them by default: no player ID, no device identifier, coarse device facts only. Install and full modes are opt-in, and [PRIVACY.md](https://github.com/glitchless-gg/glitchless/blob/main/unity/Packages/gg.glitchless.sdk/PRIVACY.md) lists every field.
---
Source: https://www.glitchless.gg/unreal.md
# Glitchless for Unreal Engine
> Glitchless (glitchless.gg) is a free, open-source Unreal Engine 5 plugin. It records frame times with each hitch's cause (PSO compiles, garbage collection, level streaming), crashes and ensures with callstacks, player bug reports and where players quit, and gives coding agents the same data over MCP.
Website: https://www.glitchless.gg/unreal.html · Read the plugin guide: https://github.com/glitchless-gg/glitchless/blob/main/unreal/Plugins/Glitchless/README.md
A plugin that records how your Unreal game runs for the people playing it: every stutter with its cause, every crash with its callstack, and where players quit. Your coding agent reads the same data over MCP and fixes what they hit.
## What it records in an Unreal game
- **Stutters, with a cause.** Every frame is timed on the game, render and RHI threads and the GPU. Each hitch says what held it up: a pipeline state compiled on the spot, garbage collection, a map load, a streamed level or an asset load.
- **Crashes and ensures.** `UE_LOG` errors, ensures and Blueprint runtime errors such as Accessed None, with callstacks. A crash is written to disk as the game dies and sent on the next launch, with the frames before it.
- **Bug reports from playtesters.** A key (F8) opens a small box over the game. The report carries the player's note, a screenshot, the level and the last events.
- **Levels, deaths and quits.** Maps are levels on their own. Deaths, fails and quits feed the level funnel, death heatmaps and rage-quit detection.
- **Leaks.** UObject and actor counts and memory at each level load, so a count that only climbs stands out.
- **Your game's own events.** Track anything from C++ or Blueprints. Purchases add up to revenue, and install mode adds D1, D7 and D30 retention.
## Set it up
No code needed: everything is in Project Settings, and every call has a Blueprint node.
1. Unzip the [plugin release](https://github.com/glitchless-gg/glitchless/releases/tag/glitchless-unreal/v0.1.0) to `Plugins/Glitchless` and enable it.
2. Project Settings → Plugins → Glitchless: Service, Intake and Client Token.
3. Sessions show on your dashboard, from the editor and Development builds too.
Config/DefaultGame.ini:
```ini
[/Script/Glitchless.GlitchlessSettings]
Service=my-game
Intake=https://api.glitchless.gg
ClientToken=gl_pub_...
Privacy=Anonymous
bSendInDevelopmentBuilds=True
```
## Built for working with a coding agent
Glitchless's MCP server gives Claude Code, Codex, Cursor or any other agent the findings, frame times, errors and bug reports from your players, so it can find what to fix and check the result.
Any build measures itself for an agent: `MyGame.exe -glitchless-warmup=2 -glitchless-measure=10` prints frame-time percentiles, thread times and hitches with their causes as JSON, for a before-and-after comparison.
With Epic's Unreal MCP (UE 5.8, Experimental), the optional Glitchless Toolset adds `Status` and `Measure` tools while you Play In Editor.
## Tested
- Win64: a packaged Development build passes the end-to-end test 23/23, and Fab's BuildPlugin builds it with no warnings.
- macOS: the end-to-end test passes 23/23.
- iOS: a Development build passes 15/15 on an iPhone 16 Pro Max.
- Engine: UE 5.8.4.
## Not yet
- Android and Linux builds haven't been built yet.
- UEFN projects can't load plugins, so Fortnite islands aren't covered.
- On Mac and iOS, crash stacks name functions but not lines.
## Frequently asked questions
### Does it work in a Blueprint-only project?
Yes. The plugin is C++ source, so a Blueprint-only project becomes a C++ project once (Tools → New C++ Class). After that, setup is in Project Settings and every call has a Blueprint node.
### Is it free?
The plugin and the server are open source under the MIT licence. Glitchless Cloud is free for 5,000 play sessions a month, or you can run the server yourself with Docker. See [pricing](https://www.glitchless.gg/pricing.md).
### Can I use it in the Epic AI MegaJam?
Yes, on the Unreal Engine track. Teams get a free jam tier with code MEGAJAM26, and the [MegaJam page](https://www.glitchless.gg/megajam.md) has the line to declare it in your submission.
### How is it different from Unreal Insights?
Insights shows in depth why one session on your machine is slow. Glitchless shows which hitches real players hit, across every session, and hands that to your agent. They work well together: [Glitchless vs Unreal Insights](https://www.glitchless.gg/glitchless-vs-unreal-insights.md).
### What does it send about players?
Nothing that identifies them by default: no player ID, no device identifier, coarse device facts only. Install and full modes are opt-in, and [PRIVACY.md](https://github.com/glitchless-gg/glitchless/blob/main/unreal/Plugins/Glitchless/PRIVACY.md) lists every field.
---
Source: https://www.glitchless.gg/web.md
# Glitchless for web games
> Glitchless (glitchless.gg) is a free, open-source SDK for web games made with Three.js, Phaser, Babylon.js or PlayCanvas. It records frame times with each hitch's cause, errors including WebGL context loss, player bug reports and where players quit, and lets coding agents read the data over MCP and drive the running game over WebMCP.
Website: https://www.glitchless.gg/web.html · Read the SDK guide: https://github.com/glitchless-gg/glitchless/blob/main/packages/sdk/README.md
One init call records how your web game runs on your players' machines: every stutter with its cause, every error with what led to it, and where players quit. Your coding agent reads it over MCP, drives the running game over WebMCP, and checks its fix with playtest bots.
## What it records in a web game
- **Frame times that mean something.** The budget comes from the display's refresh rate, 60, 120 or 144 Hz. Every 5 seconds the frames are summarised into a mergeable sketch, so p95 and p99 cover every player.
- **Stutters, with a cause.** Each hitch is tied to a long script, a shader compile, a GC pause or an asset load. With Three.js, also to what the frame uploaded to the GPU: textures, buffers, shader programs.
- **Errors.** Uncaught errors and WebGL context loss, with the gameplay events before each one. With [source maps](https://www.npmjs.com/package/glitchless-sourcemaps), a minified build's errors show the original file and line.
- **Bug reports from playtesters.** F8 or a button opens a small form. The note, a screenshot, the level and the last events go to your dashboard.
- **Levels, deaths and quits.** The level funnel, deaths and rage quits. In full mode, death places and positions feed heatmaps and replays.
- **An overlay for you.** Press the backtick key in the game for live frame times. It works with no server at all.
## Set it up
Install from npm and call `init` once. Phaser, Babylon.js and PlayCanvas each have a one-line adapter too.
1. `npm install glitchless`, or the script tag your server hosts.
2. `Glitchless.init`, then the adapter for your engine.
3. Load the game: sessions show on your dashboard.
Three.js:
```js
import * as THREE from 'three';
import { Glitchless, instrumentThree } from 'glitchless';
const glitchless = Glitchless.init({
service: 'my-game',
version: '1.0.0',
intake: 'https://api.glitchless.gg',
clientToken: 'gl_pub_...',
});
instrumentThree(glitchless, { renderer, scene, camera, THREE });
// Phaser: instrumentPhaser(glitchless, { game, Phaser }) from 'glitchless/phaser'
// Babylon.js: instrumentBabylon(glitchless, { engine, scene, BABYLON }) from 'glitchless/babylon'
// PlayCanvas: instrumentPlayCanvas(glitchless, { app, pc }) from 'glitchless/playcanvas'
```
## Built for working with a coding agent
Glitchless's MCP server gives Claude Code, Codex, Cursor or any other agent the findings, frame times, errors and bug reports from your players.
WebMCP tools let the agent drive the game itself: measure frame times, inspect the scene, take a screenshot and reproduce a problem in the running game.
[glitchless-playtest](https://www.npmjs.com/package/glitchless-playtest) plays fixed-seed runs in Chrome with your game's autoplay bot and compares them with a saved baseline, so the agent can prove a fix before it ships.
## Tested
- Four demo games with planted problems: Zoomies (Three.js), Fetch! (Phaser 3 and 4), Comet Garden (Babylon.js) and Spark Run (PlayCanvas).
- About 30 ns of work a frame, and one small gzipped request every 15 seconds.
## Not yet
- Death places and positions are only sent in full mode, or with `positions: true`.
- Session replay is a timeline and positions, not video.
## Frequently asked questions
### Which engines does it support?
Three.js (and React Three Fiber), Phaser 3 and 4, Babylon.js and PlayCanvas, or plain WebGL with the core SDK.
### Is it free?
The SDK and the server are open source under the MIT licence. Glitchless Cloud is free for 5,000 play sessions a month, or you can run the server yourself with Docker. See [pricing](https://www.glitchless.gg/pricing.md).
### Why not Sentry or PostHog?
Keep them if you use them: Sentry's browser SDK reports errors and PostHog does product analytics, but neither times a web game's frames or names a stutter's cause. See [Glitchless vs Sentry](https://www.glitchless.gg/glitchless-vs-sentry.md) and [Glitchless vs PostHog](https://www.glitchless.gg/glitchless-vs-posthog.md).
### What does it send about players?
Nothing that identifies them by default: no player ID, no device fingerprint. [PRIVACY.md](https://github.com/glitchless-gg/glitchless/blob/main/packages/sdk/PRIVACY.md) lists every field in each mode.
---
Source: https://www.glitchless.gg/megajam.md
# Glitchless for the Epic AI MegaJam
> Jamming in Unreal? Glitchless is free until judging ends: 50,000 play sessions a month, room for your team of five and 90 days of data, with code MEGAJAM26 until January 31, 2027.
Website: https://www.glitchless.gg/megajam.html · Get the jam tier: https://app.glitchless.gg/?promo=megajam26 · Jam guide: https://github.com/glitchless-gg/glitchless/blob/main/docs/jam/megajam-2026.md
The Epic AI MegaJam runs November 12–15, 2026. Glitchless isn't affiliated with Epic Games; Unreal Engine is a trademark of Epic Games.
## What you get during the jam
- **See why your build stutters.** Every hitch comes with its cause: shader and pipeline compiles, garbage collection, level streaming.
- **Playtesters press F8.** They send a note and a screenshot. Your agent reads it with the session behind it.
- **Show your work for Glass Box.** Share a read-only board pinned to jam week in your dev log.
## Set it up before the kickoff
1. By November 11, sign up with code MEGAJAM26.
2. Add the Unreal plugin and turn on Send in Development builds (jam submissions are Development builds).
3. When you submit, declare Glitchless as a tool you set up before the jam.
Or paste the [setup prompt](https://github.com/glitchless-gg/glitchless/blob/main/docs/agent-setup-prompt.md) into your coding agent: it adds the plugin and builds a dashboard for your game.
This is for the Unreal Engine track: UEFN projects can't load plugins.
## Your submission's tool declaration
The rules ask you to declare what you made or set up before the jam. Paste this into your submission and edit it to match what you did:
```text
Set up before the jam: Glitchless (glitchless.gg), a third-party observability plugin for Unreal (MIT licence, github.com/glitchless-gg/glitchless). It records frame times, errors, crashes and bug reports from our builds, and its MCP server (glitchless-mcp) lets our coding agent read them. Before November 12 we only installed it, set it up and connected it to our agent; all game code, prompts and assets were made during the jam.
```
## A Glass Box dev log outline
```text
# [Game name]: how we built it with AI
## The team and the tools
Who we are, the agents and MCP servers we used, and what we set up before the jam.
## Day by day
What we built each day, and the prompts and decisions that mattered.
## A problem our playtesters hit
A stutter, a crash, a level people quit on. The data: [share link, pinned to Nov 12-16]
## What the agent found and changed
The finding it read, the change it made, and what we fixed by hand.
## Did the fix work?
Before and after: -glitchless-measure frame times, or the crash-free rate on day 1 against day 4. [share link]
## What we'd do differently
```
Make each share link from a board: Share, then Fixed dates from Nov 12 to Nov 16, and an expiry of Never. Judges see jam week whenever they open it, with no player IDs or stack traces. The [jam guide](https://github.com/glitchless-gg/glitchless/blob/main/docs/jam/megajam-2026.md#a-share-link-for-the-glass-box-dev-log) has the details.
## When it ends
The jam tier ends January 31, 2027, 23:59 UTC. Your workspace goes back to the Free plan's limits (5,000 play sessions a month, one person, 14 days of data) and nothing is deleted.
---
Source: https://www.glitchless.gg/pricing.md
# Glitchless pricing
> Pay for play sessions, not seats. A play session is one sitting, from opening your game to closing it.
Website: https://www.glitchless.gg/pricing.html · Start free: https://app.glitchless.gg · Terms: https://www.glitchless.gg/terms.md
## Plans
| Plan | Price | Play sessions a month | Includes |
| --- | --- | --- | --- |
| Free | $0 | 5,000 | 1 game, 14 days of data, every dashboard, finding and alert, the MCP server, WebMCP tools and playtest bots |
| [Indie](https://app.glitchless.gg/?plan=indie) | $19 a month | 25,000 | Up to 5 games, 90 days of data, export to Datadog or any OpenTelemetry backend, spike protection when a game goes viral |
| [Studio](https://app.glitchless.gg/?plan=studio) | $99 a month | 250,000 | Up to 25 games, 400 days of data, up to 100 people with roles, everything in Indie |
| Self-hosted | Free (MIT) | Unlimited, on your own server | The same SDKs, dashboard and agent tools, on a laptop or your own server; your data never leaves your machines |
Going over? Extra sessions are $1 per 1,000, or set a cap and Glitchless stops counting there.
In the Epic AI MegaJam? Code MEGAJAM26 gives Free 50,000 play sessions a month until January 31, 2027: [the jam tier](https://www.glitchless.gg/megajam.md).
## Compare plans
| | Free | Indie | Studio | Self-hosted |
| --- | --- | --- | --- | --- |
| **Usage** | | | | |
| Play sessions a month | 5,000 | 25,000 | 250,000 | Unlimited |
| Past the allowance | New sessions wait for next month | $1 per 1,000 | $1 per 1,000 | No limit |
| Spike protection (counting pauses once a month's overage reaches the plan's price) | No | Yes | Yes | No |
| A spending cap you set | No | Yes | Yes | No |
| Autoplay bot runs (free, up to the plan's sessions a month) | 5,000 | 25,000 | 250,000 | Unlimited |
| **Team** | | | | |
| Games | 1 | Up to 5 | Up to 25 | Unlimited |
| People | 1 | 1 | Up to 100, with roles | Your own access |
| **Data** | | | | |
| Data kept | 14 days | 90 days | 400 days | Your choice |
| Source maps kept | 400 days | 400 days | 400 days | Your choice |
| Share links | 25 per game | 25 per game | 25 per game | No |
| CSV export | Yes | Yes | Yes | Yes |
| Export to Datadog or OpenTelemetry | No | Up to 4 per game | Up to 4 per game | Yes |
| **Features** | | | | |
| A dashboard built for your game | Yes | Yes | Yes | Yes |
| Findings with suggested fixes | Yes | Yes | Yes | Yes |
| MCP server and WebMCP tools | Yes | Yes | Yes | Yes |
| Playtest bots | Yes | Yes | Yes | Yes |
| Bug reports from players | Yes | Yes | Yes | Yes |
| Retention and players now | Yes | Yes | Yes | Yes |
| Revenue and conversion | Yes | Yes | Yes | Yes |
| Source maps for web builds | Yes | Yes | Yes | Yes |
| Alerts (Discord, Slack or email) | Yes | Yes | Yes | Discord or Slack webhook |
## Questions
### What counts as a play session?
One sitting, from opening your game to closing it. Autoplay bot runs don't count.
### What happens when my game goes viral?
On Indie and Studio, spike protection keeps a sudden rush from turning into a surprise bill. Once a month's overage reaches your plan's own price ($19 or $99), counting pauses and you get an email. It starts again when you choose to keep counting or set a cap, so no bill passes double your plan without your yes.
### Does it slow my game down?
No. The SDK does about 30 ns of work a frame and sends one small gzipped request every 15 seconds.
### What do you collect about my players?
By default, nothing that identifies them: no player IDs, no device fingerprints and no positions. You can turn on full mode for heatmaps and replays.
### Can I move from self-hosted to Glitchless Cloud?
Yes. Point the SDK at your cloud project's intake address and token. Nothing else in your game changes.
---
Source: https://github.com/glitchless-gg/glitchless/blob/main/README.md
# Glitchless
Glitchless shows you how your game plays for real players, and gives your coding agent the same data so it can fix what they hit. It works with Three.js, Phaser, Babylon.js and PlayCanvas games in the browser, and Godot 4.5+, Unity (2022.3 LTS and 6) Unreal Engine 5.8 and GameMaker (2024.2+) games on phones and desktops.
- **Stutter.** Every frame is timed on every player's device, and each hitch comes with its likely cause: a long script, a shader compile, garbage collection or an asset load.
- **Crashes.** Errors are grouped into issues, each with the gameplay just before it.
- **Where players give up.** Level funnels, death heatmaps and rage-quit detection.
- **What players tell you.** Playtesters press F8 in a web game to send a note with a screenshot, the level and the session behind it (opt-in).
- **Your coding agent.** An MCP server for production data, WebMCP tools exposed by the running game, and autoplay bots that check a fix before it ships.
It's open source and runs anywhere Node runs: one server with a file store, on a laptop, a VPS or in a container. It's anonymous by default ([what it sends](packages/sdk/PRIVACY.md)), and it can pass the same data on to Datadog or any OpenTelemetry backend.

*The dashboard on simulated history from [`scripts/seed.js`](scripts/seed.js). Also: [performance](docs/screenshots/performance.png) and [death heatmaps](docs/screenshots/heatmaps.png).*
## What's in the box
| Path | What |
| --- | --- |
| [`packages/sdk`](packages/sdk) | `glitchless`: SDK, Three.js, Phaser (3 and 4), Babylon.js and PlayCanvas adapters, overlay, WebMCP tools, Datadog RUM bridge. ~120 KB as one ``, then use `Glitchless.init(...)` and `Glitchless.instrumentThree(glitchless, {...})`.
**Retention needs install mode.** In anonymous mode (the default) nothing is kept between visits, so the dashboard can't tell a returning player from a new one. `privacy: 'install'` keeps a random `p_…` player ID in localStorage and sends it with every batch, and nothing else changes: the Players page and `retention` board panels then show D1, D7 and D30. `glitchless.resetInstallId()` forgets the ID (for a "reset my ID" setting): the page keeps the one it started with, and the next load makes a new one. A Steam Deck is tagged `steam_deck: true` when its browser shows the Deck's GPU.
## Phaser
Phaser 3.60+ and Phaser 4. Init before you create the game, then hand the game to `instrumentPhaser`:
```js
import * as Phaser from 'phaser';
import { Glitchless } from 'glitchless';
import { instrumentPhaser } from 'glitchless/phaser';
const glitchless = Glitchless.init({ service: 'my-game', version: '1.0.0', intake: 'http://localhost:7777', clientToken: 'gl_pub_dev' });
const game = new Phaser.Game(config);
instrumentPhaser(glitchless, { game, Phaser });
```
Script tag: `Glitchless.instrumentPhaser(glitchless, { game })`.
What the adapter adds:
- **Frames from Phaser's loop.** Each Phaser step is one frame, so a game with an fps limit is measured by the frames it draws. Each frame is split into update time (your scenes' `update`, physics, tweens) and render time.
- **Draw calls, triangles and shader compiles**, counted on the WebGL context (Phaser keeps no counters), or the Canvas renderer's draw count.
- **Scenes as levels.** A scene that starts is a level (`level_start`), unless its key reads as a loader, menu, overlay or end screen (`Boot`, `Preloader`, `MainMenu`, `UIScene`, `HUD`, `GameOver`…). Its shutdown ends the level; a restart is a new attempt. Pass `levels: ['Level1', 'Level2']`, `levels: (scene) => name or null`, or `levels: false`. Calling `glitchless.levelStart()` yourself turns scene levels off.
- **Gauges** in every frame window and hitch: `game_objects`, `particles`, `physics_bodies`, `tweens`, `listeners` (event handlers on the game and its scenes: a count that grows on every restart is a leak) and `sounds`. Turn them off with `gauges: false`.
- **Hitch causes** from what the slow step did: `object_spawn` (50+ objects added), `texture_upload` (a texture made mid-game), `shader_compile`, `scene_change`, `script`, `render`.
- **Errors**: failed loads (`kind: 'asset'`), WebGL context loss, and `game_loop_stopped`: an uncaught error inside `update` or a physics callback stops Phaser's loop for good, the screen freezes, and the dashboard counts it as a crash.
- **WebMCP**: `glitchless_inspect_scene` lists each running scene's display list by type and texture, physics bodies, tweens and the busiest event listeners; `glitchless_screenshot` captures the canvas right after Phaser renders.
`captureMap` isn't there for Phaser yet, so heatmaps draw positions without a level picture. [apps/phaser-demo](https://github.com/glitchless-gg/glitchless/tree/main/apps/phaser-demo) is a small Phaser game wired this way, with planted bugs.
## Babylon.js
Babylon.js 5 and later (tested with 9), on WebGL or WebGPU. Hand the engine and your first scene to
`instrumentBabylon` right after you create them:
```js
import * as BABYLON from '@babylonjs/core';
import { Glitchless } from 'glitchless';
import { instrumentBabylon } from 'glitchless/babylon';
const glitchless = Glitchless.init({ service: 'my-game', version: '1.0.0', intake: 'http://localhost:7777', clientToken: 'gl_pub_dev' });
const engine = new BABYLON.Engine(canvas, true);
const scene = new BABYLON.Scene(engine);
scene.metadata = { level: 'forest' }; // optional: this scene is the level "forest"
instrumentBabylon(glitchless, { engine, scene, BABYLON }); // BABYLON is optional: it lets captureMap draw levels
engine.runRenderLoop(() => scene.render());
```
Script tag: `Glitchless.instrumentBabylon(glitchless, { engine, scene })` (the `BABYLON` global is found on its own).
What the adapter adds:
- **Frames from Babylon's loop**, from one `onBeginFrameObservable` to the next, so `engine.maxFPS` or a paused loop is measured as drawn. Each frame is split into update time (before-render observers, animations, physics and your render-loop code) and render time.
- **Babylon's own counters**: draw calls (the engine's counter, as `SceneInstrumentation` reads it), triangles from the active indices, compiled effects as `programs`, and GPU frame time where timer queries exist (`gpuTime: false` turns it off).
- **Scenes as levels.** A scene whose `metadata.level` is set starts that level when it first renders; setting a new name starts the next, and disposing the scene ends it. Scenes made later are picked up on their own. Or pass `levels: (scene) => name or null`, or `levels: false`. Calling `glitchless.levelStart()` yourself turns scene levels off.
- **Gauges** in every frame window and hitch: `meshes`, `active_meshes`, `materials`, `textures`, `particles` and `render_observers` (before- and after-render observers: a count that grows with every spawn or restart is a leak). Turn them off with `gauges: false`.
- **Hitch causes**: `shader_compile` (from the engine's compile observables; with parallel shader compiles the stall comes a few frames later, so compiles in the second before it count), `object_spawn` (50+ meshes in a frame), `texture_upload`, `geometry_upload`, `scene_change`, `script`, `render`, with `shaders_compiled`, `shader_compile_ms` and `gpu_ms`.
- **Errors**: context loss (or a lost WebGPU device), shaders that failed to compile (`kind: 'shader'`, with the compiler's message), textures that failed to load (`kind: 'asset'`), and `game_loop_stopped`: an uncaught error inside the render loop stops `runRenderLoop` for good, the screen freezes, and the dashboard counts it as a crash.
- **WebMCP**: `glitchless_inspect_scene` lists each scene's meshes by name and class, flagging copies that should be instances or thin instances and materials that should be shared, with what is frozen, lights, shadow generators and observers; `glitchless_screenshot` captures the canvas at the end of a frame. `captureMap` renders an orthographic picture of the level with Babylon's render-target screenshot.
[apps/babylon-demo](https://github.com/glitchless-gg/glitchless/tree/main/apps/babylon-demo) is a small Babylon.js game wired this way, with planted bugs.
## PlayCanvas
The PlayCanvas engine 2.x, editor projects and engine-only games, on WebGL2 or WebGPU:
```js
import * as pc from 'playcanvas';
import { Glitchless } from 'glitchless';
import { instrumentPlayCanvas } from 'glitchless/playcanvas';
const glitchless = Glitchless.init({ service: 'my-game', version: '1.0.0', intake: 'http://localhost:7777', clientToken: 'gl_pub_dev' });
const app = new pc.Application(canvas);
instrumentPlayCanvas(glitchless, { app, pc }); // pc is optional: it lets captureMap draw levels
app.start();
```
In a PlayCanvas Editor project, do this in a script on the first entity to load (in its `initialize`, with
`this.app`). Script tag: `Glitchless.instrumentPlayCanvas(glitchless, { app })` (the `pc` global is found on its own).
What the adapter adds:
- **Frames from the app's loop**, from one `frameupdate` to the next, split into update time (scripts, animation and physics, up to `framerender`) and render time (`prerender` to `postrender`).
- **Draw calls** from the device's own per-frame count, **triangles** counted as the device draws (PlayCanvas counts them only in its profiler build), shaders as `programs`, the device's textures and vertex buffers.
- **Scenes as levels.** `app.scenes.changeScene()` and `loadSceneHierarchy()` start a level named after the scene, unless the name reads as a loader, menu, overlay or end screen; changing to a menu ends it. Pass `levels: ['Forest', 'Cave']`, `levels: (name) => name or null`, or `levels: false`. A game that builds its levels in code calls `glitchless.levelStart()`, which turns scene levels off.
- **Gauges**: `entities`, `mesh_instances`, `materials`, `textures` and `lights`. Turn them off with `gauges: false`.
- **Hitch causes**: `shader_compile` (a new shader: the first time a material variant is drawn), `object_spawn` (50+ render, model, sprite, element or particle components added in a frame), `geometry_upload`, `texture_upload`, `scene_change`, `script`, `render`, with `shaders_compiled`, `objects_added` and `buffers_added`.
- **Errors**: failed asset loads (`kind: 'asset'`), context loss (or a lost WebGPU device), and script errors. PlayCanvas's debug build (what Vite's dev server serves) catches an error in a script, logs it and turns the script off, so it never reaches `window.onerror`; the adapter reports it as the same `js` error the production build gives.
- **WebMCP**: `glitchless_inspect_scene` lists the hierarchy's entities by name with their meshes and materials, flagging copies that should be instanced or batched, plus scripts by type, lights, cameras and the device's shaders and textures; `glitchless_screenshot` captures the canvas at the end of a frame. `captureMap` renders the level from an orthographic camera into a texture.
[apps/playcanvas-demo](https://github.com/glitchless-gg/glitchless/tree/main/apps/playcanvas-demo) is a small PlayCanvas game wired this way, with planted bugs.
**What it sends, and what it costs the game.** [PRIVACY.md](https://github.com/glitchless-gg/glitchless/blob/main/packages/sdk/PRIVACY.md) lists every field in each privacy mode. The per-frame work is a sketch insert, a few counters and two clock reads around `renderer.render`: about 30 ns for the sketch, measured. Every 5 s it summarises a window (about 10 µs); every 15 s it serialises and gzips one batch (about 15 µs to serialise a real batch of about 5 KB, which gzips to about 1 KB) and sends it with `fetch`, which runs off the main thread. Batches go out uncompressed with `sendBeacon` when the page is hidden or closed.
## Game API
| Call | Use |
| --- | --- |
| `glitchless.levelStart(id)` / `glitchless.levelComplete(id, { durationS, score })` / `glitchless.levelFail(id, { cause })` | level funnel, difficulty spikes |
| `glitchless.death({ x, y, z, cause })` | death heatmap (places sent only when positions are on), rage-quit detection |
| `glitchless.track(name, attrs)` | pickups, power-ups, purchases; also breadcrumbs for errors and hitches |
| `glitchless.setPosition(vec3)` (every frame) | movement heatmap and session replay (sampled at 4 Hz, sent only when positions are on) |
| `glitchless.setContext({ particles, enemies })` (every frame) | numeric gauges correlated with hitches |
| `glitchless.captureMap(levelId, { axis: 'xy' \| 'xz', bounds: { min: [a, b], max: [a, b] } })` | renders an orthographic thumbnail used as the heatmap background |
| `glitchless.error(err, attrs)` | report a handled error |
| `glitchless.registerTool(name, description, inputSchema, handler)` | expose a game tool to coding agents over WebMCP |
| `glitchless.measure(seconds)` | frame-time stats over the next N seconds (the A/B primitive) |
| `glitchless.reportBug({ note, screenshot })` | send a player's bug report from your own UI (`screenshot: false` leaves the picture out); resolves `{ sent }` |
| `glitchless.openBugReport()` | open the built-in bug report form, from your pause menu for example |
### Your own metrics
Every game counts different things, so `track` takes any name and attributes:
```js
glitchless.track('open', { edition: 'free' });
glitchless.track('play', { mode: 'endless' });
glitchless.track('paywall', { where: 'settings' });
glitchless.track('buy', { step: 'done' });
```
The dashboard's Events page counts each one over time, breaks it down by each attribute (plays by `mode`,
paywall views by `where`) and builds funnels between them (`paywall` → `buy=done`). Each game also gets its own
dashboard, pages of the panels that matter for it, which your coding agent can design with the
`glitchless_set_dashboard` MCP tool. Attributes are strings, numbers or booleans: at most 12 per event, and strings
are cut to 64 characters. Use the game's words, never anything that identifies a player.
**Purchases** have one convention in every Glitchless SDK:
```js
glitchless.track('purchase', { amount: 4.99, currency: 'USD', item: 'gems_500' });
```
Send it when the store confirms the purchase, with `amount` a number in the currency's units (4.99, not 499) and `currency` the ISO code the store charged in. Boards add the amounts up into revenue, revenue per player and revenue by item, in the game's one currency: purchases in another are listed as left out, not added in. Never send receipts, order ids or payment details.
### Bug reports
Turn them on for playtests, jams and early access:
```js
const glitchless = Glitchless.init({
// …
bugReports: { hotkey: 'F8', onOpen: () => game.pause(), onClose: () => game.resume() },
});
```
F8 (or the **Report a bug** button in the overlay) opens a small form over the game: a preview of the screenshot, a
note, Send and Cancel (Ctrl+Enter and Esc work too). While it's open the SDK keeps keys, clicks and mouse movement
from reaching the game; `onOpen` and `onClose` are where the game pauses, which also stops gamepad input. The
report carries the note, a JPEG of the canvas (from the engine adapter, shrunk to fit 200,000
characters), the level, the position when positions are on, the last 20 game events and hitches, the last frame
windows' fps and p95, and the device. It is sent on its own request straight away.
Reports show in the dashboard's Bug Reports page with a link to each session's replay, as a finding once three
come from one level, and to agents through the `glitchless_bug_reports` MCP tool. The note is whatever the player
types: read [PRIVACY.md](PRIVACY.md#bug-reports-the-players-own-words) before you ship it.
## WebMCP
Tools the running game exposes:
- `glitchless_live_performance`
- `glitchless_measure`
- `glitchless_inspect_scene`
- `glitchless_errors`
- `glitchless_game_events`
- `glitchless_screenshot`
- `glitchless_session`
- `glitchless_annotate`
- anything you add with `glitchless.registerTool()`
They're registered two ways:
- **[webmcp.dev](https://webmcp.dev) bridge.** Works with Claude Code, Codex and Cursor today. Run `claude mcp add webmcp -- npx -y @jason.today/webmcp@latest --mcp`, ask the agent for a token, then paste it into the WebMCP widget: the small square in the bottom-right corner, just left of the overlay (`webmcp: { position: 'top-left' }` moves it).
- **W3C `document.modelContext.registerTool`.** Used when the browser supports it (Chrome origin trial).
The webmcp.dev client is vendored from `@jason.today/webmcp` (MIT) in `src/vendor/`.
## Wire format
See [docs/SPEC.md](https://github.com/glitchless-gg/glitchless/blob/main/docs/SPEC.md).
---
Source: https://github.com/glitchless-gg/glitchless/blob/main/packages/sdk/PRIVACY.md
# What Glitchless for the web sends
This file lists every field the browser SDK sends in each privacy mode and what it keeps in the browser, so a game's privacy page can say the same. It was written from `src/core.js`, `src/device.js`, `src/bugreport.js` and the engine adapters (`src/three.js`, `src/phaser.js`, `src/babylon.js`, `src/playcanvas.js`); keep it in step with them. The Godot, Unity and Unreal SDKs have their own lists (`godot/addons/glitchless/PRIVACY.md`, `unity/Packages/gg.glitchless.sdk/PRIVACY.md`, `unreal/Plugins/Glitchless/PRIVACY.md`), and the modes mean the same thing in all of them (`docs/SPEC.md` §4b).
Nothing here is legal advice.
## The three modes
`Glitchless.init({ privacy: 'anonymous' })` is the default. `privacy: 'install'` adds a stored player ID, so the game can see how many players come back (retention). `privacy: 'full'` adds that and the fields marked **full**.
| | anonymous | install | full |
| --- | --- | --- | --- |
| Session ID | random, made per page load, **kept in memory only** | the same | the same |
| Player ID | **none**. The server files each session under its session ID. A `glitchless.pid` left in localStorage by an install- or full-mode build is deleted | random `p_…`, made on the first load and stored in localStorage as `glitchless.pid`. Not tied to any account or advertising ID | the same ID as install mode |
| Device | browser and its major version, OS, phone or not, GPU tier (low/mid/high), WebGL version, screen size, pixel ratio, and whether it is a Steam Deck | the same as anonymous | adds the full browser version, the GPU renderer and vendor strings, CPU core count and device memory |
| Positions | **not sent**: deaths go without a place, no movement samples | the same as anonymous | death places and 4 Hz position samples, in game-world units |
| Error text | URLs in messages and stacks lose their query strings and fragments | the same as anonymous | sent as captured |
**What install mode adds to anonymous mode:** one thing, the player ID. It goes in every batch as `player_id` and stays in localStorage, so the next visit from the same browser sends the same ID. Everything else is sent exactly as in anonymous mode. Install and full mode share the stored ID. `glitchless.resetInstallId()` deletes it: the page keeps sending the ID it started with until it closes, and the next page load makes a new one, which nothing links to the old one. If localStorage is blocked (some private windows), the ID lasts one page load.
`positions: true` turns positions on in anonymous and install mode. They are places in the game's own world, not the player's location.
In every mode:
- the page is sent as origin and path only (no query string), and the referrer as its origin only;
- nothing typed by the player (except a bug report's note, below), no names, emails or accounts, and no cookies;
- the Glitchless server does not read or store the address a request came from. The host it runs on may log it, so check that host's settings.
The GPU renderer string sent in full mode helps tell slow devices apart, but it is also one of the inputs browsers treat as fingerprintable. Anonymous and install mode send only the tier, and `steam_deck: true` when the renderer string is a Steam Deck's (the browser reads the string to work these out, and doesn't send it).
## Field by field
Every batch has an envelope: `service`, `env`, `version` (the game's), `session_id`, `player_id` (null in anonymous mode, the stored player ID in install and full mode) and `sdk`. Every event has `type`, `ts` (milliseconds, UTC) and `level` (the game's own level name, or null).
| event | fields | when |
| --- | --- | --- |
| `session_start` | `session_type` (`user` or `synthetic`), `privacy` (`anonymous`, `install` or `full`), `device` (above; `steam_deck: true` only on a Steam Deck, with `gpu_tier` `low`), `page`, `nav_ms`; with the Phaser, Babylon.js or PlayCanvas adapter, `engine` (`phaser`, `babylon` or `playcanvas`, its version and renderer: the same for every player of a build) | the first batch |
| `session_end` | `reason`, `duration_s` | page hidden or closed |
| `frame_window` | frames, fps, refresh rate and budget, frame-time sketch and percentiles, slow/jank/hitch/frozen counts, render ms, update ms (Phaser, Babylon.js, PlayCanvas), draw calls, triangles, geometry/texture/shader counts, JS heap MB, `ctx` (the game's gauges; with Phaser also object, body, tween, listener and sound counts; with Babylon.js mesh, material, texture, particle and observer counts; with PlayCanvas entity, mesh instance, material, texture and light counts) | every 5 s, sent in batches every 15 s |
| `hitch` | duration, likely causes (Long Animation Frame script attribution, shader compile, GC, asset load; with Phaser, Babylon.js and PlayCanvas also scene change, object spawn, texture or geometry upload, update or render time, and what the frame compiled, added or uploaded), draw calls, `ctx`, `recent` (names of the game events in the last 1.5 s) | a frame over 50 ms and over twice the recent median, at most 3 per window |
| `load` | time to first frame, asset load timings | once |
| `error` | `kind`, `message`, `stack`, `source` (file name), `line`, `col`, `fn`, `fingerprint`, `attrs` (what the game passes to `glitchless.error`; for errors an adapter reports, engine counts, the Phaser scene key, how long the loop stalled, a shader compiler's message, or a PlayCanvas script's name and method). The server builds breadcrumbs from the session's earlier events | each error, at most 25 of a kind per session |
| `level_start` / `level_complete` / `level_fail` | `duration_s`, `score`, `cause`, as the game passes them | the game calls them |
| `death` | `cause`; with positions on: `x`, `y`, `z` | the game calls it |
| `custom` | `name`, `attrs`: the game's own words via `glitchless.track()`; also `rage_input` (a key code and count) from the SDK | as they happen |
| `pos` | `[time, x, y, z]` samples | with positions on, in each batch |
| `map` | an orthographic thumbnail of a level, rendered from the scene by `glitchless.captureMap()` | the game calls it |
| `bug_report` | `note` (what the player typed), `screenshot` (a JPEG of the game's canvas, not of the rest of the page), `device` (the same fields as `session_start`, at the same mode), `recent` (the last 20 game events and hitches, as above), `frames` (fps and p95 of the last frame windows); with positions on: `x`, `y`, `z` | the player sends one from the form, or the game calls `glitchless.reportBug()`. Off unless the game turns it on |
**Game words.** Event names, attributes, level names, causes and gauges are whatever the game passes. Use the game's own words, never anything typed by or particular to the player.
## Bug reports: the player's own words
Bug reports are off until the game turns them on (`bugReports` in `Glitchless.init`, or its own button calling
`glitchless.reportBug()`). A report is the one place the SDK sends text the player typed, and Glitchless sends the note
as it is: it can't tell a bug description from a name, an email address, a phone number or anything else a player
chooses to write. If your game turns reports on:
- say so in your game's privacy page: a report sends the player's note, a screenshot of the game and where they were;
- don't ask players for contact details in the note. If you want to reply to them, collect that with consent in your own UI;
- the screenshot is the game's canvas only, but it shows whatever the game showed: a player's name or chat on screen goes with it;
- a developer who opens a report sees all of it. Treat reports like support email: when a player asks, delete their
report with the Delete button on it in the dashboard (or `DELETE /v1/report?service=&id=`), which removes the note
and the screenshot.
The form tells players what it sends ("your note, the screenshot and where you are in the game").
## Kept in the browser
| key | what | mode |
| --- | --- | --- |
| `glitchless.overlay` (localStorage) | whether the developer overlay is open | all, only when the overlay runs (off by default when `env` is `prod`) |
| `glitchless.pid` (localStorage) | the random player ID | **install** and **full**. Anonymous mode deletes it |
Unsent batches are held in memory only, and are dropped if the page closes before they can be sent.
## The Datadog bridge
`datadog: {…}` loads Datadog's own RUM SDK, which collects its own data under Datadog's terms. It is off by default, and nothing above covers it.
---
Source: https://github.com/glitchless-gg/glitchless/blob/main/packages/mcp/README.md
# glitchless-mcp
The [Glitchless](https://github.com/glitchless-gg/glitchless) MCP server. It lets any coding agent
(Claude Code, Codex, Cursor or another MCP client) read how your game runs for real players, from a
Glitchless server: ranked findings, frame-time breakdowns, errors, level funnels, death hotspots and
version comparisons. It can also run playtest bots before and after a change, to check a fix.
It's a stdio server. Run it with `npx`, no install needed:
```bash
npx -y glitchless-mcp
```
## Settings
| Variable | What |
| --- | --- |
| `GLITCHLESS_URL` | Your Glitchless server. Default `http://localhost:7777` |
| `GLITCHLESS_API_KEY` | A secret read key (`gl_sk_…`), if the server needs one. Keep it out of git and out of the game |
| `GLITCHLESS_SERVICE` | The game to use when a tool call doesn't name one. Optional |
## Add it to your agent
Claude Code:
```bash
claude mcp add glitchless -e GLITCHLESS_URL=http://localhost:7777 -e GLITCHLESS_API_KEY=gl_sk_… -- npx -y glitchless-mcp
```
Codex, in `~/.codex/config.toml`:
```toml
[mcp_servers.glitchless]
command = "npx"
args = ["-y", "glitchless-mcp"]
env = { GLITCHLESS_URL = "http://localhost:7777", GLITCHLESS_API_KEY = "gl_sk_…" }
```
Cursor, in `.cursor/mcp.json` (add the file to `.gitignore` if it holds the key):
```json
{
"mcpServers": {
"glitchless": {
"command": "npx",
"args": ["-y", "glitchless-mcp"],
"env": { "GLITCHLESS_URL": "http://localhost:7777", "GLITCHLESS_API_KEY": "gl_sk_…" }
}
}
}
```
Any other MCP client: the command is `npx`, the arguments are `-y glitchless-mcp`, and the settings
above go in its environment.
## Tools
| Tool | What it answers |
| --- | --- |
| `glitchless_list_games` | Which games report to the server, with their versions and environments |
| `glitchless_triage` | Ranked findings: stutter per level and what it correlates with, leaks, top errors, difficulty spikes. Start here |
| `glitchless_overview` | Headline numbers: sessions, players, crash-free sessions, frame time against the budget, rage quits |
| `glitchless_performance` | Frame time per level, with draw calls, geometry counts and game gauges |
| `glitchless_get_error` | One error issue, with its stack and the gameplay before it |
| `glitchless_player_funnel` | Per level: starts, completions, deaths and their causes, rage quits |
| `glitchless_death_hotspots` | Where players die on a level, clustered in world coordinates |
| `glitchless_events` | The game's own events (whatever it sends with `track`): counts, attribute breakdowns and funnels |
| `glitchless_dashboard` | The game's dashboard: its pages, and one board's panels with their numbers |
| `glitchless_set_dashboard` | Save the game's pages: boards of panels in its own words, and the built-in views it keeps. Needs a key with the dashboard scope |
| `glitchless_compare_versions` | Two or more versions side by side, in the game's own stats first, to confirm a fix with real players |
| `glitchless_session` | One play session in detail, with its whole event timeline |
| `glitchless_bug_reports` | Bug reports players sent from inside the game: their notes, levels, positions and sessions, and one report's screenshot when asked |
| `glitchless_playtest` | Autoplay bots play the game and compare with a saved baseline ([glitchless-playtest](https://www.npmjs.com/package/glitchless-playtest); needs Playwright in the game project) |
| `glitchless_deck_send` | Build the game, send it to the paired Steam Deck, add it to the Deck's library and optionally start it ([glitchless-deck](https://www.npmjs.com/package/glitchless-deck); pair once with `npx glitchless-deck pair`) |
| `glitchless_deck_measure` | Measure a Unity, Godot or Unreal build on the Steam Deck and compare it with a saved run |
To let the agent drive the running game as well (measure frame times, inspect the scene, load a
level), add the WebMCP server beside this one: `npx -y @jason.today/webmcp@latest --mcp`.
## License
MIT
---
Source: https://github.com/glitchless-gg/glitchless/blob/main/packages/playtest/README.md
# glitchless-playtest
Playtest every change. Autoplay bots play your web game (Three.js, Phaser, Babylon.js, PlayCanvas or plain WebGL) on a saved
baseline and on the current code, and [Glitchless](https://github.com/glitchless-gg/glitchless) compares
the two runs: slow frames, render and update time, draw calls, hitches, errors, deaths and level
completion. A regression exits with code 1, so it can gate a pull request.
```bash
npx glitchless-playtest --save baseline # before changing the code
npx glitchless-playtest # after: compares with "baseline"
```
## What it needs
- **Your game running**, by default at `http://localhost:5173/`, with the Glitchless SDK
([`glitchless`](https://www.npmjs.com/package/glitchless)) reporting to a Glitchless server.
- **An autoplay mode in the game.** Each scenario opens the game with URL parameters such as
`?autoplay=1&seed=7&skill=0.95&level=1`. A fixed seed makes the bot play the same way on both builds,
so the only thing that changes is the code. The bot's sessions should use `sessionType: 'synthetic'`,
so they stay out of player numbers.
- **A Glitchless server**, by default `$GLITCHLESS_URL` or `http://localhost:7777`.
- **Playwright** installed in the game project (`npm install -D playwright`). It uses your installed
Chrome by default, so frame times come from your real GPU.
A run ends after `max_s` seconds, or as soon as the page sets `window.__GLITCHLESS_PLAYTEST_DONE__ = true`.
On a fast machine, vsync hides a slower build in frame time. Render CPU and update CPU per frame don't
hide it, so they fail a run on their own: update CPU is the game's update code, which the SDK times
where the engine lets it (Phaser's scene updates and physics). The Phaser demo in the Glitchless repo,
`apps/phaser-demo`, has a working `glitchless.playtest.json`.
## Options
```
--save NAME save this run as NAME (e.g. baseline)
--compare NAME compare with the saved run NAME (default: baseline, if saved)
--compare-only A B compare two saved runs without playing
--baseline-url URL play another running build as the baseline first (for CI)
--game URL game to play (default http://localhost:5173/)
--server URL Glitchless server (default $GLITCHLESS_URL or http://localhost:7777)
--service NAME game service name (default: read from the page)
--api-key KEY Glitchless API key (default $GLITCHLESS_API_KEY)
--quick play only the first scenario
--max SECONDS cap each run
--channel NAME browser: chrome (default) or chromium
--headed show the browser windows
--json print the full result as JSON instead of markdown
```
Saved runs go in `.glitchless/playtest/` (add it to `.gitignore`).
## Configuration
A `glitchless.playtest.json` in the project root can set any default: `game`, `server`, `scenarios`,
`thresholds`, `done_when` (a JavaScript expression that ends a run when it's true), `viewport` and
`runs_dir`.
```json
{
"game": "http://localhost:3000/",
"scenarios": [
{ "name": "speedrun", "params": { "autoplay": 1, "seed": 3, "skill": 0.95 }, "max_s": 60 }
]
}
```
A metric only counts as worse when it's worse by both its relative and its absolute margin, so noise
on a fast machine doesn't fail a build.
## From code or an agent
```js
import { playtest } from 'glitchless-playtest';
const result = await playtest({ saveAs: 'baseline' }); // later: playtest({ compareTo: 'baseline' })
```
The [glitchless-mcp](https://www.npmjs.com/package/glitchless-mcp) server exposes the same thing to
coding agents as the `glitchless_playtest` tool.
## License
MIT
---
Source: https://github.com/glitchless-gg/glitchless/blob/main/packages/sourcemaps/README.md
# glitchless-sourcemaps
Readable errors from your minified web game. A player's error in a production build reads
`e (index-3f2a.js:1:48213)`, which neither you nor your coding agent can act on. Upload the build's
source maps to [Glitchless](https://github.com/glitchless-gg/glitchless) and the same error shows as
`fireShotgun (weapons.js:141)`, with the original stack and the line of code, in the dashboard, in
findings and for agents over MCP (`glitchless_get_error`). An error also keeps one fingerprint across
builds, so 1.2.0 and 1.2.1 don't split one bug into two issues.
Nothing changes in your game or the SDK: the server reads the stack it already gets.
## Vite
```bash
npm install -D glitchless-sourcemaps
```
```js
// vite.config.js
import { defineConfig } from 'vite';
import glitchlessSourcemaps from 'glitchless-sourcemaps/vite';
export default defineConfig({
plugins: [glitchlessSourcemaps({ deleteMaps: true })],
});
```
The plugin builds with `build.sourcemap: 'hidden'` (maps with no comment pointing at them, so players'
browsers never ask for them) unless you set `build.sourcemap` yourself. After `vite build` it uploads the
maps when `GLITCHLESS_API_KEY` is set, and with `deleteMaps: true` it removes them from `dist`, so they
never ship. Without a key it uploads nothing (and still deletes them with `deleteMaps`).
| option | default | |
| --- | --- | --- |
| `version` | `$GLITCHLESS_VERSION`, else `version` in package.json | must be the version your game passes to `Glitchless.init` |
| `service` | `$GLITCHLESS_SERVICE`, else `name` in package.json | the game's service name |
| `url` | `$GLITCHLESS_URL`, else `http://localhost:7777` | your Glitchless server (hosted: `https://api.glitchless.gg`) |
| `apiKey` | `$GLITCHLESS_API_KEY` | a secret key that can upload source maps |
| `deleteMaps` | `false` | delete the maps from the build after the upload |
| `strict` | `false` | fail the build when an upload fails (otherwise it warns) |
## CLI (any bundler, CI)
```bash
GLITCHLESS_API_KEY=gl_sk_… npx glitchless-sourcemaps upload dist --version 1.2.0
```
It finds every `*.js.map` under `dist` and the script each belongs to (the one whose
`sourceMappingURL` names it, else the map's name without `.map`, else the map's `file`), and uploads
each map with the script's path in the build (`assets/index-3f2a.js`). In CI, upload before you deploy:
```yaml
- run: npm run build
- run: npx glitchless-sourcemaps upload dist --version "$(node -p "require('./package.json').version")" --delete
env:
GLITCHLESS_API_KEY: ${{ secrets.GLITCHLESS_API_KEY }}
GLITCHLESS_URL: https://api.glitchless.gg
```
```
--version VERSION the version your game passes to Glitchless.init (required)
--service NAME the game's service name (default $GLITCHLESS_SERVICE, else
the "name" in ./package.json)
--url URL Glitchless server (default $GLITCHLESS_URL or http://localhost:7777)
--delete delete each map after it is uploaded, so maps don't ship
--dry-run list the maps and their scripts without uploading
```
Exit code: 0 uploaded, 1 an upload failed, 2 usage or setup error. A local Glitchless server started
without `GLITCHLESS_API_KEY` needs no key.
## Keys
In the hosted dashboard, create a secret key in the game's **Settings** and tick **Can upload source
maps**. Keep it in CI secrets: like every secret key it can also read the game's data.
## How it matches
A stack frame's script URL is matched to the longest uploaded path it ends with, so
`https://cdn.example.com/play/blastpit/assets/index-3f2a.js` finds `assets/index-3f2a.js` wherever the
game is served from. Maps are kept per game and version: upload them for every version you ship, before
players get it. An error that arrives before its version's maps is still shown with original frames once
they are uploaded, but keeps the fingerprint it arrived with.
Maps with `sourcesContent` (the default in Vite, esbuild, Rollup and webpack) give the best results: the
server reads the function's name from the source and shows the failing line.
## Limits
10 MB per request on your own server and 4 MB on the hosted service (gzipped; the CLI gzips, and 4 MB is a
12–20 MB map), 40 MB per map unpacked, 200 maps per version. A game keeps
the 50 versions uploaded most recently; an upload past that deletes the oldest. The hosted service keeps
a map for 400 days after its upload.
`GET /v1/sourcemaps?service=` lists a game's maps; `DELETE /v1/sourcemaps?service=&version=` deletes a
version's (the key needs the sourcemaps scope).
---
Source: https://github.com/glitchless-gg/glitchless/blob/main/unreal/Plugins/Glitchless/README.md
# Glitchless for Unreal
Game observability for Unreal Engine 5.8 (and 5.7). Glitchless records how your game runs for real players and gives your coding agent the same data, over MCP, so it can fix what they hit.
- **Stutter.** Every frame is timed, and each 5-second frame window carries a mergeable frame-time sketch and where the time went: the game thread, the render and RHI threads, the GPU. Each hitch (a frame over 50 ms and twice the recent median) comes with the thread that held it up and its likely causes: pipeline states compiled on the spot, shaders compiled (in the editor), garbage collection, a map load, a streamed level, assets loaded, or long game, render or frozen frames.
- **Errors and crashes.** `UE_LOG` errors, ensures with their callstacks, Blueprint runtime errors (Accessed None and the rest), and crashes, written to disk as the game dies and sent on the next launch. Grouped by the same fingerprint every Glitchless SDK uses, with the game events just before each one.
- **Bug reports.** A screenshot and the player's note, from one call or a key that opens a small box over the game.
- **Levels and players.** Maps are levels on their own. Deaths, level fails and quits feed the level funnel, death heatmaps and rage-quit detection.
- **Leaks.** UObject and actor counts and memory per level load.
- **Anonymous by default.** No player ID, no device identifier, coarse device facts only. [PRIVACY.md](PRIVACY.md) lists every field in each of the three modes.
It sends the same events as the browser, Unity and Godot SDKs, so the dashboard, the findings and the MCP tools work with an Unreal game as they are.
## Install
From Fab: install it to your engine, then enable Glitchless in **Edit → Plugins** (it is under Analytics). Fab installs it into the engine already built, so a Blueprint-only project can use it as it is.
From GitHub: download `glitchless-unreal-0.1.0.zip` from the [glitchless-unreal 0.1.0 release](https://github.com/glitchless-gg/glitchless/releases/tag/glitchless-unreal/v0.1.0) and unzip it to your project's `Plugins/Glitchless` (or copy this folder there), or add it to a project's `AdditionalPluginDirectories`. Enable it in **Edit → Plugins** and rebuild. A Blueprint-only project needs to become a C++ project once (**Tools → New C++ Class**), because the plugin is source.
## Set it up
No code: **Project Settings → Plugins → Glitchless**, fill in **Service** (your game's id, lowercase), **Intake** (`http://localhost:7777` for a local server, or `https://api.glitchless.gg`) and **Client Token**. The game instance starts Glitchless with them. They are saved in `Config/DefaultGame.ini`:
```ini
[/Script/Glitchless.GlitchlessSettings]
Service=my-game
Intake=http://localhost:7777
ClientToken=gl_pub_dev
Privacy=Anonymous
bSendInDevelopmentBuilds=True
```
Or from C++, as early as you can (your `UGameInstance::Init`), leaving Service empty in the settings:
```cpp
#include "Glitchless.h"
FGlitchless::Init(TEXT("my-game"), TEXT("https://api.glitchless.gg"), TEXT("gl_pub_..."));
```
A packaged game starts capturing when the plugin loads, so frame times and errors from the first frames are kept; nothing is sent until Init. In the editor it runs only during Play In Editor. Play In Editor and development builds send too, as env `dev`; shipping builds send as `prod`. Turn **Send In Development Builds** off to keep your own play out; `-glitchless` turns it back on for one run.
## Tell it about your game
| Game moment | C++ | Blueprint |
| --- | --- | --- |
| A level starts (turns automatic map levels off) | `FGlitchless::LevelStart(TEXT("forest-2"))` | Level Start |
| Level beaten | `FGlitchless::LevelComplete(TEXT(""), Score)` | Level Complete (Score below 0: none) |
| Level lost or quit | `FGlitchless::LevelFail(TEXT(""), TEXT("timeout"))` | Level Fail |
| The player died | `FGlitchless::Death(GetActorLocation(), TEXT("fell"))` | Death |
| Anything worth counting | `FGlitchless::Track(TEXT("powerup"), TEXT("magnet"))`, or a `TMap` of attributes and one of numbers | Track Event, Track Event (What) |
| A purchase (the convention in every SDK) | `FGlitchless::Track(TEXT("purchase"), {{TEXT("currency"), TEXT("USD")}, {TEXT("item"), TEXT("gems_500")}}, {{TEXT("amount"), 4.99}})` | Track Event, with `amount` in Numbers |
| What is on screen | `FGlitchless::SetContext(TEXT("enemies"), Enemies.Num())` | Set Context |
| Where the player is | `FGlitchless::SetPosition(GetActorLocation())` (sent in full mode or with Positions) | Set Position |
| A problem the game noticed | `FGlitchless::Error(TEXT("save failed"))` | Report Error |
| The player reports a bug | `FGlitchless::ReportBug(Note)` or `FGlitchless::ShowBugReportDialog()`; `FGlitchless::OnBugReportDialog()` says when the box opens and closes | Report Bug, Show Bug Report Dialog; the Glitchless subsystem's On Bug Report Dialog |
Bug reports are off until the game turns them on: set **Bug Report Key** (F8, as in the other SDKs) in the settings, or call Show Bug Report Dialog from the game's own menu. The box takes the game's input while it is open (the mouse is freed) and pauses the game (**Pause For Bug Report**, on by default; a game paused already stays paused).
| Send now | `FGlitchless::Flush()` | Flush |
A purchase is sent when the store confirms it: `amount` goes in the numbers (a number in the currency's units, 4.99, not 499) and `currency` is the ISO code the store charged in. Boards add the amounts up into revenue, revenue per player and revenue by item, in the game's one currency: purchases in another are listed as left out, not added in. Never send receipts, order ids or payment details.
Every call is safe before Init and from any thread. Positions are taken in Unreal units (centimetres, Z up) and sent as every SDK sends them, metres with y up: x = X/100, y = Z/100, z = Y/100.
Maps are levels except those that read as a menu, lobby, loader, overlay or end screen (`Entry`, `MainMenu`, `L_Frontend`, `Lobby`, `Loading`, `GameOver`, ...). The first `LevelStart` call turns that off: the game's own levels win.
## Crashes
A crash is written to `Saved/Glitchless/outbox.json` (on a phone, in the app's own data folder) as the game dies and sent on the next launch, with the frames before it and the session's end (`reason: crash`). Where it is caught: Unreal's `OnHandleSystemError` (the editor, builds that ship CrashReportClient, and every Windows build: Unreal raises it there before it has read the exception, so the plugin keeps each possibly fatal exception as it happens, with a vectored exception handler that does nothing else, and walks the crashed thread's stack from it); the `appError called` line every platform's error device logs as an assert or `UE_LOG` Fatal takes the game down; and, in a packaged Mac, Linux, iOS or Android game where nothing else handles them (Unreal installs no signal handlers there without CrashReportClient), the plugin's own handlers for SIGSEGV, SIGBUS, SIGILL, SIGFPE, SIGABRT and SIGTRAP, which then pass the signal on so the OS still writes its report. Which file and line a stack frame names depends on the platform: a Windows build resolves them from the PDB beside the game (for the game's and the plugin's code; an engine installed from the Launcher has no line numbers of its own unless its debug symbols are installed), and a Linux build should from its `.sym` files (not tried yet). A packaged Mac or iOS game has function names only. Unreal names its frames with `dladdr` there, and turns on the system symbolicator only for tools outside the App Sandbox (`MacPlatformMisc.cpp`), so a dSYM beside the game doesn't help. Keep the dSYM anyway (`-EnableDSYM` when building) to read a crash's lines with `atos` or lldb.
## Measure from the command line
Agents and CI can ask a build how it runs:
```bash
MyGame -glitchless-warmup=2 -glitchless-measure=10 -glitchless-out=measure.json
```
It waits for the first map plus the warmup, measures for the given seconds, prints `{"glitchless_measure": {...}}` as one line on stdout (and in the log as LogGlitchless), writes it to `-glitchless-out`, and quits. The numbers are the browser SDK's `glitchless_measure` (frame_ms percentiles, fps, pct_frames_over_budget, update_cpu_ms for the game thread, render_cpu_ms, rhi_cpu_ms, gpu_ms, hitches with causes, draw calls, memory and its change, new errors) plus `session_id`. A Windows build opens no console, but its stdout can be piped or redirected, and `-glitchless-out` works too. UE 5.8 signs Mac builds with App Sandbox: such a build can write `-glitchless-out` only inside its container (`~/Library/Containers//Data`, whose `tmp` is a good place), and its Saved folder is in there too; stdout works anywhere.
| Flag (`-` or `--`) | |
| --- | --- |
| `-glitchless` | send from a development build when Send In Development Builds is off |
| `-glitchless-intake=URL` | send to this server instead |
| `-glitchless-measure=S` | print frame stats as JSON after S seconds, then quit |
| `-glitchless-warmup=S` | with measure: wait this long after the first map first |
| `-glitchless-out=PATH` | with measure: write the JSON here too |
| `-glitchless-synthetic`, `-autoplay` | mark the session as a bot's (measure does too) |
| `-glitchless-off` | send nothing |
| `-glitchless-debug` | log each batch as it is sent |
On a phone, launch it from a Mac with the flags after the bundle ID, and the line comes back on the console: `xcrun devicectl device process launch --console --device -glitchless-measure=10`.
## Builds that can't reach the network
- **Shipping Mac builds** are signed with `Sandbox.NoNet.entitlements` by default (Project Settings → Platforms → Xcode Projects → Shipping Specific Mac Entitlements): no network at all, so Glitchless can't send. Set it to `/Game/Build/Mac/Resources/Sandbox.Client.entitlements` (copied from `Engine/Build/Mac/Resources`), or `Sandbox.Server`, which Development builds already use.
- **iOS** allows only HTTPS. Send to `https://api.glitchless.gg` (or your HTTPS server). For a plain-HTTP server on your network, in a development build:
- tick Allow web connections to non-HTTPS websites (`bDisableHTTPS=True` under `[/Script/IOSRuntimeSettings.IOSRuntimeSettings]`);
- send to the computer's address on the network, not `localhost`, with the phone on the same network;
- say why the game wants the local network, in Additional Plist Data: `NSLocalNetworkUsageDescription…`. iOS asks the player once, at the first send, and nothing gets through until they allow it. The test game's `Config/DefaultEngine.ini` does all three.
## Agents in the editor: Unreal MCP
Unreal 5.8 ships Unreal MCP (Experimental), an MCP server inside the editor. The optional **Glitchless Toolset** plugin (`unreal/Plugins/GlitchlessToolset`, beside this one) gives it two tools, so an agent can measure the game itself while it plays in the editor, as WebMCP's `glitchless_measure` lets it in a browser game:
| tool | |
| --- | --- |
| `Status` | what Glitchless sees now: whether and where it sends, the level, its stats, the last frame windows and hitches with their causes, the gauges, memory, this session's errors |
| `Measure(Seconds)` | frame stats over the next seconds, the same JSON as `-glitchless-measure` |
Enable **Unreal MCP**, **All Toolsets** (or just Toolset Registry) and **Glitchless Toolset** in **Edit → Plugins**, start the server (`ModelContextProtocol.StartServer`, or Editor Preferences → Model Context Protocol → Auto Start Server), run `ModelContextProtocol.GenerateClientConfig ClaudeCode` (or Cursor, Codex, ...) for the project's `.mcp.json`, then start Play In Editor before calling the tools. It is a plugin of its own because Unreal MCP is Experimental and isn't in 5.7; the Glitchless plugin doesn't need it.
## For the Glitchless server and docs (SPEC §5c)
The envelope's `sdk` is `glitchless-unreal/0.1.0`. Differences from the browser SDK on the wire:
| | |
| --- | --- |
| envelope | `player_id` is null in anonymous mode. Install mode (new: a stored random player ID with coarse device facts) and full mode send one |
| `session_start` | `privacy` is `anonymous`, `install` or `full`. `device`: `browser: "Unreal"`, `browser_version` (`5.8.4`), `os` (`Windows`, `macOS`, `Linux`, `SteamOS`, `iOS`, `Android`), `mobile`, `device_tier`, `gpu_tier` (`low` on a Steam Deck), `renderer` (`deferred`, `forward`, `mobile`, `null`), `driver` (`D3D12`, `Metal`, `Vulkan`), `screen`, `dpr` (null), `refresh_hz` (phones only), `steam_deck`; full mode adds `model` (`Steam Deck OLED` on one), `os_version`, `gpu`, `gpu_vendor`, `cores`, `memory_gb`. `page` and `nav_ms` are null |
| `frame_window` | frame times from the clock at the start of each frame. Adds `game_ms` (game thread without its waits, GGameThreadTime), `process_ms` (the same, under the name the playtest gate reads), `render_ms` (render thread), `rhi_ms` (RHI thread, null without one), `gpu_ms`, `draw_calls` and `triangles` where the RHI counts them (it does only while its GPU profiler collects draw stats, so they are usually null in a packaged 5.8 build), `gc_collections`, `gc_ms`, `pso_compiles` (runtime PSO creations over 20 ms, UE 5.8), `shader_compiles` (editor only), `packages_loaded` (UE 5.8), `objects` (UObjects), `actors`, `heap_mb` (the process's memory), `texture_mb`, `screen`, `window`; `geometries`, `textures`, `programs` are null; `ctx` also holds `objects` and `actors`. A window never spans two levels |
| `hitch` | `duration_ms` is the frame's time on the clock, as Unreal's own hitch tracking times it (Unreal leaves waits out of its thread times, so a frame spent waiting on a pipeline state, the GPU or the disk would look short by them); `bound` names the slowest of the game, render and RHI threads and the GPU. `causes` from `pso_compile`, `shader_compile`, `gc`, `map_load`, `level_stream`, `asset_load`, `script` (game thread at 40% of the frame), `render` (render, RHI or GPU at 40%), `frozen` (over 700 ms). Adds `bound` (`game`, `render`, `rhi`, `gpu`, or null when no thread was measured), `game_ms`, `process_ms`, `render_ms`, `rhi_ms`, `gpu_ms`, `draw_calls` and `triangles` where the RHI counts them (it does only while its GPU profiler collects draw stats, so they are usually null in a packaged 5.8 build), `gc_collections`, `gc_ms`, `pso_compiles`, `shader_compiles`, `packages_loaded`; `loaf` is empty |
| `level_start` | also sent when a map that reads as a level loads |
| `error` | `kind`: `crash` (the game died: fatal, sent on the next launch), `ensure`, `script` (a Blueprint runtime error; Unreal stops that Blueprint there, so it is unhandled, like `exception`), `log_error` (`UE_LOG` Error; `fn` is the log category), `warning`, `manual`; adds `recent` (last 6 game events) and, for `script`, `attrs.blueprint` |
| `death`, `pos`, `bug_report` | positions only when they are on, in metres with y up |
| `bug_report` | as §2 has it, on a request of its own, from every session: `note`, `screenshot` (JPEG data URL of at most 200,000 characters, its longer side at most 960 px, shrunk through 800, 640, 480 and 320, as the Godot and Unity SDKs do), `recent` (the last 20 events and hitches as objects), `frames` (the last 6 windows' fps and p95), `device`; `x`, `y`, `z` when positions are on. `ts` and `level` are when the box opened. Neither the frame that reads the viewport back nor the box's first draw is timed |
| `session_end` | `reason`: `quit`, `background` (phones), `measure`, `crash` |
| `custom` | also `screen_change`, `app_resume`, `rage_input` |
What the server side needs (stream U2): `crash` (and `script` for Unreal, already) as fatal kinds for crash-free sessions; `ensure` as an error kind; engine-aware findings for `glitchless-unreal` (the leak finding should read `objects` and `actors`, and the hitch advice the Unreal causes above: PSO precaching and bundled PSO caches for `pso_compile`, `gc.TimeBetweenPurgingPendingKillObjects` and pooling for `gc`, async loading for `asset_load` and `map_load`, World Partition streaming for `level_stream`).
In anonymous mode a crash arrives under a fresh random session ID, because nothing on disk may tie it to the session it ended; in install and full mode it joins its session.
## How it is built
`Source/Glitchless/Private/Core` is the engine-independent core, a C++17 port of the Unity SDK's `GlitchlessCore.cs` with no Unreal types, no exceptions and no RTTI. It builds and runs without the engine: `node unreal/test/core.mjs` (clang or MSVC) runs its tests, among them JavaScript parity: the frame-time sketch matches `packages/sdk/src/sketch.js` byte for byte (its logarithm is fdlibm's, as V8's is, so every platform buckets alike; the copy is in `Source/ThirdParty/fdlibm`, with Sun's notice), and so do number formatting, rounding and error fingerprints. `node unreal/test/make-fixtures.mjs` rewrites the fixture from the JavaScript SDK.
The rest is the Unreal side: `GlitchlessRuntime` (frames and causes, maps, errors, crashes, lifecycle, the measure flag), `GlitchlessTransport` (FHttpModule, gzip through FCompression), `GlitchlessDevice`, `GlitchlessOutputDevice` (GLog), `GlitchlessBugReport` (Unreal's screenshot request, IImageWrapper JPEG, a Slate box), `GlitchlessInput`, the settings, subsystem and Blueprint library. Automation tests: `UnrealEditor-Cmd GlitchlessTest.uproject -ExecCmds="Automation RunTests Glitchless; Quit" -unattended -nullrhi` in `unreal/test/GlitchlessTest`, a test game that loads this plugin through `AdditionalPluginDirectories`. The end-to-end test, `node unreal/test/e2e.mjs`, packages that game as a Development build (BuildCookRun) and plays it against a local Glitchless server behind a recorder: a scripted run with planted problems (`-autoplay`), a crash and a fatal error each sent on the next launch, the bug report box typed into, and `-glitchless-measure`. `node unreal/test/ios.mjs` does the same on an iPhone: it packages and signs an iOS Development build (automatic signing, with the team in the test game's `DefaultEngine.ini`), installs it with `devicectl`, and plays it against a server on the Mac over the network. On Windows, `e2e.mjs` run over SSH renders with `-RenderOffscreen`, since a session with no desktop can't make a window (`--offscreen` does it anywhere). UE 5.8 won't cook with a Visual C++ runtime older than 14.50; it ships the installer in `Engine/Extras/Redist/en-us/vc_redist.x64.exe`. Build the editor target with `-NoHotReload` when an Unreal Editor is open, or UBT writes hot-reload module names the manifest doesn't point to.
---
Source: https://github.com/glitchless-gg/glitchless/blob/main/unity/Packages/gg.glitchless.sdk/README.md
# Glitchless for Unity
Game observability for Unity 2022.3 LTS and Unity 6. Glitchless records how your game runs for real players and gives your coding agent the same data, over MCP, so it can fix what they hit.
- **Stutter.** Every frame is timed from `Time.unscaledDeltaTime`. Each frame window carries a mergeable frame-time sketch and where the time went: physics, your `Update` code, rendering, the GPU. Each hitch (a frame over 50 ms and twice the recent median) comes with its likely causes: a garbage collection, a shader compile, a scene load, a burst of new objects, or long script, physics or render work.
- **Errors.** Uncaught exceptions, `Debug.LogError`, assertions and engine errors, through `Application.logMessageReceivedThreaded`, grouped by the same fingerprint the web and Godot SDKs use, with the game events just before each one.
- **Levels and players.** Scenes are levels on their own. Deaths, level fails and quits feed the level funnel, death heatmaps and rage-quit detection.
- **Leaks.** Mesh, material, texture and object counts per level load, where a development build has them.
- **Bug reports from players**, when you turn them on: F8 (or your own button) opens a short form, and the report carries what the player typed, a screenshot, where they were and what had just happened.
- **Anonymous by default.** No player ID, no device identifier, coarse device facts only. `Privacy = Privacy.Install` adds a saved random player ID so retention (D1, D7, D30) can be counted, and nothing else. [PRIVACY.md](PRIVACY.md) lists every field.
It sends the same events as the [browser](https://github.com/glitchless-gg/glitchless/tree/main/packages/sdk) and [Godot](https://github.com/glitchless-gg/glitchless/tree/main/godot/addons/glitchless) SDKs, so the dashboard, the findings and the MCP tools work with a Unity game as they are.
## Install
In Unity: **Window → Package Manager → + → Add package from git URL…**, and paste:
```
https://github.com/glitchless-gg/glitchless.git?path=unity/Packages/gg.glitchless.sdk
```
Or add it to `Packages/manifest.json`, pinned to a release:
```json
"gg.glitchless.sdk": "https://github.com/glitchless-gg/glitchless.git?path=unity/Packages/gg.glitchless.sdk#gg.glitchless.sdk/v0.2.0"
```
Once it's listed on [OpenUPM](https://openupm.com): `openupm add gg.glitchless.sdk`.
## Set it up
Call `Glitchless.Init` once, as early as you can: the `Awake` of a script in your first scene.
```csharp
using GlitchlessSdk;
using UnityEngine;
public class Telemetry : MonoBehaviour
{
void Awake()
{
Glitchless.Init(new GlitchlessOptions
{
Service = "my-game", // lowercase and stable
Intake = "http://localhost:7777", // your Glitchless server, or https://api.glitchless.gg
ClientToken = "gl_pub_dev", // public and write-only, fine inside the build
});
}
}
```
No code: add **Glitchless → Glitchless Bootstrap** to a GameObject in your first scene and fill in the same three fields.
**Retention needs install mode.** In anonymous mode (the default) nothing is kept between launches, so the dashboard can't tell a returning player from a new one. `Privacy = Privacy.Install` (or **Install ID** on the Bootstrap) keeps a random `p_…` player ID in `persistentDataPath/glitchless/player_id` (the one full mode uses; PlayerPrefs in WebGL builds) and sends it with every batch; nothing else changes. The Players page and `retention` board panels then show D1, D7 and D30. `Glitchless.ResetInstallId()` forgets it (for a "reset my ID" setting): this session keeps its ID, the next launch makes a new one. On a Steam Deck, `session_start` carries `steam_deck: true` and, on SteamOS, `os: "SteamOS"` (Linux players and Windows players under Proton).
Glitchless starts itself before the first scene loads, so frame times and errors from the very first frames are kept; nothing is sent until `Init`. Define `GLITCHLESS_DISABLE` in **Player Settings → Scripting Define Symbols** to stop that automatic start.
The Editor and development builds send too, with `Env` set to `dev` unless you set it, so you can check data arrives from Play mode. Release builds default to `prod`.
## Tell it about your game
| Game moment | Call |
| --- | --- |
| a level starts (a restart counts) | automatic for scenes, or `Glitchless.LevelStart("level-2")` |
| a level is beaten | `Glitchless.LevelComplete("level-2", score: 1200)` |
| the player gives up or runs out of lives | `Glitchless.LevelFail("level-2", "fell")` |
| the player dies | `Glitchless.Death(player.position, "spikes")`: short, low-cardinality causes |
| pickups, purchases, checkpoints | `Glitchless.Track("powerup", "magnet")` |
| counts of anything that scales, every frame | `Glitchless.SetContext("enemies", enemies.Count)` |
| the player's place, every frame | `Glitchless.SetPosition(player.position)` (sent only when positions are on) |
| an exception you caught | `Glitchless.Error(e)` |
| a player's bug report from your own UI | `Glitchless.ReportBug(note)`, or `Glitchless.OpenBugReport()` for the built-in form ([below](#bug-reports)) |
Use the game's own words for names, causes and levels, never anything a player typed.
**Your own metrics.** `Track` takes any event: `Glitchless.Track("open", "full")`, `Glitchless.Track("play", "endless")`, or a `Dictionary` of attributes. One word becomes the attribute `what`. The dashboard's Events page counts each event over time, breaks it down by attribute and builds funnels between events (`paywall` → `buy=done`), and the game's own dashboard, whose pages your coding agent designs with `glitchless_set_dashboard`, shows the ones that matter. Attributes are strings, numbers or booleans, at most 12 per event.
**Purchases** have one convention in every Glitchless SDK: `Glitchless.Track("purchase", new Dictionary { ["amount"] = 4.99, ["currency"] = "USD", ["item"] = "gems_500" });` (with Unity IAP, `product.metadata.localizedPrice` as a `double` and `isoCurrencyCode`). Send it when the store confirms the purchase, with `amount` a number in the currency's units (4.99, not 499) and `currency` the ISO code the store charged in. Boards add the amounts up into revenue, revenue per player and revenue by item, in the game's one currency: purchases in another are listed as left out, not added in. Never send receipts, order ids or payment details.
**Scenes as levels.** When a scene becomes the active one, it's a level and `level_start` is sent, unless its name reads as a menu, loader, overlay or end screen: `Boot`, `Bootstrap`, `Init`, `Preloader`, `Loading`, `Splash`, `Title`, `MainMenu`, `UI`, `HUD`, `Pause`, `GameOver`, `Settings`, `Credits`, `Shop`, `Empty` and the like (the words are matched after splitting camelCase). The level ends when its scene unloads. A menu made active on top doesn't end it. `SceneManager.LoadScene` of the same scene is a restart. Change it with:
- `LevelForScene = name => name.StartsWith("World") ? name : null` to choose yourself;
- `SceneLevels = false` to turn it off;
- any `Glitchless.LevelStart()` call, which turns scene levels off for the rest of the session. Use this when one scene plays many levels.
## Bug reports
Players and playtesters flag a problem where they see it, and you (or your coding agent, with the `glitchless_bug_reports` MCP tool) see it in Glitchless's Bug Reports view with the screenshot and a link to the session's replay. Turn the hotkey on in the options:
```csharp
Glitchless.Init(new GlitchlessOptions
{
Service = "my-game",
Intake = "https://api.glitchless.gg",
ClientToken = "gl_pub_…",
BugReports = new BugReportOptions(), // F8 opens the form
});
Glitchless.OpenBugReport(); // a pause menu's "Report a bug" (works without BugReports)
Glitchless.ReportBug(note, done: (sent, reason) => …); // your own form; reason is "offline" or "error" when not sent
if (Glitchless.IsBugReportOpen) return; // input you read from UnityEngine.Input while the form is open
```
| `BugReportOptions` | default | |
| --- | --- | --- |
| `Hotkey` | `KeyCode.F8` | opens and closes the form; `KeyCode.None` for no hotkey. Read from the Input System when the game uses it, else from the Input Manager |
| `PauseGame` | `true` | while the form is open, `Time.timeScale` is 0 and the Input System's enabled actions are off, so the game neither moves nor acts on what the player types. Both come back on close. Set it to `false` if the game pauses itself in `OnOpen` |
| `Screenshot` | `true` | send a screenshot with each report |
| `OnOpen`, `OnClose` | | the form opened or closed: pause and resume the game, mute its music |
- **The screenshot** is `ScreenCapture.CaptureScreenshotAsTexture()` at the end of the frame, before the form is drawn, so the form is never in it. It is shrunk until it fits the server's 200,000-character limit: the longer side at most 960 px, then 800, 640, 480 and 320, at lower JPEG quality. The frame that reads it back isn't timed, so it never shows as a hitch. With `-nographics` nothing is drawn, so reports have no screenshot.
- **The form** is drawn with IMGUI, so the game needs no Canvas or EventSystem, and on top of the game's own `OnGUI`. It reads keys the way uGUI's input field does (`Event.PopEvent`) and clicks from the Input System or the Input Manager, so it works with either, or both. Esc cancels, Ctrl+Enter (Cmd+Enter) sends; on a phone the system keyboard opens. A locked cursor is freed while it's open.
- **What it can't hold back:** input the game reads straight from `UnityEngine.Input` or a device (`Keyboard.current.spaceKey`), and clicks on a uGUI Canvas behind it. Check `Glitchless.IsBugReportOpen` there, as the Fetch! sample does.
- **Sending:** a report goes on a request of its own straight away, from every session (`SampleRate` doesn't apply), with the last 20 game events and hitches, the last frame windows and the device. It waits in the outbox meanwhile, so a report made offline goes with a later batch, even on a later launch.
- **The note is the player's own words.** [PRIVACY.md](PRIVACY.md#bug-reports-the-players-own-words) says what to tell players, and the dashboard can delete a report a player asks about.
## What it measures, and where
| | Release build | Development build and Editor |
| --- | --- | --- |
| frame time, hitches, GC collections | yes | yes |
| physics, update and render time per frame (timing points in the player loop) | yes | yes |
| draw calls, SetPass calls, batches, triangles | yes | yes |
| GC heap and system memory | yes | yes |
| GPU time | with **Player Settings → Frame Timing Stats** on | the same |
| mesh, material, texture and object counts, allocation per frame, shader compiles | no | yes |
The counts come from Unity's profiler counters (`ProfilerRecorder`), which Unity only fills in development builds. That's where leaks show up, so run your playtest bots on a development build.
**Stack traces.** Mono builds report files and lines. IL2CPP builds report method names; for files and lines, set **Player Settings → IL2CPP Stacktrace Information** to *Method Name, File Name, and Line Number*.
**Error kinds.** `exception` is an exception nothing caught: Unity skipped the rest of that `Update` or coroutine, and the dashboard counts it as unhandled. `log_error` is `Debug.LogError` or an error from the engine, `assert` a failed assertion, `manual` what you pass to `Glitchless.Error`. Warnings are off unless `CaptureWarnings` is on.
## Sending
Events are summed up per 5-second frame window and sent every 15 seconds (the first batch after 2), gzipped, with `UnityWebRequest`. A batch that fails is retried with backoff (5 s, doubling to 5 minutes). Batches that couldn't be sent are saved in `Application.persistentDataPath/glitchless/` and sent on a later launch; in anonymous mode they're saved without any ID.
When a phone app goes to the background the session ends and the last batch is sent at once, waiting at most 1.5 s, because iOS and Android often stop a backgrounded app without warning. Back within 5 minutes, the session carries on; later, it's a new session. Quitting sends the last batch the same way.
WebGL builds send without gzip and keep nothing on disk (an install or player ID goes in PlayerPrefs). For a web game, the [browser SDK](https://github.com/glitchless-gg/glitchless/tree/main/packages/sdk) also measures the page itself.
## For coding agents
Run a built player with `--glitchless-measure=SECONDS` and it prints one line of JSON with frame stats over that many seconds, then quits: the same numbers as the browser SDK's `glitchless_measure` tool, for a before-and-after comparison of a fix.
| Flag | Does |
| --- | --- |
| `--glitchless-measure=SECONDS` | print `{"glitchless_measure": {...}}` and quit |
| `--glitchless-warmup=SECONDS` | with measure: wait this long first |
| `--glitchless-out=PATH` | with measure: write the JSON to a file too |
| `--glitchless-intake=URL` | send to this server instead |
| `--glitchless-synthetic` | mark the session as a bot's (`--autoplay` and batch mode do too) |
| `--glitchless-off` | send nothing |
| `--glitchless-debug` | log each batch as it is sent |
In the Editor, `Glitchless.Measure(5, r => Debug.Log(Json.Serialize(r)))` does the same from code, for example through an agent that can run C# in a live Editor.
## The sample
**Package Manager → Glitchless → Samples → Fetch! → Import** adds a small runner with an autoplay bot and four planted bugs for Glitchless to find: a mesh and material leak on every level load, confetti made of physics cubes that are never destroyed, an exception on the level 3 frisbee, and a jump almost nobody makes. Open `FetchBoot.unity` and press Play; set the intake on the **Fetch** GameObject. A built player takes `--autoplay --seed=7 --skill=0.9 --level=2`. F8 opens the bug report form; `--bug-report-at=5` sends a report from code five seconds in.
## Tests
The package has EditMode tests (the frame-time sketch and error fingerprints are checked bucket for bucket against the JavaScript SDK; bug reports' contents, sending and screenshot shrinking) and PlayMode tests (the runner, and the bug report form pausing the game). Add `"testables": ["gg.glitchless.sdk"]` to your `Packages/manifest.json` to see them in the Test Runner.
---
Source: https://github.com/glitchless-gg/glitchless/blob/main/godot/addons/glitchless/README.md
# Glitchless for Godot
A Glitchless SDK for Godot 4.5 and later, built for 4.7 and the Mobile renderer. It sends the same events as the browser SDK (`packages/sdk`), so a Godot game shows up in the same Glitchless server, dashboard, findings, Datadog forwarding and MCP tools with no server changes beyond the small ones listed under [Limits](#limits).
It reports:
- **Frame times.** Each 5 s window carries a mergeable 2% sketch, so the server's p95 and p99 cover every player. Slow frames are flagged the way the browser SDK flags them.
- **Hitches with likely causes.** A pipeline (shader) compile, a burst of new nodes, a scene change, slow `_process` code, physics, rendering, or a frozen frame. Each hitch also carries the game's gauges and the events in the 1.5 s before it.
- **Engine counts.** Draw calls, primitives, objects, nodes, orphan nodes, video memory, and static memory (debug builds only). They're the leak watch.
- **Errors** through Godot 4.5's custom loggers, with script backtraces and the last game events as breadcrumbs.
- **Play events:** the level funnel, deaths, game events and gauges.
- **The device:** a coarse tier, renderer, screen size and refresh rate. The screen size also shows whether an iPhone Duo is open or closed.
- **Bug reports from players**, when the game turns them on: F8 (or the game's own button) opens a short form, and the report carries what the player typed, a screenshot, where they were and what had just happened.
It also has an agent flag, `--glitchless-measure=SECONDS`, which prints frame stats as JSON and quits. It's the native version of the browser SDK's `glitchless_measure` tool.
Anonymous mode, the default, sends nothing that can be tied to a person. [PRIVACY.md](PRIVACY.md) lists every field each mode sends. [PORTING.md](PORTING.md) shows how a game that counts events with its own `Metrics.count(event, word)` moves over.
## Install
1. Copy `addons/glitchless/` into the game's `addons/`.
2. Add the autoload. Either turn on the plugin under *Project Settings → Plugins*, or add this line under `[autoload]` in `project.godot`:
```ini
Glitchless="*res://addons/glitchless/glitchless.gd"
```
3. Optional, for script lines on errors in release builds: *Project Settings → Debug → GDScript → Always Track Call Stacks* (`debug/settings/gdscript/always_track_call_stacks=true`). It makes every GDScript call do a little more work. Without it, a release build reports the engine's own file and function instead.
The addon uses no `class_name`, so it adds no global names and works in a project that has never been opened in the editor.
## Use
```gdscript
func _ready() -> void:
Glitchless.init("starlift", "", "prod", "https://glitchless.example.com") # "" = application/config/version
Glitchless.level_start("voyage_3")
Glitchless.level_complete("voyage_3", score) # duration measured since level_start
Glitchless.level_fail("voyage_3", "lost")
Glitchless.death(pos.x, pos.y, "meteor") # the place is sent only when positions are on
Glitchless.track("play", "endless") # one word, a number, or a dictionary
Glitchless.set_context({"blocks": blocks.size()}) # at the end of the frame's work
Glitchless.set_position(player.position) # ignored unless positions are on
Glitchless.error("save slot unreadable", {"slot": 2}) # an error the game caught itself
var stats := await Glitchless.measure(5.0) # what --glitchless-measure prints
Glitchless.open_bug_report() # the bug report form, from a pause menu
await Glitchless.report_bug(note) # or a report from the game's own form
```
`track` is for the game's own metrics, whatever they are: `track("open", "full")`, `track("play", "endless")`,
`track("buy", {"step": "done"})`. One word becomes the attribute `what`. The dashboard's Events page counts each
event over time, breaks it down by attribute and builds funnels between events, and the game's own dashboard (its
pages designed by your coding agent with `glitchless_set_dashboard`) shows the ones that matter. Attributes are strings, numbers or
booleans, at most 12 per event.
Purchases have one convention in every Glitchless SDK:
`Glitchless.track("purchase", {"amount": 4.99, "currency": "USD", "item": "gems_500"})`. Send it when the store confirms the purchase, with `amount` a number in the currency's units (4.99, not 499) and `currency` the ISO code the store charged in. Boards add the amounts up into revenue, revenue per player and revenue by item, in the game's one currency: purchases in another are listed as left out, not added in. Never send receipts, order ids or payment details.
`init(service, version, env, intake, options)` options. Every one is optional.
| option | default | |
| --- | --- | --- |
| `privacy` | `"anonymous"` | `"install"` adds a random player ID kept in `user://`, so retention can be counted; `"full"` that, device detail and positions. See [PRIVACY.md](PRIVACY.md) |
| `positions` | on in full mode only | death places and 4 Hz position samples |
| `client_token` | `"gl_pub_dev"` | the server's `GLITCHLESS_CLIENT_TOKENS` |
| `sample_rate` | `1.0` | the share of sessions that send |
| `flush_interval` | `5.0` | seconds between batches; the first goes 2 s after `init` |
| `session_type` | `"user"` | `"synthetic"` is automatic with `--autoplay`, `--glitchless-synthetic`, `--glitchless-measure` or `--headless` |
| `session_timeout` | `300.0` | seconds in the background after which coming back starts a new session |
| `engine_gauges` | `true` | put `nodes`, `orphan_nodes` and `objects` in `ctx` |
| `measure_render_time` | `true` | renderer CPU (and GPU, where the driver reports it) time per frame |
| `capture_warnings` | `false` | also report `push_warning` and engine warnings |
| `rage_input` | off on phones | the same key or tap four times in a second; fast tapping is normal play in a phone game |
| `enabled` | off in debug builds | force sending on or off |
| `debug` | `false` | print each batch |
| `bug_reports` | `false` | `true`, or `{"hotkey": KEY_F8, "pause": true, "screenshot": true}`: F8 opens the [bug report form](#bug-reports) |
Frame times, errors and events are collected from the first frame. They wait in memory until `init()`, so a game can call it after loading its settings.
### Retention needs install mode
In anonymous mode (the default) nothing is kept between launches, so the dashboard can't tell a returning player from a new one. `{"privacy": "install"}` keeps a random `p_…` player ID in `user://glitchless/player_id` (the one full mode uses) and sends it with every batch; nothing else changes. The Players page and `retention` board panels then show D1, D7 and D30. `Glitchless.reset_install_id()` forgets it (for a "reset my ID" setting): this session keeps its ID, the next launch makes a new one. On a Steam Deck, `session_start` carries `steam_deck: true` and, on SteamOS, `os: "SteamOS"` (Linux builds and Windows builds under Proton).
### Off in debug builds
As with the `Metrics` autoload in Linebreakers and Starlift, a debug build (the editor, or an export with debug on) sends nothing unless it is run with `--glitchless`. That keeps a developer's phone and the agents' bot runs out of the players' numbers. Release exports send by default.
### Bug reports
Players and playtesters flag a problem where they see it, and you (or your coding agent, with the `glitchless_bug_reports` MCP tool) see it in Glitchless's Bug Reports view with the screenshot and a link to the session's replay.
```gdscript
Glitchless.init("starlift", "", "prod", intake, {"bug_reports": true}) # F8 opens the form
Glitchless.init("starlift", "", "prod", intake, {"bug_reports": {"hotkey": KEY_F1, "pause": false}})
Glitchless.bug_report_opened.connect(_pause) # bug_report_closed too; for games that pause themselves
Glitchless.open_bug_report() # a pause menu's "Report a bug" (works without bug_reports)
var result := await Glitchless.report_bug(note) # the game's own form: {"sent": true}, or {"sent": false, "reason": "offline" | "error"}
```
| `bug_reports` key | default | |
| --- | --- | --- |
| `hotkey` | `KEY_F8` | a `Key`, its name (`"F1"`), or `null` for none. It opens and closes the form |
| `pause` | `true` | pause the tree while the form is open, so the game neither runs nor reads what the player types (nodes set to always process still do). The pause state the game had comes back on close. Use `false` and the signals if the game has its own pause |
| `screenshot` | `true` | send a screenshot with each report |
- **The screenshot** is the root viewport's last frame, read before the form shows, so the form is never in it. It is shrunk until it fits the server's 200,000-character limit: the longer side at most 960 px, then 800, 640, 480 and 320, at lower JPEG quality. The frames that read it back and draw the form aren't timed, so they never show as hitches. A headless run draws nothing, so its reports have no screenshot; `report_bug(note, image)` sends an `Image` you pass, and `report_bug(note, false)` none.
- **The form** is a `CanvasLayer` on layer 128 with the note box, Send and Cancel. Esc cancels, Ctrl+Enter (Cmd+Enter) sends, and a captured mouse is set free while it is open.
- **Sending:** a report goes on a request of its own straight away, from every session (`sample_rate` doesn't apply), with the last 20 game events and hitches, the last frame windows and the device. It waits in the outbox meanwhile, so a report made offline goes with a later batch, even on a later launch. A debug build without `--glitchless` sends nothing, and says so.
- **The note is the player's own words.** [PRIVACY.md](PRIVACY.md#bug-reports-the-players-own-words) says what to tell players, and the dashboard can delete a report a player asks about.
### Flags
These go after `--` on the command line, like the games' own debug flags.
| flag | |
| --- | --- |
| `--glitchless` | send from a debug build too |
| `--glitchless-intake=URL` | send to this server instead of the one in `init()`, and turn sending on |
| `--glitchless-measure=SECONDS` | print frame stats as one line of JSON, `{"glitchless_measure": {...}}`, then quit |
| `--glitchless-warmup=SECONDS` | with `--glitchless-measure`: wait this long before measuring |
| `--glitchless-out=PATH` | with `--glitchless-measure`: write the line to this file too |
| `--glitchless-synthetic` | mark the session as a bot's (`--autoplay` does too) |
| `--glitchless-debug` | print what is sent |
## For agents: `--glitchless-measure`
```bash
godot --path . -- --autoplay --seed=7 --glitchless-measure=10 | grep '^{"glitchless_measure"'
```
It works in any build, sends nothing unless sending is on, and quits when done. The fields match the browser SDK's `glitchless_measure`:
| field | |
| --- | --- |
| `frame_ms` | `p50`, `p95`, `p99`, `worst` |
| `fps`, `frame_budget_ms`, `refresh_hz`, `pct_frames_over_budget` | the budget is the refresh rate the game actually runs at (60/90/120 Hz, or its own cap), read from the median early frame |
| `hitches`, `hitch_count` | up to 10 hitches: `ms`, `causes`, `ctx`, `recent` events |
| `render_cpu_ms`, `gpu_ms` | the renderer's time per frame; `gpu_ms` is null where the driver doesn't report it (Metal) |
| `process_ms` | all the game's `_process` code per frame |
| `draw_calls`, `triangles_avg` | |
| `peak_context` | the highest value of each gauge |
| `memory`, `memory_delta` | objects, nodes, orphan nodes, resources, video MB and static MB, now and change over the run |
| `new_errors`, `errors` | errors during the run, grouped, with locations |
| `level_start`, `level_end`, `headless` | under `--headless` nothing is drawn and frames run uncapped, so frame times mean little there |
Run the same seed before and after a change to compare.
## What it measures
**Frame times** are the wall-clock time between frames, read in `_process`. The `delta` Godot passes in is smoothed (`application/run/delta_smoothing` is on by default) and scaled by `Engine.time_scale`, either of which would hide a stutter. These rules follow `packages/sdk/src/core.js`:
- A **hitch** is a frame over 50 ms *and* over twice the median of the last 60.
- **Jank** is a frame over 33.4 ms.
- A **frozen** frame is over 700 ms.
- A run of long frames is the OS throttling a hidden window, so it is dropped.
- Time spent in the background is never counted as a frame.
**Hitch causes** come from what the slow frame measured:
| cause | when |
| --- | --- |
| `shader_compile` | the engine's pipeline-compile counters went up |
| `scene_change` | `current_scene` changed |
| `node_spawn` | 50 or more nodes were added |
| `script` | `_process` code took 40% of the frame. Glitchless runs first in each frame and has a child node that runs last, so the gap between them is every other node's `_process` |
| `physics` | physics steps took 40% of the frame |
| `render` | the renderer's CPU and GPU time took 40% of the frame |
| `frozen` | over 700 ms |
**Engine counts** go in each frame window, and with `engine_gauges` they also go in `ctx`. That is where the findings compare a slow level with the smoothest one: *"`orphan_nodes` sits at 2,500 for this whole level vs 0 on level_1, so objects created earlier are never cleaned up"*. Orphan nodes are nodes that were taken out of the tree and never freed, the commonest Godot leak.
A frame window never spans two levels: `level_start` closes the current one.
## Errors
A custom `Logger` (Godot 4.5+) hears every engine and script error, on any thread. Glitchless groups them with the browser SDK's fingerprint, made from the kind, the message with long numbers blanked, and the function. Each error event also carries the last six game events (`recent`). The server adds breadcrumbs from the session's stored events.
| kind | what |
| --- | --- |
| `script` | a GDScript runtime error, which stops the function part way. Counted as unhandled, like an uncaught JS exception |
| `engine` | an error from the engine's own code, usually a bad call from a script (a missing node, a failed load). The engine's C++ location is the first stack line, the script lines follow |
| `push_error` | the game's own `push_error()` |
| `shader` | a shader compile error |
| `manual` | `Glitchless.error()` |
| `warning` | only with `capture_warnings` |
Paths on the device are cut out of messages before they're sent: the user-data folder becomes `user://`, the user-name part of `/Users/…` or `C:\Users\…` becomes `~`, and iOS container IDs become ``. Errors raised inside the addon are never reported, so a failing send can't feed itself. Each kind of error is sent at most 25 times per session.
**Release builds catch fewer errors than debug builds.** Godot leaves most GDScript runtime checks out of release exports. This was measured with a macOS release export of Godot 4.7.2, with *Always Track Call Stacks* on:
| in the game's code | debug build | release export |
| --- | --- | --- |
| `get_node()` on a missing path, `load()` of a missing file, a null argument to an engine call | `engine`, with script line | `engine`, with script line |
| `push_error()` | `push_error` | `push_error` |
| index out of range, missing dictionary key, missing method, call on an untyped null, divide by zero | `script` | **nothing**: the code carries on |
| call on a *typed* null (`var n: Node = null; n.free()`) | `script` | **the game crashes** (SIGSEGV) |
So run the bots (`--autoplay --seed`) in debug builds with `--glitchless` to catch script mistakes, and let release builds report engine errors, `push_error` and performance. Native crashes are left to the platform: Xcode Organizer and App Store Connect on iOS, Android vitals in the Play Console. Glitchless sees a crash only as a session that stops without a `session_end`.
## Phones: background, close, offline
- **Going to the background ends the session.** iOS and Android usually stop a backgrounded app without warning, so on `NOTIFICATION_APPLICATION_PAUSED` Glitchless sends `session_end` (`reason: "background"`) at once. It waits up to 1.5 s on the network, then saves whatever didn't go.
- **Coming back** within `session_timeout` carries on the same session with an `app_resume` event. A later `session_end` then replaces the first. Coming back later than that starts a new session with a new random ID.
- **Closing** (the window's close button, Android's back button, or `get_tree().quit()`) sends `session_end` the same way.
- **Offline**, batches are saved to `user://glitchless/outbox.json` and sent on a later launch. At most 4,000 events are kept, and anything over six days old is dropped. In anonymous mode the session ID is not saved: each earlier session's batches go out under a new random ID.
## What goes on the wire
`POST {intake}/v1/intake?token=…` with the browser SDK's envelope and `sdk: "glitchless-godot/0.2.0"`. Event types and core fields are the same; see [docs/SPEC.md](../../../docs/SPEC.md#5-godot-sdk) for what the Godot SDK adds. The browser fields it can't fill (`geometries`, `textures`, `programs`, `page`, `loaf`) are sent as null or empty.
## Limits
- **Godot 4.5 or later.** The `Logger` class came in 4.5.
- **The server:**
- GDScript runtime errors count as unhandled because `server/src/analytics.js` treats kind `script` like `js`.
- Deaths sent without a place, as anonymous mode sends them, are left off heatmaps and hotspot findings.
- The memory-leak finding still reads three.js geometry counts. Godot leaks show through the `orphan_nodes` and `nodes` gauges instead.
- Fix suggestions in findings are written for three.js.
- **Web exports** are untested. A web game is better served by the browser SDK.
- Sending uses `HTTPRequest` while the game runs and a blocking `HTTPClient` for the last batch, which has no frames left to finish an `HTTPRequest`. A batch the server received just before a close can arrive twice.
## Tests
From the repo root:
- `npm run test:godot` runs the demo game (`godot/demo`) headless against a fresh local server and checks everything above through `/v1/query`. It covers offline saving, anonymous and full mode, errors, background and resume, `--glitchless-measure`, and bug reports: one from code, one typed into the F8 form (key presses pushed into the viewport) and one sent offline. It first runs `test/bug_report_test.gd`, which checks the screenshot shrinking, the slimmed events and the hotkey names.
- `npm test` includes `server/test/godot.test.js`. It checks that the GDScript sketch buckets every frame time like the JavaScript one, and that the server merges the two.
---
Source: https://github.com/glitchless-gg/glitchless/blob/main/gamemaker/README.md
# Glitchless for GameMaker
A Glitchless SDK for GameMaker 2024.2 and later, written in GML and built for LTS 2026 (runtime 2026.0.0.23). It sends the same events as the browser SDK (`packages/sdk`), so a GameMaker game shows up in the same Glitchless server, dashboard, findings and MCP tools as the browser, Godot, Unity and Unreal games.
It reports:
- **Frame times.** Each 5 s window carries a mergeable 2% sketch, so the server's p95 and p99 cover every player.
- **Hitches with likely causes:** a texture page loaded on first draw, a room change, a garbage collection, a burst of new instances, slow Step code, slow drawing, or a frozen frame. Each hitch also carries the game's gauges and the events in the 1.5 s before it.
- **Engine counts:** instances, the runner's memory, and the surfaces, DS lists and maps and buffers in memory. GameMaker frees none of the last four by itself, so a count that only climbs is a leak.
- **Rooms as levels.** A room that reads as a level (`rm_cave_2`, not `rm_main_menu`) starts that level by itself.
- **Errors and crashes.** An unhandled exception stops a GameMaker game: Glitchless writes it down as the game dies and sends it on the next launch. Errors the game catches go through `glitchless_error`.
- **Play events:** the level funnel, deaths, the game's own events and gauges.
- **Bug reports from players**, when the game turns them on: F8 (or the game's own button) opens a short form, and the report carries what the player typed, a screenshot, where they were and what had just happened.
It also has an agent flag, `-glitchless-measure=SECONDS`, which prints frame stats as JSON and quits, like the Godot, Unity and Unreal SDKs.
Anonymous mode, the default, sends nothing that can be tied to a person. [PRIVACY.md](PRIVACY.md) lists every field each mode sends.
## Install
1. Get the local package, `Glitchless-0.1.0.yymps`: from the [0.1.0 release](https://github.com/glitchless-gg/glitchless/releases/tag/glitchless-gamemaker/v0.1.0), or make it with `node gamemaker/tools/project.mjs package` (it lands in `gamemaker/dist/`).
2. In GameMaker: **Tools → Import Local Package**, pick the file, **Add All**, **Import**. It adds a *Glitchless* folder with eight scripts and two objects, `obj_glitchless` and `obj_glitchless_tail`. Don't place the objects in a room: `glitchless_init` makes them.
3. Call `glitchless_init` once, as early as you can: the first room's creation code, or the Create event of your first object.
4. For a sandboxed macOS build (the Mac App Store): **Game Options → macOS → Allow outgoing network connections**.
The SDK's own names all start with `glitchless_`, `__glitchless_` or `GLITCHLESS_`, so they won't clash with a game's.
## Use
```gml
glitchless_init("caverun", "", "prod", "https://glitchless.example.com"); // "" = GM_version, from Game Options
glitchless_level_start("cave_3"); // only if rooms aren't your levels (see below)
glitchless_level_complete(undefined, score); // undefined: the current level; duration measured since it started
glitchless_level_fail(undefined, "lost");
glitchless_death(x, y, "spikes"); // the place is sent only when positions are on
glitchless_track("play", "endless"); // one word, a number, or a struct
glitchless_set_context({ enemies: instance_number(obj_enemy) }); // in an End Step event
glitchless_set_position(obj_player.x, obj_player.y); // ignored unless positions are on
try { load_save(); } catch (_e) { glitchless_error(_e, { slot: 2 }); } // an error the game caught
glitchless_measure(5, function(_stats) { show_debug_message(json_stringify(_stats)); });
glitchless_open_bug_report(); // the bug report form, from a pause menu
glitchless_report_bug(note); // or a report from the game's own form
glitchless_quit(); // the Quit button: sends the last batch, then game_end()
```
`glitchless_track` is for the game's own metrics, whatever they are: `glitchless_track("open", "full")`, `glitchless_track("buy", { step: "done" })`. One word becomes the attribute `what`. The dashboard's Events page counts each event over time, breaks it down by attribute and builds funnels between events. Attributes are strings, numbers or booleans, at most 12 per event.
| function | |
| --- | --- |
| `glitchless_init(service, version, env, intake, options)` | who the game is and where to send. Nothing is measured or sent before it |
| `glitchless_level_start(level)` | closes the current frame window, so a window never spans two levels. The first call turns rooms as levels off |
| `glitchless_level_complete([level], [score], [duration_s])`, `glitchless_level_fail([level], [cause])` | |
| `glitchless_death(x, y, [cause], [z])` | |
| `glitchless_track(name, [what])` | |
| `glitchless_set_context(struct)` | numbers that describe what is going on: the largest value in each window is sent, and the current ones go with each hitch |
| `glitchless_set_position(x, y, [z])` | sampled 4 times a second for heatmaps, when positions are on |
| `glitchless_error(message_or_exception, [attrs])` | |
| `glitchless_crash(exception)` | for a game with its own `exception_unhandled_handler` (see [Errors](#errors)) |
| `glitchless_report_bug([note], [screenshot], [callback])` | `callback({sent})`, or `{sent: false, reason: "offline" or "error"}` |
| `glitchless_open_bug_report()`, `glitchless_bug_report_is_open()` | |
| `glitchless_measure(seconds, callback)` | what `-glitchless-measure` prints |
| `glitchless_flush()` | closes the window and sends what is waiting; runs by itself every few seconds |
| `glitchless_quit()` | see [Quitting](#quitting-offline-and-crashes) |
| `glitchless_has_flag(name)`, `glitchless_flag_value(name, [fallback])` | read the game's own flags (`-autoplay`, `-seed=7`) where Glitchless reads its own |
| `glitchless_stats()`, `glitchless_session_id()` | for a debug overlay or a support screen |
`glitchless_init` options. Every one is optional.
| option | default | |
| --- | --- | --- |
| `privacy` | `"anonymous"` | `"install"` keeps a random player ID on the device, so returning players can be counted; `"full"` adds device detail and positions. See [PRIVACY.md](PRIVACY.md) |
| `positions` | on in full mode only | death places and 4 Hz position samples |
| `client_token` | `"gl_pub_dev"` | the server's `GLITCHLESS_CLIENT_TOKENS` |
| `sample_rate` | `1` | the share of sessions that send |
| `flush_interval` | `5` | seconds between batches; the first goes 2 s after `glitchless_init` |
| `session_type` | `"user"` | `"synthetic"` is automatic with `-autoplay`, `-glitchless-synthetic` or `-glitchless-measure` |
| `session_timeout` | `300` | seconds in the background (a phone) after which coming back is a new session |
| `rooms_as_levels` | `true` | a room that reads as a level starts that level |
| `engine_gauges` | `true` | put `instances`, `surfaces`, `ds_lists`, `ds_maps`, `ds_grids`, `buffers`, `particle_systems` and `vertex_buffers` in `ctx` |
| `rage_input` | off on phones | the same key or click four times in a second |
| `enabled` | off when run from the IDE | force sending on or off |
| `gzip` | `true` | gzip each batch (plain JSON in a web build, or where the runner's zlib can't make gzip) |
| `debug` | `false` | log what is sent with `show_debug_message` |
| `bug_reports` | `false` | `true`, or `{ hotkey: vk_f8, pause: true, screenshot: true, on_open, on_close }`: F8 opens the [bug report form](#bug-reports) |
| `exception_handler` | none | the game's own unhandled-exception function, called after Glitchless writes the crash down |
### Rooms as levels
When a room starts, Glitchless asks whether its name reads as a level. Names with a word like `menu`, `title`, `boot`, `init`, `loading`, `splash`, `options`, `credits`, `lobby` or `game over` in them don't (`rm_main_menu`, `rmTitle`, `room_game_over`); others do, and the room's name is the level. A menu room leaves the level: its frames have none. The same words decide Unity scenes and Unreal maps.
A game whose levels aren't rooms (an endless runner, waves in one room) calls `glitchless_level_start` itself. The first call turns rooms as levels off for the session.
### Off when run from the IDE
A build run from the IDE (or Igor's `Run`, where `GM_build_type` is `"run"`) sends nothing unless it is run with `-glitchless`, so a developer's test runs and the agents' bot runs stay out of the players' numbers. Executables (`GM_build_type` `"exe"`) send by default.
### Bug reports
Players and playtesters flag a problem where they see it, and you (or your coding agent, with the `glitchless_bug_reports` MCP tool) see it in Glitchless's Bug Reports view with the screenshot and a link to the session.
```gml
glitchless_init("caverun", "", "prod", intake, { bug_reports: true }); // F8 opens the form
glitchless_init("caverun", "", "prod", intake, { bug_reports: { hotkey: vk_f1, pause: false, on_open: my_pause, on_close: my_resume } });
glitchless_open_bug_report(); // a pause menu's "Report a bug" (works without bug_reports)
glitchless_report_bug(note, true, function(_r) { if (_r.sent) show_message("Thanks!"); });
```
| `bug_reports` key | default | |
| --- | --- | --- |
| `hotkey` | `vk_f8` | a key code, or `undefined` for none. It opens and closes the form |
| `pause` | `true` | deactivate every instance but Glitchless's own while the form is open, and wake exactly those afterwards (not ones the game had deactivated itself). Use `false` with `on_open` and `on_close` if the game has its own pause |
| `screenshot` | `true` | send a screenshot with each report |
| `on_open`, `on_close` | none | functions called as the form opens and closes |
- **The screenshot** is the application surface at the end of the frame, taken before the form shows, so the form is never in it. The GUI layer (whatever Draw GUI events draw: a HUD, menus) is drawn after the application surface, so it isn't in it either. GameMaker can only save PNG, so it is a PNG, shrunk until it fits the server's 200,000-character limit: the longer side at most 960 px, then 800, 640, 480 and 320. With the application surface turned off, it is `screen_save`'s. The frame that saves it and the form's first frame aren't timed, so they never show as hitches.
- **The form** draws over everything in Draw GUI End with GameMaker's built-in font: the screenshot, the note, Send and Cancel. The note is what the player types (`keyboard_string`, which keeps up to 1,024 characters). Enter sends, Esc cancels. The game's `keyboard_string` comes back on close.
- **Sending:** a report goes on a request of its own straight away, from every session (`sample_rate` doesn't apply), with the last 20 game events and hitches, the last frame windows and the device. It waits in the outbox meanwhile, so a report made offline goes with a later batch, even on a later launch.
- **The note is the player's own words.** [PRIVACY.md](PRIVACY.md#bug-reports-the-players-own-words) says what to tell players, and the dashboard can delete a report a player asks about.
### Flags
| flag | |
| --- | --- |
| `-glitchless` | send from a build run from the IDE too |
| `-glitchless-intake=URL` | send to this server instead of the one in `glitchless_init`, and turn sending on |
| `-glitchless-privacy=MODE` | `anonymous`, `install` or `full`, for a test run |
| `-glitchless-measure=SECONDS` | print frame stats as one line of JSON, `{"glitchless_measure": {...}}`, write it to `glitchless/measure.json` in the save area, then quit |
| `-glitchless-warmup=SECONDS` | with `-glitchless-measure`: wait this long before measuring |
| `-glitchless-out=PATH` | with `-glitchless-measure`: write the line to this file too (a path the game's file sandbox allows) |
| `-glitchless-synthetic` | mark the session as a bot's (`-autoplay` does too) |
| `-glitchless-debug` | log what is sent |
One or two dashes both work. They are read from the command line (`parameter_string`) and from the `GLITCHLESS_ARGS` environment variable. Igor's `Run`, which the IDE and `gm-cli run` use, starts the game with macOS's `open`, which passes on neither, so a build run from the IDE or Igor also reads `glitchless/args.txt` in its save area (`~/Library/Application Support/com.yoyogames.macyoyorunner/` on a Mac): one flag per line or space. An executable ignores that file.
## For agents: `-glitchless-measure`
```bash
# after a VM build (Igor's Mac Compile, as gamemaker/test/e2e.mjs does it), with the game's files beside game.ios:
open -n -W -a "$RUNTIME/mac/YoYo Runner.app" --args -game "$PWD/build/game.ios" -autoplay -seed=7 -glitchless-measure=10
cat ~/Library/Application\ Support/com.yoyogames.macyoyorunner/glitchless/measure.json
```
The runner's save area is `com.yoyogames.macyoyorunner` for a build run that way, and the game's own (`game_save_id`) for an executable.
It works in any build, sends nothing unless sending is on, and quits when done. The fields match the other SDKs':
| field | |
| --- | --- |
| `frame_ms` | `p50`, `p95`, `p99`, `worst` |
| `fps`, `frame_budget_ms`, `refresh_hz`, `pct_frames_over_budget` | the budget is the game's speed (`game_set_speed`), which GameMaker holds the frame rate to |
| `hitches`, `hitch_count` | up to 10 hitches: `ms`, `causes`, `ctx`, `recent` events |
| `process_ms` | every instance's Begin Step, Step and End Step in a frame, and what runs between them (collisions, alarms) |
| `render_cpu_ms` | the draw events' CPU time per frame. `gpu_ms`, `draw_calls` and `triangles_avg` are null: GameMaker doesn't report them |
| `gc_ms` | time in garbage collections over the run |
| `peak_context` | the highest value of each gauge |
| `memory`, `memory_delta` | instances, the runner's MB, surfaces, DS lists and maps and buffers, now and change over the run |
| `new_errors`, `errors` | errors during the run, grouped, with locations |
| `renderer` | `vm` or `yyc` |
| `level_start`, `level_end`, `session_id` | |
Run the same seed before and after a change to compare.
## What it measures
**Frame times** come from `delta_time`, the wall-clock time between frames. A global time source reads it at the start of every frame, and keeps running when the game deactivates instances for its own pause menu; it also brings Glitchless's two objects back if the game deactivated or destroyed them. These rules follow `packages/sdk/src/core.js`:
- A **hitch** is a frame over 50 ms *and* over twice the median of the last 60.
- **Jank** is a frame over 33.4 ms.
- A **frozen** frame is over 700 ms.
- A run of long frames is the OS throttling a hidden window, so it is dropped.
- Time out of focus, and in the background on a phone, is never counted as a frame.
**Hitch causes** come from what the slow frame measured:
| cause | when |
| --- | --- |
| `texture_upload` | a texture group's status turned loaded or fetched (`texturegroup_get_status`): a page was loaded the first time something on it was drawn |
| `scene_change` | the room changed. This is the room's load: the findings count it apart from stalls during play |
| `gc` | a garbage collection (`gc_get_stats`) took a quarter of the frame, and at least 4 ms |
| `object_spawn` | 50 or more instances were added |
| `script` | Step events took 40% of the frame. `obj_glitchless` is drawn first and `obj_glitchless_tail` last, so the time from the frame's start to the first Pre-Draw is every Step event, and from there to the last Draw GUI End is every draw event |
| `render` | draw events took 40% of the frame |
| `frozen` | over 700 ms |
Each hitch also has `process_ms`, `render_ms`, `gc_ms`, `instances_added` and `texture_loads`. Each frame window adds `process_ms`, `render_ms`, `gc_collections`, `gc_ms`, `texture_loads`, `heap_mb` (`debug_event("DumpMemory")`), `instances`, `surfaces`, `ds_lists`, `ds_maps` and `buffers` (`debug_event("ResourceCounts")`). With `engine_gauges` the counts also go in `ctx`, where the findings compare a slow level with the smoothest one: *"`instances` sits at 2,540 for this whole level vs 33 on rm_level_1, so objects created earlier are never cleaned up"*.
## Errors
| kind | what |
| --- | --- |
| `crash` | an unhandled exception. GameMaker stops the game, so it counts as a crash: the session ends with `session_end` reason `crash`, written to the outbox in the exception handler and sent on the next launch |
| `manual` | `glitchless_error`: a message, or the exception a `catch` got, with its callstack |
Glitchless groups errors with the browser SDK's fingerprint: the kind, the message with long numbers and hex blanked, and the function. Callstack lines become functions: `gml_Object_obj_player_Step_0 (line 12)` is `obj_player.Step_0` at line 12, `gml_Script_spawn_wave` is `spawn_wave`. The `stack` keeps GameMaker's own names. Each error carries the last six game events (`recent`), and paths on the computer are cut down before sending: the save folder becomes `save:/`, and the user-name part of `/Users/…`, `/home/…` or `C:\Users\…` becomes `~`. Each kind of error is sent at most 25 times per session.
`glitchless_init` sets `exception_unhandled_handler`. A game that sets its own after that replaces Glitchless's: pass yours as the `exception_handler` option instead, and Glitchless calls it after writing the crash down (its return value is the game's exit code), or call `glitchless_crash(ex)` first thing in yours.
## Quitting, offline and crashes
- **`glitchless_quit()`** ends the session, gives the network up to 1.5 s to take the last batch, then calls `game_end()`. GameMaker can't wait on a request while the game closes, so a game that quits with `game_end()` or the window's close button saves its last batch, and the next launch sends it.
- **Offline**, batches are saved to `glitchless/outbox.json` in the save area and sent on a later launch, as is a crash. At most 4,000 events are kept, and anything over six days old is dropped. In anonymous mode the session ID is not saved: each earlier session's batches go out under a new random ID.
- **On a phone**, coming back from the background within `session_timeout` carries on the session with an `app_resume` event; later than that starts a new session.
## What goes on the wire
`POST {intake}/v1/intake?token=…` with the browser SDK's envelope and `sdk: "glitchless-gamemaker/0.1.0"`, as gzip with `Content-Type: text/plain` (the server knows gzip by its first bytes). GameMaker has no gzip, so the SDK wraps `buffer_compress`'s zlib stream in a gzip header and trailer. It checks once what `buffer_crc32` gives for a known input and corrects for it: runtime 2026.0.0.23 returns the inverted CRC-32. A runner where neither fits sends plain JSON. See [docs/SPEC.md](../docs/SPEC.md#5d-gamemaker-sdk) for what the GameMaker SDK adds; the browser fields it can't fill are sent as null or empty.
## Limits
- **GameMaker 2024.2 or later**: the project format, `texturegroup_get_status`, time sources and `debug_event`'s structs.
- **No draw calls, triangles or GPU time.** GameMaker doesn't report them to the game. The debug overlay (`show_debug_overlay(true)`) shows texture swaps and vertex batches.
- **Web and GX.games builds** are untested. They send plain JSON.
- **Phones** are untested beyond the background rule above.
- **A batch can arrive twice** if the game closes while the server is answering it: it stays in the outbox until the reply comes.
## Tests
From the repo root:
- `npm run test:gamemaker:core` runs the SDK's GML in Node, with no GameMaker needed. `test/gml.mjs` turns the GML into JavaScript (it refuses JavaScript-only syntax, and reading a missing struct field throws, as in GameMaker). `test/core.test.mjs` checks that every GameMaker name the GML uses is a real one (`test/gml-builtins.txt`) and that the sketch, fingerprints, path scrubbing and gzip match the JavaScript SDK. It also plays whole sessions against a local server: offline and the outbox, install and full mode, hitches and their causes, rooms as levels, errors, a crash sent on the next launch, bug reports and measure.
- `npm run test:gamemaker` builds the demo with GameMaker's Igor (a VM build for the mac target, in `gamemaker/.build`) and plays it against a local server: see [the demo](#the-demo). First the runner computes the sketch, fingerprints, gzip and JSON numbers of the JavaScript fixture (`-glitchless-selftest`), then come offline and the outbox, full and install mode, measure, the planted crash sent on the next launch, and bug reports from code and the F8 form, 29 checks in all. The bug report's screenshot is saved as `gamemaker/.build/bug-report.png`. Each run starts the runtime's YoYo Runner with its flags on the command line and its output in a `-debugoutput` file, so it opens a game window for a few seconds and needs a logged-in macOS desktop.
- `npm test` includes `server/test/gamemaker-findings.test.js`: GameMaker sessions get GameMaker's fixes.
### Setting up GameMaker for the end-to-end test
A person does this once, because it accepts GameMaker's terms. The e2e never signs in or fetches a licence: it uses the runtime and `licence.plist` it finds, or `GM_RUNTIME` (a runtime folder) and `GM_LICENCE` (a `licence.plist`). Either of these is enough:
- **The IDE.** Install GameMaker LTS 2026 from [gamemaker.io/download](https://gamemaker.io/en/download), open it, sign in (a free account) and let it install runtime 2026.0.0.23 with the macOS module. The e2e uses `/Users/Shared/GameMakerStudio2-LTS2026/Cache/runtimes/runtime-*` and the signed-in user folder in `~/Library/Application Support/GameMakerStudio2-LTS2026/`.
- **YoYo Games' [`gm-cli`](https://github.com/YoYoGames/gm-cli)**, with no IDE and no account (a free guest licence). It needs Node 24 or later and about 1 GB:
```bash
export GAMEMAKER_CLI_CACHE_DIR="$HOME/Library/Caches/GameMakerCLI-glitchless"
mkdir -p ~/gm-setup && cd ~/gm-setup
npx @gamemaker/gm-cli@2.4.1 init --template "Blank Game" --no-interactive
npx @gamemaker/gm-cli@2.4.1 run --target=mac --runtime=vm
```
A blank game window opens; close it. The e2e reads the runtime and licence from the same `GAMEMAKER_CLI_CACHE_DIR`.
Builds are VM builds (YYC needs Xcode). The free tier is non-commercial: shipping a commercial game needs GameMaker Professional, whatever Glitchless does. Two things found on runtime 2026.0.0.23: the runner can't load a game from a temporary folder (`/tmp`, `/var/folders`), hence `gamemaker/.build`; and builds run from the IDE or Igor save to the runner's own folder, `~/Library/Application Support/com.yoyogames.macyoyorunner/`, where the e2e removes only Glitchless's files.
## The demo
`Glitchless.yyp` in this folder is the SDK and a demo game in one project: the *Glitchless* folder is the SDK (what the local package holds), the *Demo* folder the game. It has a boot room and three levels, each a room, with planted problems for the findings to find:
- **rm_level_2:** a power-up makes 2,500 spark instances in one step and sorts them with a bubble sort (a stutter, `object_spawn` and `script`). The sparks are persistent and never destroyed, so rm_level_3 carries every one, and each power-up makes a glow surface it never frees: two leaks.
- **rm_level_3:** a bonus reads a variable from an object with no instances, which stops the game. Glitchless sends the crash on the next launch.
- **rm_level_1** also has an error the game catches and reports, with the save folder in its message.
`-autoplay` plays it by itself and quits after the last level; `-seed=N`, `-level-seconds=S`, `-no-crash`, `-bug-report-at=S` and `-bug-form-at=S` are its test flags, and `-glitchless-selftest` makes the runner compute the sketch and fingerprints of `datafiles/glitchless_fixture.json` and write them to its save area, so the e2e can check this runner against JavaScript. `node gamemaker/tools/project.mjs` rewrites the project's `.yyp` and `.yy` files after a script, object or event is added.
---
Source: https://github.com/glitchless-gg/glitchless/blob/main/plugin/glitchless/skills/instrument/SKILL.md
---
name: instrument
description: Add Glitchless observability to a game: Three.js or React Three Fiber, Phaser 3 or 4, Babylon.js, PlayCanvas, Unity 2022.3+, Godot 4.5+, Unreal Engine 5.8 or GameMaker 2024.2+. Frame-time percentiles, hitch attribution, errors, gameplay events, level heatmaps, and tools that let coding agents measure the running game. Use when the user wants telemetry, analytics, performance monitoring or "observability" in a game, or asks to add Glitchless.
---
# Instrument a game with Glitchless
Goal: after this skill, every play session reports how the game *felt* (frame times, hitches and their causes),
what broke (errors, crashes) and what the player did (levels, deaths, pickups, positions), and you (the agent)
have a way to measure the running game before and after a change.
## 0. Find the engine, and the settings
| You find | Engine | Follow |
| --- | --- | --- |
| `three` or `@react-three/fiber` in package.json | Three.js | [Three.js](#threejs) |
| `phaser` in package.json, or a `` and `Glitchless.init(...)` / `Glitchless.instrumentThree(...)`.
**Telemetry must never break the game.** Load the SDK through a small facade with try/catch and a no-op fallback
(see `apps/zoomies/src/telemetry.js` in the Glitchless repo for the pattern) and support a `?nogd=1` kill switch.
### 3. Wire game semantics
| Game moment | Call |
| --- | --- |
| level/stage starts (incl. restarts) | `glitchless.levelStart('level-2')` |
| level beaten | `glitchless.levelComplete('level-2', { durationS, score })` |
| player gives up / game over | `glitchless.levelFail('level-2', { cause })` |
| player dies | `glitchless.death({ x, y, z, cause: 'fell' })`: short, low-cardinality causes |
| pickups, power-ups, checkpoints | `glitchless.track('powerup', { kind: 'speed' })` |
| a purchase the store confirmed | `glitchless.track('purchase', { amount: 4.99, currency: 'USD', item: 'gems_500' })` |
| every frame | `glitchless.setPosition(player.position)` |
| every frame | `glitchless.setContext({ particles, enemies, projectiles })`: counts of anything that scales |
| after a level is built | `glitchless.captureMap(levelId, { axis: 'xy' /* side-scroller */ \| 'xz' /* top-down */, bounds: { min: [a, b], max: [a, b] } })` |
| handled errors worth knowing about | `glitchless.error(err, { where: 'save-game' })` |
### 4. Give agents hands: register game tools (WebMCP)
Register 3–5 tools that let an agent reproduce problems. Handlers return a plain object or string and never throw.
```js
glitchless.registerTool('mygame_get_state', 'Current level, player position, score, enemy/particle counts', { type: 'object', properties: {} }, () => ({ ... }));
glitchless.registerTool('mygame_load_level', 'Jump to a level', { type: 'object', properties: { level: { type: 'string' } }, required: ['level'] }, ({ level }) => { loadLevel(level); return `loaded ${level}`; });
```
Good candidates: load level, teleport player, trigger the expensive effect (explosion, power-up), toggle pause.
### 4b. Bug reports from players (ask first)
For playtests, jams and early access, players can flag a problem where it happens: a key (F8) or a button in the
overlay opens a small form, and the note, a screenshot of the canvas, the level, the position (when positions are
on) and the last events go to Glitchless. They show under Bug Reports, in findings once a level has three, and in
`glitchless_bug_reports`. The note is free text the player types, so ask the user before turning it on and tell
them to mention it in the game's privacy page (`packages/sdk/PRIVACY.md` in the Glitchless repo has the wording).
```js
const glitchless = Glitchless.init({
// …
bugReports: { hotkey: 'F8', onOpen: () => pauseGame(), onClose: () => resumeGame() },
});
```
While the form is open the SDK holds keys, clicks and mouse movement back from the game; `onOpen`/`onClose` pause
it (a game that reads a gamepad needs the pause). A game with its own pause menu can add "Report a bug" there with
`glitchless.openBugReport()`, or send one from its own UI with `glitchless.reportBug({ note })`.
### 4c. Readable errors from the production build (source maps)
A minified build reports its errors as `e (index-3f2a.js:1:48213)`. With the build's source maps uploaded,
Glitchless shows them as `fireShotgun (weapons.js:141)`, with the original stack and the failing line, in the
dashboard and in `glitchless_get_error`, and an error keeps one fingerprint across builds. Most web games build
with Vite; add the plugin there:
```bash
npm install -D glitchless-sourcemaps
```
```js
// vite.config.js
import glitchlessSourcemaps from 'glitchless-sourcemaps/vite';
import { GAME_VERSION } from './src/version.js'; // wherever the version passed to Glitchless.init lives
export default defineConfig({
plugins: [glitchlessSourcemaps({ version: GAME_VERSION, service: '', deleteMaps: true })],
});
```
`version` and `service` must match what the game passes to `Glitchless.init` (the defaults are package.json's
`version` and `name`). The plugin builds with `build.sourcemap: 'hidden'`; leave `build.sourcemap` unset or set it to
`'hidden'`, because `true` points players' browsers at the maps. It uploads only when `GLITCHLESS_API_KEY` holds a
secret key that can upload source maps (tick "Can upload source maps" when creating it); put the key in CI secrets
or the user's environment, never in the game or a committed file. `deleteMaps: true` keeps the maps out of `dist`.
Without a key the build works as before and uploads nothing.
Other bundlers, or a CI step after the build (upload before deploying):
```bash
GLITCHLESS_API_KEY=… npx glitchless-sourcemaps upload dist --version 1.2.0 --service --delete
```
`--url` (or `GLITCHLESS_URL`) is the Glitchless server, as for the MCP server.
### 5. Verify
1. Run the game, press `` ` ``: the Glitchless overlay shows fps, p95 frame time, draw calls, context gauges.
2. Play a level and die once. Then confirm data arrived: call `glitchless_list_games`, or open the dashboard.
3. If WebMCP is connected, call `glitchless_live_performance` and `glitchless_inspect_scene`.
## Phaser
The same `glitchless` package, with a Phaser adapter (Phaser 3.60+ and 4). It times frames from Phaser's own loop,
counts draw calls, reports scenes as levels, and catches failed loads and a game loop that stopped.
### 1. Read the game
Note file:line for: `new Phaser.Game(config)` and the scene list; which scene keys are levels and which are
boot/preload/menu/UI/game-over scenes; player death and respawn, level complete, game over, pickups; anything
spawned in bulk (Groups, particle emitters, bullets); where the version lives.
### 2. Install and initialize
```bash
npm install glitchless
```
Init before the game is created, so the session starts with the page, and hand the game over right after:
```js
import * as Phaser from 'phaser';
import { Glitchless } from 'glitchless';
import { instrumentPhaser } from 'glitchless/phaser';
const glitchless = Glitchless.init({
service: '',
version: GAME_VERSION,
intake: 'http://localhost:7777',
clientToken: 'gl_pub_dev',
sessionType: isBot ? 'synthetic' : 'user',
});
const game = new Phaser.Game(config);
instrumentPhaser(glitchless, { game, Phaser });
```
Script-tag games: load `{GLITCHLESS_URL}/sdk/glitchless.js`, then `Glitchless.instrumentPhaser(glitchless, { game })`.
### 3. Levels
A scene that starts is a level, named by its key, unless the key reads as a loader, menu, overlay or end screen
(`Boot`, `Preloader`, `MainMenu`, `UIScene`, `HUD`, `GameOver`, `Pause`, `Settings`, `Credits`…). Check the
game's keys against that:
- Levels are scenes (`Level1`, `Level2`): nothing to do.
- One `Game` scene plays every level: call `glitchless.levelStart('world-1-2')` in its `create()` with the real
level name. Any manual call turns scene levels off.
- Odd keys: `instrumentPhaser(glitchless, { game, Phaser, levels: ['Forest', 'Cave'] })` or `levels: (scene) => name or null`.
### 4. Wire game semantics
The same calls as for Three.js: `levelComplete`, `levelFail`, `death({ x, y, cause })`, `track`, `setPosition`,
`setContext`, `error`. The adapter already sends `game_objects`, `physics_bodies`, `particles`, `tweens`,
`listeners` and `sounds` as gauges; add the game's own counts (enemies, bullets) with `setContext`.
Phaser's y axis points down, so heatmaps show a Phaser level upside down for now; leave the coordinates as they are.
Register WebMCP tools as for Three.js (`glitchless.registerTool`). Bug reports work as for Three.js too
(`bugReports` in init, [step 4b](#4b-bug-reports-from-players-ask-first)): the form's screenshot is the Phaser canvas.
So do source maps for the production build ([step 4c](#4c-readable-errors-from-the-production-build-source-maps)).
### 5. Bots and playtests
If the game has (or can get) an autoplay bot, make it read `?autoplay=1&seed=N&skill=0..1&level=N`, mark its
sessions synthetic, and set `window.__GLITCHLESS_PLAYTEST_DONE__ = true` when a run is over. Then
`glitchless-playtest` can compare builds. `apps/phaser-demo` in the Glitchless repo is a working example,
including `glitchless.playtest.json`.
### 6. Verify
Run the game, press `` ` `` for the overlay (draw calls, the gauges, the current level), play a level and die once,
then call `glitchless_list_games`. With WebMCP connected, `glitchless_inspect_scene` lists each running scene's
display list, physics bodies and busiest event listeners.
## Babylon.js
The same `glitchless` package, with a Babylon.js adapter (Babylon.js 5+, WebGL or WebGPU). It times frames from the
engine's own loop, reads Babylon's draw-call, triangle and shader-compile counters, reports scenes as levels, and
catches shader errors, failed textures, context loss and a render loop that stopped.
### 1. Read the game
Note file:line for: `new Engine(...)` (or `WebGPUEngine`), each `new Scene(engine)` and whether each level is its own
scene or one scene rebuilt; the render loop (`engine.runRenderLoop`) and the game update (`scene.onBeforeRenderObservable`
or `registerBeforeRender`); player death and respawn, level complete, game over, pickups; anything created in bulk at
runtime (meshes, `ShaderMaterial`s, particle systems, `DynamicTexture`s); where the version lives.
### 2. Install and initialize
```bash
npm install glitchless
```
Init before the engine starts, and hand over the engine and the first scene right after creating them:
```js
import * as BABYLON from '@babylonjs/core';
import { Glitchless } from 'glitchless';
import { instrumentBabylon } from 'glitchless/babylon';
const glitchless = Glitchless.init({
service: '',
version: GAME_VERSION,
intake: 'http://localhost:7777',
clientToken: 'gl_pub_dev',
sessionType: isBot ? 'synthetic' : 'user',
});
const engine = new BABYLON.Engine(canvas, true);
const scene = new BABYLON.Scene(engine);
instrumentBabylon(glitchless, { engine, scene, BABYLON }); // BABYLON (the namespace) lets captureMap draw levels
```
A game that imports single modules (`import { Engine } from '@babylonjs/core/Engines/engine'`) can leave `BABYLON`
out; then `captureMap` is off. Script-tag games: `Glitchless.instrumentBabylon(glitchless, { engine, scene })` (the
`BABYLON` global is found on its own).
### 3. Levels
- Each level is its own scene: set `scene.metadata = { level: 'forest' }` when you create it. The level starts when the
scene first renders and ends when it is disposed; scenes created later are picked up on their own.
- One scene rebuilt for each level: set `scene.metadata.level` to the next name, or call `glitchless.levelStart('forest-2')`
(any manual call turns scene levels off).
- Odd cases: `instrumentBabylon(glitchless, { engine, scene, levels: (scene) => name or null })`.
### 4. Wire game semantics
The same calls as for Three.js: `levelComplete`, `levelFail`, `death({ x, y, z, cause })`, `track`, `setPosition`
(every frame, from the player's mesh position), `setContext`, `error`. The adapter already sends `meshes`,
`active_meshes`, `materials`, `textures`, `particles` and `render_observers` as gauges; add the game's own counts with
`setContext`. After a level is built, `glitchless.captureMap(level, { axis: 'xz', bounds: { min: [x0, z0], max: [x1, z1] }, exclude: [player] })`
draws its picture for the heatmaps (pass `BABYLON` for it). Register WebMCP tools, bug reports and source maps as for
Three.js ([4](#4-give-agents-hands-register-game-tools-webmcp), [4b](#4b-bug-reports-from-players-ask-first),
[4c](#4c-readable-errors-from-the-production-build-source-maps)).
### 5. Bots and playtests
As for Phaser ([5](#5-bots-and-playtests)): `?autoplay=1&seed=N&skill=0..1&level=N`, synthetic sessions and
`window.__GLITCHLESS_PLAYTEST_DONE__`. `apps/babylon-demo` in the Glitchless repo is a working example.
### 6. Verify
Run the game, press `` ` `` for the overlay (draw calls, triangles, the gauges, the level), play a level and die
once, then call `glitchless_list_games`. With WebMCP connected, `glitchless_inspect_scene` lists each scene's mesh
groups and flags copies that should be instances or thin instances.
## PlayCanvas
The same `glitchless` package, with a PlayCanvas adapter (the PlayCanvas engine 2.x, WebGL2 or WebGPU; editor
projects and engine-only games). It times frames from the app's loop, reads the device's draw calls and shaders,
counts triangles, reports editor scenes as levels, and catches failed assets, context loss and script errors.
### 1. Read the game
Note file:line for: where the `pc.Application` (or `AppBase`) is created, or for an editor project the scripts and
scenes; how levels load (`app.scenes.changeScene()`, `loadSceneHierarchy()`, or built in code); the scripts that handle
death, level complete, pickups; anything spawned in bulk (entities cloned or built per effect, meshes made at runtime);
where the version lives.
### 2. Install and initialize
Engine-only game (`npm install glitchless`):
```js
import * as pc from 'playcanvas';
import { Glitchless } from 'glitchless';
import { instrumentPlayCanvas } from 'glitchless/playcanvas';
const glitchless = Glitchless.init({
service: '',
version: GAME_VERSION,
intake: 'http://localhost:7777',
clientToken: 'gl_pub_dev',
sessionType: isBot ? 'synthetic' : 'user',
});
const app = new pc.Application(canvas);
instrumentPlayCanvas(glitchless, { app, pc }); // pc (the namespace) lets captureMap draw levels
app.start();
```
PlayCanvas Editor project: add `{GLITCHLESS_URL}/sdk/glitchless.js` as an external script in the project settings,
then a script on the first entity that loads:
```js
const GlitchlessInit = pc.createScript('glitchlessInit');
GlitchlessInit.prototype.initialize = function () {
const glitchless = Glitchless.init({ service: '', version: '1.0.0', intake: 'http://localhost:7777', clientToken: 'gl_pub_dev' });
Glitchless.instrumentPlayCanvas(glitchless, { app: this.app });
};
```
### 3. Levels
- Editor scenes: `app.scenes.changeScene('Level2')` and `loadSceneHierarchy()` start a level named after the scene,
unless the name reads as a loader, menu, overlay or end screen. Odd names: `levels: ['Forest', 'Cave']` or
`levels: (name) => name or null`.
- Levels built in code: call `glitchless.levelStart('track-2')` where the level is built (this turns scene levels off).
### 4. Wire game semantics
The same calls as for Three.js, from the scripts that handle them (`levelComplete`, `levelFail`, `death`, `track`,
`setPosition` every frame from the player entity's `getPosition()`, `setContext`, `error`). The adapter already sends
`entities`, `mesh_instances`, `materials`, `textures` and `lights` as gauges. After a level is built,
`glitchless.captureMap(level, { axis: 'xz', bounds, exclude: [player] })` draws its picture (pass `pc` for it). WebMCP
tools, bug reports and source maps work as for Three.js. Vite's dev server runs PlayCanvas's debug build, which turns
a script off after it throws; the adapter reports those errors too.
### 5. Bots and playtests
As for Phaser ([5](#5-bots-and-playtests)). `apps/playcanvas-demo` in the Glitchless repo is a working example.
### 6. Verify
Run the game, press `` ` `` for the overlay, play a level and fail once, then call `glitchless_list_games`. With
WebMCP connected, `glitchless_inspect_scene` lists entity groups and flags copies that should be instanced or batched.
## Unity
The `gg.glitchless.sdk` package, for Unity 2022.3 LTS and Unity 6.
### 1. Read the project
Note: the Unity version (`ProjectSettings/ProjectVersion.txt`; 2022.3 or later), the first scene
(`ProjectSettings/EditorBuildSettings.asset`), which scenes are levels, the scripts for player death and respawn,
level complete and fail, pickups, and anything spawned in bulk; where the version lives (`bundleVersion` in
`ProjectSettings/ProjectSettings.asset` is `Application.version`).
### 2. Install
Add one line to `dependencies` in `Packages/manifest.json`:
```json
"gg.glitchless.sdk": "https://github.com/glitchless-gg/glitchless.git?path=unity/Packages/gg.glitchless.sdk"
```
Unity resolves it the next time the Editor has focus. Pin a release by adding `#gg.glitchless.sdk/v0.1.0`.
### 3. Initialize, without touching a scene file
Create `Assets/Scripts/GlitchlessSetup.cs`:
```csharp
using GlitchlessSdk;
using UnityEngine;
static class GlitchlessSetup
{
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
static void Init() => Glitchless.Init(new GlitchlessOptions
{
Service = "",
Intake = "http://localhost:7777",
ClientToken = "gl_pub_dev",
});
}
```
It runs before the first scene loads, so no scene needs editing. (If the project has an assembly definition for
its scripts, add `GlitchlessSdk` to its references.) The Editor and development builds send with `Env` `dev`;
release builds with `prod`.
### 4. Levels and game semantics
Scenes are levels on their own: each scene that becomes active is a level unless its name reads as a menu, loader,
overlay or end screen (`Boot`, `Loading`, `MainMenu`, `UI`, `HUD`, `GameOver`…). If one scene plays many levels,
call `Glitchless.LevelStart("world-1-2")` where a level begins (that turns scene levels off), or set
`LevelForScene` in the options.
| Game moment | Call |
| --- | --- |
| level beaten | `Glitchless.LevelComplete("level-2", score: 1200)` |
| player gives up / game over | `Glitchless.LevelFail("level-2", "fell")` |
| player dies | `Glitchless.Death(player.position, "spikes")` |
| pickups, checkpoints | `Glitchless.Track("powerup", "magnet")` |
| a purchase the store confirmed | `Glitchless.Track("purchase", new Dictionary { ["amount"] = 4.99, ["currency"] = "USD", ["item"] = "gems_500" })` |
| counts of anything that scales, each frame | `Glitchless.SetContext("enemies", enemies.Count)` |
| the player's place, each frame | `Glitchless.SetPosition(player.position)` |
| a caught exception | `Glitchless.Error(e)` |
Uncaught exceptions, `Debug.LogError` and engine errors are reported without any code.
### 4b. Bug reports from players (ask first)
As for [Three.js](#4b-bug-reports-from-players-ask-first): ask the user first, since the note is free text the
player types, and point them at the privacy wording in the package's `PRIVACY.md`. Then add one line to the options:
```csharp
BugReports = new BugReportOptions(), // F8 opens the form; OnOpen / OnClose to pause, Hotkey to change the key
```
The form is IMGUI, so the game needs no Canvas or EventSystem, and it works with the Input Manager and the Input
System. While it's open, `Time.timeScale` is 0 and the Input System's actions are off (`PauseGame`). Input the game
reads straight from `UnityEngine.Input` or a device isn't held back: guard it with `if (Glitchless.IsBugReportOpen)
return;`. A pause menu can call `Glitchless.OpenBugReport()`, and the game's own form `Glitchless.ReportBug(note)`.
### 5. Bots and measuring
If the game has an autoplay or test mode, start it from the command line (`--autoplay --seed=N`): the SDK marks
those sessions synthetic. To measure a build before and after a change, run the player with
`--glitchless-measure=10 --glitchless-out=measure.json` (add `-batchmode -nographics` on a machine without a
screen); it writes frame stats as JSON and quits. In a live Editor you can drive, `Glitchless.Measure(5, r => Debug.Log(Json.Serialize(r)))`
does the same.
### 6. Verify
Press Play in the Editor, play a level and die once, then call `glitchless_list_games` or open the dashboard.
The **Glitchless** GameObject under DontDestroyOnLoad shows the SDK is running.
## Godot
Follow the addon's README (`godot/addons/glitchless/README.md`): install, `Glitchless.init`, levels, deaths,
`track`, and `--glitchless-measure` for bots.
### Bug reports from players (ask first)
As for [Three.js](#4b-bug-reports-from-players-ask-first), and the same question to the user first. In Godot,
`"bug_reports": true` in the options of `Glitchless.init` adds the F8 form; it pauses the tree while it's open, so
a game with its own pause logic passes `"bug_reports": {"pause": false}` and connects `Glitchless.bug_report_opened` and
`bug_report_closed`. A pause menu can call `Glitchless.open_bug_report()`, and the game's own form
`await Glitchless.report_bug(note)`. The screenshot is the root viewport; headless runs have none. Privacy wording:
`godot/addons/glitchless/PRIVACY.md`.
## Unreal
The Glitchless plugin (`unreal/Plugins/Glitchless` in the Glitchless repo), for Unreal Engine 5.8. It is C++
source: it times every frame on the game, render and RHI threads and the GPU, names each hitch's likely causes
(pipeline states created on the spot, garbage collection, map loads, streamed levels, asset loads), and reports
`UE_LOG` errors, ensures, Blueprint runtime errors and crashes. Maps are levels on their own.
### 1. Read the project
Note: `EngineAssociation` in the `.uproject` (5.8), whether there is a `Source/` folder (a C++ project) or not
(Blueprint only), `GameDefaultMap` in `Config/DefaultEngine.ini` and which maps are levels, the classes for player
death and respawn, level complete and fail, pickups, and anything spawned in bulk (projectiles, enemies, effects);
where the version lives (`ProjectVersion` under `[/Script/EngineSettings.GeneralProjectSettings]` in
`Config/DefaultGame.ini`). Blueprints are binary `.uasset` files: you can't read them as text. If Unreal MCP is
connected (below), use it to look inside them; otherwise ask the user where those moments live.
### 2. Install
Copy the plugin into the project, so it builds with the game (from the agent kit, or the GitHub release zip):
```bash
cp -R ~/.glitchless/unreal/Plugins/Glitchless "/Plugins/Glitchless"
```
Add it to the `.uproject`'s `Plugins` list: `{ "Name": "Glitchless", "Enabled": true }`. The plugin is source, so a
Blueprint-only project needs a C++ module once: ask the user to add one in the editor (**Tools → New C++ Class**,
any class) or add a minimal `Source/` folder yourself (a module, `.Target.cs` and `Editor.Target.cs`).
Then build the editor target: on a Mac `"/Users/Shared/Epic Games/UE_5.8/Engine/Build/BatchFiles/Mac/Build.sh"
Editor Mac Development -project="/.uproject"`, on Windows `Engine\Build\BatchFiles\Build.bat
Editor Win64 Development -project=...`. Add `-NoHotReload` if an editor has the project open. C++ code that
calls Glitchless adds `"Glitchless"` to `PublicDependencyModuleNames` in its `.Build.cs`. If the user installed it
from Fab instead, it is an engine plugin that comes built: enabling it is all, and a Blueprint-only project needs no
C++ module. `unreal/demo` in the Glitchless repo is a working example, with three planted problems and a script
that checks Glitchless finds them.
### 3. Settings, without code
Add this to `Config/DefaultGame.ini` (or set it in **Project Settings → Plugins → Glitchless**); the game instance
starts Glitchless with it, so nothing calls Init:
```ini
[/Script/Glitchless.GlitchlessSettings]
Service=
Intake=http://localhost:7777
ClientToken=gl_pub_dev
Privacy=Anonymous
bSendInDevelopmentBuilds=True
```
Play In Editor and Development builds send as env `dev`, Shipping builds as `prod`. The version is the project's
`ProjectVersion` unless `Version=` is set here: bump it on every release. For a game jam, keep
`bSendInDevelopmentBuilds=True`: jam submissions are Development builds.
### 4. Levels and game semantics
Maps are levels, except those that read as a menu, lobby, loader or end screen (`Entry`, `MainMenu`, `L_Frontend`,
`Lobby`, `Loading`, `GameOver`…). If one map plays many levels, call `LevelStart` where each begins; the first call
turns map levels off.
| Game moment | C++ (`#include "Glitchless.h"`) | Blueprint node |
| --- | --- | --- |
| level starts | `FGlitchless::LevelStart(TEXT("forest-2"))` | Level Start |
| level beaten | `FGlitchless::LevelComplete(TEXT(""), Score)` (empty: the current level) | Level Complete |
| player gives up, game over | `FGlitchless::LevelFail(TEXT(""), TEXT("timeout"))` | Level Fail |
| player dies | `FGlitchless::Death(GetActorLocation(), TEXT("fell"))` | Death |
| pickups, checkpoints | `FGlitchless::Track(TEXT("powerup"), TEXT("magnet"))` | Track Event (What), Track Event |
| a purchase the store confirmed | `FGlitchless::Track(TEXT("purchase"), {{TEXT("currency"), TEXT("USD")}, {TEXT("item"), TEXT("gems_500")}}, {{TEXT("amount"), 4.99}})` | Track Event, `amount` in Numbers |
| counts of anything that scales, each frame | `FGlitchless::SetContext(TEXT("enemies"), Enemies.Num())` | Set Context |
| the player's place, each frame | `FGlitchless::SetPosition(GetActorLocation())` | Set Position |
| a problem the game noticed | `FGlitchless::Error(TEXT("save failed"))` | Report Error |
Pass Unreal units: the plugin sends positions in metres with y up, like the other SDKs. Every call is safe before
Init and from any thread. `UE_LOG` errors, ensures, Blueprint runtime errors (Accessed None…) and crashes are
reported without code; a crash is sent the next time the game starts. UObject and actor counts are sent as gauges
already; add the game's own counts with `SetContext`. To find a Blueprint-only game's moments, add the nodes in the
Blueprints that handle them (with Unreal MCP, or by asking the user to).
### 4b. Bug reports from players (ask first)
As for [Three.js](#4b-bug-reports-from-players-ask-first): ask the user first, since the note is free text the
player types, and point them at the privacy wording in the plugin's `PRIVACY.md`. Then add `BugReportKey=F8` to the
settings above: F8 opens a small Slate box over the game with a screenshot and a note. It takes the game's input
and pauses the game while open (`bPauseForBugReport`, on by default; a game paused already stays paused). A pause
menu can call **Show Bug Report Dialog** (`FGlitchless::ShowBugReportDialog()`), the game's own form **Report Bug**
(`FGlitchless::ReportBug(Note)`), and `FGlitchless::OnBugReportDialog()` (or the Glitchless subsystem's On Bug
Report Dialog) says when the box opens and closes.
### 5. Bots and measuring
Package a Development build, then run it with flags:
```bash
"/Users/Shared/Epic Games/UE_5.8/Engine/Build/BatchFiles/RunUAT.sh" BuildCookRun -project="/.uproject" -platform=Mac -clientconfig=Development -build -cook -stage -pak -unattended
-glitchless-warmup=2 -glitchless-measure=10 -glitchless-out=measure.json
```
`-glitchless-measure=S` waits for the first map plus the warmup, measures S seconds, prints
`{"glitchless_measure": {...}}` as one line on stdout and quits; the numbers are `glitchless_measure`'s (frame_ms
percentiles, fps, game/render/RHI/GPU ms, hitches with causes, memory, new errors) plus `session_id`. If the game has
an autoplay or test mode, start it with `-autoplay`; that and `-glitchless-synthetic` mark the session synthetic.
`-glitchless-intake=URL` sends to another server for one run. A Windows build has no console: read the
`-glitchless-out` file. A UE 5.8 Mac build is sandboxed: it can write `-glitchless-out` only inside
`~/Library/Containers//Data` (its `tmp` is a good place), and stdout works anywhere. Don't start a build
or cook while another Unreal build, cook or shader compile is running on the machine.
### 6. Agents in the editor: Unreal MCP
UE 5.8 ships Unreal MCP (Experimental), Epic's MCP server inside the editor: with it you can open maps, look inside
Blueprints and actors, edit them and run Play In Editor. Glitchless doesn't replace it; the two split the work:
- **Unreal MCP** is your hands in the editor: find where deaths and level ends happen in Blueprints, add the
Glitchless nodes there, change the scene or material a finding points at, and start Play In Editor.
- **Glitchless** says what is wrong and whether a change fixed it: the `glitchless` MCP server reads what players and
bots sent (findings, hitches with causes, errors, sessions), and the optional **Glitchless Toolset** plugin
(`unreal/Plugins/GlitchlessToolset`) adds `Status` and `Measure` tools to Unreal MCP, so you can measure the game
while it plays in the editor, as `glitchless_measure` does for a web game.
To set it up, copy `GlitchlessToolset` next to `Glitchless` in `Plugins/`, enable **Unreal MCP**, **All Toolsets** (or
**Toolset Registry**) and **Glitchless Toolset** in **Edit → Plugins** (or the `.uproject`), start the server
(`ModelContextProtocol.StartServer` in the editor console, or Editor Preferences → Model Context Protocol → Auto
Start Server), and run `ModelContextProtocol.GenerateClientConfig ClaudeCode` (or Cursor, Codex…) for the client
config. Start Play In Editor before calling `Measure`. Numbers from the editor run slower than a packaged build
(shaders compile on first use there): confirm a fix with a packaged Development build and `-glitchless-measure`.
### 7. Verify
Press Play in the editor (or run a Development build), play a level and die once, then call `glitchless_list_games`
or open the dashboard. The log shows `LogGlitchless` lines; `-glitchless-debug` logs each batch as it is sent.
## GameMaker
The Glitchless SDK for GameMaker (`gamemaker/` in the Glitchless repo; `~/.glitchless/gamemaker` if it's cloned
there) is GML: eight scripts and two objects. Its README (`gamemaker/README.md`) has the whole API; this is the
order to wire it in.
### 1. Read the project
Note: the `.yyp` (it must be GameMaker 2024.2 or later: `"$GMProject"` at its top; an older project needs opening
once in a newer GameMaker), the first room in `RoomOrderNodes` and its creation code, the controller objects that
live through the game (`persistent`), which rooms are levels and which are menus, the objects for the player, its
death and respawn, level complete and fail, pickups, and anything created in bulk (bullets, enemies, particles,
sparks). Check for an existing `exception_unhandled_handler` call and any analytics the game already sends.
### 2. Install
Without the IDE (an agent), from the Glitchless repo:
```bash
node ~/.glitchless/gamemaker/tools/install.mjs path/to/Game.yyp
```
It copies the scripts and objects into the project and lists them in the `.yyp` under a *Glitchless* folder; run it
again to update. In the IDE, **Tools → Import Local Package** with `Glitchless-.yymps` does the same. Never
place `obj_glitchless` in a room: `glitchless_init` makes it.
### 3. Initialize
Once, as early as possible: the first room's creation code, or the Create event of the game's first persistent
controller.
```gml
glitchless_init("my-game", "", "prod", "", { client_token: "" }); // "" = GM_version
```
A build run from the IDE sends nothing without `-glitchless`; executables send. If the game sets its own
`exception_unhandled_handler`, move it into the `exception_handler` option, or call `glitchless_crash(ex)` first
thing in it, or crashes go unreported.
### 4. Levels and game semantics
- **Levels:** a room that reads as a level (`rm_cave_2`, not `rm_main_menu` or `rm_title`) starts that level on its
own. A game whose levels aren't rooms calls `glitchless_level_start(id)` (the first call turns the rooms off), and
every game calls `glitchless_level_complete(undefined, score)` and `glitchless_level_fail(undefined, cause)` where
a level ends.
- **Deaths:** `glitchless_death(x, y, cause)` where the player dies.
- **Gauges:** `glitchless_set_context({ enemies: instance_number(obj_enemy), bullets: instance_number(obj_bullet) })`
in a controller's End Step: whatever is created in bulk. The SDK already sends `instances`, `surfaces`, `ds_maps`,
`ds_lists` and `buffers`.
- **Errors the game catches:** `glitchless_error(_e)` in its `catch` blocks.
- **Quit button:** `glitchless_quit()` in place of `game_end()`, so the last batch goes now, not next launch.
### 4b. Bug reports from players (ask first)
As for [Three.js](#4b-bug-reports-from-players-ask-first), and the same question to the user first. In GameMaker,
`bug_reports: true` in the options of `glitchless_init` adds the F8 form; it deactivates the game's instances while
it's open, so a game with its own pause passes `bug_reports: { pause: false, on_open: f, on_close: g }`. A pause menu
calls `glitchless_open_bug_report()`, and the game's own form `glitchless_report_bug(note, true, callback)`. The
screenshot is a PNG of the application surface. Privacy wording: `gamemaker/PRIVACY.md`.
### 5. Bots and measuring
Give the game an `-autoplay` flag (read it with `glitchless_has_flag("autoplay")`) that plays a few levels and
calls `glitchless_quit()`. Sessions run with `-autoplay` are synthetic. GameMaker must be set up once by a person
(`gamemaker/README.md`, "Setting up GameMaker for the end-to-end test"). Igor's `Run` (the IDE's, and `gm-cli run`)
starts the game with macOS's `open`, which passes no command line or environment on, so for a run from the IDE or
Igor put the flags in `glitchless/args.txt` in the runner's save area:
```bash
mkdir -p ~/Library/Application\ Support/com.yoyogames.macyoyorunner/glitchless
echo "-autoplay -glitchless-measure=10" > ~/Library/Application\ Support/com.yoyogames.macyoyorunner/glitchless/args.txt
npx @gamemaker/gm-cli@2.4.1 run Game.yyp --target=mac --runtime=vm # or Run in the IDE
```
Or compile once and start the runner with the flags on its command line, as `gamemaker/test/e2e.mjs` does. The run
prints the stats as one line of JSON and where it saved them (`glitchless/measure.json` in the save area). Delete
`args.txt` afterwards. Measure before and after a change.
### 6. Verify
Run the game with `-glitchless` (from the IDE or gm-cli, in `args.txt` as above), play a level and die once, then
call `glitchless_list_games` or open the dashboard. `-glitchless-debug` logs each batch as it is sent.
## Every engine: the game's own metrics, and its board
Games differ: a puzzle game cares about solve time and hints, a mobile game about opens, modes, the paywall and
purchases, a platformer about where players die. Glitchless doesn't assume a genre, so this part is yours.
1. Find the moments this game's designer would want counted, and send each with `track`: one event name per
moment, plus one word or a few attributes about it. Never anything about the player.
- Three.js or Phaser: `glitchless.track('play', { mode: 'endless' })`
- Godot: `Glitchless.track("play", "endless")` (one word becomes the attribute `what`) or a dictionary
- Unity: `Glitchless.Track("play", "endless");` or a `Dictionary`
- Unreal: `FGlitchless::Track(TEXT("play"), TEXT("endless"));` (the attribute `what`) or a `TMap` of attributes
and one of numbers; in Blueprint, Track Event (What) or Track Event
- GameMaker: `glitchless_track("play", "endless");` (the attribute `what`) or a struct
Good candidates: the game opening (with the edition, free or full), a mode or run starting, a run ending and
how, shop and paywall views and where they were opened, purchases started and finished, hints, choices that
matter (difficulty), shares and rating prompts. Levels, stages and puzzles go through levelStart,
levelComplete and levelFail, not `track`. Attributes are strings, numbers or booleans, at most 12 per event.
Purchases have one convention in every engine, so boards can add them up into revenue: a `purchase` event
when the store confirms it, with `amount` (a number in the currency's units: 4.99, not 499), `currency` (the
ISO code the store charged in) and `item`. A game has one currency on its boards; purchases in another are
listed as left out, not added in. Never send receipts, order ids or payment details.
- Web games (Three.js, Phaser, Babylon.js, PlayCanvas): `glitchless.track('purchase', { amount: 4.99, currency: 'USD', item: 'gems_500' })`
- Godot: `Glitchless.track("purchase", {"amount": 4.99, "currency": "USD", "item": "gems_500"})`
- Unity: `Glitchless.Track("purchase", new Dictionary { ["amount"] = 4.99, ["currency"] = "USD", ["item"] = "gems_500" });`
- Unreal: `FGlitchless::Track(TEXT("purchase"), {{TEXT("currency"), TEXT("USD")}, {TEXT("item"), TEXT("gems_500")}}, {{TEXT("amount"), 4.99}});`
(in Blueprint, Track Event with `amount` in Numbers)
2. Once events arrive, design the game's own dashboard: follow the `dashboard` skill (next to this one), which
turns the developer's questions into collectors, panels and pages and saves them with the
`glitchless_set_dashboard` MCP tool.
## Report
Tell the user what you wired (with file:line), the gauges you chose and why, the events you track and the board
you designed, the agent tools or measure commands you set up, and anything you weren't sure about.
---
Source: https://github.com/glitchless-gg/glitchless/blob/main/plugin/glitchless/skills/dashboard/SKILL.md
---
name: dashboard
description: Build a game's own Glitchless dashboard. Work out what this game's developer wants to know, add the collectors for it (custom events with track(), levels, gauges, positions, game tools agents can call), check the data arrives, then design the game's dashboard (its pages: boards of panels and the built-in views it needs) with the glitchless_set_dashboard MCP tool. Use when the user asks for a dashboard, metrics, KPIs or analytics for their game, wants to track or count something, asks "how many players…", "which mode…", "does the paywall convert", or wants a different view of their game in Glitchless.
---
# Build a game's own dashboard
Every game needs different numbers. A mobile game cares whether players come back, which mode they pick and
whether the paywall turns into purchases; a puzzle game, how long a puzzle takes and how often hints are used; a
platformer or shooter, where players die and whether frames hold up in a fight. Glitchless doesn't guess: you
add the collectors this game needs, and design its dashboard from them: its own pages, in its own words. The
developer then sees it in Glitchless, and you can read the same data back over MCP to debug.
Before this skill, the game should already send data (the `instrument` skill). If it doesn't, do that first.
## 1. Find out what they want to know
Write down three to eight questions, in the developer's words. Ask if they haven't said; read the game to
suggest some (menus, modes, shops, level lists, settings, anything with a counter). Good questions are about
decisions: "Do players finish the tutorial?", "Which mode do they play?", "Does the paywall convert?", "Which
stage is too hard?", "Does it stutter on phones?". Leave out what nobody would act on.
## 2. Pick a collector for each question
| The question is about | Collector | Shows on a board as |
| --- | --- | --- |
| how often something happens, or which option players choose | `track(name, attrs)` | `stat`, `series`, `breakdown` |
| how much: revenue, coins, seconds | a number in `track`'s attributes (`purchase` with `amount` and `currency`) | `stat` or `breakdown` with `sum` |
| how many players do it at all (conversion) | `track(name)` | `stat` with `per: "$players"` |
| a sequence (tutorial → first win, paywall → purchase) | one `track` per step | `funnel` |
| progress or difficulty through stages, puzzles, waves, stops | `levelStart` / `levelComplete` / `levelFail` | `levels` |
| dying, and where | `death` (and `setPosition` for places) | `deaths`, the Heatmaps view |
| smoothness, and what makes it worse | automatic frame times, plus `setContext` gauges (enemies, particles) | `frame_time`, `stat: frame_p95` |
| crashes and errors | automatic, plus `error()` for handled ones | `errors`, `stat: crash_free_pct` |
| whether players come back (D1, D7, D30) | install mode: privacy `install` in the SDK, a stored random player ID (ask the user first: the privacy page must say so) | `retention`, the Players view |
| how many are playing right now | automatic | `stat: players_now` |
The calls, per engine:
| | Three.js or Phaser | Godot | Unity | Unreal (C++; the Blueprint nodes match) |
| --- | --- | --- | --- | --- |
| a game event | `glitchless.track('play', { mode: 'endless' })` | `Glitchless.track("play", "endless")` | `Glitchless.Track("play", "endless");` | `FGlitchless::Track(TEXT("play"), TEXT("endless"));` (Track Event (What)) |
| several attributes | `glitchless.track('buy', { step: 'done', item: 'full_game' })` | `Glitchless.track("buy", {"step": "done"})` | `Glitchless.Track("buy", new Dictionary { ["step"] = "done" });` | `FGlitchless::Track(TEXT("buy"), TMap{{TEXT("step"), TEXT("done")}});` (Track Event) |
| a purchase | `glitchless.track('purchase', { amount: 4.99, currency: 'USD', item: 'gems_500' })` | `Glitchless.track("purchase", {"amount": 4.99, "currency": "USD", "item": "gems_500"})` | `Glitchless.Track("purchase", new Dictionary { ["amount"] = 4.99, ["currency"] = "USD", ["item"] = "gems_500" });` | `FGlitchless::Track(TEXT("purchase"), {{TEXT("amount"), TEXT("4.99")}, {TEXT("currency"), TEXT("USD")}, {TEXT("item"), TEXT("gems_500")}});` (Track Event) |
| a level | `levelStart(id)`, `levelComplete(id, { score })`, `levelFail(id, { cause })` | `level_start`, `level_complete`, `level_fail` | `LevelStart`, `LevelComplete`, `LevelFail` | `LevelStart`, `LevelComplete`, `LevelFail` (maps are levels on their own) |
| a gauge | `glitchless.setContext({ enemies })` | `Glitchless.set_context({"enemies": n})` | `Glitchless.SetContext("enemies", n)` | `FGlitchless::SetContext(TEXT("enemies"), N)` |
Rules that keep the numbers useful:
- One event name per moment, in the game's own words (`open`, `play`, `paywall`, `buy`), and keep names stable:
renaming an event splits its history.
- Put the choice in an attribute, not the name: `play` with `mode: 'endless'`, not `play_endless`. Godot, Unity
and Unreal turn one word into the attribute `what`.
- Attributes are strings, numbers or booleans, at most 12 per event; strings are cut to 64 characters. Keep their
values to a small set (modes, places, outcomes): a value per player or per run makes a useless breakdown.
- Never send anything that identifies a player: no names, emails, account ids or anything they typed.
- Purchases: one `purchase` event when the store confirms it (not when the button is tapped), with `amount` (a
number, in the currency's units: 4.99, not 499), `currency` (the ISO code the store charged in) and `item`. A
number sent next to `currency` counts as money, so put other numbers (gems granted) on an event of their own.
Never send receipts, order ids or payment details. Taps and failures are other events (`buy` with `step`).
Add the calls where the moment happens in code, then run the game (or let the bots play it) and check with
`glitchless_events` that every event arrives with the attributes you expect.
## 3. Design the dashboard
A dashboard is the game's pages, in the order its sidebar shows them; the first opens first. Each page is a
board, panels you choose, or a built-in view the game keeps: `events` (every event, breakdowns, funnels),
`performance` (frame time and hitches), `errors`, `players`, `heatmaps` (positions on the level), `sessions`
(each with its replay), `versions`, `live` (events as they arrive; `options.events` keeps only some) and `reports`
(bug reports players sent, with screenshots). Give the game the pages its developer would open, named in the game's
words, and drop the views it has no data for: no Heatmaps for a puzzle or mobile game, no Performance for a game
that sends no frame times, no reports page for a game that doesn't turn bug reports on.
Start from what is there: `glitchless_dashboard` returns the current pages (`spec`; an automatic dashboard until
someone saves one). Then call `glitchless_set_dashboard` with every page. On a board: a row of stats, then the
game's own events and funnels, then levels, then frame time and errors. Title each panel with the question it
answers. Each panel takes `size`: `s` (a quarter row, for stats), `m` (half) or `l` (full width); and a `note`,
one line on why it matters.
| type | fields |
| --- | --- |
| `stat` | `metric` (sessions, players, avg_session_s, crash_free_pct, error_free_pct, frame_p95, avg_fps, hitches_per_min, errors, deaths, rage_quit_pct, players_now: playing now, whatever the range), or `event` with an optional `per`: another event (a percentage) or `$players` / `$sessions` (the share of them that sent it: conversion). With `sum` (a numeric attribute), it adds that up instead, and `per: "$players"` gives it per player |
| `series` | `event`, optional `by` (split by an attribute, or `$version` / `$level`), `limit`, `sum` |
| `breakdown` | `event`, `by`, `limit`, `sum` (each value's sum, largest first) |
| `funnel` | `steps`: events in order, comma-separated |
| `levels` | `label`: what the game calls a level ("Stop", "Puzzle", "Wave") |
| `sessions` | `sent`: only sessions that sent this event ("buy=done"), `limit` |
| `versions` | none: this board's stats, version by version |
| `live` | `events`: only these names or types, as they arrive |
| `findings`, `frame_time`, `errors`, `deaths`, `events`, `reports` | none (`limit` for findings, errors and reports) |
| `retention` | `limit`: how many of the latest first-seen days (default 14). Needs player IDs: a game in anonymous mode gets a panel saying to turn install mode on |
An event is written `name`, `name=value` (any attribute has that value) or `name:key=value`. With `sum` and `by`
an attribute, the event is a name only (`purchase`, not `purchase=done`).
**Money.** A sum of events that carry a `currency` is money: the dashboard shows it with the currency's symbol and
two decimals. A game has one currency, the dashboard's `currency` (`glitchless_set_dashboard` with
`currency: "USD"`, sent again with every save) or else the one most purchases are sent in; purchases in another
currency are listed on the panel as left out, never added in. If one shows up, fix the game (convert before
sending, or send the store's price in one currency) rather than the board. Revenue is hidden on a shared board
unless the link is made to show it.
Every panel and the tables of the Events, Errors, Sessions, Players and Versions views have a CSV button, so a
developer can take any number to a spreadsheet: no export panel is needed.
A mobile game with modes, stops and a paywall:
```json
{
"currency": "USD",
"pages": [
{ "title": "Overview", "icon": "overview", "panels": [
{ "type": "stat", "metric": "players", "size": "s" },
{ "type": "stat", "event": "open", "title": "App opens", "size": "s" },
{ "type": "stat", "event": "buy=done", "per": "paywall", "title": "Paywall to purchase", "size": "s" },
{ "type": "stat", "event": "purchase", "sum": "amount", "title": "Revenue", "size": "s" },
{ "type": "findings", "limit": 3, "size": "l" },
{ "type": "breakdown", "event": "play", "by": "what", "title": "Which modes they play", "size": "m" },
{ "type": "series", "event": "open", "by": "what", "title": "Opens, free vs full", "size": "m" }
] },
{ "id": "paywall", "title": "Paywall", "icon": "bolt", "panels": [
{ "type": "stat", "event": "purchase", "sum": "amount", "per": "$players", "title": "Revenue per player", "size": "s" },
{ "type": "stat", "event": "purchase", "per": "$players", "title": "Players who bought", "size": "s" },
{ "type": "breakdown", "event": "purchase", "sum": "amount", "by": "item", "title": "Revenue by item", "size": "m" },
{ "type": "funnel", "steps": "paywall,buy=tapped,buy=done", "title": "Paywall to purchase", "size": "l" },
{ "type": "breakdown", "event": "paywall", "by": "what", "title": "Where the paywall opens", "size": "m" },
{ "type": "versions", "title": "Paywall numbers by version", "size": "m" },
{ "type": "sessions", "sent": "buy=done", "title": "Sessions that bought", "size": "l" }
] },
{ "id": "stops", "title": "Stops", "icon": "flag", "panels": [
{ "type": "levels", "label": "Stop", "title": "Stops won and lost", "size": "l" },
{ "type": "breakdown", "event": "difficulty", "by": "what", "title": "Difficulty picked", "size": "m" }
] },
{ "id": "purchases-live", "title": "Purchases, live", "view": "live", "options": { "events": ["paywall", "buy"] } },
{ "view": "events" },
{ "view": "sessions" },
{ "view": "versions" },
{ "view": "errors" }
]
}
```
A puzzle game: an Overview with `stat` players and average session, `levels` with `"label": "Puzzle"` (completion
and time), a `breakdown` of `hint` by `what` and a `funnel` of `tutorial=done,puzzle_solved`; then Events, Sessions
and Errors. No Performance unless it's slow, no Heatmaps, no deaths.
A platformer or shooter: an Overview with `stat` frame_p95 and crash_free_pct, `findings`, `frame_time`, `levels`,
`deaths` and a `breakdown` of `weapon` or `powerup`; then Performance, Heatmaps, Errors, Sessions and Versions.
Saving checks every page and answers with each panel as it looks over the last week; a panel without data says
why ("No "buy" events in this window"), and `glitchless_dashboard` with `page` shows one board's numbers. Show the
developer, then change what they ask for: "split opens by version", "add the shop" is one more `track` call and one
more panel, "a page for the tutorial" one more board. People can rename, reorder, add and remove pages in the
dashboard too (Edit pages), so read the current pages before saving over them.
## 4. Use it to debug
The boards also decide what a release is judged on: the Versions view and `glitchless_compare_versions` compare
the stats on every board, version by version (purchases per paywall view, revenue per session, opens per session), so ship a change,
bump the version, and read whether it moved the numbers this game cares about. For one event across versions on a
board, use `by: "$version"`; for a board's own stats, a `versions` panel.
The dashboard's numbers are what you read when something looks wrong: `glitchless_events` with a `funnel` finds
where players drop off, `glitchless_triage` ranks performance, crash and difficulty problems, and in a browser
game the WebMCP tools let you reproduce one in the running game. Add a panel for anything you find yourself
checking twice.
## Report
List the questions, the events and calls you added (file:line), and the dashboard's pages and panels, and say
which panels will fill in only once players or bots send that data.
---
Source: https://github.com/glitchless-gg/glitchless/blob/main/plugin/glitchless/skills/triage/SKILL.md
---
name: triage
description: Find and fix the most important problem in a game (Three.js, Phaser, Babylon.js, PlayCanvas, Unity, Godot or Unreal) using Glitchless player telemetry — frame-time hitches, leaks, errors, difficulty spikes and rage quits — then verify the fix with measurements. Use when the user asks what's wrong with their game, why it stutters or lags, why players quit, or to "fix the Glitchless findings".
---
# Triage a game with Glitchless
You have two sources of truth:
- **Glitchless MCP** (`glitchless_*` tools without "live") — production telemetry from every player, aggregated.
- **The running game.** For a web game (Three.js, Phaser, Babylon.js, PlayCanvas): **WebMCP** (`glitchless_live_performance`, `glitchless_measure`, `glitchless_inspect_scene`, `glitchless_errors`, plus the game's own tools like `_load_level`) in the user's browser right now. Tool names may carry a channel prefix (e.g. `localhost_5173-glitchless_measure`).
If WebMCP tools are missing, ask the user to open the game, call the `_webmcp_get-token` tool and have them paste the token into the WebMCP widget. Work without it if they decline.
For Unity, Godot and Unreal there is no WebMCP: measure a build with `--glitchless-measure=SECONDS` (Unity: `--glitchless-out=FILE` writes the JSON; Godot prints one line starting `{"glitchless_measure"`; Unreal takes `-glitchless-measure=SECONDS`, prints that line too and writes `-glitchless-out`), started with the game's autoplay flags if it has them. The JSON has the same fields as `glitchless_measure`. In the Unreal editor, Unreal MCP with the Glitchless Toolset adds a `Measure` tool for Play In Editor (the instrument skill sets it up); confirm with a packaged Development build, since the editor compiles shaders on first use.
## Loop
1. **Rank.** Call `glitchless_triage` (24h by default). Pick the top finding, or the one the user named. Say in one line which one and why (severity × share of players affected).
2. **Get the evidence.** Drill in by category:
- performance → `glitchless_performance` with `level`: look at `hitch_context` (which gauges spike during hitches, which events precede them) and `slowest_recent_hitches[].loaf` (script attribution).
- memory → `glitchless_performance` `memory` + `by_level.geometries_max`.
- stability → `glitchless_get_error` with the fingerprint: stack, `location`, breadcrumbs.
- design → `glitchless_death_hotspots` for the level, then `glitchless_player_funnel`.
- the game's own metrics (plays, purchases, the paywall, whatever it sends with `track`) → `glitchless_events`,
with a `funnel` for drop-off between steps; `glitchless_dashboard` shows the pages and panels this game cares about.
3. **Find the code.** Use the evidence as search keys: event names from `preceding_events` (e.g. the `glitchless.track('powerup')` call site), gauge names passed to `glitchless.setContext`, the error's function/file, level data near the hotspot's x range.
4. **Reproduce and baseline.** Web: load the level / trigger the effect with the game's tools, then run `glitchless_measure` for 5–10s. Unity, Godot and Unreal: build (Unreal: a packaged Development build), then run the measure flag on the level in question. Record p95, worst frame, draw calls, render and update CPU, peak gauges, the memory delta. For errors, reproduce and confirm with `glitchless_errors` (web) or the session in `glitchless_session`.
5. **Save a playtest baseline before touching code** (web games). Call `glitchless_playtest` with `save_as: "baseline"`. The game's autoplay bots play fixed-seed runs on the current code so you can prove later that your change made nothing else worse. It needs the game and Glitchless server running and Playwright in the project; if it can't run, say so and continue. For Unity, Godot and Unreal, keep the measure JSON from step 4 as the baseline.
6. **Fix the root cause**, minimally and idiomatically:
- many short-lived meshes → one `InstancedMesh`/`THREE.Points` or a pool; shared geometry/material; remove **and** `dispose()` when done
- geometry growth per level load → dispose geometries/materials/textures on unload; share per-tile geometry
- shader-compile hitches → `renderer.compileAsync(scene, camera)` during loading
- unchecked lookups (`pool.find()` → undefined) → handle exhaustion gracefully, and apply game state (score) before anything that can throw
- **Phaser:** objects and bodies that pile up → destroy them or recycle through a Group, one collider per group; `listeners` growing per restart → remove handlers on `shutdown` (scene events outlive a restart); `texture_upload` hitches → make textures once in the preloader; `game_loop_stopped` → the uncaught error stopped Phaser's loop for good, so make that callback fail safe
- **Unity:** `gc` hitches → stop per-frame allocations (LINQ, string building, new lists); `object_spawn` → pool with `UnityEngine.Pool.ObjectPool`; mesh or material counts growing per level → `Destroy()` runtime meshes and materials, use `sharedMaterial`; `physics` → fewer rigidbodies, simpler colliders; `exception` → the rest of that `Update` never ran: guard the null or missing key
- **Godot:** follow the finding's suggestion; it names Godot APIs
- **Unreal:** `pso_compile` hitches → keep PSO precaching on, ship a bundled PSO cache (record with `-logPSO`), spawn first-use effects behind the loading screen; `-LogCmds="LogPSOHitching Verbose"` names each material; `gc` hitches → pool actors and objects instead of SpawnActor/NewObject per shot, keep short-lived data out of UObjects; `ensure` or `crash` → a UObject that is null or pending kill: test with `IsValid()`, keep it in a `UPROPERTY()`; `script` → a Blueprint read None: add an Is Valid node. With Unreal MCP connected, make Blueprint changes through it
- **design findings** (a jump nobody makes): propose 1–2 concrete level-data changes with the numbers, and ask the user before changing game design.
7. **Verify.** Repeat the exact reproduction and measure again (web: reload and `glitchless_measure`; Unity, Godot and Unreal: rebuild and run the measure flag). Show a before/after table. On the web, drop a breadcrumb with `glitchless_annotate` (e.g. "agent: confetti → InstancedMesh, p95 48→15ms").
8. **Playtest the change** (web games). Call `glitchless_playtest` with no arguments to compare every scenario against the baseline. A FAIL means the work isn't done: read the regressions and new errors, fix them, and run it again. Report the verdict and the improvements table to the user.
9. **Version it.** Bump the patch version the game reports (`version` in `Glitchless.init`; Unity's `Application.version`; Godot's `application/config/version`; Unreal's `ProjectVersion` in `DefaultGame.ini`) so production data from the fix is separable, and tell the user to run `glitchless_compare_versions` (or open Versions in the dashboard) once players have played the new build.
Never claim a fix worked without a measurement and a passing playtest; if you couldn't run either, say so and say what to watch in production.
---
Source: https://github.com/glitchless-gg/glitchless/blob/main/docs/jam/megajam-2026.md
# Glitchless for the Epic AI MegaJam 2026
Glitchless records how your Unreal game runs: stutters and what caused them, errors, ensures and crashes,
and bug reports with screenshots from your teammates and playtesters. Its MCP server lets your coding
agent read all of it and fix what people hit. Jam teams get a free jam tier on Glitchless Cloud.
This guide gets a team from nothing to telemetry before the jam starts, for Unreal Engine 5.8 with
Claude Code or another coding agent, with or without Epic's Unreal MCP.
- Jam announcement: https://www.unrealengine.com/news/2026-epic-ai-megajam
- Jam page, rules summary and submission: https://itch.io/jam/2026-epic-ai-megajam
- Official Rules: https://epicgames.box.com/s/a7xcgl07fqoieeb99sjtd3juhj4agc4z
| When | What |
| --- | --- |
| Nov 11, 11 AM PST / 2 PM EST | Kickoff livestream and theme reveal |
| Nov 12, 12:00 AM EST | The jam starts. Don't build your game before this |
| Nov 15, 5:59 PM PST / 8:59 PM EST | Submissions close. Late ones are disqualified |
| By Jan 29, 2027 | Winners announced |
| Jan 31, 2027, 23:59 UTC | The `MEGAJAM26` jam tier ends |
## The jam tier: `MEGAJAM26`
| | Free | With `MEGAJAM26` |
| --- | --- | --- |
| Play sessions a month | 5,000 | 50,000 |
| People in the workspace | 1 | 5 (the jam's team limit) |
| Data kept | 14 days | 90 days |
| Games, bot sessions, exporters | 1 game, 5,000 bot sessions, no exporters | the same |
It's free and needs no card. It runs until **January 31, 2027, 23:59 UTC**. Then the workspace goes back to
Free on its own: nothing is deleted, teammates stay but no one new can join, intake stops at 5,000 sessions
a month, and data older than 14 days ages out as usual.
On a paid plan (Indie or Studio) the code is kept but changes nothing: your plan's sessions, retention and
bill stay as they are. It applies if the workspace goes back to Free before the code ends. Each workspace
holds one promo at a time, and only its Owner can apply one.
## Before Nov 11: set up (about 20 minutes)
The rules let you set up tools, agents and MCP before the jam, but not start your game. So rehearse in a
throwaway Unreal project, and on Nov 12 copy two things into your jam project.
1. **Sign up with the code:** https://app.glitchless.gg/?promo=megajam26 (Google or GitHub). Already have
an account? Settings → Plan & billing → Promo code → `MEGAJAM26`. Plan & billing then shows the jam
allowance and its end date.
2. **Create your game** and name it after your jam game (you can rename it later). Get started shows its
**ingest key** (`gl_pub_…`, goes in the game) and makes a **secret key** (`gl_sk_…`, for your agent;
shown once, keep it out of git).
3. **Invite your team:** Settings → Members, up to four more people. Developers can make their own secret
keys.
4. **Install the plugin** in the throwaway project (next section), play for a minute, and check the
session shows on the Overview.
5. **Add the MCP server** to each teammate's agent (below).
## Install the Unreal plugin
1. Download `glitchless-unreal-0.1.0.zip` from the [glitchless-unreal 0.1.0 release](https://github.com/glitchless-gg/glitchless/releases/tag/glitchless-unreal/v0.1.0) and unzip
it so the folder is `/Plugins/Glitchless`.
2. **Edit → Plugins**, find Glitchless under Analytics, enable it, restart the editor.
3. The plugin is C++ source. A Blueprint-only project has to become a C++ project once: **Tools → New C++
Class**, any class. You need Visual Studio 2022 with C++ on Windows, or Xcode on a Mac.
4. **Project Settings → Plugins → Glitchless:**
- **Service:** your game's id, lowercase, for example `my-jam-game`
- **Intake:** `https://api.glitchless.gg`
- **Client Token:** your ingest key
- **Send In Development Builds:** on. It is on by default; leave it on. Jam submissions are packaged
Development builds, so with it off nothing would arrive from your build or from anyone who plays it.
- **Bug Report Key** (under Advanced): `F8`
Those settings are saved in `Config/DefaultGame.ini`, so they travel with the project. The ingest key can
only send data, so it's fine to commit and ship:
```ini
[/Script/Glitchless.GlitchlessSettings]
Service=my-jam-game
Intake=https://api.glitchless.gg
ClientToken=gl_pub_...
Privacy=Anonymous
bSendInDevelopmentBuilds=True
```
Play In Editor and Development builds report as env `dev`. Anonymous mode, the default, keeps no player ID
and sends coarse device facts only. Mac and iOS Development builds can reach `https://api.glitchless.gg` as
they are.
Maps are levels on their own, and errors, ensures, Blueprint errors and crashes are caught with no code.
For deaths, level funnels and your own counters, ask your agent to add the calls from the plugin's README
(`Plugins/Glitchless/README.md`), or paste this:
```text
Glitchless is in Plugins/Glitchless and set up in Config/DefaultGame.ini. Read Plugins/Glitchless/README.md.
Call FGlitchless::LevelStart when a level starts and FGlitchless::Death where the player dies, and use
FGlitchless::Track for the few moments that matter in this game (ask me which). Leave the privacy setting
as it is. Then play in the editor for a minute and check the session arrived with glitchless_list_games.
```
## Add the MCP server to your agent
**Claude Code**, from your project folder:
```bash
claude mcp add glitchless -e GLITCHLESS_URL=https://api.glitchless.gg -e GLITCHLESS_API_KEY=gl_sk_... -- npx -y glitchless-mcp
```
That saves it for you on this machine, not in the project's `.mcp.json`, so the secret key stays out of
git. Or install the Claude Code plugin, which adds the MCP server and the instrument, dashboard and triage
skills:
```text
/plugin marketplace add glitchless-gg/glitchless
/plugin install glitchless@glitchless
```
and start Claude Code from a terminal where you've run:
```bash
export GLITCHLESS_URL=https://api.glitchless.gg
export GLITCHLESS_API_KEY=gl_sk_...
```
**Codex, Cursor and other agents:** Get started → "Any coding agent" has a prompt to paste. It adds the
same server (command `npx`, arguments `-y glitchless-mcp`, the two variables above) in your agent's own MCP
settings.
**Next to Epic's Unreal MCP.** They do different jobs, and one agent can use both:
- Unreal MCP runs inside the editor and drives it: actors, materials, automation tests. Turn on the Unreal
MCP and All Toolsets plugins, start the server (Editor Preferences → Model Context Protocol → Auto Start
Server, or `ModelContextProtocol.StartServer`), then run `ModelContextProtocol.GenerateClientConfig
ClaudeCode` in the editor console. That writes `.mcp.json` in the project root, pointing at
`http://127.0.0.1:8000/mcp`. Start your agent from the project root. Epic's guide:
https://dev.epicgames.com/documentation/unreal-engine/unreal-mcp-in-unreal-editor
- Glitchless reads what happened when the game ran, on anyone's machine: hitches and their causes, errors,
crashes, bug reports. Its secret key belongs in your own MCP settings, not in that `.mcp.json`.
- Optional: the **Glitchless Toolset** plugin (`glitchless-unreal-toolset-0.1.0.zip` from the same [release](https://github.com/glitchless-gg/glitchless/releases/tag/glitchless-unreal/v0.1.0), unzipped to `/Plugins/GlitchlessToolset`)
adds two tools to Unreal MCP while you Play In Editor: `Status` (what Glitchless sees right now) and
`Measure` (frame stats over the next few seconds). Enable it next to Unreal MCP and All Toolsets.
## Declare it in your submission
The rules ask you to declare anything made before November 12, including "prompts, scripts, or assets",
and to list assets you didn't make in your submission. They don't set a format, so add this to your
submission's description (or wherever you list your tools and assets), and edit it to match what you did:
```text
Set up before the jam: Glitchless (glitchless.gg), a third-party observability plugin for Unreal
(MIT licence, github.com/glitchless-gg/glitchless). It records frame times, errors, crashes and bug
reports from our builds, and its MCP server (glitchless-mcp) lets our coding agent read them. Before
November 12 we only installed it, set it up and connected it to our agent; all game code, prompts and
assets were made during the jam.
```
## Bug reports from teammates and playtesters (F8)
- With **Bug Report Key** set to `F8`, pressing F8 in the game opens a small box over it. The game pauses
behind it, and the report sends a screenshot, the note, the level and the last few events. You can also
open it from your own menu with the **Show Bug Report Dialog** Blueprint node.
- Read reports in the dashboard's Reports page, or ask your agent: "List the new bug reports for
my-jam-game and fix the worst one" (it uses `glitchless_bug_reports`).
- Put "Press F8 to report a bug" on your itch page and in the game. The note is whatever the player types,
so don't ask for names or emails in it.
## A share link for the Glass Box dev log
Glass Box is the prize for the best dev log showing how you built your game. The dev log link is optional,
but you need one to be considered for Glass Box.
- Open a board (the Overview, or a board your agent made for your game), press **Share**, give the link a
name and an expiry ("Never" or "In 90 days" lasts through judging).
- Under **Shows**, choose **Fixed dates**, from Nov 12 to Nov 16, then copy the link. It shows jam week
whenever it's opened, so judges reading your dev log in January see the week you built the game, not the
last 30 days. (Dates are whole UTC days; Nov 12 to 16 covers the start at midnight EST and the close.)
- Anyone with the link sees that one board, read only and signed out. Session and player IDs, devices,
stack traces and live events are left out. Revoke it from the same Share box.
- The jam tier keeps your data 90 days, so jam week stays on the link through judging. After the jam tier
ends on Jan 31 a Free workspace keeps 14 days, and the link has nothing left to show for November.
- Good dev log material: a hitch your agent found and the `-glitchless-measure` numbers before and after
its fix, the bug reports your teammates sent and the changes that fixed them, the crash-free rate on day 1
against day 4.
## Quick perf checks: `-glitchless-measure` and bots
Any build measures itself and quits:
```bash
# Windows (a packaged game has no console, so read the file)
MyGame.exe -glitchless-warmup=2 -glitchless-measure=10 -glitchless-out=C:\Temp\measure.json
# Mac: run the binary inside the .app and read the one JSON line it prints
./MyGame.app/Contents/MacOS/MyGame -glitchless-warmup=2 -glitchless-measure=10
```
It waits for the first map plus the warmup, measures for 10 seconds and reports frame time percentiles,
fps, game, render, RHI and GPU time, hitches with their causes, memory and new errors. Ask your agent to
run it before and after a change and compare. In the editor, the Glitchless Toolset's `Measure` does the
same through Unreal MCP.
Measure runs count as bot sessions: they stay out of your player numbers and don't use your 50,000 (bots
get 5,000 a month). Glitchless's playtest bots play browser games; for an Unreal game the bot is your own.
If your game has an autoplay mode (an AI pawn that plays a level), have it start on `-autoplay`, a flag
Glitchless already treats as a bot's, so a measure run plays the same way before and after a change.
## Your first hour of the jam
- [ ] Make the jam project in UE 5.8 (a C++ project, or add one C++ class).
- [ ] Copy `Plugins/Glitchless` from your rehearsal project, and the `[/Script/Glitchless.GlitchlessSettings]`
block into `Config/DefaultGame.ini`. Check Service, Intake, Client Token, Send In Development Builds
and Bug Report Key in Project Settings.
- [ ] Turn on Unreal MCP if you use it, generate `.mcp.json`, and start your agent from the project root.
Ask it to call `glitchless_list_games`.
- [ ] Play In Editor for a minute. The session shows on the Overview.
- [ ] Package a Development build for the platform you'll submit, run it once, press F8 and send a test
report. It shows in Reports.
- [ ] Check every teammate is in the workspace and has Glitchless in their agent.
- [ ] Paste the declaration above into your submission draft, and start your dev log notes.
---
Source: https://github.com/glitchless-gg/glitchless/blob/main/docs/SPEC.md
# Glitchless technical spec (v0.1)
This is the contract between the pieces of Glitchless: the browser SDK, the intake/query API,
the dashboard, the MCP server and the demo game. If you change a shape here, change it everywhere.
## 1. Browser SDK — `glitchless`
```js
import { Glitchless } from 'glitchless';
import { instrumentThree } from 'glitchless/three';
const glitchless = Glitchless.init({
service: 'zoomies', // game id (required)
version: '1.0.0', // build version (required — powers version compare)
env: 'dev', // dev | staging | prod
intake: 'http://localhost:7777', // Glitchless API base URL. Omit → offline/overlay-only mode
clientToken: 'gl_pub_dev', // public, write-only token: safe to ship inside the game
overlay: true, // in-game HUD, toggle with the backtick (`) key. Default: on unless env is prod
privacy: 'anonymous', // default. 'install' adds a stored random player ID (retention, §4b); 'full' that, GPU/CPU detail and positions (packages/sdk/PRIVACY.md)
sampleRate: 1, // 0..1 fraction of sessions that send data
positionHz: 4, // player position sampling rate for heatmaps / replay (positions are on in 'full' mode)
windowMs: 5000, // frame stats are summarised per window
flushIntervalMs: 15000, // batches go out this often, gzipped, on a window boundary
datadog: null, // optional { applicationId, clientToken, site } → mirrors to Datadog RUM
bugReports: null, // { hotkey: 'F8', onOpen, onClose } (or true): an in-game form players send bug reports from (§2 bug_report)
});
instrumentThree(glitchless, { renderer, scene, camera, THREE }); // THREE is optional (enables LoadingManager hooks)
```
### Game-semantic API (call these from game code)
| Call | When |
| --- | --- |
| `glitchless.levelStart(levelId)` | a level/stage begins (also sets current level on every event) |
| `glitchless.levelComplete(levelId, { durationS, score })` | level beaten |
| `glitchless.levelFail(levelId, { cause })` | level failed / player gave up / restart |
| `glitchless.death({ x, y, z, cause })` | player died; `cause` is a short string (`fell`, `squirrel`, …) |
| `glitchless.track(name, attrs)` | any custom design event (`bone_collected`, `powerup`) |
| `glitchless.setPosition({ x, y, z })` | every frame; SDK samples at `positionHz` |
| `glitchless.setContext({ particles, entities, ... })` | numeric gauges attached to frame windows and hitches |
| `glitchless.captureMap(levelId, { axis: 'xy' \| 'xz', bounds, exclude })` | after a level is built: renders an orthographic thumbnail used as the heatmap background, with the renderer's own tone mapping and output colour space (three.js r155+), so it looks like the game. `bounds = {min:[a,b], max:[a,b]}` in world units for the two axes; `exclude` = objects to hide while capturing (player, sky dome, effects) |
| `glitchless.registerTool(name, description, inputSchema, handler)` | expose a game-specific tool to coding agents over WebMCP |
| `glitchless.error(err, attrs)` | manually report a handled error |
| `glitchless.flush()` | force send (SDK also flushes on `pagehide`) |
| `glitchless.reportBug({ note, screenshot })` | send a player's bug report from the game's own UI (`screenshot: false` sends none); resolves `{ sent, reason? }` |
| `glitchless.openBugReport()` | open the built-in bug report form (from a pause menu, say) |
| `glitchless.resetInstallId()` | forget the stored player ID (§4b), for a "reset my ID" setting: this page load keeps its ID, the next makes a new one |
Everything else (frames, hitches, errors, device/GPU, load timing, memory, session lifecycle) is automatic.
### Phaser adapter (`glitchless/phaser`)
```js
import { instrumentPhaser } from 'glitchless/phaser';
const game = new Phaser.Game(config);
instrumentPhaser(glitchless, { game, Phaser, levels: 'auto' }); // Phaser 3.60+ and Phaser 4
```
The same SDK, with Phaser's own loop as the frame clock. Differences on the wire:
| | |
| --- | --- |
| envelope | `"sdk": "glitchless-phaser/"`, so findings give Phaser advice |
| `session_start` | adds `engine: { name: 'phaser', version, renderer: 'webgl' \| 'canvas' \| 'headless' }` |
| `frame_window` | one frame per Phaser step (`prestep` to `prestep`), so an fps limit or a `setTimeout` loop is measured as drawn; adds `process_ms` (scene updates, `prestep` to `poststep`); `render_ms` is `prerender` to `postrender`; `draw_calls` and `triangles` are counted on the WebGL context (the Canvas renderer's `drawCount` otherwise); `textures` is the texture manager's key count; `programs` counts WebGL programs linked; `geometries` is null; `ctx` adds `game_objects`, `particles`, `physics_bodies`, `tweens`, `listeners`, `sounds` |
| `hitch` | `causes` adds `scene_change`, `object_spawn` (50+ objects added in one step), `texture_upload`, `shader_compile` (a program linked), `script`, `render`; adds `process_ms`, `render_ms`, `objects_added`, `textures_added`, `programs_linked` |
| `level_start` | sent when a scene that reads as a level starts (`levels: 'auto'`); a scene's shutdown sets the level to null. `levels` can be an array of keys, a function, or `false` |
| `error` | adds `kind: 'asset'` (a loader `loaderror`) and `kind: 'game_loop_stopped'`: the loop stopped stepping after an uncaught error, with that error's stack. The server counts that session as crashed |
### Babylon.js adapter (`glitchless/babylon`)
```js
import { instrumentBabylon } from 'glitchless/babylon';
instrumentBabylon(glitchless, { engine, scene, BABYLON, levels: 'auto', gauges: true, gpuTime: true }); // Babylon.js 5+, WebGL or WebGPU
```
The same SDK, with Babylon's own frame loop and counters. `BABYLON` (the namespace) is optional: `captureMap` needs it.
| | |
| --- | --- |
| envelope | `"sdk": "glitchless-babylon/"`, so findings give Babylon.js advice |
| `session_start` | adds `engine: { name: 'babylon', version, renderer: 'webgl' \| 'webgpu' \| 'headless' }` |
| `frame_window` | one frame per engine frame (`onBeginFrameObservable` to the next), so work Babylon does just after drawing (a shader compiled in a promise) belongs to the frame; `render_ms` is each scene's render targets to `onAfterRenderObservable`, `process_ms` the rest of the frame's CPU time (before-render observers, animations, physics, the game's render-loop code); `draw_calls` from the engine's draw-call counter, `triangles` from the scenes' active indices; `geometries` and `textures` are the scenes' counts and `programs` the compiled effects; `ctx` adds `meshes`, `active_meshes`, `materials`, `textures`, `particles`, `render_observers` |
| `hitch` | `causes` adds `shader_compile` (compiles in the frame or the second before it: with `KHR_parallel_shader_compile` the stall comes a few frames after the compile), `scene_change` (a scene created, disposed, first drawn, or a level start), `object_spawn` (50+ meshes added), `texture_upload`, `geometry_upload` (as Three.js, on WebGL), `script`, `render`; adds `process_ms`, `render_ms`, `gpu_ms`, `shaders_compiled`, `shader_compile_ms`, `meshes_added`, `materials_added`, `textures_added`, `textures_uploaded`, `buffers_uploaded` |
| `level_start` | when a scene whose `metadata.level` is set first renders, or when that name changes; disposing the scene sets the level to null. `levels` can be a function `(scene) => name` or `false`; a manual `levelStart` turns scene levels off |
| `error` | adds `kind: 'shader'` (an effect that failed to compile; `attrs.errors` holds the compiler's message), `kind: 'asset'` (a texture that failed to load), `kind: 'webgl_context_lost'` (also a lost WebGPU device) and `kind: 'game_loop_stopped'`: the render loop queued no frame after an uncaught error, with that error's stack. The server counts that session as crashed |
### PlayCanvas adapter (`glitchless/playcanvas`)
```js
import { instrumentPlayCanvas } from 'glitchless/playcanvas';
instrumentPlayCanvas(glitchless, { app, pc, levels: 'auto', gauges: true }); // the PlayCanvas engine 2.x, WebGL2 or WebGPU
```
`pc` (the namespace) is optional: `captureMap` and the engine version need it.
| | |
| --- | --- |
| envelope | `"sdk": "glitchless-playcanvas/"`, so findings give PlayCanvas advice |
| `session_start` | adds `engine: { name: 'playcanvas', version, renderer: 'webgl' \| 'webgpu' \| 'headless' }` |
| `frame_window` | one frame per app tick (`frameupdate` to the next); `process_ms` is `frameupdate` to `framerender` (scripts, animation, physics), `render_ms` is `prerender` to `postrender`; `draw_calls` is the device's per-frame count, `triangles` are counted at `device.draw`; `geometries` counts vertex buffers, `textures` the device's textures, `programs` its shaders; `ctx` adds `entities`, `mesh_instances`, `materials`, `textures`, `lights` |
| `hitch` | `causes` adds `shader_compile` (a shader created in the frame or the second before it), `scene_change` (a scene loaded, or a level start), `object_spawn` (50+ render, model, sprite, element or particle components added), `texture_upload`, `geometry_upload`, `script`, `render`; adds `process_ms`, `render_ms`, `shaders_compiled`, `shader_compile_ms`, `objects_added`, `textures_added`, `buffers_added`, `textures_uploaded`, `buffers_uploaded`, `buffer_kb` |
| `level_start` | when `app.scenes.changeScene()` or `loadSceneHierarchy()` loads a scene whose name reads as a level (as Phaser's scene keys do); `changeScene()` to any other scene sets the level to null. `levels` can be an array of names, a function `(name) => name` or `false`; a manual `levelStart` turns scene levels off |
| `error` | adds `kind: 'asset'` (the asset registry's `error`), `kind: 'webgl_context_lost'` (also a lost WebGPU device) and, for a script error the debug build caught (it turns the script off), `kind: 'js'` with `attrs: { script, method, script_disabled: true }`: the same kind and message as the production build's uncaught error (with source maps uploaded, the same issue) |
## 2. Wire format
`POST {intake}/v1/intake?token={clientToken}` with `Content-Type: text/plain` (avoids CORS preflight; `sendBeacon` compatible).
```json
{
"service": "zoomies", "env": "dev", "version": "1.0.0",
"session_id": "s_8f2c…", "player_id": "p_19ab…",
"sdk": "glitchless-browser/0.1.0",
"events": [ { "type": "frame_window", "ts": 1760540000000, "level": "level-2", "…": "…" } ]
}
```
Every event has `type`, `ts` (epoch ms) and `level` (current level or `null`). The server adds
`project_id` from the client token (§6); a payload can't set it.
| type | fields |
| --- | --- |
| `session_start` | `privacy` (`anonymous`, `install`, `full`), `device {browser, browser_version, os, mobile, gpu, gpu_vendor, gpu_tier, cores, memory_gb, screen:[w,h], dpr, webgl, steam_deck}`, `page {url, referrer}`. The browser sends `steam_deck: true` only when it can tell it runs on a Steam Deck (§4b) |
| `frame_window` | every window (5 s; sent in batches every 15 s): `frames, fps, ft {sketch}, ft_p50, ft_p95, ft_p99, ft_max, jank` (frames > 33.3ms), `hitches` (frames > 50ms), `render_ms` (avg CPU ms inside `renderer.render`), `draw_calls, draw_calls_max, triangles, geometries, textures, programs, heap_mb, ctx {…gauges}`, `budget_ms` (from the display's refresh rate, read off the fastest frames and checked again as the session goes). A window never spans two levels: `levelStart` closes it, with the positions sampled so far |
| `hitch` | single frame > 50ms: `duration_ms, draw_calls, triangles, ctx, recent [last 5 game events], loaf [{source, fn, ms}], causes` (`script`, `shader_compile`, `gc`, `asset_load`, `frozen`). With `instrumentThree`, what the frame sent to the GPU, counted on the WebGL context: `causes` adds `shader_compile` (a program linked), `texture_upload` (4+ textures uploaded or re-uploaded, or a megapixel), `geometry_upload` (32+ buffers or 4 MB), `render` (`renderer.render` took 40% of the frame); adds `render_ms`, `textures_uploaded`, `texture_mpx`, `buffers_uploaded`, `buffer_kb`, `programs_linked` |
| `load` | `ttff_ms` (navigation → first rendered frame), `assets {count, bytes, ms, slowest:[{url, ms, bytes}]}` |
| `error` | `kind` (`js` \| `promise` \| `webgl_context_lost` \| `manual`), `message, stack, source, line, col, fn, fingerprint`. See below |
| `level_start` | — |
| `level_complete` | `duration_s, score` |
| `level_fail` | `cause` |
| `death` | `x, y, z, cause` |
| `pos` | `pts: [[ts, x, y, z], …]` (batched samples) |
| `custom` | `name, attrs` |
| `map` | `axis, bounds, image` (JPEG data URL, ≤ 60 KB) |
| `session_end` | `reason` (`pagehide` \| `hidden_timeout`), `duration_s` |
| `bug_report` | `note` (typed by the player), `screenshot` (JPEG data URL, ≤ 200,000 characters; dropped when larger, keeping the note), `x, y, z`, `recent` (last game events), `frames` (the last windows' fps and p95), `device`. See below |
**`error`.** `fingerprint` groups occurrences into one issue: the kind, the message (numbers of 3+ digits and hex
masked) and the function of the top frame, the first frame that isn't in a `glitchless*` file. File names are left
out, so a dev server's `fx.js` and a bundle's `index-3f2a.js` group alike. `stack` is the browser's own stack
string (at most 4,000 characters; Chrome, Firefox and Safari write it differently). In a minified web build the
top frame's function is a minified name that changes from build to build; when the build's source maps are
uploaded (§3d) the server stores the original `fn`, `source`, `line` and `col` instead, with a fingerprint on the
original function, and keeps what the SDK sent as `minified: {fingerprint, fn, source, line, col}`. The SDK
doesn't change.
**`bug_report`.** A player's report from inside the game. Reports are off until the game turns them on: in the
browser, `bugReports` in init options adds a hotkey (F8) and an overlay button that open a short form (a
screenshot preview, the note, Send and Cancel), and `reportBug()` sends one from the game's own UI. While the form
is open the game gets no keys, clicks, mouse or touch input (key releases still pass); `onOpen`/`onClose` let it
pause. Every SDK sends the same event:
| field | |
| --- | --- |
| `ts`, `level` | when the player opened the form (or called `reportBug`), and the level then |
| `note` | what the player typed, up to 2,000 characters. Free text: see each SDK's PRIVACY.md |
| `screenshot` | the game's view as a JPEG data URL of at most 200,000 characters. SDKs shrink it until it fits (960, 800, 640, 480, then 320 px wide, lower quality); the server drops a larger one, or one that isn't `data:image/`, and keeps the rest |
| `x, y, z` | where the player was, only when positions are on (as `death`) |
| `recent` | the last 20 game events and hitches, oldest first: `{ts, type, level}` plus whichever of `name, cause, kind, message, reason, duration_s, score, duration_ms, causes, x, y, z, attrs` the event had |
| `frames` | the last frame windows (up to 6, the one in progress last), oldest first: `{ts, level, fps, p95}` |
| `device` | the `session_start` device at the session's privacy level. Optional: without it the server shows the session's |
A report goes on a request of its own as soon as it is sent, from every session (`sampleRate` doesn't apply);
a failed send goes out with the next batch. Its id is a hash of project, service, session, `ts` and note, so a
batch sent twice stores it once.
Reports never join the events, rollups or summaries: each store keeps a small record (everything but the
screenshot) to list, and the screenshot apart, as it keeps level maps. FileStore: `reports.jsonl` and
`reports/.jpg`, kept 14 days like events; another store (§7) keeps them as long as it keeps events. They are read
through the `reports` and `report` queries (§3), the Bug Reports view and `reports` panel (§3b), each session's
timeline (`{type: 'bug_report', ts, level, id, note, has_screenshot}`), a finding once a level has three in the
range (`reports-`, quoting the notes), and the `glitchless_bug_reports` MCP tool. `DELETE
/v1/report?service=&id=` (the `delete` scope) removes one report and its screenshot. The live tail shows reports
without their screenshot, and exporters (Datadog, OTLP) don't send them.
`ft.sketch` is a DDSketch-style mergeable histogram of frame times (relative accuracy 2%):
`{ "a": 0.02, "b": { "": count }, "n": totalCount, "z": zeroCount }` where
`index = ceil(log(ms) / log(gamma))`, `gamma = (1 + a) / (1 - a)`. The server merges sketches across
sessions to compute exact-within-2% global percentiles (never average percentiles).
## 3. Query API
`GET {api}/v1/query/{name}?service=zoomies&from={ms}&to={ms}[&version=][&env=][&level=]`
Header `x-glitchless-api-key: …` when the server has a read key configured. Every query reads only the project
its key belongs to (§6); `service` picks a game inside that project.
| name | returns |
| --- | --- |
| `services` | `{services:[{service, versions[], envs[], last_seen}], max_range_days}`: the longest range this store answers for the reader (the plan's retention on Glitchless Cloud; `null`: no limit) |
| `overview` | `{kpis, series, top_errors, funnel, findings}` |
| `performance` | `{distribution, by_level, by_gpu_tier, by_browser, by_version, hitches, memory, load}` |
| `errors` | `{issues:[{fingerprint, message, kind, location, fn, source, line, count, sessions, players, first_seen, last_seen, first_version, versions, levels, stack, sample_session, breadcrumbs}]}`; for a web build with source maps (§3d), original frames with `frames` and `minified` |
| `players` | `{kpis, funnel, session_length, death_causes, rage_quits, retention}`. `retention: {status, id_sessions_pct, new_players, d1, d7, d30, cohorts:[{day, players, d1, d7, d30, returned:{d1, d7, d30}}], first_seen}` (§4): cohorts oldest first, `dN` a percentage (null until day N has ended in the range), `returned` the counts; `d1`… over all cohorts that have one. `status` is `ok`, `anonymous` (no session in the range sent a player ID: nothing to count, not zero) or `empty`; `first_seen` is `index` (the store's first-seen index) or `range` (a store without one: new on the first day in the range) |
| `players_now` | `{players_now, window_s}`: sessions of the game that sent anything in the last two minutes (§4), whatever the range |
| `heatmap` | `{level, map, deaths:[{x,y,z,cause,version}], density:{…}, stats}` (requires `level`); `stats` is the level's funnel row plus `no_win`, true for a game nobody wins (§4), which has runs rather than a completion rate |
| `sessions` | `{sessions:[…]}` ; `session?id=` → `{session, events}` (its timeline includes its bug reports) |
| `reports` | Players' bug reports, newest first (at most 500): `{reports:[{id, ts, session_id, level, version, env, note, position: {x, y, z} \| null, device (one line), has_screenshot}]}`. Filters: `env`, `version`, `type`, `sessions` |
| `report` | One bug report: `id=` → `{report:{…as listed, sdk, recent, frames, screenshot}}` (`screenshot=0` leaves the picture out), from any time the store still keeps; `{error}` if there is none |
| `versions` | `{versions:[{version, sessions, crash_free_pct, frame_p95, level_completion_pct, rage_quit_pct, errors_per_session, …, board:[…], events:{name: per_session}}], has:{frames, levels, deaths, fails, geometry}, board:[{title, unit, metric, event, per, sum?, currency?}]}`. `board` holds each version's value of the stats on the game's boards (each once, in the order of the top-level `board`, §3b); `unit` is `per session` for counts and sums compared per session, `per player` for a sum per player, `%` or a metric's unit, and a sum that is money has its `currency`; `has` says which built-in measures the game reports |
| `findings` | Ranked findings with evidence — what the MCP server hands to coding agents |
| `events` | The game's own metrics (`custom` events): `{ts, sessions, events:[{name, count, sessions, players, per_session, series, attrs:[{key, values:[{value, count, sessions}], avg}], sums:[{attr, currency, sum, events}]}]}`. `sums` adds up each numeric attribute, per currency (null for events sent without one, §3b), the most sent first. `name=` keeps one event, `sdk=1` adds the SDKs' own; `funnel=open,paywall,buy=done` adds `funnel:[{step, sessions, pct_of_first, pct_of_previous, median_s_from_previous}]` |
| `dashboard` | The game's pages, in order: `{dashboard:{source, updated_at, updated_by, spec:{currency?, pages}, pages:[{id, title, icon, view, options} or {id, title, icon, panels: count}]}}`, `source` being `saved` or `auto` (§3b) |
| `board` | One board page with each panel's data: `page=` its id (default: the first board) → `{board:{page, title, source, updated_at, updated_by, spec:{panels}, panels:[…]}}`; a built-in view's id gives `{error, view}` |
### 3b. Dashboards and boards
Every game gets a dashboard that fits it (`server/src/board.js`): its list of pages, in the order the sidebar shows
them, the first opening first. A page is a board (`panels`, below) or one of the built-in views (`view`: events,
performance, errors, players, heatmaps, sessions, versions, live, reports), which a game keeps, renames, reorders or drops:
```json
{ "currency": "USD", "pages": [
{ "id": "overview", "title": "Overview", "icon": "overview", "panels": [ … ] },
{ "id": "money", "title": "Paywall", "icon": "bolt", "panels": [ … ] },
{ "id": "purchases-live", "title": "Purchases, live", "view": "live", "options": { "events": ["paywall", "buy"] } },
{ "id": "errors", "title": "Errors", "view": "errors" }
] }
```
`id` (default: from the view or the title) is the page's address, `#/`; `icon` is one of board, overview,
events, perf, errors, players, heatmap, sessions, versions, live, flag, bolt, play, clock, bug. At most 20 pages and 24
panels a board. A built-in view a dashboard leaves out still opens from links (`#/errors/`,
`#/sessions/`, `#/reports/`). The live view takes `options.events`: names or types to show. The reports view
(Bug Reports) lists what players sent, newest first, with each screenshot, its session's replay (opened at the
report's moment) and the events and frame rate before it.
A dashboard is saved per game: `GET /v1/dashboard?service=` needs the read scope; `PUT /v1/dashboard?service=` with
`{currency?, pages}` and `DELETE` (back to the automatic dashboard) need the `dashboard` scope. `currency` (an ISO
4217 code such as `USD`) is the game's one currency for money (below). A game without a saved one gets
the automatic dashboard, made from the data it sent in the last week: an Overview board (the stats every game has,
findings, its own events, then levels, frame time, errors and deaths only if it reports them), then Events if it
sends custom events, Performance if it sends frame times, Errors, Bug Reports if players sent bug reports, Players,
Heatmaps if it sends positions, Sessions, Versions and Live Tail. Versions are compared on the stat panels of every board.
| panel `type` | fields | shows |
| --- | --- | --- |
| `stat` | `metric` (sessions, players, avg_session_s, crash_free_pct, error_free_pct, frame_p95, avg_fps, hitches_per_min, errors, deaths, rage_quit_pct, players_now) or `event`, optional `per`, optional `sum` | one number; `event` per another event is a percentage; per `$players` or `$sessions`, the share of them that sent the event (conversion). With `sum`, the attribute added up (revenue), per `$players` or `$sessions` divided by them (revenue per player). `players_now` is the sessions playing now, whatever the range, and is left out of version comparisons |
| `series` | `event`, optional `by`, `limit`, `sum` | the event's count (or its `sum`) over time, split by an attribute's top values; `by` can also be `$version` or `$level` |
| `breakdown` | `event`, `by`, `limit`, `sum` | the attribute's values with counts and sessions, or with each value's `sum`, largest first (revenue by item) |
| `funnel` | `steps` | sessions through the steps, in order |
| `levels` | `label` | starts, completion, fails, deaths and times per level; columns only for what the game reports |
| `frame_time`, `errors`, `deaths`, `events` | `limit` (errors) | frame-time p50/p95, top errors, deaths by level and cause, every custom event |
| `findings` | `limit` | the ranked findings |
| `sessions` | `sent`, `limit` | recent sessions, or only those that sent an event, each opening its replay |
| `reports` | `limit` | the latest bug reports players sent, each opening its report |
| `retention` | `limit` (days, 1 to 90; default 14) | D1, D7 and D30 by first-seen day (§4), the latest days; a game that sends no player IDs gets `anonymous: true` and a line on turning install mode on |
| `versions` | none | the board's own stat panels, version by version |
| `live` | `events` | events as they arrive, optionally only these names or types |
Every panel takes `title`, `note` and `size` (`s`, `m` or `l`). An event is written like a funnel step: `name`,
`name=value` (any attribute) or `name:key=value`. A panel with no data in the window says why instead of showing
zeros.
Custom events are counted in the per-minute cells (by name, and by name, attribute and value) and in the session
states (the first 16 occurrences of each name with their attributes, for ordered funnels), so every event store
answers the same. Names, attributes and values are the game's own, so each is capped: 200 names and 400 values a
minute, 64 names a session and 40 values per attribute. Past a cap the rest is counted as `(other)`.
**Sums, purchases and money** (`server/src/sums.js`). The per-minute cells also add up every numeric attribute of
custom events, in fixed point (thousandths): per name, attribute and currency (`[sum, events]`), and the same per
string or boolean attribute value of the event (revenue by item, by store). Hour summaries and daily rollups merge
the cells, so a sum is exact over any range and every store gives the same one. At most 400 sums of each kind a cell;
past that the rest goes under `(other)` and a daily answer names `event_sums` in `meta.approximate`. Cells written
before sums existed carry none, so sums start from then.
Purchases follow one convention in every SDK: `track('purchase', { amount: 4.99, currency: 'USD', item: 'gems_500' })`.
An event's `currency` (upper-cased) keeps each currency's sums apart, and a sum is money when its events carry one.
A game has one currency: the dashboard's `currency`, else the one most of its events were sent in. Events without a
currency count in it; amounts in another currency are never added in: a money panel answers `currency` and lists
them in `mismatch: [{currency, sum, events}]`. A sum of events that never carry a currency (coins, seconds) is a
plain number. The dashboard shows money with the currency's symbol and two decimals.
```json
[ { "type": "stat", "event": "purchase", "sum": "amount", "title": "Revenue" },
{ "type": "stat", "event": "purchase", "sum": "amount", "per": "$players", "title": "Revenue per player" },
{ "type": "stat", "event": "purchase", "per": "$players", "title": "Players who bought" },
{ "type": "breakdown", "event": "purchase", "sum": "amount", "by": "item", "title": "Revenue by item" },
{ "type": "series", "event": "purchase", "sum": "amount", "by": "$version" } ]
```
A stat with `sum` answers `{value, currency, mismatch?, events}`, or with `per` `{value, currency, total, base}`; a
conversion `{value, unit: '%', count, base}`. A sum split `by` an attribute takes the event by name only (the cells
add up one attribute value at a time); by `$version` or `$level` it can be filtered (`purchase:store=ios`). Over
more than 31 days, players who sent an event are counted per name (a conversion per `$players` of `purchase=gems`
says so instead of answering); `$sessions` always answers. In a version comparison a sum is per session unless its
`per` says per player.
**CSV.** Every board panel with data, and the tables of the Errors, Sessions, Events, Players and Versions views,
download what they show as `---.csv` (UTC minutes, `2026-10-23T1530Z`), built in the page from
the data it already has: a header row with the columns on screen, times in ISO 8601, durations in seconds, money
in its currency's units. A shared board exports only what it shows. (`?format=csv` on tabular queries may come
later.)
### 3c. Share links
A read-only link to one board page of one game, for a dev log, a Discord server or a publisher: anyone who has
it sees that board without signing in, and nothing else. Glitchless Cloud makes the links; the core enforces what
they read for any reader its auth hook marks with `share` (§6), so a self-hosted control plane can offer them too.
**Making one.** On Glitchless Cloud, members who may manage the game (Owner, Admin, Developer) press Share on a
board, or call the control API:
| route | does |
| --- | --- |
| `POST /api/projects/{p}/shares` | `{service, page, pageTitle?, name?, days?, from?, to?, sums?}` → `{share, token, url}`. `days` is 1, 7, 30, 90 or 365; without it the link lasts until it's revoked. `from` and `to` pin the window it shows (below). `sums: true` shows the board's sums such as revenue, which a link leaves out otherwise. At most 25 live links a game |
| `GET /api/projects/{p}/shares` | the game's links: `{id, name, service, page, pageTitle, hint, status, from, to, sums, createdAt, createdBy, expiresAt, revokedAt}`, `status` being Active, Expired or Revoked, `from` and `to` (ISO times) the pinned window or null, and `sums` whether it shows sums |
| `DELETE /api/projects/{p}/shares/{id}` | revokes it: at once on the server that revoked it, within 15 s on the others |
The link is `https://app.glitchless.gg/#/share/gl_share_…` (32 base64url characters after the prefix). Like a
secret key it is shown once: the control table keeps its SHA-256 and the last four characters.
**Rolling or pinned.** A link without `from` and `to` shows the last days, from whenever it's opened: the viewer
picks the past hour up to the past 30 days. A pinned link always shows its own window, such as a jam's week for a
dev log that judges read two months later. `from` and `to` are times in ms or ISO strings; a date without a time
is that whole UTC day, so `{from: "2026-11-12", to: "2026-11-16"}` is five days. Both or neither; the window is at
most 30 days and falls between 400 days ago and a year from now. A pinned window still stops at what the reader's
plan keeps, counted back from now, so once those days have aged out the board shows nothing for them.
**Reading.** The token goes in `x-glitchless-share` (or `?share=`). The reader hook answers
`{projectId, scopes: ['read'], share: {service, page, from?, to?, sums?}, keyId, rps}` (`from` and `to` in ms for a
pinned link; `sums: true` for one made with sums). A link outranks a session cookie, so a member
who opens one sees what everyone sees. A share reader may only:
- `GET /v1/query/board`, for its page, and
- `GET /v1/query/services`, which lists its service only, with `max_range_days` at most 30 and `window`: a
pinned link's `{from, to}`, or null.
Everything else is 403: other pages, services and queries, `/v1/live`, `/v1/dashboard` and `/v1/data`. The
request's parameters are replaced by `service`, `page`, `from` and `to` (a pinned link's own, whatever the request
asked), with `from` no earlier than 30 days before `to`, so env, version, session-type and session-id filters never
reach the query. Share readers are rate limited per link (10 requests a second per server process with bursts of
20 by default; `GLITCHLESS_SHARE_RPS` on Glitchless Cloud), and their answers are cached apart from members' (and
pinned links' apart from rolling ones', and links with sums apart from those without).
**What a shared board leaves out** (`evaluateBoard(…, { shared: true })` in `server/src/board.js`):
- sessions panels: session and player ids, and devices (browser, OS, GPU, GPU tier)
- errors panels: anything but the message, kind, counts, last seen and levels (no stacks)
- findings: their evidence; an error finding's summary becomes its counts, with no breadcrumbs, code location
or suggested fix, and a bug-report finding's becomes its counts, without the players' notes
- live panels, and any panel type not listed as safe. Stat, series, breakdown, funnel, levels, frame_time, deaths,
events, versions and retention panels show counts and the game's own event names (retention: new players and
returns per day, never an id), and pass as they are; a new panel type stays off shared boards until it is added
there.
- sums (revenue, revenue per player, revenue by item), unless the link was made with `sums: true`: a panel with
`sum` says it isn't shown, and a versions panel marks its sum columns `hidden` and leaves their values out.
Conversion (a share of players or sessions) is shown: it is a rate, not an amount.
**The page.** `#/share/` opens before the sign-in gate and renders the board read only, with no sidebar,
editor or links into other views. Its filters are fixed, its range picker goes from an hour to 30 days (a pinned
link shows its dates instead, as UTC days: "Nov 12 – 16, 2026"), and it links to glitchless.gg ("Made with
Glitchless"). A revoked or expired link says so. The token sits in the URL
fragment, which browsers never send to a server or in a Referer. Pages can't be framed (`frame-ancestors 'none'`):
CloudFront never sees the fragment, so allowing embeds for shared boards alone would need a path of their own.
### 3d. Source maps
A minified web build's errors read `e (index-3f2a.js:1:48213)`. With the build's source maps uploaded, the server
shows them as `fireShotgun (weapons.js:141)`, with the original stack and line of code, and groups an error the
same way in every build. The SDK doesn't change: the server reads the stack string it already gets
(`server/src/sourcemaps.js`).
**Uploading.** One map per request. The `glitchless-sourcemaps` package (`packages/sourcemaps`: a CLI and a Vite
plugin) uploads a whole build directory.
| route | scope | does |
| --- | --- | --- |
| `POST /v1/sourcemaps?service=&version=&file=` | `sourcemaps` | the body is the `.map` file, as JSON or gzipped (recognised by its magic bytes). `file` is the minified script's path in the build, e.g. `assets/index-3f2a.js` (default: the map's own `file`); `version` is the one the game passes to `init`. → 201 `{service, version, file, bytes, sources, sources_with_content, pruned?}` |
| `GET /v1/sourcemaps?service=[&version=]` | `read` | `{service, sourcemaps:[{version, file, size, uploaded_at}]}` |
| `DELETE /v1/sourcemaps?service=&version=` | `sourcemaps` | deletes that version's maps; 404 when it had none |
An upload must be a version 3 map (`sources` a list of paths, `mappings` valid base64 VLQ, or an index map with
`sections` placed by offset) of at most 10 MB on the wire (a host passes `createApp` a smaller `maxUploadBytes`: the hosted service takes 4 MB,
under its web function's 6 MB) and 40 MB unpacked, with `file` a relative path that
doesn't climb out (`..`); otherwise 400 or 413. A version holds at most 200 maps, and a game keeps the 50 versions
uploaded most recently: an upload past that deletes the oldest (`pruned`).
**Storage.** Per project, service, version and file, apart from the events. FileStore:
`sourcemaps////.map`. A store that expires data keeps a map 400 days after its
upload (`RETENTION_DAYS`), whatever it keeps events for: a version stays in players' hands far longer. `DELETE /v1/data` deletes a game's maps with its data.
**Matching a frame.** A stack line (Chrome `at fn (url:line:col)`, Firefox and Safari `fn@url:line:col`) belongs
to the longest uploaded `file` of the event's version that its URL path ends with, so the game can be served from
any host or folder. Its line and column are looked up in the map's `mappings`, giving the original source (made
relative: `../../src/weapons.js` is `src/weapons.js`), line and column. The function's name is read from
`sourcesContent`: the function around that position, named as V8 names it (`fireShotgun`, `Player.update`,
`new Player`, `` for a callback; TypeScript return types and generics are understood). A source without
`sourcesContent` takes the `names` entry at the caller's call site, then the minified name.
**At ingest.** For a browser error (not Godot, Unity or Unreal) whose version has maps, the server maps the stack,
takes the first frame that isn't Glitchless's own (as the SDK picks its top frame), and stores that frame's
original `fn`, `source` (the file name), `line` and `col`, with a fingerprint worked out as the SDK's on the
original function (`file:line` when no name could be read). Minified variables that engines quote in a message
are masked for it (`t.getPos is not a function` groups as `?.getPos is not a function`; property names stay). So
one bug in 1.2.0 and 1.2.1 is one issue. What the SDK sent stays in `minified` (§2). Each server process keeps a
game's list of maps for a minute and parsed maps up to 96 MB, so a game without maps costs one cached lookup and a
map is read once. An error that arrives before its version's maps keeps the SDK's fingerprint; it is still mapped
when shown.
**At query time.** `errors`, `findings`, `overview`, `board` and `playtest` map each issue's stack (its first
occurrence's, with that version's maps): `location`, `fn`, `source`, `line` and `stack` become the original ones,
and the issue gains
- `frames: [{fn, file, line, col, mapped, code?, minified?}]`, one per stack frame: `code` is the original line of
source (trimmed, at most 200 characters) for the first 5 mapped frames, `minified` where the frame was in the
build;
- `minified: {location, stack}`, what the browser reported.
A stability finding's evidence and advice name the original location, and a board's errors panel shows it. A
`session` timeline's errors are mapped the same way (with `minified.stack`). `glitchless_get_error` returns all of
it. Share links (§3c) show none of it: a shared errors panel keeps the message and counts only and findings lose
their evidence, so no file, function or line of source reaches a shared board.
### 3e. Alerts
A game's alerts tell its developers when it gets worse, in Discord, Slack or email, with a line to paste into their
coding agent. The rules, the check and the messages are the core's
([`server/src/alerts.js`](../server/src/alerts.js)). Glitchless Cloud keeps settings per game and checks every
15 minutes; a self-hosted server posts to one webhook (`GLITCHLESS_ALERT_WEBHOOK`, README "Alerts").
**A check** reads each service that sent data in the last day, over the last day: the range `glitchless_triage`
reads by default, so the finding an alert names is in the agent's triage. It reads summaries only (no drill-downs).
| rule | fires when | holds while | key |
| --- | --- | --- | --- |
| `new_error` | an error first occurred after alerts went on and within the last day, and has reached `newErrorSessions` sessions (default 3; 1–1,000) | it occurs in the last day | `error-` |
| `crash_free` | of the two newest versions with 5+ sessions (as the version finding picks them), each has 20+ sessions in the day, and the newer one's crash-free sessions are 2+ points lower with z ≥ 2 (two-proportion test) | the drop is 1+ point | `crash-free--` |
| `frame_p95` | the same two versions, 20+ sessions each, and the newer one's p95 frame time is 20% and 2 ms worse | it is 10% worse | `frame-p95--` |
| `finding` | a finding (§4) is high or critical. Version findings belong to the two rules above while either is on | it is medium or above | the finding's id |
An error without a fingerprint is skipped. The finding an alert names is `error-`,
`version--` (the version finding also names a crash-free drop of 2 points or more) or the finding's own.
`env` narrows a check to one env's sessions (default: all).
**Firing once.** Rules give one signal per key: a new error that is also a high error finding is one alert, said as
the new error. A key fires when it starts to hold: it is claimed with a conditional write, so checks running at
once send it once. It stays claimed while it holds at the lower bar, and clears after it has not held for 24 hours;
then it can fire again. The first check after alerts are turned on (or on again) is the **baseline**: what already
holds is claimed without a message. New errors are the exception: whether they are new comes from their first
occurrence, which a 7-day read of errors on a service's first check provides. Fingerprints are remembered (first
and last seen) for 30 days, at most 2,000 a service. A check sends at most 5 alerts a service; the rest wait for
the next.
**The message**: a title (one line: what changed), facts (the numbers), a link to the dashboard page, the finding id
and the line to paste: `Use glitchless_triage on , then look at finding .` (a new error adds
`(glitchless_get_error with fingerprint has its stack)`).
- Discord: `content` is the title in bold; one embed has the facts, the finding, the paste line in a code block,
the page as `url`, and a colour by severity. `allowed_mentions: {parse: []}`.
- Slack: Block Kit sections (title, facts, paste line, a context line with the link and the finding), no unfurls.
- Email: plain text and simple HTML, from `Glitchless alerts `, saying why the reader gets it
and linking to Settings → Alerts.
Text from a game (error messages, level and version names) is cut to one line without control characters, then
escaped for each channel (Discord markdown and `@`, Slack's `&<>`, HTML), so it can't ping, link or format.
**Glitchless Cloud.** Settings → Alerts, per game: a webhook URL, email to every active member of the workspace,
which rules, the new-error sessions and an env. Changing them, and the test button, needs the exporters' permission
(Owner, Admin). Discord (`discord.com/api/webhooks/…`) and Slack (`hooks.slack.com/services/…`) URLs are
recognised; any other public https URL takes the format chosen (Slack's suits Mattermost, Discord's Guilded). The
URL is sealed like an exporter's secrets (KMS, encryption context `{project}`) and never returned: the dashboard
gets the host and its last four characters. Sends go through the exporters' guarded fetch: https, no redirects,
and no private, loopback, link-local or metadata address after DNS. A send's result is `HTTP ` or why it
could not connect, never the URL. Links open the game: `https://app.glitchless.gg/?ws=&game=#/`.
| route | does |
| --- | --- |
| `GET /api/projects/{p}/alerts` | `{alerts: {on, onSince, webhook: {format, host, hint} \| null, email, emailRecipients, rules, newErrorSessions, env}, log, defaults, mail, permissions}`; `log` is the last 20 sends, tests included, each with what every channel answered |
| `PATCH /api/projects/{p}/alerts` | any of `{webhookUrl, webhookFormat, email, rules: {rule: bool}, newErrorSessions, env}`; `webhookUrl: null` removes the webhook. Alerts are on when there is a channel and a rule |
| `POST /api/projects/{p}/alerts/test` | sends a test alert to every channel → `{sent: [{channel, ok, to?, error?}]}`; 10 an hour |
| `POST /api/dev/alerts/check` | the dev server only: runs the check now |
Glitchless Cloud checks every game with alerts on every 15 minutes, one run at a time.
## 4. Derived concepts
- **Crash-free session**: a session that didn't crash. A session crashed when the run broke: the WebGL context
was lost for good, the game stopped (`game_loop_stopped`, or an Unreal `crash`, sent on the next launch), the
session ended inside a frozen frame (its last `frame_window` has `frozen`), or it ended ≤ 10s after an unhandled
error (`js`, `promise`, `script`, `exception`) with nothing played after it (a level event, a death, or one of the
game's own events; the SDKs' own events don't count). A frozen frame the game recovers from is a hitch, and an
error the player plays through is an error, not a crash.
- **Rage quit**: session ends ≤ 20s after a `death`/`level_fail` with no later `level_start` or `level_complete`.
It counts at the death behind it (a `level_fail` right after a death is the same moment). A game nobody wins
(5+ sessions in the range, levels started and failed, none completed: an endless or flappy game) has no rage
quits: every run ends in a death, and leaving after one is the end of a run.
- **Climbing gauge**: a `setContext` gauge that, over a session's frame windows (3 or more), ends within 10% of
its peak, at least 4× and 50 above where it started, and was last at its low point in the first half. Enemies
come and go between waves; a leak only grows. A gauge that climbs in 60% of the sessions that report it (3 or
more) is a memory finding, unless it is a progress counter by name (score, time, kills, wave and the like).
- **Hitch**: a single frame > 50ms. **Jank**: a frame > 33.3ms (misses two 60Hz vsyncs).
- **GPU tier**: `high` (discrete NVIDIA/AMD, Apple M-Pro/Max), `mid` (Apple M base, recent Intel Iris/Arc), `low` (old Intel HD/UHD, mobile Mali/Adreno/PowerVR, a Steam Deck), `software` (SwiftShader/llvmpipe).
- **Retention** (D1, D7, D30): of the players first seen on a UTC day (a cohort), the share that played again on the
1st, 7th and 30th day after it. Only players with an ID count (install or full mode, §4b): an anonymous session is
filed under its own ID. A player's first-seen day is the day of their first event the store has (its first-seen
index, which lasts as long as the store keeps data: the plan's retention on Glitchless Cloud, so a player gone
longer than that is new again). They join that day's cohort
when a session of theirs in the selection (env, version) started on it, as sessions count on the day they start.
A cohort is a whole day of the range, and its Dn is null until day N has ended inside the range. Up to 31 days it
is exact from session states; past 31 days a store with daily rollups (§7) reads each day's sample of new players
against the days' samples of active players, exact while small and named in `meta.approximate` (`retention`) once
capped.
- **Players now**: sessions that sent anything in the last two minutes, counted as batches arrive. The open-source
server counts in memory, per process; Glitchless Cloud counts across every server instance (whole minutes: this
one and the two before) through the `auth.playersNow` hook.
- **Platform**: a Steam Deck (`steam_deck`), whatever OS the build is for, else the OS. Performance breaks frame times
down by it (`by_platform`).
## 4b. Privacy levels
This is the one definition every SDK follows (the browser, Godot, Unity and Unreal SDKs, and any SDK added later):
the same levels, the same ID and the same Steam Deck rule. `session_start` says which level (`privacy`), and each
SDK's PRIVACY.md lists exactly what it sends at each.
| | anonymous (default) | install | full |
| --- | --- | --- | --- |
| `player_id` | null: the server files the session under its session ID | the **player ID**: random `p_` + 12 characters `[a-z0-9]`, made on first launch and kept on the device | the same stored ID |
| kept in | nothing; an ID another build left is deleted | browser `localStorage['glitchless.pid']`; Godot `user://glitchless/player_id`; Unity `persistentDataPath/glitchless/player_id` (PlayerPrefs `glitchless.player_id` in WebGL builds); Unreal `Saved/Glitchless/player_id` | the same |
| session ID | random per launch, in memory only | random per launch, saved with unsent batches until they go | the same as install |
| device | coarse: OS, tiers, screen, renderer, `steam_deck` | as anonymous | adds model, OS version, GPU, cores, memory |
| positions | off (on with `positions`) | as anonymous | on |
| unsent batches on the device (engine SDKs; the browser keeps none) | without IDs | with their session and player IDs | the same as install |
Install mode adds one thing to anonymous mode, the stored player ID (and, since that ID is on the device anyway,
unsent batches keep their IDs), so retention (D1, D7, D30, §4) can be counted without knowing who the player is: the
ID is random, made by the SDK, and tied to no account, device or advertising ID. Install and full mode share it, so a
game that moves between them keeps its players; anonymous mode deletes it. The browser, Godot and Unity SDKs can
reset it (`resetInstallId()`, `reset_install_id()`, `Glitchless.ResetInstallId()`): that deletes the stored ID, the
session in progress keeps sending the ID it started with (so it is never split), and the next launch makes a new ID
that nothing links to the old one. Where nothing can be stored (a browser that blocks localStorage) the ID lasts one
launch.
**Steam Deck.** A Steam Deck is one when Steam says so (`SteamDeck=1`, set under Steam and Proton), the board is
Valve's Jupiter (LCD) or Galileo (OLED) (`/sys/class/dmi/id/board_vendor` and `board_name`), or the OS is SteamOS
(`ID=steamos` in `/etc/os-release`); a Windows build under Proton reads the same files through Wine's `Z:` drive. On
one, in every mode, the engine SDKs send `steam_deck: true` (and `false` elsewhere), `gpu_tier: "low"`, and
`os: "SteamOS"` when it runs SteamOS or Steam set `SteamDeck` (a Deck under another Linux keeps its OS name). The
browser SDK sends `steam_deck: true` when the browser shows the Deck's GPU (AMD Custom GPU 0405 or 0932,
"vangogh") or a user agent names it, with `gpu_tier: "low"`, and nothing when unsure. Only these fields are sent,
never what they were read from.
## 5. Godot SDK
`godot/addons/glitchless` sends the same envelope with `"sdk": "glitchless-godot/0.2.0"` and the same event types. See its [README](../godot/addons/glitchless/README.md) and [PRIVACY.md](../godot/addons/glitchless/PRIVACY.md). Differences:
| | |
| --- | --- |
| envelope | `player_id` is null in anonymous mode (the default), and the server files the session under `session_id`; install and full mode send the stored `p_…` player ID (`user://glitchless/player_id`, §4b) |
| `session_start.device` | `browser: "Godot"`, `browser_version` (engine), `os`, `mobile`, `device_tier`, `gpu_tier`, `renderer`, `driver`, `screen`, `dpr`, `refresh_hz`, `steam_deck` (§4b: `os` is `SteamOS` on a Deck running it); full mode adds `model`, `os_version`, `gpu`, `gpu_vendor`, `cores`, `memory_gb`. `page` and `nav_ms` are null |
| `frame_window` | adds `gpu_ms`, `process_ms`, `physics_ms`, `objects`, `nodes`, `orphan_nodes`, `resources`, `video_mb`, `texture_mb`, `screen`, `window`; `heap_mb` is static memory (debug builds only, else null); `geometries`, `textures`, `programs` are null; `ctx` also holds `nodes`, `orphan_nodes`, `objects`. A window never spans two levels |
| `hitch` | `causes` from `shader_compile`, `scene_change`, `node_spawn`, `script`, `physics`, `render`, `frozen`; adds `process_ms`, `physics_ms`, `render_ms`, `gpu_ms`, `pipeline_compiles`, `nodes_added`; `loaf` is empty |
| `error` | `kind`: `script` (GDScript runtime error; unhandled, like `js`), `engine`, `push_error`, `shader`, `manual`, `warning`; adds `recent` (last 6 game events) |
| `death` | `x`, `y`, `z` only when positions are on (full mode or `positions: true`); the server leaves unplaced deaths off heatmaps and hotspots |
| `session_end` | `reason`: `background`, `close`, `quit`, `measure` |
| `custom` | also `screen_change`, `app_resume` |
| `bug_report` | `bug_reports` in init options adds the F8 form: a `CanvasLayer` that pauses the tree while open, with `bug_report_opened` and `bug_report_closed` signals. `report_bug(note)` sends one from the game's own UI. `screenshot` is the root viewport's last frame with its longer side at most 960 px (the browser caps the width), none in headless runs |
## 5b. Unity SDK
`unity/Packages/gg.glitchless.sdk` (Unity 2022.3 and 6) sends the same envelope with `"sdk": "glitchless-unity/0.2.0"`. See its [README](../unity/Packages/gg.glitchless.sdk/README.md) and [PRIVACY.md](../unity/Packages/gg.glitchless.sdk/PRIVACY.md). Differences:
| | |
| --- | --- |
| envelope | `player_id` is null in anonymous mode (the default); `Privacy.Install` and `Privacy.Full` send the stored `p_…` player ID (§4b) |
| `session_start.device` | `browser: "Unity"`, `browser_version` (engine), `os`, `mobile`, `device_tier`, `gpu_tier`, `renderer` (`built-in`, `urp`, `hdrp`), `driver` (graphics API), `screen`, `dpr` (null), `refresh_hz`, `steam_deck` (§4b: `os` is `SteamOS` on a Deck running it); full mode adds `model`, `os_version`, `gpu`, `gpu_vendor`, `cores`, `memory_gb`. `page` and `nav_ms` are null |
| `frame_window` | frame times from `Time.unscaledDeltaTime`; adds `process_ms` (Update to LateUpdate), `physics_ms` (FixedUpdate and physics), `render_ms` (PostLateUpdate), `gpu_ms`, `setpass_calls`, `batches`, `gc_collections`, `gc_alloc_kb`, `materials`, `objects`, `game_objects`, `gc_reserved_mb`, `system_mb`, `screen`, `window`; `geometries` is the mesh count and `heap_mb` the GC heap; counts Unity only has in development builds are null in release builds. `ctx` also holds `game_objects`, `meshes`, `materials`. A window never spans two levels |
| `hitch` | `causes` from `gc`, `shader_compile`, `scene_change`, `object_spawn`, `script`, `physics`, `render`, `frozen`; adds `process_ms`, `physics_ms`, `render_ms`, `gpu_ms`, `gc_collections`, `gc_alloc_kb`, `shader_compiles`, `objects_added`; `loaf` is empty |
| `level_start` | also sent when a scene that reads as a level becomes active |
| `error` | `kind`: `exception` (uncaught; unhandled, like `js`), `log_error`, `assert`, `manual`, `warning`; adds `recent` (last 6 game events) |
| `death` | `x`, `y`, `z` only when positions are on |
| `session_end` | `reason`: `background` (phones), `quit`, `measure` |
| `custom` | also `screen_change`, `app_resume` |
| `bug_report` | `BugReports` in the options adds the F8 form: IMGUI (no EventSystem), with `Time.timeScale` 0 and the Input System's actions off while open, and `OnOpen`/`OnClose`. `Glitchless.ReportBug(note)` sends one from the game's own UI. `screenshot` is `ScreenCapture` at the end of the frame with its longer side at most 960 px, none with `-nographics` |
## 5c. Unreal SDK
`unreal/Plugins/Glitchless` (Unreal Engine 5.8; 5.7 is guarded but not built) sends the same envelope with `"sdk": "glitchless-unreal/0.1.0"` and the same event types. See its [README](../unreal/Plugins/Glitchless/README.md) and [PRIVACY.md](../unreal/Plugins/Glitchless/PRIVACY.md). Positions are in metres with y up (x = X/100, y = Z/100, z = Y/100). Differences:
| | |
| --- | --- |
| envelope | `player_id` is null in anonymous mode (the default); install and full mode send a stored random `p_…` ID |
| `session_start` | `privacy` (`anonymous`, `install`, `full`). `device`: `browser: "Unreal"`, `browser_version` (`5.8.4`), `os` (`Windows`, `macOS`, `Linux`, `SteamOS`, `iOS`, `Android`), `mobile`, `device_tier`, `gpu_tier` (`low` on a Steam Deck), `renderer` (`deferred`, `forward`, `mobile`), `driver` (`D3D12`, `Metal`, `Vulkan`), `screen`, `dpr` (null), `refresh_hz` (phones only), `steam_deck`; full mode adds `model`, `os_version`, `gpu`, `gpu_vendor`, `cores`, `memory_gb`. `page` and `nav_ms` are null |
| `frame_window` | frame times from a clock at the start of each frame. Adds `game_ms` (game thread, GGameThreadTime), `process_ms` (the same, under the name the playtest gate reads), `render_ms` (render thread), `rhi_ms` (null without an RHI thread), `gpu_ms`, `gc_collections`, `gc_ms`, `pso_compiles` (pipeline states created at draw time that took over 20 ms, UE 5.8), `shader_compiles` (the editor only), `packages_loaded`, `objects` (UObjects), `actors`, `heap_mb` (the process), `texture_mb`, `screen`, `window`. `draw_calls` and `triangles` only while the RHI counts them (usually null in a packaged build); `geometries`, `textures`, `programs` are null. `ctx` also holds `objects` and `actors`. A window never spans two levels |
| `hitch` | `duration_ms` is the frame's time on the clock (Unreal leaves waits out of its thread times, so a frame stalled on a pipeline state, the GPU or the disk would look short by them). `causes` from `pso_compile`, `shader_compile`, `gc`, `map_load`, `level_stream`, `asset_load`, `script` (game thread at 40% of the frame), `render` (render, RHI or GPU at 40%), `frozen` (over 700 ms). Adds `bound` (the slowest of `game`, `render`, `rhi`, `gpu`), `game_ms`, `process_ms`, `render_ms`, `rhi_ms`, `gpu_ms`, `gc_collections`, `gc_ms`, `pso_compiles`, `shader_compiles`, `packages_loaded`; `loaf` is empty |
| `level_start` | also sent when a map that reads as a level loads (not `Entry`, `MainMenu`, `Lobby`, `Loading`…); the game's first `LevelStart` turns that off |
| `error` | `kind`: `crash` (fatal; written to the outbox as the game dies and sent on the next launch, with `session_end` reason `crash`; in anonymous mode under a fresh session ID), `ensure` (an error, not fatal: the game carries on, and each ensure reports once a run), `script` (a Blueprint runtime error such as Accessed None; Unreal stops that Blueprint function, so it is unhandled, like `js`), `log_error` (`UE_LOG` Error; `fn` is the log category), `warning`, `manual`; adds `recent` (last 6 game events) and, for `script`, `attrs.blueprint` |
| `death`, `pos` | positions only when they are on (full mode, or Positions) |
| `session_end` | `reason`: `quit`, `background` (phones), `measure`, `crash` |
| `custom` | also `screen_change`, `app_resume`, `rage_input` |
| `bug_report` | a **Bug Report Key** (F8) in the plugin's settings, or `ShowBugReportDialog()`, opens a Slate box that takes the game's input and pauses it (Pause For Bug Report); `ReportBug(note)` sends one from the game's own UI. `screenshot` is the viewport read back, its longer side at most 960 px. A report is its own one-event batch in the outbox, sent after the session's events; neither the read-back frame nor the box's first draw is timed |
`-glitchless-measure=S` (with `-glitchless-warmup`, `-glitchless-out`) prints `{"glitchless_measure": {…}}` as Unity and Godot do, plus `session_id`; `-autoplay` and `-glitchless-synthetic` mark the session synthetic. The optional GlitchlessToolset plugin gives Unreal MCP (UE 5.8, Experimental) `Status` and `Measure` tools for Play In Editor.
On the server, findings for an `unreal` game name Unreal's fixes (PSO precaching and bundled caches, GC pooling and clustering, streaming budgets, Insights and `stat` commands, scalability groups). `crash` is a fatal kind that halts its session, so the session counts as crashed (§4), like `game_loop_stopped`.
## 5d. GameMaker SDK
`gamemaker/` (GameMaker 2024.2 and later, built for LTS 2026) is GML: the `Glitchless` script's `glitchless_*` functions, internal `__glitchless_*` scripts and two persistent objects that `glitchless_init` creates. It sends the same envelope with `"sdk": "glitchless-gamemaker/0.1.0"` and the same event types, gzipped (a gzip wrapper around `buffer_compress`'s zlib stream; plain JSON in a web build). See its [README](../gamemaker/README.md) and [PRIVACY.md](../gamemaker/PRIVACY.md). Positions are room coordinates. Differences:
| | |
| --- | --- |
| envelope | `player_id` is null in anonymous mode (the default); install and full mode send a stored random `p_…` ID |
| `session_start` | `privacy` (`anonymous`, `install`, `full`). `device`: `browser: "GameMaker"`, `browser_version` (the runtime, `2026.0.0.23`), `os`, `mobile`, `device_tier` (`unknown`: GameMaker can't read cores or memory), `gpu_tier` (from the GPU's name in `os_get_info`; `low` on a Steam Deck), `renderer` (`vm` or `yyc`), `driver`, `screen`, `dpr` (null), `refresh_hz`, `steam_deck` (as Unreal decides it: `SteamDeck=1`, a Valve board, or SteamOS, with `os` `SteamOS`); full mode adds `os_version`, `gpu`, `gpu_vendor`, and `model` on a Steam Deck (`cores`, `memory_gb` are null). `page` and `nav_ms` are null |
| `frame_window` | frame times from `delta_time`; `hz` and `budget_ms` from the game speed. Adds `process_ms` (the Step events: the frame's start to the first Pre-Draw), `render_ms` (the draw events: the first Pre-Draw to the last Draw GUI End), `gc_collections`, `gc_ms` (`gc_get_stats`), `texture_loads` (texture groups loaded or fetched), `heap_mb` (`debug_event("DumpMemory")`), `instances`, `surfaces`, `ds_lists`, `ds_maps`, `buffers` (`debug_event("ResourceCounts")`), `screen`, `window`. `draw_calls`, `triangles`, `gpu_ms`, `physics_ms`, `geometries`, `textures`, `programs` are null. `ctx` also holds `instances` and the resource counts. A window never spans two levels |
| `hitch` | `causes` from `texture_upload` (a texture page fetched on first draw), `scene_change` (a room change: the level's load), `gc` (a collection over a quarter of the frame), `object_spawn` (50+ instances added), `script` (Step events at 40% of the frame), `render` (draw events at 40%), `frozen`. Adds `process_ms`, `render_ms`, `gc_ms`, `gc_collected`, `instances_added`, `texture_loads`; `loaf` is empty |
| `level_start` | also sent when a room that reads as a level starts (not `rm_main_menu`, `rm_title`, `rm_boot`, `rm_loading`…, the words Unity and Unreal use); the game's first `glitchless_level_start` turns that off. A menu room leaves the level |
| `error` | `kind`: `crash` (an unhandled exception, through `exception_unhandled_handler`: GameMaker stops the game, so the crash and `session_end` reason `crash` are written to the outbox and sent on the next launch; in anonymous mode under a fresh session ID) and `manual` (`glitchless_error`, a message or a caught exception). `fn` is `obj_player.Step_0` for an object's event and the function's name for a script function; `stack` keeps GameMaker's frame names. Adds `recent` (last 6 game events) |
| `death`, `pos` | positions only when they are on (full mode, or `positions: true`) |
| `session_end` | `reason`: `quit` (`glitchless_quit`, or Game End: then it is sent on the next launch), `measure`, `crash` |
| `custom` | also `screen_change`, `app_resume`, `rage_input` |
| `bug_report` | `bug_reports` in the options adds the F8 form, drawn in Draw GUI End, which deactivates the game's instances while open (`pause: false` and `on_open`/`on_close` for a game's own pause); `glitchless_report_bug(note)` sends one from the game's own UI. `screenshot` is a **PNG** data URL (GameMaker writes no JPEG) of the application surface, its longer side at most 960 px |
`-glitchless-measure=S` (with `-glitchless-warmup`, `-glitchless-out`) prints `{"glitchless_measure": {…}}` as the other SDKs do, plus `session_id` and `gc_ms`, and writes it to `glitchless/measure.json` in the save area; `-autoplay` and `-glitchless-synthetic` mark the session synthetic. Igor's `Run` starts the game with macOS's `open`, which passes no command line or environment on, so the SDK also reads its flags from the `GLITCHLESS_ARGS` environment variable and, in a build run from the IDE or Igor, from `glitchless/args.txt` in the save area (the runner's, `com.yoyogames.macyoyorunner`).
On the server, findings for a `gamemaker` game name GameMaker's fixes (texture groups and prefetching, light room Create events, GC pacing, instance pooling and particle systems, surface reuse, freeing DS structures). `crash` is a fatal kind that halts its session, as for Unreal.
## 6. Projects, keys and storage
A **project** is the tenancy boundary. The server resolves every request to one project through two
hooks passed to `createApp({ auth })` ([`server/src/auth.js`](../server/src/auth.js)):
| hook | called for | returns | refuses with |
| --- | --- | --- | --- |
| `resolveIngest(token, req)` | `POST /v1/intake?token=` | `{ projectId, keyId?, rps? }` | `AuthError(403)` |
| `resolveReader(req)` | every other `/v1/` route but `/v1/health` | `{ projectId, scopes, keyId?, rps?, share? }` (`read`: queries and live tail; `delete`: `DELETE /v1/data` and `DELETE /v1/report`; `dashboard`: save or reset a dashboard; `sourcemaps`: upload source maps). `rps` rate-limits the reader per `keyId`; `share: { service, page }` limits it to one board (§3c) | `AuthError(401 \| 403)` |
Client tokens ship inside game builds, so they are write-only: a reader hook never accepts one. Intake
stamps `project_id` on every event and stores it under that project; queries, the live tail, the agent
activity feed and the query cache are all per project.
- **Open-source default** (`envAuth`): one project, `default`. `GLITCHLESS_CLIENT_TOKENS` (comma-separated,
default `gl_pub_dev`) may write; `GLITCHLESS_API_KEY`, if set, is needed for everything else (header
`x-glitchless-api-key` or `?api_key=`), and must differ from every client token. No API key means open reads.
- **Hosted**: a control plane implements the same hooks with hashed per-project keys and Auth0/Cognito sessions.
- **Rate limit**: a token bucket per ingest key, per server process, of `rps` requests/s with bursts of 2 s
worth; 429 with `Retry-After` past it. `GLITCHLESS_INGEST_RPS` sets the default (1000; 0 turns it off). The
payload caps still apply: 1 MB per request, 500 events, 4000-character strings.
**Storage.** Every store method takes the `projectId`: `put`, `query`, `rollups`, `getMaps`, `reports`,
`getReport`, `deleteReport`, `putSourcemap`, `getSourcemap`, `sourcemaps`, `deleteSourcemaps`, `services`, `clear`
([`server/src/stores/file.js`](../server/src/stores/file.js) lists them), and optionally `cohorts` (the players first
seen on each day of a range, for retention): the file store keeps that first-seen index in memory over everything it
holds; a store without one counts players as new on their first day in the range (`first_seen: 'range'`).
Data written before projects existed (JSONL lines without `project_id`) is read as the `default` project, so no
migration is needed.
**Rollups.** Intake also folds every event into per-minute cells per (project, service, env, version, level):
a merged frame-time sketch, event counts by type, frames, seconds, hitches, frozen frames, frame-budget
counts, and sums, counts and maxima of draw calls, render ms, triangles, geometries, heap and context gauges
([`server/src/rollups.js`](../server/src/rollups.js)). `overview`, `performance`, `versions` and
`findings` take their frame-time and count sections from the stored minutes that lie fully inside the range,
and fold raw events for the partial minutes at each end. Without stored rollups (filters by session type or
ids, or ranges before `since`) they fold every raw event the same way. Sums are fixed-point, so both paths
give identical numbers in any arrival order. On the file store, session-level numbers (crash-free, rage quits,
players), device breakdowns, hitch context and findings evidence are folded from the raw range, into mergeable
forms (session states, §7) that a store can also keep precomputed.
The file store rebuilds rollups from its event files at startup.
Every answer carries `meta`: the range answered (`from`, `to`, `bucket_ms`), the filters, and from a store that
keeps day rollups (§7) `resolution: 'day'` for ranges of more than 31 days, `approximate` (the parts estimated
from sketches or samples), `note` (a view answered over less of the range) and `retention_days` (the reader's
plan, when the host has plans).
## 7. Other stores
`createApp({ store })` takes any store with the methods in §6, so a server can keep its events somewhere other than
files (a database, object storage). To check one, run the server's tests on it:
`GLITCHLESS_TEST_STORES=./my-stores.js npm test -w glitchless-server`
([`server/test/helpers/stores.js`](../server/test/helpers/stores.js) says what that module exports).
A store may keep precomputed state instead of raw events: session states and facts merge exactly, so a store that
keeps them per minute or hour answers as the raw events of the range would. For long ranges it may keep **daily
rollups** ([`server/src/daily.js`](../server/src/daily.js)), and then answers ranges of more than 31 days in whole
days (`meta.resolution: 'day'`):
- **Exact**: everything from cells and facts, session counts, rates, sums and averages, the level funnel's
counts, custom events' sessions and values, the latest sessions and rage quits. A session counts on the day it
started, with all its events.
- **Exact while small, then estimated** (named in `meta.approximate`): distinct players (overall, per level, per
event, per error) and error sessions (HyperLogLog past 128–512 per day), returning players and retention past
31 days (a uniform sample of 512 players a day), medians of per-session values (3 then 2 significant digits past
512 values), ordered funnels (256 sessions a day, weighted), GPUs past the top 30 a day, difficulty hotspots past
100 deaths per level a day.
- **Over the last 31 days of the range** (`meta.note`): heatmaps and session-type filters, which read raw rows;
recent hitches.
Glitchless Cloud runs its own store of this kind, with retention set by each workspace's plan; it is not part of
this repo.