Skip to documentation
Verify and troubleshoot
Check delivery and lifecycle behavior before releasing your integration. For a custom client, apply these checks to your own persisted queue and diagnostics.
Verify your integration
- Enable collection for the selected game and initialize after consent.
- Track one custom action and start two progression attempts. Complete one and fail the other.
- Flush, check accepted delivery in SDK diagnostics, then check the daily charts after the next dbt run. Open the authenticated example integration with
?product=<your Coal project ID or slug> appended to its URL.
- Check acknowledged and pending counts in SDK diagnostics, then verify daily session and progression counts after dbt publishes. A successful collector response means the batch committed durably.
- Use the example's offline transport control: enqueue an action while offline, verify
pending increases, then reconnect and flush. Raw events should arrive once per event ID.
- Reload with pending events, grant consent again, and initialize. Verify the persisted queue recovers.
The example page reads the selected game's setup and displays SDK diagnostics and daily dbt aggregates. A hosted player build needs only the public ingestion key; the SDK supplies the collector endpoint. Coal displays the resulting daily metrics.
Use an empty local game scope for these repeatable checks.
| Check |
Expected daily result |
| One consenting installation sends one new session on a UTC day. |
One new installation, one active installation and one session. |
The same session sends cumulative durations of 0, 30000 and 60000 in reserved session events. |
One session with 60 seconds observed active time. For the SDK, compare against its actual measured foreground duration. |
One tutorial attempt starts and completes. |
One tutorial attempt and one completion. |
Two level_1 attempts start; one fails and one completes. |
Two level attempts, one failure and one completion. |
| A later new session reuses the same installation ID. |
The session count increases; the installation count stays the same. Respect the SDK's 30-minute away boundary when testing a new session. |
| That installation sends activity on the following UTC day. |
It is active on the return day and qualifies as a D1 return. The D1 report appears only after that return day ends. |
Check each local result after dbt publishes. Start/outcome events must use matching installation, attempt and stage IDs. Follow Events and metrics for the underlying calculation and Direct API retry rules for custom clients.
Also check that a restarted app retains its installation and outstanding attempt state, foreground time stops increasing while backgrounded, reporting-off stops delivery, and an offline queued batch survives restart and is acknowledged after reconnection. A repeated delivery should acknowledge the same events as duplicates.
Troubleshooting
| Symptom |
What to check |
init() rejects |
Explicit consent, a nonempty key (and a valid endpoint if overriding it), browser support, and accessible IndexedDB. Catch the initialization error. |
| Browser CORS error |
The shared collector supports all origins. Check network access, the endpoint if overridden, and any proxy or host restrictions. |
pending grows while offline |
Expected. Reconnect and allow retry time; the bounded queue continues to retain events. |
batch_rejected, status 403 |
Game enabled, key active, owning account eligible, and collector access renewed by Coal. Delivery pauses. |
batch_rejected, status 400/409 |
Invalid payload or conflicting reuse of an event ID. Correct the cause; preserve the diagnostic before clearing. |
| HTTP 429 |
Rate or concurrency limit. The SDK backs off automatically. |
| HTTP 503 |
Durable commit unavailable. The SDK retries the same IDs. |
persistence_error |
Browser storage transaction failed. Inspect browser storage restrictions and report the loss. |
queue_overflow / queue_expired |
Queue bounds were exceeded or offline events aged out. Inspect dropped / expired. |
| No final session event |
Unload delivery is best effort; use the last observed cumulative heartbeat duration. |
Diagnostics contain enqueued, acknowledged, dropped, expired, retries, permanentErrors, persistenceErrors, pending, enabled, blocked, and retryAt (Unix milliseconds). Counters describe this SDK instance; acknowledged can include recovered events from earlier instances. A healthy queue can temporarily have pending events between flushes.
[{"group":"Overview","href":"/user-metrics#start-with-a-small-event-plan","text":"Use the minimal metric examples for installation identity, return visits, session starts, foreground duration and progression, with SDK calls and equivalent Direct API fields. For the basic activity and retention metrics, keep one stable installation identity and report sessions whenever the app is used. The JavaScript SDK handles that lifecycle automatically after initialization. With the Direct API, your client must generate and persist the identities and emit the session events itself. For progression, instrument a few meaningful stages—such as the tutorial and first playable level—with a start and a matching completion or failure for each attempt. See the metric requirements and a sample event plan. Progression charts show daily attempt and outcome counts for those stages. ","title":"Start with a small event plan"},{"group":"Overview","href":"/user-metrics#available-clients","text":"The JavaScript SDK handles collection and delivery for your app. The Direct API supports custom clients in any language that can send HTTPS and JSON. Unity, Unreal, Godot and native integrations can use the Direct API. ","title":"Available clients"},{"group":"Overview","href":"/user-metrics#before-you-start","text":"Select your game in Coal and open User metrics → Setup. An organization administrator with access to the game creates and copies its public ingestion key. Creating the first key activates collection. Your game’s owning organization must retain eligible Coal access. Your app owns its consent and reporting controls. Keep event names consistent between releases and send small structured gameplay properties. ","title":"Before you start"},{"group":"Overview","href":"/user-metrics#understand-your-data","text":"All games use the shared collector, and the key determines ownership. Coal displays daily UTC aggregates. An accepted batch appears in the charts after the next aggregation run, about every 15 minutes. Read Shared concepts for collector addresses, key lifecycle and metric meaning. Use Verify and troubleshoot before release. For coding agents, AI integration guide links to the complete Markdown reference. ","title":"Understand your data"},{"group":"JavaScript SDK","href":"/user-metrics/javascript#quick-start","text":"In Coal, select your existing game and open User metrics → Setup. An organization administrator with access to the game clicks Create key and copies the public ingestion key. Creating the first key activates collection. The SDK uses Coal’s shared collector automatically.Download the SDK package, move it into your app's directory, and install it: Initialize one SDK instance after your app's consent policy permits collection. Replace yourkey with the public ingestion key from Coal. Initialization is asynchronous because it loads or creates a persisted installation ID. Await it before tracking. Initialize with the public ingestion key from the game’s Setup page. Preview distribution: download the package from Coal. Keep the downloaded package in source control or your artifact store to reproduce builds. Collection is available only when your game's Coal access is eligible and the collector is configured. ","title":"Quick start"},{"group":"JavaScript SDK","href":"/user-metrics/javascript#install-without-a-bundler","text":"You can also download the standalone ES module as coal-events.js and serve it from your own app. It has no runtime package dependencies. Use HTTPS for your deployed app, or localhost while developing. The collector accepts game keys from browser, embedded and packaged app origins. Download and bundle or self-host the module. Catch initialization errors and report them through your app’s diagnostics. ","title":"Install without a bundler"},{"group":"JavaScript SDK","href":"/user-metrics/javascript#configure-the-sdk","text":"OptionDefaultPurposeconsentRequired: trueExplicitly permit initialization after your host app's consent flow.keyRequiredPublic ingestion key from the selected game's settings.endpointhttps://coal-events.fly.devOptional override for a local or separately hosted collector; use the collector base URL.appVersionEmpty stringYour release/build version; set it consistently. Maximum 100 UTF-8 bytes.platform'web'Platform label. Required on the wire; maximum 64 UTF-8 bytes.foregroundtrueInitial host app foreground state. Native wrappers should update it through setForeground(boolean); document visibility must also permit activity.onErrorNo callbackReceives observable queue, persistence, and delivery problems. Initialization errors also reject init().heartbeatMs30000Heartbeat interval while the page is visible; minimum 1,000 ms.awayMs1800000Hidden/away interval that starts a new session on return; minimum 1,000 ms.flushMs5000Timed delivery interval; minimum 100 ms.batchSize50Maximum events in one request, from 1 to 100.batchBytes262144Maximum encoded request size, from 16,450 to 262,144 bytes.queueLimitsSee offline deliveryOverride maxEvents, maxBytes, and maxAgeMs with positive values. Use defaults initially. Heartbeats and flushes are separate: a heartbeat creates an event; a flush attempts delivery of a bounded batch. Each flush() attempts one bounded batch. ","title":"Configure the SDK"},{"group":"JavaScript SDK","href":"/user-metrics/javascript#track-custom-events","text":"Use custom events for actions that answer a specific game design question. Keep names stable and properties small, structured, and free of personal data. track(name, properties) synchronously returns an event UUID, then persists the queued event asynchronously. Delivery runs asynchronously using the saved payload snapshot. Calls while disabled return null. Names begin with a letter and contain up to 100 ASCII characters using letters, digits, _, ., :, or -. Properties must be a JSON-serializable object, up to 8 KiB when encoded. Each whole event is limited to 16 KiB. Use finite numbers, valid Unicode text and small structured gameplay properties. session_started, session_heartbeat, session_ended, progression_started, progression_completed, and progression_failed are reserved. Use the SDK lifecycle and progression helpers for them. ","title":"Track custom events"},{"group":"JavaScript SDK","href":"/user-metrics/javascript#track-progression","text":"A progression ID identifies a level, mission, tutorial step, or other repeatable stage. Each start creates a separate attempt ID. Use the returned ID for its outcome, so retries and overlapping attempts remain distinguishable. progressionStarted(id, options) returns the attempt UUID. A progression ID must be a nonempty string up to 100 characters; keep its UTF-8 encoding within 100 bytes for the collector. order is an optional numeric sequence for your stages. Outcome helpers return the queued event UUID. The SDK fills progression_id, progression_order when provided, and attempt_id. Resolve each outstanding attempt once with its returned UUID. Outstanding attempts survive a tab reload through sessionStorage; retain the returned IDs in your app's own state if gameplay needs to reference them after reload. Up to 1,000 unresolved attempts are retained. Outstanding attempts appear as unresolved. ","title":"Track progression"},{"group":"JavaScript SDK","href":"/user-metrics/javascript#sessions-and-active-time","text":"After initialization, the SDK creates session_started and emits session_heartbeat approximately every 30 seconds while visible. session_ended is best effort on page exit. Every activity event carries cumulative active_duration_ms for that session. Active time measures foreground app time using a monotonic clock. Each one-second sample is capped at two seconds. A reload within 30 minutes resumes the tab's session and saved duration without counting reload downtime. A return after at least 30 minutes away starts a new session. Tabs have separate session IDs and share the installation's delivery queue. The installation ID is random and stored locally. It remains stable for the same browser profile, app origin, collector endpoint, and game, including key rotation. Clearing storage, changing origin/profile, or resetting identity creates a new installation. Coal uses the maximum observed cumulative duration per session. The first observed activity day establishes the installation cohort. ","title":"Sessions and active time"},{"group":"JavaScript SDK","href":"/user-metrics/javascript#consent-and-reset","text":"The host app owns its consent experience. Initialize with consent: true after collection is permitted. Stop collection when consent is withdrawn: disable() stops timers and lifecycle listeners and aborts in-flight delivery. Pending events are kept by default; clearPending: true clears them after earlier enqueues. Committed events remain in the daily aggregates. Initialize again only after consent permits it. For an explicit local identity reset: reset() stops collection and clears the session/attempt state. These reset options default to true. Use clearPending: true with resetIdentity: true to reset local identity and pending events together. ","title":"Consent and reset"},{"group":"JavaScript SDK","href":"/user-metrics/javascript#offline-delivery-and-retries","text":"The default IndexedDB queue retains up to 1,000 events, 2 MiB, and seven days. Oldest events are evicted on overflow and aged events expire when the queue is accessed. These losses are observable through diagnostics and onError. Events are removed only after a successful response acknowledges the full batch. Retries keep their original IDs and payloads, so a lost response after a server commit can be retried safely. Network failures, HTTP 408/429, and 5xx use exponential backoff with jitter, capped at 60 seconds. A longer Retry-After is respected. Returning online triggers another attempt. Other HTTP failures pause the queue, retain its events, and report batch_rejected. Fix the key, access, payload, or configuration issue, then call events.resumeDelivery() and await events.flush(). A rejected payload stays queued until explicitly cleared or reset. To use a newly created key, disable the old instance and initialize a new one with the new key; the same game's persisted identity and pending queue are retained. Use normal timed delivery and monitor queue diagnostics for persistence failures, dropped events and expiry. ","title":"Offline delivery and retries"},{"group":"JavaScript SDK","href":"/user-metrics/javascript#custom-adapters","text":"Advanced integrations may provide persistence and transport options to CoalEvents.init(). The default exports are IndexedDBPersistence and FetchTransport. Use these interfaces to connect your app’s storage and HTTP transport. Persistence methodResult / behavioridentity(makeId)Promise of the stable installation UUID; persist a generated ID atomically.enqueue(event, limits, now)Promise of the number evicted; atomically store and enforce count/byte/age limits.read(limit, now, maxAgeMs)Promise of { events, expired }; return oldest retained events and expire aged entries.remove(eventIds)Promise; remove only the named acknowledged events.count()Promise of pending count.clear({ identity: false })Promise; clear pending events and optionally persisted installation identity. transport.send(endpoint, key, batch, { signal, keepalive }) returns a Promise of { status, retryAfter, acknowledged }. Honor the AbortSignal: the SDK aborts after ten seconds or when disabled. Reject on network failure. Parse the collector's JSON acknowledgment for a successful response; pass Retry-After through when present. keepalive is an optional unload hint. The SDK's tab-session and progression metadata use sessionStorage separately from the queue adapter. Connect native app state through setForeground as described below. Preserve host consent, monotonic active time, bounded durable storage, and original retry IDs in any custom integration. ","title":"Custom adapters"},{"group":"JavaScript SDK","href":"/user-metrics/javascript#native-wrapper-lifecycle-and-resumed-attempts","text":"Keep one SDK instance per app session. Connect the host wrapper's authoritative foreground/background callback to events.setForeground(isActive). Active time accumulates while the app is foregrounded and the document is visible. Persist your own run UUID with the game checkpoint and reuse it as attemptId: Outstanding attempt metadata persists in IndexedDB independently of sessionStorage. Repeating a start for the same UUID and progression recovers the outstanding attempt. Completed/failed attempts are removed from the outstanding set. Resetting identity clears this metadata; a custom persistence adapter must implement loadAttempts and saveAttempt for cross-session recovery. IndexedDB can still be evicted by the platform. Use the same consent policy across analytics providers, initialize each independently, and keep production and test games in separate collector scopes. All keys for one game contribute to the same aggregates. ","title":"Native wrapper lifecycle and resumed attempts"},{"group":"JavaScript SDK","href":"/user-metrics/javascript#method-reference","text":"MethodReturnUseCoalEvents.init(options)Promise\u003cCoalEvents\u003eConsent-gated initialization.events.track(name, properties?)Event UUID or nullQueue a custom event synchronously.events.progressionStarted(id, options?)Attempt UUID or nullStart a separately identifiable attempt.events.progressionCompleted(attemptId, options?)Event UUID or nullResolve an attempt as completed.events.progressionFailed(attemptId, options?)Event UUID or nullResolve an attempt as failed.events.flush(options?)Promise\u003cboolean\u003eTry one bounded batch; false can mean paused, retrying, or disabled.events.diagnostics()Promise\u003cobject\u003eRead instance counters and persisted queue count.events.resumeDelivery()No valueExplicitly unpause after fixing a permanent error.events.disable(options?)Promise\u003cvoid\u003eStop collection; optionally clear pending events.events.reset(options?)Promise\u003cvoid\u003eStop and clear local session/identity as requested. ","title":"Method reference"},{"group":"Direct API","href":"/user-metrics/api#get-a-game-key","text":"Select your game in Coal and open User metrics → Setup. An organization administrator with access to the game can create and copy its public ingestion key. Creating the first key activates collection. The game's owning organization must retain eligible Coal access. Send the key in X-Coal-Ingestion-Key and JSON in Content-Type: application/json. All games share the same collector address. Browser, itch, packaged and server clients use the same protocol. Set browser fetch requests to credentials: 'omit'. The collector handles browser preflight with wildcard CORS. The public key identifies the game. Use the event envelope below, place custom fields in properties, and send uncompressed JSON to the endpoint. ","title":"Get a game key"},{"group":"Direct API","href":"/user-metrics/api#send-your-first-batch","text":"Send a JSON batch with your game's key. For gameplay, generate the event and session UUIDs, reuse the saved installation UUID, and record the UTC occurrence time. For local testing, use your local collector URL and key. Response: Retry the same batch unchanged. An identical retry returns {\"acknowledged\":1,\"inserted\":0,\"duplicates\":1}. Persist event snapshots in your app’s delivery queue until acknowledged. ","title":"Send your first batch"},{"group":"Direct API","href":"/user-metrics/api#event-envelope-and-limits","text":"FieldRequiredContractBatch schema_versionYes1.Batch eventsYes1–100 event objects; entire request up to 256 KiB.event_idYesNonzero UUID in 36-character hyphenated form, unique per new event within the game.installation_idYesNonzero UUID in the same form, persisted by the client.session_idYesNonzero UUID in the same form for this session.nameYes1–100 ASCII characters; starts with a letter, followed by letters, digits, _, ., :, or -.occurred_atYesUTC RFC3339 timestamp, optionally with fractional seconds.propertiesNo for custom eventsJSON object, up to 8 KiB; numbers in the JavaScript range.platformYesNonempty string, up to 64 UTF-8 bytes.app_versionRecommendedString, up to 100 UTF-8 bytes; the SDK always supplies it.Event schema_versionYes1. Sizes are UTF-8 encoded bytes: 256 KiB per request, 16 KiB per event and 8 KiB per properties object. Custom events may omit properties; when supplied, use a JSON object with finite numbers and valid Unicode text compatible with PostgreSQL JSONB. Place custom fields inside properties. Keep the original occurrence time for offline delivery. ","title":"Event envelope and limits"},{"group":"Direct API","href":"/user-metrics/api#installation-and-session-identity","text":"Generate a random installation UUID once after your app's consent policy permits collection, then persist it in app storage scoped to this game. Reuse it across launches and key changes for the same game. Clearing storage or explicitly resetting identity creates a new installation. Generate a random session UUID when a session starts. All events in that session share it and the installation UUID, while every new event gets its own event UUID. To match the SDK's session behavior, resume the same persisted session and cumulative duration after a reload or return within 30 minutes away, without counting the downtime. Start a new session after at least 30 minutes away. Keep separate sessions for independent concurrent app windows. Persist session state and away timestamps if your runtime can be restarted during a session. ","title":"Installation and session identity"},{"group":"Direct API","href":"/user-metrics/api#sessions-and-active-duration","text":"These reserved event names feed Coal's session duration metrics: NameWhen to emitRequired propertiessession_startedOnce when a new session begins.{\"active_duration_ms\":0}session_heartbeatAbout every 30 seconds while the app is in the foreground.{\"active_duration_ms\":30000} for 30 seconds accumulated so far.session_endedBest effort when a session finishes.The final cumulative active_duration_ms. active_duration_ms is a nonnegative JSON number no greater than 31,536,000,000. Use integer milliseconds for predictable aggregates. It is cumulative for the whole session. For example, heartbeats at 30 and 60 active seconds carry 30000 and 60000; the observed duration is 60 seconds. Send duration on these reserved session events. Measure elapsed time with a monotonic clock, accumulate time while foregrounded and cap samples during timer stalls. The JS SDK samples each second and caps a sample at two seconds. Preserve the accumulated value when resuming a session. Use wall-clock UTC for occurred_at. Persist regular heartbeats throughout the session. Sessions and their maximum observed duration belong to their first observed UTC day, even when they cross midnight. ","title":"Sessions and active duration"},{"group":"Direct API","href":"/user-metrics/api#progression-attempts","text":"Use progression_started, then one matching progression_completed or progression_failed event. All three require properties.attempt_id (a nonzero UUID) and properties.progression_id (a nonempty string up to 100 UTF-8 bytes). Use an optional integer properties.progression_order to order your levels or stages, consistently across events. An attempt is one try at a stage. Generate its UUID once at the actual start and save it with your game checkpoint. Reuse it for the outcome, even after restart or a new session; keep the same installation and progression ID. A new try gets a new attempt UUID. The start and outcome are distinct events and have distinct event UUIDs. Send the start once, retrying its original event if delivery fails. Emit one terminal outcome per attempt. Here is a complete two-event batch illustrating the fields. Replace all example UUIDs and timestamps for actual new gameplay events. For failure, use progression_failed and your own small properties such as reason. Outstanding attempts appear as unresolved. Daily progression metrics belong to the attempt start day and use the earliest matching outcome. ","title":"Progression attempts"},{"group":"Direct API","href":"/user-metrics/api#durable-queues-and-acknowledgments","text":"At the gameplay action, create the event UUID and snapshot its occurrence time and full payload. Persist it before attempting delivery.Send a bounded batch of 1–100 queued events within the byte limits. Flush regularly, for example every five seconds.Remove that batch only after HTTP 200 and a parsed integer acknowledged equal to the number you sent. Both inserted events and duplicates count as acknowledged.On a timeout, lost connection or retryable response, retain and retry the same event IDs and payloads. A timeout may happen after commit; deduplication makes the retry safe. The whole validated batch commits atomically. Equivalent JSON retries deduplicate by game and event ID, including when delivered with another active key for that game. Preserve the original event snapshot for every retry. Bound the durable queue and expose failures to developers without affecting gameplay. The SDK's defaults are a useful starting point: 1,000 events, 2 MiB and seven days, evicting oldest events on overflow and expiring aged entries. These are client queue policies. Record dropped/expired counts. Coordinate simultaneous queue writers and flushers so acknowledgments remove only their submitted events. ","title":"Durable queues and acknowledgments"},{"group":"Direct API","href":"/user-metrics/api#errors-and-retry-policy","text":"Error responses are JSON, for example: Status / conditionMeaningClient action200 with matching acknowledgedWhole batch committed or was already stored.Remove the submitted events from the queue. inserted and duplicates explain the result.Network error, timeout, 408, 429, or 5xxDelivery failed or is uncertain; rate/concurrency limits may apply.Keep events and retry with exponential backoff and jitter. Respect Retry-After when accessible.400 / invalid_batchInvalid JSON, field, value or schema version.Pause automatic retries, inspect the message and fix the producer. Quarantine invalid events.401 / keyMissing or malformed key header.Fix the key/header before resuming.403 / access_deniedUnknown/revoked key or unavailable game access.Check User metrics → Setup and account eligibility; retain pending events subject to queue bounds.409 / event_conflictAn event UUID already has a different payload.Inspect the conflicting event and restore its original saved snapshot.413 / sizeBody too large or unreadable.Reduce batch size while keeping individual events unchanged; quarantine an oversized event.415 / content_typeWrong content type, query string or compression.Send uncompressed JSON to https://coal-events.fly.dev/v1/events.405 / methodWrong HTTP method.Use POST.2xx without a matching acknowledgmentDelivery is uncertain or the response is unexpected.Retain the batch; investigate and retry the saved event snapshots. A practical retry schedule starts around one second, doubles with jitter, and caps the delay at 60 seconds unless Retry-After requires longer. Use this backoff policy for cross-origin browser requests. Keep permanent errors observable and resume only after fixing their cause. Apply a finite request timeout, such as ten seconds, and retry the same saved events on timeout. ","title":"Errors and retry policy"},{"group":"Direct API","href":"/user-metrics/api#consent-verification-and-daily-charts","text":"Your app must decide when collection is permitted, independently of transport. On reporting-off or consent withdrawal, stop creating events and stop flushing; clear pending events if they should no longer be sent. Committed events remain in the daily aggregates. On an explicit identity reset, clear pending events and saved session/attempt state before creating a new installation UUID. Use a local collector to verify delivery, unchanged retries, offline persistence, foreground timing, progression recovery and reporting-off. Send small structured gameplay properties. An HTTP acknowledgment confirms durable ingestion immediately. Coal displays daily UTC aggregates after the next dbt run, about every 15 minutes. Overview, Retention and Progression use installation/session/attempt identities as described above. Custom actions also contribute to activity and session counts. For an AI implementing a custom client, provide the Markdown guide and ask it to follow this Collection API section, including persisted identities, reserved event properties, cumulative foreground duration, original retry snapshots, exact acknowledgments, consent and local test coverage. ","title":"Consent, verification and daily charts"},{"group":"Events and metrics","href":"/user-metrics/event-design#what-coal-calculates","text":"The collector stores accepted events and dbt produces daily UTC aggregates. Coal reads those aggregates. This table describes the current metrics and the inputs your game must provide. MetricWhat it meansInputs you must preserveNew installationsNumber of installation IDs whose earliest observed activity falls on that UTC day. This measures first observed use.A random, persistent installation_id and at least one accepted event. Coal derives this from the first event.Active installationsDistinct installation IDs with any accepted event on a UTC day.The same installation ID across sessions, with an event on each day the app is used.D1 / D7 / D30 retentionShare of the first observed installation cohort with activity on exactly the UTC day 1, 7 or 30 days later.Stable installation identity and events on the actual return day. Coal derives this from return-day activity after the return day ends.SessionsDistinct session IDs, counted on their first observed UTC day.A different session_id for each new session, reused by all events within it. Any accepted event can establish a session; explicit lifecycle events keep instrumentation consistent.Average active sessionTotal observed active time divided by sessions. A session's duration is its maximum observed cumulative active time.session_started, session_heartbeat or session_ended carrying properties.active_duration_ms.Progression attempts / completed / failed / unresolvedAttempts at each stage, grouped by their start day. Unresolved means attempts minus completed and failed.progression_started plus a matching progression_completed or progression_failed, correlated by installation, attempt_id and progression_id. An outstanding attempt is unresolved. An installation is a local app/browser identity. Weekly and monthly active-installation charts average observed daily counts. A retention cohort starts on the earliest observed UTC activity day. For example, an installation first observed on January 10 returns for D1 if it sends activity on January 11; that return day must finish before its D1 rate appears. Late offline delivery can revise earlier daily summaries and cohort membership. ","title":"What Coal calculates"},{"group":"Events and metrics","href":"/user-metrics/event-design#the-smallest-useful-integration","text":"After your app's consent policy permits collection, initialize one SDK instance or load/create your custom client's persistent installation ID. Keep it stable across launches and key changes for the same game.Collect a session each time the app is used. Reuse its session ID for that session; start a new one after a new session boundary. The SDK handles its 30-minute away rule automatically.Measure cumulative foreground app time and send regular session heartbeats, approximately every 30 seconds. Accumulate time while foregrounded. Ending a session is best effort; regular heartbeats keep duration useful if the app closes unexpectedly.Persist the events and deliver them with their original IDs and occurrence times. Retry unchanged events and remove them only when acknowledged. Reporting-off stops event creation and delivery. With the JavaScript SDK, initialization handles installation identity, sessions, heartbeats and the offline delivery queue. Use its automatic session lifecycle. Connect native wrapper foreground/background callbacks where applicable. With the Direct API, implement those responsibilities yourself using the session contract and delivery rules. This baseline provides activity, new-installation, retention and session metrics. Progression requires the game-specific instrumentation below. Verify delivery through acknowledgments and metric results through the daily charts. ","title":"The smallest useful integration"},{"group":"Events and metrics","href":"/user-metrics/event-design#choose-meaningful-progression-stages","text":"Write down the questions you want answered before choosing events. A useful first event plan is small enough to review with the person designing the game: QuestionStage or eventTrigger in the gameDo first-time installations reach the tutorial?Progression stage tutorial, order 1.Start when the tutorial actually begins. Complete when its finish condition is reached.Do attempts at the first playable stage succeed?Progression stage level_1, order 2.Start when playable gameplay begins. Complete on a win; fail on a defined loss.Do installations come back?Session activity on a later day.Initialize/resume collection when the app is used again, preserving the installation ID.Is a hint used during play?Optional custom action tutorial_hint_used, properties such as hint: \"movement\".Track the actual hint action once. Custom actions contribute to activity; progression uses the stage helpers. A progression stage ID is a stable name for the content, such as tutorial or level_1. An attempt ID is a random UUID for one try at that stage. Keep stage IDs and their optional numeric order consistent between builds. Start a new attempt for a new try, including retries after a loss. Save its attempt UUID with the game checkpoint. Send exactly one terminal outcome for that attempt, using the same installation ID and stage ID, even if the app restarted or the session changed. A tutorial and a level have separate attempts. Report completion at the game's win condition and failure at its defined loss condition. Keep an unfinished attempt outstanding until its outcome is known. ","title":"Choose meaningful progression stages"},{"group":"Events and metrics","href":"/user-metrics/event-design#example-tutorial-followed-by-a-level","text":"After initializing events through the JavaScript SDK: Place these calls in the corresponding gameplay callbacks. Use a saved UUID as attemptId when restoring an outstanding attempt, following the resumed-attempt guide. With the Direct API, send the corresponding reserved names and these properties in separate event envelopes at the same gameplay moments: Event nameRequired progression propertiesprogression_startedattempt_id, progression_id; optional progression_order.progression_completedThe same attempt_id and progression_id; use the same order if supplied.progression_failedThe same attempt_id and progression_id; optional custom reason. Each start and outcome is a different event and needs its own event_id; delivery retries reuse the original event ID and payload. See the complete HTTP batch example for the envelope, timestamps and identity fields. ","title":"Example: tutorial followed by a level"},{"group":"Events and metrics","href":"/user-metrics/event-design#minimal-examples-shared-setup","text":"The following recipes show both integration choices. Choose the snippets for your integration and trigger each at the described gameplay moment. Use a local collector/game for synthetic examples. JavaScript SDK: initialize one instance after consent. This is enough to start the activity, installation, retention and session metrics; add progression callbacks for stage metrics. You supply the key, consent and app metadata. The SDK supplies event_id, persistent installation_id, session_id, UTC occurred_at and schema versions. It also creates session events, accumulates foreground time and queues delivery. Use the automatic lifecycle for session events. Direct API: use the curl request and full JSON envelope. Every event supplies event_id, installation_id, session_id, occurred_at, platform, app_version and schema_version. The examples below show the event's name and properties to put into that envelope. Generate a UUID for each new event and record its UTC occurrence time. Persist the installation UUID across launches, reuse the session UUID within a session, and retry saved payloads unchanged. ","title":"Minimal examples: shared setup"},{"group":"Events and metrics","href":"/user-metrics/event-design#example-count-a-new-installation","text":"Trigger: the first consenting use of this game installation. Result: its first accepted event establishes one new installation, one active installation and one session on that event's UTC day. SDK: the shared CoalEvents.init(...) example does this automatically by creating identity and emitting session_started for a new session. Initialize on later launches with the same game configuration to reuse the saved identity. Direct API: create and persist an installation UUID, create a session UUID, then send: Coal derives “new” from the first accepted event for that installation UUID. ","title":"Example: count a new installation"},{"group":"Events and metrics","href":"/user-metrics/event-design#example-count-activity-and-a-return-visit","text":"Trigger: the installation plays again. Result: any accepted event with the same installation UUID counts activity on its occurrence day. Activity exactly 1, 7 or 30 UTC days after its first day contributes to D1, D7 or D30 retention. Coal calculates retention from these events. SDK: initialize on the next launch after consent, as in the shared setup. Persisted identity and automatic session events establish the return. During a session that crosses UTC midnight, automatic foreground heartbeats establish activity on the next day without a new installation. Direct API: reuse the saved installation UUID. At a new session boundary, generate a new session UUID and send session_started with active_duration_ms: 0 using the envelope above. Record the UTC time when the activity occurs. A January 10 first use followed by January 11 activity qualifies for D1 after January 11 ends. ","title":"Example: count activity and a return visit"},{"group":"Events and metrics","href":"/user-metrics/event-design#example-begin-and-resume-a-session","text":"Trigger: the first launch, or return after at least 30 minutes away. Result: a new session count; every event in this session carries its UUID. SDK: initialization and the away rule handle this automatically. Browser visibility is wired automatically; a native wrapper must forward its actual app-state changes: A short return resumes the saved session without counting background time. At least 30 minutes away starts a new one. The installation identity stays the same. Direct API: generate a session UUID at the new session boundary and send session_started with properties.active_duration_ms: 0. Save the session UUID, accumulated foreground duration and last-away timestamp. For a return within 30 minutes away, restore that session state and continue its heartbeats. See session identity. ","title":"Example: begin and resume a session"},{"group":"Events and metrics","href":"/user-metrics/event-design#example-populate-active-session-duration","text":"Trigger: approximately every 30 foreground seconds, plus a best-effort session finish. Result: session active time is the maximum cumulative duration observed on a reserved session event. Coal divides total observed time by sessions for the average. SDK: the SDK measures foreground time and emits heartbeats automatically. Connect the native callbacks above where applicable. Direct API: send each event at the described moment with the same session UUID and its own event UUID and occurrence time: MomentEvent nameproperties30 seconds of total foreground timesession_heartbeat{\"active_duration_ms\":30000}60 seconds of total foreground timesession_heartbeat{\"active_duration_ms\":60000}Session finishes after 75 foreground secondssession_ended{\"active_duration_ms\":75000} Use your timer's accumulated foreground milliseconds. This example produces 75 seconds of observed duration. ","title":"Example: populate active session duration"},{"group":"Events and metrics","href":"/user-metrics/event-design#example-start-complete-or-fail-a-stage","text":"Trigger: actual playable stage start, followed by its defined win or loss. Result: one attempt and its matching outcome; an attempt without an outcome remains unresolved. SDK: supply a stable stage ID, optional order and optional custom properties; retain the returned attempt UUID for the outcome: The SDK translates order to properties.progression_order and fills attempt_id and progression_id on both events. To recover a saved attempt after restart, call progressionStarted('level_1', { attemptId: savedAttemptId, order: 1 }) with the same configuration before its outcome; the SDK recovers its outstanding state. A new try uses a new UUID. See resumed attempts. Direct API: generate an attempt UUID at stage start, save it with the checkpoint and send: At victory, use the same installation and attempt UUIDs and send: For a defined loss, use progression_failed with the same correlation fields and an optional reason property. Each event gets its own event UUID and occurrence time. Outstanding attempts appear as unresolved. See the complete progression batch. ","title":"Example: start, complete or fail a stage"},{"group":"Events and metrics","href":"/user-metrics/event-design#progression-charts-and-funnel-interpretation","text":"The first observed activity, tutorial attempts, level attempts and later returns describe useful parts of your user funnel. The current charts provide daily installation, session, retention and stage-attempt/outcome summaries. Progression counts describe attempts at each stage. One installation can make several attempts, each with its own outcome. Use the progression helpers or reserved progression events for stage outcomes. Custom actions contribute to installation activity and session counts. ","title":"Progression charts and funnel interpretation"},{"group":"Events and metrics","href":"/user-metrics/event-design#implementation-checklist","text":"InputImplementationInstallation identityPersist one random installation UUID for this game across launches.Session identityGenerate a UUID at each new session boundary and reuse it throughout that session.Active durationSend cumulative foreground milliseconds on reserved session events, or use the SDK lifecycle.ProgressionPair each start with its outcome using the same installation, attempt and stage IDs.DeliveryPersist event snapshots and retry them with their original IDs, timestamps and payloads.VerificationCheck acknowledgments, then the daily summaries after aggregation. Use Verify and troubleshoot to check the expected results. ","title":"Implementation checklist"},{"group":"Shared concepts","href":"/user-metrics/concepts#collector-address","text":"All games use Coal’s shared hosted collector at https://coal-events.fly.dev. The ingestion key identifies the game and its owning organization. SDK integrations normally supply only the key, consent, and their app version. All hosted games use https://coal-events.fly.dev; the SDK supplies this address automatically. For a local or separately hosted collector, override it explicitly: Keep the public collector URL stable when moving collector machines or databases. If the URL itself changes, existing builds need an updated endpoint/default, and the SDK uses a new local identity/queue namespace. Omitting the endpoint preserves the same namespace as explicitly supplying the default URL. Keys authorize event ingestion for the game. ","title":"Collector address"},{"group":"Shared concepts","href":"/user-metrics/concepts#keys-and-collection-access","text":"An organization administrator with access to the game manages ingestion keys in User metrics → Setup. Create key activates collection and adds an independent public key for the game, while preserving other keys and their expiry dates. New keys stay active until revoked. Deploy a new key in your game, then revoke the old key when older builds no longer need it. Revocation immediately stops delivery from builds using that key. Control event delivery through the SDK’s consent and disable() controls, or revoke keys in Coal to stop accepting their events. Account eligibility still determines whether ingestion is permitted. Use the public ingestion key from Setup in your game client. Coal manages collection access for the owning organization. ","title":"Keys and collection access"},{"group":"Shared concepts","href":"/user-metrics/concepts#read-metrics-in-coal","text":"Open User metrics → Setup for the game's write-only public key. The SDK uses the shared collector automatically; its optional endpoint override is documented above for local or separately hosted collectors. Organization administrators create and revoke individual keys here. Use the example integration to grant consent, initialize the browser SDK and send a session, custom event and progression attempt. SDK diagnostics confirm accepted delivery immediately. Daily charts update after the collector's next dbt run (about 15 minutes). Overview, Retention and Progression show daily UTC aggregates. Installation cohorts start at first observed activity. Exact-day D1/D7/D30 retention appears after the return day has ended. Sessions use maximum cumulative foreground duration, attributed to their first observed UTC day. Outstanding progression attempts appear as unresolved. Today is provisional; offline delivery can update earlier summaries. ","title":"Read metrics in Coal"},{"group":"Shared concepts","href":"/user-metrics/concepts#aggregate-access","text":"Coal reads account/game-scoped daily metrics computed by the collector’s dbt worker. Your game client sends events using its ingestion key; users view the resulting metrics in Coal. ","title":"Aggregate access"},{"group":"Verify and troubleshoot","href":"/user-metrics/verification#verify-your-integration","text":"Enable collection for the selected game and initialize after consent.Track one custom action and start two progression attempts. Complete one and fail the other.Flush, check accepted delivery in SDK diagnostics, then check the daily charts after the next dbt run. Open the authenticated example integration with ?product=\u003cyour Coal project ID or slug\u003e appended to its URL.Check acknowledged and pending counts in SDK diagnostics, then verify daily session and progression counts after dbt publishes. A successful collector response means the batch committed durably.Use the example's offline transport control: enqueue an action while offline, verify pending increases, then reconnect and flush. Raw events should arrive once per event ID.Reload with pending events, grant consent again, and initialize. Verify the persisted queue recovers. The example page reads the selected game's setup and displays SDK diagnostics and daily dbt aggregates. A hosted player build needs only the public ingestion key; the SDK supplies the collector endpoint. Coal displays the resulting daily metrics. ","title":"Verify your integration"},{"group":"Verify and troubleshoot","href":"/user-metrics/verification#verify-your-metric-inputs","text":"Use an empty local game scope for these repeatable checks. CheckExpected daily resultOne consenting installation sends one new session on a UTC day.One new installation, one active installation and one session.The same session sends cumulative durations of 0, 30000 and 60000 in reserved session events.One session with 60 seconds observed active time. For the SDK, compare against its actual measured foreground duration.One tutorial attempt starts and completes.One tutorial attempt and one completion.Two level_1 attempts start; one fails and one completes.Two level attempts, one failure and one completion.A later new session reuses the same installation ID.The session count increases; the installation count stays the same. Respect the SDK's 30-minute away boundary when testing a new session.That installation sends activity on the following UTC day.It is active on the return day and qualifies as a D1 return. The D1 report appears only after that return day ends. Check each local result after dbt publishes. Start/outcome events must use matching installation, attempt and stage IDs. Follow Events and metrics for the underlying calculation and Direct API retry rules for custom clients. Also check that a restarted app retains its installation and outstanding attempt state, foreground time stops increasing while backgrounded, reporting-off stops delivery, and an offline queued batch survives restart and is acknowledged after reconnection. A repeated delivery should acknowledge the same events as duplicates. ","title":"Verify your metric inputs"},{"group":"Verify and troubleshoot","href":"/user-metrics/verification#troubleshooting","text":"SymptomWhat to checkinit() rejectsExplicit consent, a nonempty key (and a valid endpoint if overriding it), browser support, and accessible IndexedDB. Catch the initialization error.Browser CORS errorThe shared collector supports all origins. Check network access, the endpoint if overridden, and any proxy or host restrictions.pending grows while offlineExpected. Reconnect and allow retry time; the bounded queue continues to retain events.batch_rejected, status 403Game enabled, key active, owning account eligible, and collector access renewed by Coal. Delivery pauses.batch_rejected, status 400/409Invalid payload or conflicting reuse of an event ID. Correct the cause; preserve the diagnostic before clearing.HTTP 429Rate or concurrency limit. The SDK backs off automatically.HTTP 503Durable commit unavailable. The SDK retries the same IDs.persistence_errorBrowser storage transaction failed. Inspect browser storage restrictions and report the loss.queue_overflow / queue_expiredQueue bounds were exceeded or offline events aged out. Inspect dropped / expired.No final session eventUnload delivery is best effort; use the last observed cumulative heartbeat duration. Diagnostics contain enqueued, acknowledged, dropped, expired, retries, permanentErrors, persistenceErrors, pending, enabled, blocked, and retryAt (Unix milliseconds). Counters describe this SDK instance; acknowledged can include recovered events from earlier instances. A healthy queue can temporarily have pending events between flushes. ","title":"Troubleshooting"},{"group":"AI integration guide","href":"/user-metrics/ai#for-ai-coding-tools","text":"Use the Markdown version of this guide as context. It is the same maintained source as this page, including exact method names, limits, and protocol examples. The downloaded npm package includes guide.md, so an agent can read it offline. Give your coding tool this instruction together with your app's actual consent and lifecycle code: ","title":"For AI coding tools"},{"group":"AI integration guide","href":"/user-metrics/ai#custom-http-client-prompt","text":"Use the Direct API guide when implementing without the JavaScript SDK. Its Markdown version contains the exact wire contract; the complete reference includes shared concepts and verification guidance. ","title":"Custom HTTP client prompt"}]