<!-- Kind: Integration guide. Status: Preview. Maintained with: sdk/event-collection, internal/eventcollection, analytics/event-collection and the game_events data catalog. Served at /user-metrics/event-design and /user-metrics/event-design.md. -->

# 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](/user-metrics/api#sessions-and-active-duration) and [delivery rules](/user-metrics/api#durable-queues-and-acknowledgments).

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:

```js
// 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](/user-metrics/javascript#native-wrapper-lifecycle-and-resumed-attempts).

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](/user-metrics/api#progression-attempts) 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.

```js
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](/user-metrics/api#send-your-first-batch). 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:

```json
{
  "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:

```js
// 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](/user-metrics/api#installation-and-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:

```js
// 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](/user-metrics/javascript#native-wrapper-lifecycle-and-resumed-attempts).

**Direct API:** generate an attempt UUID at stage start, save it with the checkpoint and send:

```json
{
  "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:

```json
{
  "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](/user-metrics/api#progression-attempts).

## 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](/user-metrics/verification#verify-your-metric-inputs) to check the expected results.
