The crashes your players hit and you never see. Captured from the browser and from your game server, grouped one row per bug, and readable as a brief you can hand straight to a coding agent.
In the browser there is nothing to set up. Creating the client starts it:
<script src="https://indie.fun/js/indie.js"></script>
<script>
new Indie({ appId: 'your-app-id' });
</script>On your game server, pass your App Secret as usual. Errors go with it:
import { Indie } from 'indie-sdk/server';
const indie = new Indie({
appId: process.env.INDIE_APP_ID,
appSecret: process.env.INDIE_APP_SECRET,
errors: { release: process.env.GIT_SHA }, // optional, but tells you which build broke
});indie.captureException(err, { level: 4 }) from a catch block, or indie.captureMessage('save file was corrupt') when there is no exception to pass. Both take an optional object of context, which is stored with the error and shown alongside it.Your process keeps behaving the way it did
The server SDK watches for uncaught errors, it does not take them over. If your code already handlesuncaughtException, yours stays in charge. If it doesn't, the SDK reproduces Node's default (print the error, exit 1) once the report is away, or after two seconds, whichever comes first. Turn the whole thing off with errors: { captureUnhandled: false }.One row is one bug, however many times it has happened. Two occurrences group together when they are the same failure in the same place: the exception type, the shape of the message, and the top of the stack.
Tune any of it, or turn it off:
new Indie({
appId: 'your-app-id',
errors: {
release: 'v1.4.2', // which build this is
maxPerMinute: 20, // rolling ceiling
captureResources: false, // stop reporting failed asset loads
beforeSend(event) { // last look: edit it, or return null to drop it
if (event.message.includes('ResizeObserver')) return null;
return event;
},
},
});
new Indie({ appId: 'your-app-id', errors: false }); // off entirelyEvery error can be read as Markdown written for whoever has to fix it, what threw, where, how often, to how many players, the stack, and the context. The Copy for a coding agent button on your game's Errors tab copies exactly what this returns:
# every open error, most recently active first
curl -H "X-App-Id: $INDIE_APP_ID" -H "X-App-Secret: $INDIE_APP_SECRET" \
"https://indie.fun/api/sdk/errors?status=open&format=markdown"
# one error, with its stack and recent occurrences
curl -H "X-App-Id: $INDIE_APP_ID" -H "X-App-Secret: $INDIE_APP_SECRET" \
"https://indie.fun/api/sdk/errors?fingerprint=<id>&format=markdown"
# the same data as JSON, for a script
curl -H "X-App-Id: $INDIE_APP_ID" -H "X-App-Secret: $INDIE_APP_SECRET" \
"https://indie.fun/api/sdk/errors?status=open&runtime=server"Check it worked
1. Throw something on purpose from your game's console:
indie.captureMessage('hello from my game');
await indie.flushErrors();2. Ask whether it landed. This reports only on the session in front of you, so it needs no credentials:
await fetch('https://indie.fun/api/sdk/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
appId: indie.getAppId(),
sessionId: indie.getSessionId(),
}),
}).then(r => r.json());
// → { errors: { session: 1, lastAt: '…' }, … }3. Open your game's Errors tab. It's there, with its stack, its context, and a button that copies it for an agent.
If nothing arrives
errors: { session: 0 } means we never received it. In order of likelihood: the App ID is wrong or its keys were deleted; the report is still queued (reports batch for a few seconds, flushErrors() sends them now); the page declined analytics, so there is no session id to look up (getSessionId() returns null, and the error is still recorded, just not against a session); or the request never left the browser, check the console and your Content-Security-Policy.Reporting is deliberately incapable of breaking your game: every entry point swallows its own failures, so a broken reporter is invisible rather than fatal. That is also why the sandbox and the check above exist. You should not have to take our word for it that reports are arriving.