Error groups link work items
An error group names the work item made for it by its number in the workspace (item). One linked before keeps its old issue number as legacy_issue, whose address still leads to the work item it became.
- G1T-Session:
- ses_01m566357hejh9yeqjbetx23f3
6 files+47−100/6 viewed
| 220 | 220 | last_trace_id: string | null; | |
| 221 | 221 | /** resolved, ignored or open. A resolved group that happens again opens again. */ | |
| 222 | 222 | state: "open" | "resolved" | "ignored"; | |
| 223 | − | /** The issue made for it, so an agent can take it. */ | |
| 224 | − | issue_number: number | null; | |
| 223 | + | /** The work item made for it, by number in the workspace, so an agent can take it. */ | |
| 224 | + | item: number | null; | |
| 225 | + | /** | |
| 226 | + | * One linked before issues became work items: its old number in the | |
| 227 | + | * project's repository, whose address (`/<owner>/<repo>/issues/<n>`) | |
| 228 | + | * still leads to the work item. Null once `item` is set. | |
| 229 | + | */ | |
| 230 | + | legacy_issue: number | null; | |
| 225 | 231 | }; | |
| 226 | 232 | ||
| 227 | 233 | /** What every list query can narrow by. */ | |
| ⋯ | |||
| 334 | 340 | traces(args: ProjectRef & TraceQuery): Promise<Page<TraceSummary>>; | |
| 335 | 341 | trace(args: ProjectRef & { trace_id: string }): Promise<TraceDetail | null>; | |
| 336 | 342 | errors(args: ProjectRef & TelemetryFilter & { state?: ErrorGroup["state"] | null }): Promise<ErrorGroup[]>; | |
| 337 | − | setError(args: ProjectRef & { fingerprint: string; state?: ErrorGroup["state"]; issue_number?: number | null }): Promise<ErrorGroup | null>; | |
| 343 | + | setError(args: ProjectRef & { fingerprint: string; state?: ErrorGroup["state"]; item?: number | null }): Promise<ErrorGroup | null>; | |
| 338 | 344 | metrics(args: ProjectRef): Promise<MetricInfo[]>; | |
| 339 | 345 | metric(args: ProjectRef & TelemetryFilter & { name: string; aggregate?: "avg" | "sum" | "min" | "max" | "p50" | "p95" | "p99" | null; group_by?: string | null; step_s?: number | null }): Promise<MetricSeries>; | |
| 340 | 346 | /** What is in the store: services, environments and attribute keys seen, for filters. */ | |
| 331 | 331 | case "errors": | |
| 332 | 332 | return s.errors(args); | |
| 333 | 333 | case "set_error": | |
| 334 | − | return s.setError(String(args.fingerprint), { state: args.state as ErrorGroup["state"] | undefined, issue_number: args.issue_number }); | |
| 334 | + | return s.setError(String(args.fingerprint), { state: args.state as ErrorGroup["state"] | undefined, item: args.item }); | |
| 335 | 335 | case "metrics": | |
| 336 | 336 | return s.metrics(); | |
| 337 | 337 | case "metric": |
| 1 | + | import assert from "node:assert/strict"; | |
| 2 | + | import { DatabaseSync } from "node:sqlite"; | |
| 3 | + | import test from "node:test"; | |
| 4 | + | ||
| 5 | + | import { MIGRATIONS } from "./schema.ts"; | |
| 6 | + | ||
| 7 | + | test("an error group linked to an issue keeps its old number when issues become work items", () => { | |
| 8 | + | const db = new DatabaseSync(":memory:"); | |
| 9 | + | for (const step of MIGRATIONS.slice(0, -1)) db.exec(step); | |
| 10 | + | db.exec( | |
| 11 | + | `INSERT INTO error_groups (fingerprint, name, message, service, first_us, last_us, issue_number) | |
| 12 | + | VALUES ('fp_1', 'TypeError', 'boom', 'web', 1, 2, 7), ('fp_2', 'TypeError', 'bang', 'web', 1, 2, NULL)`, | |
| 13 | + | ); | |
| 14 | + | db.exec(MIGRATIONS.at(-1)!); | |
| 15 | + | const rows = db.prepare("SELECT fingerprint, item, legacy_issue FROM error_groups ORDER BY fingerprint").all(); | |
| 16 | + | assert.deepEqual( | |
| 17 | + | rows.map((row) => ({ ...row })), | |
| 18 | + | [ | |
| 19 | + | { fingerprint: "fp_1", item: null, legacy_issue: 7 }, | |
| 20 | + | { fingerprint: "fp_2", item: null, legacy_issue: null }, | |
| 21 | + | ], | |
| 22 | + | ); | |
| 23 | + | }); |
| 199 | 199 | ingested_bytes INTEGER NOT NULL DEFAULT 0 | |
| 200 | 200 | ); | |
| 201 | 201 | `, | |
| 202 | + | // Issues became work items, numbered in the workspace: an error group | |
| 203 | + | // links one by that number. One linked before keeps its old issue | |
| 204 | + | // number, whose address still leads to the work item it became. | |
| 205 | + | ` | |
| 206 | + | ALTER TABLE error_groups RENAME COLUMN issue_number TO legacy_issue; | |
| 207 | + | ALTER TABLE error_groups ADD COLUMN item INTEGER; | |
| 208 | + | `, | |
| 202 | 209 | ]; | |
| 203 | 210 | ||
| 204 | 211 | /** |
| 142 | 142 | assert.equal(groups.length, 1); | |
| 143 | 143 | assert.equal(groups[0].count, 2); | |
| 144 | 144 | assert.equal(groups[0].first_deployment_id, "dep_1"); | |
| 145 | − | store.setError(groups[0].fingerprint, { state: "resolved", issue_number: 12 }); | |
| 145 | + | store.setError(groups[0].fingerprint, { state: "resolved", item: 12 }); | |
| 146 | 146 | await ingestTail(store, [appEvent("bbbbbbbbbbbbbbb3", { outcome: "exception", exception: "Cannot read 'z' of user 3" })]); | |
| 147 | 147 | groups = store.errors({}); | |
| 148 | 148 | assert.equal(groups[0].state, "open"); | |
| 149 | − | assert.equal(groups[0].issue_number, 12); | |
| 149 | + | assert.equal(groups[0].item, 12); | |
| 150 | 150 | const failed = store.requests({ errors_only: true }); | |
| 151 | 151 | assert.equal(failed.items.length, 3); | |
| 152 | 152 | // An app that threw answered with an error page, whichever half said so. |
| 573 | 573 | this.sql.exec("DELETE FROM spans WHERE start_us < ?", keep); | |
| 574 | 574 | this.sql.exec("DELETE FROM metric_points WHERE time_us < ?", keep); | |
| 575 | 575 | this.sql.exec("DELETE FROM rollups WHERE minute < ?", Math.floor((now - ROLLUP_DAYS * DAY_US) / MINUTE_US)); | |
| 576 | − | this.sql.exec("DELETE FROM error_groups WHERE last_us < ? AND issue_number IS NULL", now - ROLLUP_DAYS * DAY_US); | |
| 576 | + | this.sql.exec("DELETE FROM error_groups WHERE last_us < ? AND item IS NULL AND legacy_issue IS NULL", now - ROLLUP_DAYS * DAY_US); | |
| 577 | 577 | this.sql.exec("DELETE FROM usage_days WHERE day < ?", day(now - 40 * DAY_US)); | |
| 578 | 578 | // Over its size, the store sheds a day at a time from the oldest. | |
| 579 | 579 | for (let i = 0; i < 30 && this.state.storage.sql.databaseSize > MAX_BYTES; i++) { | |
| ⋯ | |||
| 876 | 876 | return this.rows(`SELECT * FROM error_groups ${clauses.length ? `WHERE ${clauses.join(" AND ")}` : ""} ORDER BY last_us DESC LIMIT 200`, ...args).map(errorOf); | |
| 877 | 877 | } | |
| 878 | 878 | ||
| 879 | − | setError(fp: string, change: { state?: ErrorGroup["state"]; issue_number?: number | null }): ErrorGroup | null { | |
| 879 | + | setError(fp: string, change: { state?: ErrorGroup["state"]; item?: number | null }): ErrorGroup | null { | |
| 880 | 880 | if (change.state && !["open", "resolved", "ignored"].includes(change.state)) throw new Error("State is open, resolved or ignored."); | |
| 881 | 881 | if (change.state) this.sql.exec("UPDATE error_groups SET state = ? WHERE fingerprint = ?", change.state, fp); | |
| 882 | − | if (change.issue_number !== undefined) this.sql.exec("UPDATE error_groups SET issue_number = ? WHERE fingerprint = ?", change.issue_number, fp); | |
| 882 | + | if (change.item !== undefined) this.sql.exec("UPDATE error_groups SET item = ?, legacy_issue = NULL WHERE fingerprint = ?", change.item, fp); | |
| 883 | 883 | const row = this.rows("SELECT * FROM error_groups WHERE fingerprint = ?", fp)[0]; | |
| 884 | 884 | return row ? errorOf(row) : null; | |
| 885 | 885 | } | |
| ⋯ | |||
| 1128 | 1128 | last_request_id: strOrNull(r.last_request_id), | |
| 1129 | 1129 | last_trace_id: strOrNull(r.last_trace_id), | |
| 1130 | 1130 | state: String(r.state) as ErrorGroup["state"], | |
| 1131 | − | issue_number: numOrNull(r.issue_number), | |
| 1131 | + | item: numOrNull(r.item), | |
| 1132 | + | legacy_issue: numOrNull(r.legacy_issue), | |
| 1132 | 1133 | }; | |
| 1133 | 1134 | } | |
| 1134 | 1135 | ||
Comments
Sign in to comment.