UNPKG

@athenox/talos

Version:

Talos: a backend you describe instead of write. Declare a schema, write the rules in plain language, map URLs to sentences; a small built-in model turns them into query plans.

198 lines (148 loc) • 8.21 kB
# 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.