# Reading the telemetry

Measure how long your bot takes to answer with the telemetry keys on the bus. Covers what each interaction, write, and event reports, the two timing clocks, catching a failed write, joining the keys on dispatchId, and typing a payload.

When a command feels slow, you want to know whether your code or Discord caused the delay. Timing each handler yourself means a stopwatch in every `execute()`, which still misses the gates and the replies around it.

seedcord measures each dispatch for you and publishes the numbers on the bus. `interactionDispatched` fires once per interaction, after your handler finishes. `responseAttempted` fires on every write through the reply surface, so one interaction can produce several. That count includes the error card seedcord sends when your handler throws.

```ts title="src/subscribers/SlowDispatch.ts"
import { Subscribe, Subscriber } from '@seedcord/gateway';

const SLOW_MS = 2000;

@Subscribe('interactionDispatched')
export class SlowDispatch extends Subscriber<'interactionDispatched'> {
    public async execute(): Promise<void> {
        const { routeId, outcome, durationMs } = this.data;
        if (durationMs < SLOW_MS) return;

        this.logger.warn(
            `${routeId} ${outcome} in ${Math.round(durationMs)}ms`
        );
    }
}
```

`SlowDispatch` logs any dispatch that took longer than `SLOW_MS`, two seconds, with its route and outcome.

## What one `interactionDispatched` reports

{/* prettier-ignore-start */}

