Remote config, analytics & crash reporting
- Written for
- + Written for
- Deprecated
- + Deprecated
- Applies to
- + Applies to
Remote config, analytics & crash reporting
Three live-ops tools let you tune and understand your game after it ships, without uploading a new build:
Tool | In your game | Where you manage / read it |
|---|---|---|
Remote config |
| Game dashboard → Live config, or |
Analytics |
| Game dashboard → Analytics, or |
Crash reporting |
| Game dashboard → Analytics → Crashes (same endpoint) |
The platform also records plays and players for you automatically. You get daily plays, daily players and returning players even if you never call trackEvent. See Built-in metrics.
For exact signatures, see GameSDK live-ops reference. For the GameSDK global and the game lifecycle (ready(), start()), see GameSDK core reference and How games run.
Remote config
Remote config is a JSON object stored with your game. Your game reads it at runtime, and you can change it at any time from the dashboard or the API. The change takes effect without a re-upload and without another review. Typical uses:
difficulty and balance numbers (enemy speed, drop rates, timers)
feature flags (turn a new mode on or off)
seasonal or event content (a theme name, a banner message, a limited-time multiplier)
kill switches (disable a feature that turned out to be broken)
Shape
The top level must be a JSON object. It can't be an array, string or number.
Values can be any JSON: strings, numbers, booleans,
null, arrays and nested objects.A game that has never had a config set returns
{}.
{
"difficulty": "normal",
"dropRate": 0.12,
"features": { "dailyChallenge": true, "newBoss": false },
"event": { "name": "Harvest Festival", "scoreMultiplier": 2 }
}Size limit
The config must be under 16 KB once serialized. The server measures the length of the JSON text (JSON.stringify(config).length) and rejects anything over 16,000 characters with 400 too_large. Store tuning values in config, not content. Large level data belongs in your game bundle.
Editing the config
From the dashboard. Open your game's manage page in the creator dashboard and find the Live config panel. Edit the JSON and click Save. The editor checks that the JSON is valid and that it is an object before it saves.
From the API. Use an API key with the config scope (see API keys & scopes). A signed-in dashboard session can also make these calls. You must own the game.
# Read the current config
curl https://api.coolgptgames.com/v1/dev/games/$GAME_ID/config \
-H "Authorization: Bearer $COOLGPT_API_KEY"
# → { "config": { "difficulty": "normal", "dropRate": 0.12 } }
# Replace it
curl -X PUT https://api.coolgptgames.com/v1/dev/games/$GAME_ID/config \
-H "Authorization: Bearer $COOLGPT_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "config": { "difficulty": "hard", "dropRate": 0.08 } }'
# → { "ok": true, "config": { "difficulty": "hard", "dropRate": 0.08 } }
PUTreplaces the whole object. Nothing is merged. Keys that you leave out are deleted. To change a single value, read the config, modify it, then write the whole object back.
The Publisher SDK wraps these two calls as getConfig(gameId) and setConfig(gameId, config). The platform keeps no version history. To be able to roll back, keep a copy of your config yourself, for example in source control.
Config is public
Your game's config is served from a public, unauthenticated endpoint that anyone can call:
curl https://api.coolgptgames.com/v1/games/$GAME_SLUG/config
# → { "config": { ... } }Never put secrets in remote config: no API keys, no admin passwords, no unreleased spoilers you care about. Anything in the config is readable by anyone who knows your game's slug.
How the SDK fetches and caches config
The first
getConfig()call asks the platform for the config. Calls made while that request is in flight share it and resolve with the same object.A successful response is cached for the rest of the page load. Every later
getConfig()call resolves with the same cached object straight away and makes no network request.If the platform can't load the config (for example, a network error), your game receives
{}. Nothing is cached, so the nextgetConfig()call tries again.If no answer arrives within 6 seconds, the promise resolves with
{}and nothing is cached. The nextgetConfig()call tries again. If the answer arrives late, it's cached for that next call.
getConfig() never rejects. The worst case is an empty object, so your game must always have its own defaults (see Safe patterns). Because a failure isn't cached, you can call getConfig() again at a later safe point, such as the next menu screen, to pick up the config after a failed first load.
The cached object is shared. If you change it (cfg.x = 1), later getConfig() calls return your changed copy. Treat it as read-only.
Change propagation
Saving writes the new config immediately. Any page load that starts after the save gets the new values.
Players who are already playing keep the config they loaded until they reload the game, because a successful load is cached for the page load, as described above. No push or notification tells a running game that the config changed.
Design changes so that it's safe for old and new values to run at the same time for a while.
Safe patterns
Always merge over defaults. Your game has to work when the config is {}: on first launch, when the config fails to load, or after someone deletes a key.
const DEFAULTS = {
difficulty: "normal",
dropRate: 0.1,
features: { dailyChallenge: false },
};
async function loadConfig() {
const remote = await GameSDK.getConfig(); // never rejects; may be {}
return {
...DEFAULTS,
...remote,
features: { ...DEFAULTS.features, ...(remote.features || {}) },
};
}Validate types and ranges. A typo in the dashboard shouldn't break the game.
function num(v, fallback, min, max) {
return typeof v === "number" && v >= min && v <= max ? v : fallback;
}
const dropRate = num(cfg.dropRate, 0.1, 0, 1);Feature flags default to off. Check that the flag is true, not just truthy. A missing key then leaves the feature off.
if (cfg.features.newBoss === true) spawnNewBoss();Read at a known point. Call getConfig() during loading, before GameSDK.start(). Don't read it mid-run. A successful value won't change during the page load anyway, and reading only between runs keeps gameplay deterministic. This matters if you use replay-verified scores; see Scores, leaderboards & anti-cheat.
Gradual rollouts are up to you. The platform returns the same config to every player. To show a feature to only some players, put a percentage in the config and decide on the client:
// cfg.rollout.newBoss = 25 → about 25% of page loads see it
const pct = num(cfg.rollout?.newBoss, 0, 0, 100);
const enabled = Math.random() * 100 < pct;To keep a player in the same group across sessions, store the random roll with the SDK's save API (see Saves & cloud progress) and reuse it.
Keep a kill switch. For any risky new feature, add a flag such as features.shopEnabled from day one. You can then turn the feature off in seconds without a new build.
Analytics
Call trackEvent to record the moments that matter in your game: level starts and completions, deaths, purchases, tutorial steps.
GameSDK.trackEvent("level_complete", { level: 3, timeMs: 42000, stars: 2 });
GameSDK.trackEvent("boss_defeated", { boss: "dragon", attempts: 4 });
GameSDK.trackEvent("tutorial_step");trackEvent is fire-and-forget. It returns nothing, never throws, and doesn't tell you whether the event was stored.
When events are recorded
Events are attached to the player's current play session. The platform opens a session when your game calls GameSDK.start(). Requesting a replay seed also opens one (see Scores, leaderboards & anti-cheat).
Events sent before the first
GameSDK.start()are silently dropped. Callstart()when gameplay begins, then track.Events sent after the player leaves the game page are dropped.
Sessions only open for published games. Unpublished games don't record events.
Guests and signed-in players are both tracked. Signing in isn't required.
This works both on coolgptgames.com and when your game is embedded (see Embedding & the score bridge).
The platform attaches the game, the player (signed-in user, or an anonymous browser ID for guests) and the session to each event. Don't put that information in props yourself, and never put personal data such as emails or real names in props.
Event names
Rule | Value |
|---|---|
Length | 1–64 characters. The SDK cuts longer names to 64. |
Allowed characters | Letters, digits, |
Reserved |
|
A name with any other character (for example /, !, # or non-ASCII letters) is rejected, and the event is lost without an error. Use a consistent snake_case or dot.case scheme, such as level_complete or shop.open.
Event properties
Rule | Value |
|---|---|
Type | A plain JSON object. Leave |
Top-level keys | At most 50 |
Nesting | Allowed (nested keys don't count toward the 50) |
Events that break these limits are rejected, not truncated. The whole event is lost, and the SDK doesn't report the error to your game. Keep props small: IDs, numbers and short labels. Don't send whole game states.
Arrays are not accepted as props. Values that can't be sent between windows, such as functions or DOM nodes, cause the event to be dropped.
Rate limits and sampling
Up to 120 events per minute per play session. Events beyond that get
429and are lost.Event requests also count toward the platform-wide per-IP request limit that every API call shares (see Errors & limits).
The game-to-page message channel accepts at most 30 SDK messages per second across all SDK calls.
The platform does no sampling: every event that is accepted is stored. The limits above are ceilings, not targets. Don't track every frame or every input. For high-frequency actions, count them locally and send one summary event (for example run_summary with { jumps: 212, coins: 48 }) at the end of a run.
Where you see results
Open your game's manage page in the creator dashboard and find the Analytics panel. It covers the last 30 days and shows:
Total events: every event recorded, crash reports included - Unique players: distinct players (signed-in or guest) who sent at least one event - Returning players and a daily plays / daily players chart (see Built-in metrics) - Top events: your most frequent event names with counts - Crashes: your most frequent error messages with counts
The same data is available from the API with the analytics scope:
curl https://api.coolgptgames.com/v1/dev/games/$GAME_ID/analytics \
-H "Authorization: Bearer $COOLGPT_API_KEY"{
"game": { "slug": "sky-hopper", "title": "Sky Hopper" },
"totalEvents": 18234,
"uniquePlayers": 1502,
"events": [
{ "name": "level_complete", "count": 9120 },
{ "name": "boss_defeated", "count": 2210 }
],
"errors": [
{ "message": "physics step overflowed", "count": 14 }
],
"daily": [
{ "date": "2026-09-19", "plays": 640, "players": 512 },
{ "date": "2026-09-20", "plays": 702, "players": 560 }
],
"returningPlayers": 311
}Field | Meaning |
|---|---|
| All events in the last 30 days, |
| Distinct players who sent any event in the last 30 days |
| Up to 50 event names (crash reports excluded), highest count first |
| Up to 20 crash messages, highest count first |
| One row per day that had plays: valid plays and distinct players |
| Players with valid plays on 2 or more different days in the window |
The analytics endpoint returns aggregates only: counts per event name. It doesn't return individual events or their props, and the dashboard doesn't show props either.
Built-in metrics
The platform records these for every game, whether or not you call trackEvent:
Metric | How it's counted |
|---|---|
Plays (daily) | Valid play sessions started that day |
A session is valid when it runs for at least 30 seconds with at least 2 heartbeats. The platform sends heartbeats every 15 seconds while the game is open. A session that is still running counts as provisionally valid until it ends. Sessions can be excluded as invalid, for example when a single browser plays the same game more than 20 times in 24 hours, or when traffic comes from a data center.
Plays and players are counted per browser, so one person on two devices counts as two players.
The dashboard Analytics panel currently shows "No events yet" and hides the plays chart until your game has sent at least one
trackEvent/reportErrorevent. The API still returnsdailyandreturningPlayerseither way.
Crash reporting
Call reportError for errors that you catch or that reach a global handler:
try {
loadLevel(n);
} catch (e) {
GameSDK.reportError(e.message, { where: "loadLevel", level: n });
}
// Catch-all for uncaught errors
window.addEventListener("error", (ev) => {
GameSDK.reportError(ev.message, {
where: "window.onerror",
stack: String(ev.error && ev.error.stack || "").slice(0, 1500),
});
});How it works:
reportErrorsends an analytics event named$errorwithprops = { message, ...extra }.messageis converted to a string and cut to 500 characters. If you passnullorundefined, the message is"error".The top-level keys of
extraare copied into the same object. Themessageargument always wins: amessagekey inextrais ignored, so use another key name for extra text.Passing an
Errorobject asextraadds nothing, because itsmessageandstackaren't enumerable. Copy the fields you want into a plain object.All the analytics rules apply: a session is required, the whole event must stay under 4,000 characters and 50 keys, and the 120/min rate limit is shared with
trackEvent. Stacks get long, so cut them as in the example above. If the event is too large, the whole report is dropped.
Where it shows up. The Crashes list in the dashboard's Analytics panel groups reports by exact message text and shows the top 20 over the last 30 days. The API returns the same list in errors. extra fields are stored but not shown. Messages that contain changing values, such as IDs or coordinates, split into many separate rows. For clean grouping, keep message stable and put the variable details in extra.
Related
GameSDK live-ops reference: exact signatures and resolved shapes - Webhooks: server-side notifications for reviews, reports and moderation - API keys & scopes: the
configandanalyticsscopesErrors & limits: error envelope, rate limits