# Telemetry and feedback

## Ids

```ts
app.instanceId      // this defineApp: a random UUID, always Talos's own
result.callId       // every model call: in run/decide/route results, plan.callId, and the x-talos-call header
```

Someone who needs help gives one of them; everything that instance or that call
sent can then be read back.

## Feedback

```ts
await app.feedback(callId, { rating: 4 })                          // 1 to 5
await app.feedback(callId, { text: 'right rows, wrong order' })
await app.feedback(callId, { rating: 2, text: 'deleted the wrong one' })
```

A text, a rating, or both — never neither (it throws). Sent at once, even with
telemetry off: it is the developer's explicit message.

## What is sent

To `https://stats.talos.korollr.com/v1/events`, batched every 5 seconds (or
every 100 events), at most 1,000 kept in memory when the server cannot be
reached, and never able to slow down or break the app. The destination cannot be
changed; what is sent can.

## Every option

All of them, with their defaults:

```ts
defineApp({
  ...,
  telemetry: {
    errors: true,          // runtime errors right after an answer
    operations: true,      // the plans made and what they did (no values)
    prompts: true,         // the app's prompt, route templates, anonymised requests
    performance: true,     // latency of each call, load every minute
    config: true,          // versions and settings, once at start
    schema: true,          // the tables and columns, once at start
    errorWindowMs: 5000,   // how long after an answer an error still counts
    loadEveryMs: 60000,    // how often the load is sampled
  },
})
```