| field           | type                                                                                 | what it holds                                                                                        |
| --------------- | ------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- |
| `dispatchId`    | `string`                                                                             | this dispatch, unique to it                                                                          |
| `routeId`       | `string`                                                                             | the route that ran, `slash:ban` shaped                                                               |
| `interactionId` | `string`                                                                             | Discord's id for this interaction                                                                    |
| `kind`          | [`InteractionKind`](https://docs.seedcord.org/packages/core/latest/interaction-kind) | `slash`, `button`, `modal`, `autocomplete`, the five `*Menu` kinds, and the two `*ContextMenu` kinds |
| `outcome`       | [`DispatchOutcome`](https://docs.seedcord.org/packages/core/latest/dispatch-outcome) | `handled`, `refused`, or `failed`                                                                    |
| `fallback`      | `boolean`                                                                            | true when no route matched and the unhandled default ran                                             |
| `userId`        | `string \| null`                                                                     | who ran the route, null when the payload didn't carry a user                                         |
| `guildId`       | `string \| null`                                                                     | the guild it came from, null in DMs                                                                  |
| `durationMs`    | `number`                                                                             | dispatch entry until the handler returns or its error card is sent                                   |
| `queuedMs`      | `number`                                                                             | Discord's interaction creation until dispatch entry                                                  |

{/* prettier-ignore-end */}

`outcome` comes from what you threw, whether from a middleware, a gate, or the handler.

* `handled` means nothing threw.
* `refused` means a [`Silence`](https://docs.seedcord.org/packages/core/latest/silence), or a [`Notice`](https://docs.seedcord.org/packages/core/latest/notice) with `report` false.
* `failed` means a `Notice` with `report` true, or any other throw.

### The two clocks

A slow command has two possible causes: the interaction reached your bot late, or your bot took long once it arrived. The dispatch reports each one on its own clock.

```txt output
discord creates it      dispatch entry            finished
        │                     │                       │
        └───── queuedMs ──────┘                       │
                              │                       │
                              └───── durationMs ──────┘
                               on interactionDispatched

                              middleware, gates, execute()
                              one responseAttempted per write,
                              each with its own durationMs
```

`durationMs` runs on a monotonic clock, so a system clock change never moves it. The clock stops once your handler returns, or once the error card for a throw has gone out. That covers your middleware, your gates, and every reply your handler awaited. Middleware `after` callbacks run after seedcord publishes `interactionDispatched`, so their time isn't part of `durationMs`.

Every interaction id carries the moment Discord created it, built into the id itself. seedcord reads that moment back out and subtracts it from your server's clock as the dispatch starts.

```txt output
discord created it   12:00:00.000
your bot reached it  12:00:00.180
                     ────────────
queuedMs                      180
```

Here the bot reached the interaction 180ms after Discord created it, so `queuedMs` reports `180`. If the network is slow, or your bot was busy with other work, the number grows.

> **Warning**
>
> `queuedMs` compares your server's clock against Discord's. The two never agree exactly, so a server running a little behind reports a negative number. A run of negatives means your server's clock is behind, so check that it syncs over NTP. If the interaction id doesn't parse, seedcord reports `0`.

## What one `responseAttempted` reports

A dispatch can be slow because one Discord call was slow. `responseAttempted` times each write on its own, so you can tell a slow `reply` apart from a slow handler.

{/* prettier-ignore-start */}

| field           | type                                                                                 | what it holds                                                                                      |
| --------------- | ------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------- |
| `dispatchId`    | `string`                                                                             | the dispatch this write belongs to                                                                 |
| `routeId`       | `string`                                                                             | the same route id the dispatch reports                                                             |
| `interactionId` | `string`                                                                             | the same interaction id                                                                            |
| `method`        | `string` union                                                                       | `reply`, `defer`, `deferUpdate`, `update`, `followUp`, `edit`, `delete`, `showModal`, or `respond` |
| `outcome`       | [`ResponseOutcome`](https://docs.seedcord.org/packages/core/latest/response-outcome) | `sent` or `failed`                                                                                 |
| `durationMs`    | `number`                                                                             | the write itself, monotonic                                                                        |
| `messageId`     | `string \| null`                                                                     | the id of the message Discord returned                                                             |
| `error`         | `Error`                                                                              | only on a failed write                                                                             |

{/* prettier-ignore-end */}

`send` picks a method from the [ack state](/replying/ack-states), and `method` reports the one that ran. The type also allows `send`, but a published write never carries it.

`messageId` carries an id for `reply`, `update`, `followUp`, and `edit`. Every other method reports `null`, as does a failed write.

### Catching a failed write

Your handler sees its own failed write as a throw. seedcord also sends writes of its own, like the error card after a throw. Those don't run through your handler. When any write throws, seedcord publishes `outcome` `failed` with the throw on `error`. Check `outcome` first, because only the failed shape carries an `error` field.

```ts title="src/subscribers/WriteAudit.ts"
@Subscribe('responseAttempted')
export class WriteAudit extends Subscriber<'responseAttempted'> {
    public async execute(): Promise<void> {
        const write = this.data;
        if (write.outcome === 'sent') return;

        this.logger.error(
            `${write.method} failed on ${write.interactionId}`,
            write.error
        );
    }
}
```

`WriteAudit` returns early on `'sent'`, so TypeScript knows `write.error` exists on the next line.

Discord can return a `reply` or an `update` without the message it should carry. seedcord throws `ReplyCallbackMissingMessage` there. That write doesn't publish anything, since the call itself succeeded without naming a message.

## What one `eventDispatched` reports

When an event fires, seedcord runs every handler you registered for it, each behind its own fault boundary. A failing handler doesn't stop the others, so without a report per handler you'd only find it in the error log. `eventDispatched` publishes once all of those handlers have finished.

> **Gateway only**
>
> `eventDispatched` is declared in `@seedcord/gateway` alone. An http bot doesn't receive client events, so it never publishes this key.

{/* prettier-ignore-start */}

| field        | type                                                                                 | what it holds                                                        |
| ------------ | ------------------------------------------------------------------------------------ | -------------------------------------------------------------------- |
| `dispatchId` | `string`                                                                             | this dispatch of the event, the same id `eventDispatching` published |
| `name`       | `keyof ClientEvents`                                                                 | the event, `messageCreate` shaped                                    |
| `outcome`    | [`DispatchOutcome`](https://docs.seedcord.org/packages/core/latest/dispatch-outcome) | how the middleware chain ended                                       |
| `handlers`   | [`HandlerOutcome[]`](https://docs.seedcord.org/packages/core/latest/handler-outcome) | one entry per handler that ran, in order                             |
| `durationMs` | `number`                                                                             | dispatch entry until every handler finishes, monotonic               |

{/* prettier-ignore-end */}

`outcome` describes the middleware chain alone. Where the chain refuses the event, it reads `refused` and leaves `handlers` empty, since the handlers never started. Each entry in `handlers` carries its own class name and outcome.

```ts title="src/subscribers/FlakyHandlers.ts"
@Subscribe('eventDispatched')
export class FlakyHandlers extends Subscriber<'eventDispatched'> {
    public async execute(): Promise<void> {
        const { name, handlers } = this.data;

        for (const entry of handlers) {
            if (entry.outcome === 'handled') continue;
            this.logger.warn(
                `${entry.handler} ${entry.outcome} on ${name}`
            );
        }
    }
}
```

`FlakyHandlers` skips the `handled` entries and logs the rest, so one failing handler shows up even while the others succeed.

`entry` holds only the handler's name and its outcome. seedcord publishes the error itself on a fault key, with the same `dispatchId`:

* a raw throw publishes `unknownException`
* a `Notice` with `report` true publishes `handledException`

To tell which handler threw, read the fault's `origin`. On an event it names the event and the handler, like `event:messageCreate:AutoMod`. Match on `dispatchId` as well, since every dispatch of that event carries the same `origin`.

> **Warning**
>
> `queuedMs` comes from the interaction's snowflake. An event doesn't carry one, so this key omits the field. `eventDispatching` and `eventDispatched` both publish only when at least one handler is about to run, which keeps them paired.

## Joining the keys

A slow dispatch, its writes, and the fault it threw arrive as separate publishes. To see them as one story, you need a value they share. Every key on this page carries a `dispatchId`. Two runs of the same route get different ids, so keying a store on it lines up a single dispatch with everything it published.

Your code can write the same id from inside the dispatch.

```ts title="src/handlers/middlewares/Trace.ts"
@RegisterInteractionMiddleware()
export class Trace extends InteractionMiddleware {
    public async execute(): Promise<void> {
        await audit.record({
            trace: this.dispatch.id,
            route: this.dispatch.routeId
        });
    }
}
```

`Trace` records `this.dispatch.id` before any of these keys fire. A handler reaches it through `this.dispatch.id`, a gate through `ctx.dispatch.id`, and an error card through its render context.

On the subscriber side, write `dispatchId` as a column in every row and query on it.

```ts title="src/subscribers/Dispatches.ts"
@Subscribe('interactionDispatched')
export class Dispatches extends Subscriber<'interactionDispatched'> {
    public async execute(): Promise<void> {
        const { dispatchId, routeId, outcome, durationMs } =
            this.data;
        await db.insert({
            dispatchId,
            routeId,
            outcome,
            durationMs
        });
    }
}

@Subscribe('unknownException')
export class Faults extends Subscriber<'unknownException'> {
    public async execute(): Promise<void> {
        const { dispatchId, uuid, error } = this.data;
        await db.insert({
            dispatchId,
            uuid,
            message: error.message
        });
    }
}
```

`Dispatches` and `Faults` store rows you can join on that column.

> **Warning**
>
> `unknownException.dispatchId` is `string | null`. An unhandled rejection reaches Node outside any dispatch, which is the case that reports `null`.

## Typing a payload yourself

Once a subscriber grows helpers, each helper needs the payload type, and a hand-written copy of the fields goes stale when seedcord adds one. [`SubscriptionData`](https://docs.seedcord.org/packages/core/latest/subscription-data) takes a key and returns that key's payload type.

```ts title="src/subscribers/rows.ts"
function rowFor(
    data: SubscriptionData<'interactionDispatched'>
): object {
    return {
        id: data.dispatchId,
        route: data.routeId,
        ms: data.durationMs
    };
}
```

`rowFor` uses it to type its parameter. A key you declared on [`Subscriptions`](https://docs.seedcord.org/packages/core/latest/subscriptions) works the same way, as in `SubscriptionData<'levelUp'>`.

## Storing them yourself

seedcord publishes these keys and doesn't store any of them. To keep history, like a dashboard of slow routes, write a subscriber that saves each payload to your own database or metrics service.
