Guides

Remote config, analytics & crash reporting

AdminUpdated Sep 22, 2026

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

GameSDK.getConfig()

Game dashboard → Live config, or PUT /v1/dev/games/:id/config

Analytics

GameSDK.trackEvent(name, props)

Game dashboard → Analytics, or GET /v1/dev/games/:id/analytics

Crash reporting

GameSDK.reportError(message, extra)

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 } }

PUT replaces 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

  1. 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.

  2. 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.

  3. If the platform can't load the config (for example, a network error), your game receives {}. Nothing is cached, so the next getConfig() call tries again.

  4. If no answer arrives within 6 seconds, the promise resolves with {} and nothing is cached. The next getConfig() 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. Call start() 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, _, ., -, space, $, :

Reserved

$error, which counts as a crash report (use reportError instead)

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 props out if you have none.

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 429 and 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

totalEvents

All events in the last 30 days, $error included

uniquePlayers

Distinct players who sent any event in the last 30 days

events

Up to 50 event names (crash reports excluded), highest count first

errors

Up to 20 crash messages, highest count first

daily

One row per day that had plays: valid plays and distinct players

returningPlayers

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/reportError event. The API still returns daily and returningPlayers either 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:

  • reportError sends an analytics event named $error with props = { message, ...extra }.

  • message is converted to a string and cut to 500 characters. If you pass null or undefined, the message is "error".

  • The top-level keys of extra are copied into the same object. The message argument always wins: a message key in extra is ignored, so use another key name for extra text.

  • Passing an Error object as extra adds nothing, because its message and stack aren'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

Was this page helpful?