Leaving `telemetry` out is the same as the block above. Set a part to `false`
to stop sending it; the others are unaffected. `telemetry: false` sends
nothing at all, and `TALOS_TELEMETRY=0` or `DO_NOT_TRACK=1` in the environment
do the same for the whole process, whatever the code says. The instance id and
call ids exist either way — they are also what [feedback](#feedback) refers to.

Every event carries `type`, `instance` (the instance id) and `at` (an ISO date);
events about a call also carry its `callId`.

### `errors`

**Sends** an `error` event when something throws within `errorWindowMs` after
Talos answered: a function the plan called (`actions`), a route handler, an SQL
error on a plan, or an exception or rejection nobody caught — which would crash
the process: it is then written to disk and sent by the next instance that
starts.

```json
{ "type": "error", "callId": "ef3c…", "source": "uncaught exception", "sinceAnswerMs": 318,
  "name": "SyntaxError", "message": "Unexpected token 'C'…", "stack": "SyntaxError: …\n    at … (file:///~/app/server.ts:16:31)" }
```

`source` is `execute` (a function or the SQL), `route` (the HTTP handler),
`uncaught exception` or `unhandled rejection`. The home directory in stack
traces is replaced by `~`. Errors outside the window are not sent: they are
not about Talos.

**Turn off** if your error messages can contain personal data (an exception
that prints a customer's email). You lose: seeing why an answer failed.

`errorWindowMs` widens or narrows the window (0 keeps only errors tied to a call).

### `operations`

**Sends**, for each model call, the plan in a `call` event, and for each answer
(compiled routes included) an `answer` event:

```json
{ "type": "call", "callId": "d5e5…", "role": "member", "pinned": ["SELECT"], "refuseP": 0.04,
  "plan": { "op": "SELECT", "target": "recipes",
            "filters": [{ "column": "course", "value": "dessert", "said": true }, { "column": "title", "value": "<v>" }],
            "sets": [], "repaired": ["course = dessert (said)"] } }
{ "type": "answer", "callId": "d5e5…", "cached": false, "op": "SELECT", "target": "recipes", "status": 200, "rows": 2 }
```

A value stays only when the schema declares it (`enums`, `flags`); any other is
`<v>`. `role` is the caller's role, never their id. `pinned` is what the route
allowed, `refuseP` how close the model came to refusing, `repaired` what the
runtime corrected.

**Turn off** if even your table and column names are confidential. You lose:
knowing which plan answered a request — the main thing needed to help.

### `prompts`

**Sends** the app's `prompt` once at start (`prompt` event) and, in each `call`
event, the route's `template` (`delete the recipe {id}`) or, for a free-text
request, the sentence **anonymised** (`sentence`, and `read`: what the model
read after spelling corrections):

```json
{ "type": "prompt", "prompt": "show the recipes, the cooks and the comments.\na member may add a recipe…" }
{ "type": "call", "callId": "…", "sentence": "show the desserts by <w> <w>", "read": "show the desserts by <w> <w>" }
```

Anonymised means a word stays only if it is in the model's vocabulary or one of
the app's own words (tables, columns, values, `says`); any other becomes
`<w>` and a number `#`. "show the desserts by Jane Dupuis" leaves as
`show the desserts by <w> <w>`.

**Turn off** when your users type things that should never leave the server,
even anonymised, or when the prompt itself is confidential. You lose: seeing
what was asked — the plan alone rarely says why it was wrong.

### `performance`

**Sends** `ms` in each `call` (model time) and `answer` (execution time), and a
`load` event every `loadEveryMs`:

```json
{ "type": "load", "periodMs": 60000,
  "talos":   { "modelCalls": 12, "modelMsP50": 180.2, "modelMsP95": 410.5, "modelShare": 0.036, "answers": 850, "answerMsP50": 1.2, "answerMsP95": 6.8 },
  "process": { "cpu": 0.054, "rssMb": 196, "heapMb": 13, "eventLoopMsP50": 10.4, "eventLoopMsP99": 21.9, "uptimeS": 3600 },
  "machine": { "load": [0.4, 0.3, 0.3], "freeMemMb": 12761, "totalMemMb": 32638 } }
```

`modelShare` is the fraction of the period spent in the model; `cpu` the
process's share of all cores.

**Turn off** if you do not want your server's load reported. You lose: telling a
slow model from a slow database or an overloaded machine.

`loadEveryMs` changes the period (the per-call latencies are unaffected).

### `config`

**Sends** once at start:

```json
{ "type": "config", "talos": "0.1.0", "model": { "version": "e78f30fc6ad4", "vocab": 2166, "maxSrc": 512, "ops": ["SELECT", "…"] },
  "threads": 2, "maxSrc": 512, "node": "v24.14.0", "platform": "linux", "arch": "x64", "cpus": 12 }
```

**Turn off** if the versions you run are sensitive. You lose: reproducing a
problem with the same Talos and model.

### `schema`

**Sends** once at start the tables as declared — `as`, `table`, `columns`,
`enums`, `relations`, `lists`, `roles`, `rules`, `says`… — and the functions'
names and parameters. Code (hooks, converters, `defaults` functions) is
dropped, and no row is ever read.

```json
{ "type": "schema", "tables": [{ "as": "recipes", "table": "recipes", "columns": { "id": "id", "title": "title" },
  "enums": { "course": ["main", "dessert"] }, "roles": { "write": ["member", "admin"] } }], "functions": [] }
```

**Turn off** if your data model is confidential. You lose: rebuilding the app's
situation to understand a plan.

## Recipes

```ts
telemetry: { prompts: false }                      // nothing your users typed, nor your prompt
telemetry: { prompts: false, schema: false }       // + no data model
telemetry: { errors: true, operations: false, prompts: false, performance: false, config: true, schema: false }  // crashes and versions only
telemetry: false                                   // nothing
```

## What one call looks like

```json
[
  { "type": "call", "callId": "d5e5…", "role": "guest", "pinned": ["SELECT"],
    "plan": { "op": "SELECT", "target": "recipes",
              "filters": [{ "column": "difficulty", "value": "facile" }, { "column": "course", "value": "dessert" }] },
    "ms": 142.16 },
  { "type": "answer", "callId": "d5e5…", "op": "SELECT", "target": "recipes", "status": 200, "rows": 2, "ms": 0.59 },
  { "type": "feedback", "callId": "d5e5…", "rating": 5 }
]
```

## Who sees it

Events are received at stats.talos.korollr.com and read only by the Talos
team, to help someone who comes with an instance id or a call id.
