# Reporting faults

Send the throws your bot reports to a Discord channel through a webhook. Covers what seedcord checks at boot, writing a reporter of your own, and the one card a minute limit.

seedcord logs every reported throw, and nobody watches a production bot's terminal. A reporter posts each one to a Discord channel instead. seedcord ships two and registers both while your bot starts. Each one reads a webhook url from an environment variable. Set both, so a throw you didn't expect reaches a channel you read.

```txt title=".env"
HANDLED_EXCEPTION_WEBHOOK_URL=https://discord.com/api/webhooks/...
UNKNOWN_EXCEPTION_WEBHOOK_URL=https://discord.com/api/webhooks/...
```

{/* prettier-ignore-start */}

| variable                        | bus key            | what reaches it                                                                                                                                                             |
| ------------------------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `HANDLED_EXCEPTION_WEBHOOK_URL` | `handledException` | a [`Notice`](https://docs.seedcord.org/packages/core/latest/notice) you threw with `report` true, and every [`Fault`](https://docs.seedcord.org/packages/core/latest/fault) |
| `UNKNOWN_EXCEPTION_WEBHOOK_URL` | `unknownException` | every other throw, plus the two process listeners                                                                                                                           |

{/* prettier-ignore-end */}

The `handledException` card shows, from the top:

* the class name and its message
* the kind, the command, and the customId
* the user, guild, channel, and interaction ids
* the uuid and a stack, taken from the cause when the throw has one and from the throw itself otherwise

seedcord attaches the raw interaction as `source.json`. A fault raised in an event handler shows the event and the handler in place of the kind, and it drops the command, customId, and interaction id.

<Image src="/handled-exception-card.webp" alt="The handled exception card in Discord for a Fault thrown from the ban command, with the kind, command, and ids, the uuid, the cause's stack, and a source.json attachment" width="1336" height="1270" />

The `unknownException` card shows the guild and the user, then the uuid with the error's stack. Under that, seedcord attaches any metadata on the throw as `metadata.json`. Three timestamps appear only when the error is a `DiscordAPIError` from an interaction callback, like the 10062 a late reply throws. They show when Discord sent the interaction, when the error reached the log, and the gap between them. The card below came from a plain `Error`, so it has none.

<Image src="/unknown-exception-card.webp" alt="The unknown exception card in Discord, with the guild and user, the uuid, a stack trace, and a metadata.json attachment" width="1336" height="1260" />

## What seedcord checks at boot

An unset variable disables its reporter and logs one line.

```txt output
HandledException disabled, HANDLED_EXCEPTION_WEBHOOK_URL is not set
```

> **Warning**
>
> seedcord matches every url it finds against Discord's webhook form and throws when one doesn't match. It then asks Discord about every webhook it accepted, and a 404 or a 401 throws too. That second throw names every dead variable at once, so a first boot reports all of them together.

## Your own reporter

A reporter delivers one bus key to one webhook. Write your own when a kind of throw should go to a channel of its own, or when the shipped card is missing something you need. Subclass [`WebhookLog`](https://docs.seedcord.org/packages/gateway/latest/webhook-log), pass the key to [`@Subscribe`](https://docs.seedcord.org/packages/core/latest/subscribe), and pass the environment variable to [`@WebhookUrl`](https://docs.seedcord.org/packages/core/latest/webhook-url).

```ts title="src/subscribers/FaultMirror.ts"
import {
    BuilderComponent,
    Subscribe,
    WebhookLog,
    WebhookUrl
} from '@seedcord/gateway';

import type { WebhookReport } from '@seedcord/gateway';

@Subscribe('handledException')
@WebhookUrl('FAULT_MIRROR_WEBHOOK_URL')
export class FaultMirror extends WebhookLog<'handledException'> {
    public report(): WebhookReport {
        const { uuid, origin } = this.data;

        return {
            username: 'Faults',
            components: [new MirrorCard(uuid, origin).component]
        };
    }
}
```

`this.data` is that key's payload. Give the generic on `WebhookLog` the same key you passed `@Subscribe`, since `this.data` takes its type from the generic. Return a promise from `report()` when you have to load something before the card can be built.

{/* prettier-ignore-start */}

| field        | what it does                                                            |
| ------------ | ----------------------------------------------------------------------- |
| `components` | the card itself, and the only field you have to return                  |
| `username`   | the author name Discord shows, defaulting to your class name            |
| `avatarUrl`  | the author avatar, defaulting to the webhook's own                      |
| `files`      | bytes to upload, which a file or thumbnail component references by name |

{/* prettier-ignore-end */}

The shipped `handledException` reporter uses `files` to attach `source.json` every time. The `unknownException` one attaches `metadata.json` only when the throw carries metadata, so a process listener's card arrives without a file.

seedcord scans the folder set in `subscribers.path` for your reporters. The scaffold ships `null` there, so point it at your folder.

```ts title="src/bot.ts"
export const seedcord = new Seedcord({
    bot,
    subscribers: {
        path: resolve(import.meta.dirname, './subscribers')
    }
});
```

seedcord registers the two shipped reporters whether or not you set that path. A reporter is one kind of bus subscriber, and the Events tab covers the bus and the rest of what a subscriber like `FaultMirror` can do.

## One card a minute

One bug in a busy command can throw on every interaction. Unthrottled, that's a card per throw, and the first one scrolls away under the rest.

seedcord calls `throttleKey()` before it sends each card. The base returns `null`, so each publish sends its own card. Return a string from your override. Events sharing that string then collapse into one card a minute.

seedcord then calls `report()` with the number it dropped while the minute was running. Here is `FaultMirror` again with both methods.

```ts title="src/subscribers/FaultMirror.ts"
import {
    BuilderComponent,
    Subscribe,
    WebhookLog,
    WebhookUrl
} from '@seedcord/gateway';

import type { WebhookReport } from '@seedcord/gateway';

@Subscribe('handledException')
@WebhookUrl('FAULT_MIRROR_WEBHOOK_URL')
export class FaultMirror extends WebhookLog<'handledException'> {
    protected override throttleKey(): string {
        return `mirror:${this.data.origin}`;
    }

    public report(suppressed: number): WebhookReport {
        const { uuid, origin } = this.data;

        return {
            username: 'Faults',
            components: [
                new MirrorCard(uuid, origin, suppressed).component
            ]
        };
    }
}
```

`FaultMirror` keys on `origin` and ends its card with `+N more`. The two shipped reporters key on the origin plus the error's class name, and they end their card with this line.

```txt output
-# 12 more of these since the last report
```

seedcord keeps the count when a send fails, so the next card still carries those events.
