Skip to documentation
Coal Developer docs
User metricsEvents and metrics
Events and metricsRead as Markdown

Events and metrics

Coal was built as a minimal collector for the common game user funnel: first-time activity, return visits, play sessions and progress through meaningful stages. Start with session collection and a few tutorial, level or run milestones. The transport can be the JavaScript SDK or your own HTTP client; the metric inputs are the same.

What Coal calculates

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.

Metric What it means Inputs you must preserve
New installations Number 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 installations Distinct 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 retention Share 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.
Sessions Distinct 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 session Total 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 / unresolved Attempts 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.

The smallest useful integration

  1. 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.
  2. 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.
  3. 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.
  4. 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.

Choose meaningful progression stages

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:

Question Stage or event Trigger in the game
Do 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.

Example: tutorial followed by a level

After initializing events through the JavaScript SDK:

// When the tutorial actually begins:
const tutorialAttempt = events.progressionStarted('tutorial', { order: 1 });
// Save tutorialAttempt with the tutorial checkpoint if it can resume later.

// When the tutorial's finish condition is reached:
events.progressionCompleted(tutorialAttempt);

// When the first playable level begins:
const levelAttempt = events.progressionStarted('level_1', { order: 2 });
// Save levelAttempt with this level's checkpoint.

// When this attempt reaches a defined loss:
events.progressionFailed(levelAttempt, {
  properties: { reason: 'out_of_time' },
});

// If the player retries, create a new attempt UUID:
const retryAttempt = events.progressionStarted('level_1', { order: 2 });
events.progressionCompleted(retryAttempt);

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 name Required progression properties
progression_started attempt_id, progression_id; optional progression_order.
progression_completed The same attempt_id and progression_id; use the same order if supplied.
progression_failed The 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.

Minimal examples: shared setup

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.

import { CoalEvents } from '@coal/event-collection';

// Once collection is permitted by your app's consent policy:
const events = await CoalEvents.init({
  consent: true,
  key: 'yourkey',
  appVersion: '1.0.0',
  platform: 'web', // Use your app's label, for example 'ios' or 'android'.
});

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.

Example: count a new installation

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:

{
  "name": "session_started",
  "properties": { "active_duration_ms": 0 }
}

Coal derives “new” from the first accepted event for that installation UUID.

Example: count activity and a return visit

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.

Example: begin and resume a session

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:

// In the native wrapper's background callback:
events.setForeground(false);

// In its foreground callback:
events.setForeground(true);

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.

Example: populate active session duration

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:

Moment Event name properties
30 seconds of total foreground time session_heartbeat {"active_duration_ms":30000}
60 seconds of total foreground time session_heartbeat {"active_duration_ms":60000}
Session finishes after 75 foreground seconds session_ended {"active_duration_ms":75000}

Use your timer's accumulated foreground milliseconds. This example produces 75 seconds of observed duration.

Example: start, complete or fail a stage

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:

// At level start:
const attemptId = events.progressionStarted('level_1', {
  order: 1,
  properties: { difficulty: 'normal' },
});
// Save attemptId with the game's checkpoint.

// At victory (choose ONE outcome for this attempt):
events.progressionCompleted(attemptId, {
  properties: { score: 120 },
});

// Failure outcome variant:
// events.progressionFailed(attemptId, {
//   properties: { reason: 'out_of_time' },
// });

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:

{
  "name": "progression_started",
  "properties": {
    "attempt_id": "00000000-0000-4000-8000-000000000034",
    "progression_id": "level_1",
    "progression_order": 1
  }
}

At victory, use the same installation and attempt UUIDs and send:

{
  "name": "progression_completed",
  "properties": {
    "attempt_id": "00000000-0000-4000-8000-000000000034",
    "progression_id": "level_1",
    "progression_order": 1,
    "score": 120
  }
}

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.

Progression charts and funnel interpretation

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.

Implementation checklist

Input Implementation
Installation identity Persist one random installation UUID for this game across launches.
Session identity Generate a UUID at each new session boundary and reuse it throughout that session.
Active duration Send cumulative foreground milliseconds on reserved session events, or use the SDK lifecycle.
Progression Pair each start with its outcome using the same installation, attempt and stage IDs.
Delivery Persist event snapshots and retry them with their original IDs, timestamps and payloads.
Verification Check acknowledgments, then the daily summaries after aggregation.

Use Verify and troubleshoot to check the expected results.