@obsunified/collector 2.0.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth.d.ts +1 -1
- package/dist/auth.js +1 -1
- package/dist/{chunk-UGWDOGGH.js → chunk-KMEZKGSO.js} +22 -10
- package/dist/chunk-KMEZKGSO.js.map +1 -0
- package/dist/{chunk-XKQTVIRE.js → chunk-ZSF5XP3B.js} +8 -2
- package/dist/chunk-ZSF5XP3B.js.map +1 -0
- package/dist/index.d.ts +630 -27
- package/dist/index.js +5819 -853
- package/dist/index.js.map +1 -1
- package/dist/{ingest-auth-HFVO1zIp.d.ts → ingest-auth-C5-8Mly5.d.ts} +9 -0
- package/dist/{openai-FJYOQ7WO.js → openai-GSJZGFGL.js} +2 -2
- package/package.json +5 -5
- package/src/__tests__/autosre-closed-loop.test.ts +1275 -0
- package/src/analyses/derive.ts +26 -25
- package/src/analyses/investigations.ts +8 -8
- package/src/analyses/tier0.ts +22 -22
- package/src/auth/dashboard-auth.test.ts +791 -0
- package/src/auth/dashboard-auth.ts +187 -38
- package/src/auth/login-rate-limit.test.ts +129 -0
- package/src/auth/login-rate-limit.ts +143 -0
- package/src/framework/body-limit.test.ts +165 -0
- package/src/framework/collector.ts +355 -115
- package/src/framework/env.ts +9 -0
- package/src/framework/limits.ts +31 -0
- package/src/framework/scheduled-handlers.test.ts +342 -0
- package/src/index.ts +59 -1
- package/src/lib/action-retention.test.ts +472 -0
- package/src/lib/action-retention.ts +420 -0
- package/src/lib/actions-store.ts +6 -12
- package/src/lib/ai-store/calls.ts +2 -2
- package/src/lib/ai-store/sessions.ts +3 -3
- package/src/lib/ai-store/spans.ts +1 -1
- package/src/lib/ai-store/types.ts +2 -1
- package/src/lib/alerts-store.test.ts +153 -0
- package/src/lib/alerts-store.ts +122 -21
- package/src/lib/analyses-runner.test.ts +75 -0
- package/src/lib/analyses-runner.ts +69 -3
- package/src/lib/analyses-store-latest.test.ts +61 -0
- package/src/lib/analyses-store.ts +17 -10
- package/src/lib/autosre-change-correlation.ts +780 -0
- package/src/lib/autosre-findings.ts +725 -0
- package/src/lib/autosre-ontology.ts +914 -0
- package/src/lib/autosre-remediation-gate.ts +1663 -0
- package/src/lib/blob-store.ts +1 -1
- package/src/lib/eval-cases-store.ts +63 -1
- package/src/lib/evidence-references.ts +6 -0
- package/src/lib/housekeeping.test.ts +153 -0
- package/src/lib/housekeeping.ts +193 -0
- package/src/lib/identity-index/action-lookups.ts +6 -6
- package/src/lib/identity-index/key-lookups.ts +2 -2
- package/src/lib/identity-index/user-lookup.ts +8 -5
- package/src/lib/identity-index.test.ts +2 -3
- package/src/lib/metrics-store-series.test.ts +99 -0
- package/src/lib/metrics-store.test.ts +23 -0
- package/src/lib/metrics-store.ts +27 -3
- package/src/lib/projects-store.test.ts +25 -0
- package/src/lib/projects-store.ts +4 -3
- package/src/lib/sql-db-dialect.test.ts +12 -3
- package/src/lib/sql-db-postgres.integration.test.ts +1225 -0
- package/src/lib/sql-db-postgres.test.ts +314 -0
- package/src/lib/sql-db-postgres.ts +392 -54
- package/src/lib/sql-db-sqlite-time-windows.test.ts +194 -0
- package/src/lib/sql-db.ts +40 -6
- package/src/lib/store/ingest.ts +94 -6
- package/src/lib/test-utils/action-graph-fixtures.ts +307 -0
- package/src/lib/test-utils/node-sqlite-db.ts +188 -0
- package/src/lib/webhook-url.test.ts +122 -0
- package/src/lib/webhook-url.ts +155 -0
- package/src/migrations/041_ingest_key_hash_text.sql +5 -0
- package/src/migrations/042_session_replay_project_composite_pk.sql +52 -0
- package/src/migrations/043_metric_exemplars_text_timestamps.sql +5 -0
- package/src/migrations/044_actions_started_at_index.sql +10 -0
- package/src/migrations/045_user_profiles_project_composite_pk.sql +40 -0
- package/src/migrations/046_action_graph_project_composite_pk.sql +255 -0
- package/src/migrations/047_ai_span_payloads_project_composite_pk.sql +61 -0
- package/src/migrations/048_telemetry_spans_project_composite_pk.sql +92 -0
- package/src/migrations/049_alert_state_notified.sql +17 -0
- package/src/migrations/050_autosre_causal_ontology.sql +216 -0
- package/src/migrations/alert-state-notified.test.ts +111 -0
- package/src/migrations/parity.test.ts +68 -0
- package/src/migrations/project-scoped-keys.test.ts +432 -0
- package/src/migrations-postgres/009_projects_and_keys.sql +4 -4
- package/src/migrations-postgres/026_obs_dashboard_project_seed.sql +1 -1
- package/src/migrations-postgres/035_metric_exemplars.sql +5 -2
- package/src/migrations-postgres/041_ingest_key_hash_text.sql +32 -0
- package/src/migrations-postgres/042_session_replay_project_composite_pk.sql +16 -0
- package/src/migrations-postgres/043_metric_exemplars_text_timestamps.sql +47 -0
- package/src/migrations-postgres/044_actions_started_at_index.sql +8 -0
- package/src/migrations-postgres/045_user_profiles_project_composite_pk.sql +33 -0
- package/src/migrations-postgres/046_action_graph_project_composite_pk.sql +39 -0
- package/src/migrations-postgres/047_ai_span_payloads_project_composite_pk.sql +30 -0
- package/src/migrations-postgres/048_telemetry_spans_project_composite_pk.sql +37 -0
- package/src/migrations-postgres/049_alert_state_notified.sql +10 -0
- package/src/migrations-postgres/050_autosre_causal_ontology.sql +216 -0
- package/src/migrations-postgres/README.md +58 -5
- package/src/otlp/decode/body.test.ts +76 -0
- package/src/otlp/decode/body.ts +67 -6
- package/src/plugins/_json.ts +21 -0
- package/src/plugins/action-graph-processor.ts +6 -12
- package/src/plugins/ai-receiver.ts +5 -12
- package/src/plugins/ai-span-payloads-processor.ts +1 -2
- package/src/plugins/alerts-evaluator-retention.test.ts +170 -0
- package/src/plugins/alerts-evaluator.test.ts +464 -3
- package/src/plugins/alerts-evaluator.ts +181 -41
- package/src/plugins/alerts-routes.ts +10 -6
- package/src/plugins/ask-routes.ts +4 -5
- package/src/plugins/autosre-routes.ts +1019 -0
- package/src/plugins/connected-routes/manifest.ts +13 -1
- package/src/plugins/connected-routes.test.ts +106 -0
- package/src/plugins/connected-routes.ts +285 -5
- package/src/plugins/dashboard-routes.test.ts +120 -0
- package/src/plugins/dashboard-routes.ts +11 -2
- package/src/plugins/default-span-enrichment.ts +13 -1
- package/src/plugins/eval-cases-routes.ts +65 -19
- package/src/plugins/evidence-retrieval-routes.ts +300 -4
- package/src/plugins/identity-receiver.ts +9 -11
- package/src/plugins/json-body.test.ts +91 -0
- package/src/plugins/onboarding-routes.test.ts +105 -0
- package/src/plugins/onboarding-routes.ts +26 -11
- package/src/plugins/projects-routes.ts +2 -1
- package/src/plugins/replay-isolation.test.ts +922 -0
- package/src/plugins/replay-receiver.ts +8 -8
- package/src/plugins/usage-receiver.ts +3 -6
- package/dist/chunk-UGWDOGGH.js.map +0 -1
- package/dist/chunk-XKQTVIRE.js.map +0 -1
- /package/dist/{openai-FJYOQ7WO.js.map → openai-GSJZGFGL.js.map} +0 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The scheduled maintenance handlers expose a runtime-neutral `run(env)`
|
|
3
|
+
* alongside the Cloudflare `scheduled(event, env, ctx)` adapter. These tests
|
|
4
|
+
* pin two contracts non-Workers hosts (the Node collector) rely on:
|
|
5
|
+
*
|
|
6
|
+
* 1. `scheduled` is a pure delegate — it issues exactly the SQL `run` does
|
|
7
|
+
* and never reads the event or context.
|
|
8
|
+
* 2. `run` reports a summary and, for analyses, converts failures into a
|
|
9
|
+
* logged `undefined` rather than a rejection.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, expect, it, vi } from "vitest";
|
|
13
|
+
import { MemSqlDb } from "../lib/test-utils/mem-sql-db";
|
|
14
|
+
import {
|
|
15
|
+
createAnalysesRunHandler,
|
|
16
|
+
createRetentionCleanupHandler,
|
|
17
|
+
} from "./collector";
|
|
18
|
+
import type { CollectorEnv } from "./env";
|
|
19
|
+
import type { Logger } from "./logger";
|
|
20
|
+
|
|
21
|
+
const makeLogger = () =>
|
|
22
|
+
({
|
|
23
|
+
debug: vi.fn(),
|
|
24
|
+
info: vi.fn(),
|
|
25
|
+
warn: vi.fn(),
|
|
26
|
+
error: vi.fn(),
|
|
27
|
+
}) satisfies Logger;
|
|
28
|
+
|
|
29
|
+
const envFor = (db: MemSqlDb): CollectorEnv =>
|
|
30
|
+
({ DB: db }) as unknown as CollectorEnv;
|
|
31
|
+
|
|
32
|
+
// The Workers adapter must not touch these; passing non-objects proves it.
|
|
33
|
+
const NO_EVENT = undefined as unknown as ScheduledEvent;
|
|
34
|
+
const NO_CTX = undefined as unknown as ExecutionContext;
|
|
35
|
+
|
|
36
|
+
describe("createRetentionCleanupHandler", () => {
|
|
37
|
+
it("run(env) purges each table and returns the per-table counts", async () => {
|
|
38
|
+
const logger = makeLogger();
|
|
39
|
+
// Profile rows are deleted by id after their blobs are handled, so the
|
|
40
|
+
// sweep must see expired rows to count them.
|
|
41
|
+
const db = new MemSqlDb({
|
|
42
|
+
run: () => ({ changes: 3 }),
|
|
43
|
+
all: (sql) =>
|
|
44
|
+
sql.includes("FROM profile_blobs")
|
|
45
|
+
? [
|
|
46
|
+
{ id: "p1", blob_url: "default/1/p1" },
|
|
47
|
+
{ id: "p2", blob_url: "default/2/p2" },
|
|
48
|
+
{ id: "p3", blob_url: "default/3/p3" },
|
|
49
|
+
]
|
|
50
|
+
: [],
|
|
51
|
+
});
|
|
52
|
+
const handler = createRetentionCleanupHandler({ logger });
|
|
53
|
+
|
|
54
|
+
const summary = await handler.run(envFor(db));
|
|
55
|
+
|
|
56
|
+
expect(summary).toMatchObject({
|
|
57
|
+
spans: 3,
|
|
58
|
+
usage: 3,
|
|
59
|
+
logs: 3,
|
|
60
|
+
analysis_results: 3,
|
|
61
|
+
profile_blobs: 3,
|
|
62
|
+
});
|
|
63
|
+
// The AI and metrics stores purge several tables each and sum the
|
|
64
|
+
// changes, so their counts are a multiple of the per-statement value.
|
|
65
|
+
expect(summary.ai_calls).toBeGreaterThanOrEqual(3);
|
|
66
|
+
expect(summary.ai_calls % 3).toBe(0);
|
|
67
|
+
expect(summary.metric_points).toBeGreaterThanOrEqual(3);
|
|
68
|
+
expect(summary.metric_points % 3).toBe(0);
|
|
69
|
+
expect(db.callsMatching("DELETE FROM profile_blobs")).toHaveLength(1);
|
|
70
|
+
// No expired action candidates were returned, so the action graph
|
|
71
|
+
// sweep selects once and never issues a DELETE against the spine.
|
|
72
|
+
expect(summary).toMatchObject({ actions: 0, action_leaves: 0 });
|
|
73
|
+
// One deletable-tree probe plus the oversized report; no DELETEs.
|
|
74
|
+
expect(db.callsMatching("FROM actions a")).toHaveLength(2);
|
|
75
|
+
expect(db.callsMatching("DELETE FROM actions")).toHaveLength(0);
|
|
76
|
+
expect(logger.info).toHaveBeenCalledWith(
|
|
77
|
+
"[retention-cleanup] purged expired rows",
|
|
78
|
+
expect.objectContaining({ spans: 3, profile_blobs: 3, actions: 0 }),
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("deletes whole expired trees, leaves before the spine, scoped by project, and honours RETENTION_HOURS", async () => {
|
|
83
|
+
const logger = makeLogger();
|
|
84
|
+
let selects = 0;
|
|
85
|
+
const db = new MemSqlDb({
|
|
86
|
+
all: (sql) => {
|
|
87
|
+
if (!sql.includes("FROM actions a")) return [];
|
|
88
|
+
// Oversized report query: nothing to report.
|
|
89
|
+
if (sql.includes("HAVING COUNT(*) >")) return [];
|
|
90
|
+
// First batch: two trees in two projects; afterwards nothing.
|
|
91
|
+
selects += 1;
|
|
92
|
+
return selects === 1
|
|
93
|
+
? [
|
|
94
|
+
{
|
|
95
|
+
project_id: "alpha",
|
|
96
|
+
root_action_id: "root-a",
|
|
97
|
+
action_count: 3,
|
|
98
|
+
},
|
|
99
|
+
{ project_id: "beta", root_action_id: "root-b", action_count: 1 },
|
|
100
|
+
]
|
|
101
|
+
: [];
|
|
102
|
+
},
|
|
103
|
+
run: () => ({ changes: 1 }),
|
|
104
|
+
});
|
|
105
|
+
const env = { DB: db, RETENTION_HOURS: "24" } as unknown as CollectorEnv;
|
|
106
|
+
|
|
107
|
+
const summary = await createRetentionCleanupHandler({ logger }).run(env);
|
|
108
|
+
|
|
109
|
+
// MemSqlDb reports 1 change per statement, and each tree gets its own
|
|
110
|
+
// statement per table: 2 spine statements, 2 × (4 leaf tables +
|
|
111
|
+
// agent_runs) leaf statements.
|
|
112
|
+
expect(summary.actions).toBe(2);
|
|
113
|
+
expect(summary.action_leaves).toBe(2 * 5);
|
|
114
|
+
|
|
115
|
+
const select = db.callsMatching("HAVING COUNT(*) <=")[0];
|
|
116
|
+
const [cutoff, sizeCap, cutoffAgain, limit] = select?.binds ?? [];
|
|
117
|
+
expect(cutoff).toBe(cutoffAgain);
|
|
118
|
+
expect(typeof cutoff).toBe("string");
|
|
119
|
+
// RETENTION_HOURS=24 → the cutoff is ~24h ago, not the 72h default.
|
|
120
|
+
const ageHours = (Date.now() - Date.parse(cutoff as string)) / 3_600_000;
|
|
121
|
+
expect(ageHours).toBeGreaterThan(23.9);
|
|
122
|
+
expect(ageHours).toBeLessThan(24.1);
|
|
123
|
+
expect(sizeCap).toBe(500);
|
|
124
|
+
expect(limit).toBe(25);
|
|
125
|
+
|
|
126
|
+
const deletes = db.calls
|
|
127
|
+
.filter(
|
|
128
|
+
(c) =>
|
|
129
|
+
c.op === "run" &&
|
|
130
|
+
/^DELETE FROM (tool_calls|retrieval_events|eval_results|artifacts|agent_runs|actions)\s/.test(
|
|
131
|
+
c.sql.trim(),
|
|
132
|
+
),
|
|
133
|
+
)
|
|
134
|
+
.map((c) => ({
|
|
135
|
+
table: /^DELETE FROM (\w+)/.exec(c.sql.trim())?.[1],
|
|
136
|
+
sql: c.sql,
|
|
137
|
+
binds: c.binds,
|
|
138
|
+
}));
|
|
139
|
+
expect(deletes.map((d) => d.table)).toEqual([
|
|
140
|
+
"tool_calls",
|
|
141
|
+
"retrieval_events",
|
|
142
|
+
"eval_results",
|
|
143
|
+
"artifacts",
|
|
144
|
+
"agent_runs",
|
|
145
|
+
"actions",
|
|
146
|
+
"tool_calls",
|
|
147
|
+
"retrieval_events",
|
|
148
|
+
"eval_results",
|
|
149
|
+
"artifacts",
|
|
150
|
+
"agent_runs",
|
|
151
|
+
"actions",
|
|
152
|
+
]);
|
|
153
|
+
// Leaf deletes resolve the tree inside the statement and re-check
|
|
154
|
+
// eligibility; the spine delete is by tree, never by id list.
|
|
155
|
+
expect(deletes[0]?.binds.slice(0, 3)).toEqual(["alpha", "alpha", "root-a"]);
|
|
156
|
+
expect(deletes[0]?.sql).toContain("root_action_id = ?");
|
|
157
|
+
expect(deletes[5]?.binds.slice(0, 2)).toEqual(["alpha", "root-a"]);
|
|
158
|
+
expect(deletes[5]?.sql).not.toContain("IN (?");
|
|
159
|
+
expect(deletes[6]?.binds.slice(0, 3)).toEqual(["beta", "beta", "root-b"]);
|
|
160
|
+
for (const d of deletes) {
|
|
161
|
+
expect(d.binds[0]).toMatch(/^(alpha|beta)$/);
|
|
162
|
+
expect(d.sql).toContain("NOT EXISTS");
|
|
163
|
+
expect(d.binds).toContain(cutoff);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("warns about expired trees retained for exceeding the per-batch cap", async () => {
|
|
168
|
+
const logger = makeLogger();
|
|
169
|
+
const db = new MemSqlDb({
|
|
170
|
+
all: (sql) =>
|
|
171
|
+
sql.includes("HAVING COUNT(*) >")
|
|
172
|
+
? [{ project_id: "alpha", root_action_id: "huge", action_count: 900 }]
|
|
173
|
+
: [],
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const summary = await createRetentionCleanupHandler({ logger }).run(
|
|
177
|
+
envFor(db),
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
expect(summary).toMatchObject({ actions: 0, action_leaves: 0 });
|
|
181
|
+
expect(db.callsMatching("DELETE FROM actions")).toHaveLength(0);
|
|
182
|
+
expect(logger.warn).toHaveBeenCalledWith(
|
|
183
|
+
"[retention-cleanup] expired action trees retained because they exceed the per-batch action cap",
|
|
184
|
+
expect.objectContaining({
|
|
185
|
+
oversized_trees: 1,
|
|
186
|
+
trees: [{ project_id: "alpha", root_action_id: "huge", actions: 900 }],
|
|
187
|
+
}),
|
|
188
|
+
);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("logs and keeps the other purge counts when the action graph sweep fails", async () => {
|
|
192
|
+
const logger = makeLogger();
|
|
193
|
+
const db = new MemSqlDb({
|
|
194
|
+
all: (sql) => {
|
|
195
|
+
if (sql.includes("FROM actions a"))
|
|
196
|
+
throw new Error("no such table: actions");
|
|
197
|
+
return [];
|
|
198
|
+
},
|
|
199
|
+
run: () => ({ changes: 2 }),
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const summary = await createRetentionCleanupHandler({ logger }).run(
|
|
203
|
+
envFor(db),
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
expect(summary).toMatchObject({ spans: 2, actions: 0, action_leaves: 0 });
|
|
207
|
+
expect(logger.error).toHaveBeenCalledWith(
|
|
208
|
+
"[retention-cleanup] action graph purge failed",
|
|
209
|
+
expect.objectContaining({ error: "no such table: actions" }),
|
|
210
|
+
);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("scheduled(event, env, ctx) delegates to run(env) and ignores event/ctx", async () => {
|
|
214
|
+
const viaRun = new MemSqlDb({ all: () => [] });
|
|
215
|
+
const viaScheduled = new MemSqlDb({ all: () => [] });
|
|
216
|
+
const handler = createRetentionCleanupHandler({ logger: makeLogger() });
|
|
217
|
+
|
|
218
|
+
await handler.run(envFor(viaRun));
|
|
219
|
+
await expect(
|
|
220
|
+
handler.scheduled(NO_EVENT, envFor(viaScheduled), NO_CTX),
|
|
221
|
+
).resolves.toBeUndefined();
|
|
222
|
+
|
|
223
|
+
expect(viaRun.calls.length).toBeGreaterThan(0);
|
|
224
|
+
expect(viaScheduled.calls.map((c) => c.sql)).toEqual(
|
|
225
|
+
viaRun.calls.map((c) => c.sql),
|
|
226
|
+
);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("deletes expired profile blobs from PROFILES_BUCKET when present", async () => {
|
|
230
|
+
const db = new MemSqlDb({
|
|
231
|
+
all: (sql) =>
|
|
232
|
+
sql.includes("FROM profile_blobs")
|
|
233
|
+
? [
|
|
234
|
+
{ id: "p1", blob_url: "default/1/p1.pprof.gz" },
|
|
235
|
+
{ id: "p2", blob_url: "default/2/p2.pprof.gz" },
|
|
236
|
+
]
|
|
237
|
+
: [],
|
|
238
|
+
run: () => ({ changes: 2 }),
|
|
239
|
+
});
|
|
240
|
+
const bucketDelete = vi.fn(async () => {});
|
|
241
|
+
const env = {
|
|
242
|
+
DB: db,
|
|
243
|
+
PROFILES_BUCKET: { delete: bucketDelete },
|
|
244
|
+
} as unknown as CollectorEnv;
|
|
245
|
+
|
|
246
|
+
const summary = await createRetentionCleanupHandler({
|
|
247
|
+
logger: makeLogger(),
|
|
248
|
+
}).run(env);
|
|
249
|
+
|
|
250
|
+
expect(summary.profile_blobs).toBe(2);
|
|
251
|
+
expect(bucketDelete).toHaveBeenCalledTimes(2);
|
|
252
|
+
expect(bucketDelete).toHaveBeenCalledWith("default/1/p1.pprof.gz");
|
|
253
|
+
// Rows are deleted by id for the blobs that were removed, never by a
|
|
254
|
+
// blanket expires_at predicate that could outrun the blob page.
|
|
255
|
+
const deletes = db.callsMatching("DELETE FROM profile_blobs");
|
|
256
|
+
expect(deletes).toHaveLength(1);
|
|
257
|
+
expect(deletes[0]?.sql).toContain("WHERE id IN (?, ?)");
|
|
258
|
+
expect(deletes[0]?.binds).toEqual(["p1", "p2"]);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("keeps the profile row when its blob could not be deleted", async () => {
|
|
262
|
+
const db = new MemSqlDb({
|
|
263
|
+
all: (sql) =>
|
|
264
|
+
sql.includes("FROM profile_blobs")
|
|
265
|
+
? [
|
|
266
|
+
{ id: "p1", blob_url: "default/1/p1.pprof.gz" },
|
|
267
|
+
{ id: "p2", blob_url: "default/2/p2.pprof.gz" },
|
|
268
|
+
]
|
|
269
|
+
: [],
|
|
270
|
+
run: () => ({ changes: 1 }),
|
|
271
|
+
});
|
|
272
|
+
const bucketDelete = vi.fn(async (key: string) => {
|
|
273
|
+
if (key.includes("p2")) throw new Error("r2 unavailable");
|
|
274
|
+
});
|
|
275
|
+
const logger = makeLogger();
|
|
276
|
+
const env = {
|
|
277
|
+
DB: db,
|
|
278
|
+
PROFILES_BUCKET: { delete: bucketDelete },
|
|
279
|
+
} as unknown as CollectorEnv;
|
|
280
|
+
|
|
281
|
+
const summary = await createRetentionCleanupHandler({ logger }).run(env);
|
|
282
|
+
|
|
283
|
+
expect(summary.profile_blobs).toBe(1);
|
|
284
|
+
const deletes = db.callsMatching("DELETE FROM profile_blobs");
|
|
285
|
+
expect(deletes).toHaveLength(1);
|
|
286
|
+
expect(deletes[0]?.binds).toEqual(["p1"]);
|
|
287
|
+
expect(logger.warn).toHaveBeenCalledWith(
|
|
288
|
+
"[retention-cleanup] profile blob delete failed; row kept for retry",
|
|
289
|
+
expect.objectContaining({ profile_id: "p2" }),
|
|
290
|
+
);
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
describe("createAnalysesRunHandler", () => {
|
|
295
|
+
it("run(env) returns the run summary when nothing is due", async () => {
|
|
296
|
+
const logger = makeLogger();
|
|
297
|
+
const db = new MemSqlDb({ all: () => [], first: () => null });
|
|
298
|
+
const handler = createAnalysesRunHandler({ logger });
|
|
299
|
+
|
|
300
|
+
const summary = await handler.run(envFor(db));
|
|
301
|
+
|
|
302
|
+
expect(summary).toMatchObject({ ran: 0, failed: 0, narrated: 0 });
|
|
303
|
+
expect(logger.info).toHaveBeenCalledWith(
|
|
304
|
+
"[analyses] scheduled run summary",
|
|
305
|
+
expect.objectContaining({ ran: 0, failed: 0 }),
|
|
306
|
+
);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("run(env) resolves undefined and logs when the database fails", async () => {
|
|
310
|
+
const logger = makeLogger();
|
|
311
|
+
const db = new MemSqlDb({
|
|
312
|
+
all: () => {
|
|
313
|
+
throw new Error("relation analysis_definitions does not exist");
|
|
314
|
+
},
|
|
315
|
+
});
|
|
316
|
+
const handler = createAnalysesRunHandler({ logger });
|
|
317
|
+
|
|
318
|
+
await expect(handler.run(envFor(db))).resolves.toBeUndefined();
|
|
319
|
+
expect(logger.error).toHaveBeenCalledWith(
|
|
320
|
+
"[analyses] scheduled run failed",
|
|
321
|
+
expect.objectContaining({
|
|
322
|
+
error: "relation analysis_definitions does not exist",
|
|
323
|
+
}),
|
|
324
|
+
);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("scheduled(event, env, ctx) delegates to run(env) and ignores event/ctx", async () => {
|
|
328
|
+
const viaRun = new MemSqlDb({ all: () => [], first: () => null });
|
|
329
|
+
const viaScheduled = new MemSqlDb({ all: () => [], first: () => null });
|
|
330
|
+
const handler = createAnalysesRunHandler({ logger: makeLogger() });
|
|
331
|
+
|
|
332
|
+
await handler.run(envFor(viaRun));
|
|
333
|
+
await expect(
|
|
334
|
+
handler.scheduled(NO_EVENT, envFor(viaScheduled), NO_CTX),
|
|
335
|
+
).resolves.toBeUndefined();
|
|
336
|
+
|
|
337
|
+
expect(viaRun.calls.length).toBeGreaterThan(0);
|
|
338
|
+
expect(viaScheduled.calls.map((c) => c.sql)).toEqual(
|
|
339
|
+
viaRun.calls.map((c) => c.sql),
|
|
340
|
+
);
|
|
341
|
+
});
|
|
342
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import { alertsRoutesPlugin } from "./plugins/alerts-routes";
|
|
14
14
|
import { analysesRoutesPlugin } from "./plugins/analyses-routes";
|
|
15
15
|
import { askRoutesPlugin } from "./plugins/ask-routes";
|
|
16
|
+
import { autosreRoutesPlugin } from "./plugins/autosre-routes";
|
|
16
17
|
import { botFilterPlugin } from "./plugins/bot-filter";
|
|
17
18
|
import { connectedRoutesPlugin } from "./plugins/connected-routes";
|
|
18
19
|
import { dashboardRoutesPlugin } from "./plugins/dashboard-routes";
|
|
@@ -44,11 +45,15 @@ import { usageReceiverPlugin } from "./plugins/usage-receiver";
|
|
|
44
45
|
import { usersQueryRoutesPlugin } from "./plugins/users-query-routes";
|
|
45
46
|
import { utmEnrichmentPlugin } from "./plugins/utm-enrichment";
|
|
46
47
|
|
|
47
|
-
export {
|
|
48
|
+
export {
|
|
49
|
+
createDashboardAuth,
|
|
50
|
+
type DashboardAuthConfig,
|
|
51
|
+
} from "./auth/dashboard-auth";
|
|
48
52
|
export { createIngestAuth } from "./auth/ingest-auth";
|
|
49
53
|
export type { TailEvent, TailKind } from "./durable-objects/tail-hub";
|
|
50
54
|
export { TailHub } from "./durable-objects/tail-hub";
|
|
51
55
|
export {
|
|
56
|
+
type AnalysesRunSummary,
|
|
52
57
|
type CollectorAuthConfig,
|
|
53
58
|
type CollectorConfig,
|
|
54
59
|
type CollectorPlugin,
|
|
@@ -56,6 +61,8 @@ export {
|
|
|
56
61
|
createAnalysesRunHandler,
|
|
57
62
|
createRetentionCleanupHandler,
|
|
58
63
|
createTelemetryCollectorApp,
|
|
64
|
+
type RetentionCleanupSummary,
|
|
65
|
+
type ScheduledTaskHandler,
|
|
59
66
|
type SpanProcessorPlugin,
|
|
60
67
|
type SqlDbFactory,
|
|
61
68
|
type UsageEventProcessorPlugin,
|
|
@@ -69,6 +76,12 @@ export type {
|
|
|
69
76
|
|
|
70
77
|
export type { Logger } from "./framework/logger";
|
|
71
78
|
export { consoleLogger } from "./framework/logger";
|
|
79
|
+
export {
|
|
80
|
+
type ActionGraphPurgeSummary,
|
|
81
|
+
type ActionGraphRetentionOptions,
|
|
82
|
+
actionRetentionCutoff,
|
|
83
|
+
purgeExpiredActionGraph,
|
|
84
|
+
} from "./lib/action-retention";
|
|
72
85
|
export {
|
|
73
86
|
type ActionRecord,
|
|
74
87
|
ActionStore,
|
|
@@ -79,6 +92,46 @@ export {
|
|
|
79
92
|
type ToolCallRecord,
|
|
80
93
|
} from "./lib/actions-store";
|
|
81
94
|
export { AlertsStore, compareValue } from "./lib/alerts-store";
|
|
95
|
+
export {
|
|
96
|
+
computeChangeBlastRadius,
|
|
97
|
+
findCrossTraceContention,
|
|
98
|
+
getChangeEvent,
|
|
99
|
+
listChangeEvents,
|
|
100
|
+
recordChangeEvent,
|
|
101
|
+
} from "./lib/autosre-change-correlation";
|
|
102
|
+
export {
|
|
103
|
+
CounterfactualVerificationError,
|
|
104
|
+
getInvestigationFinding,
|
|
105
|
+
recordInvestigationFinding,
|
|
106
|
+
searchInvestigationFindings,
|
|
107
|
+
verifyPairedCounterfactualProof,
|
|
108
|
+
} from "./lib/autosre-findings";
|
|
109
|
+
export {
|
|
110
|
+
computeTarjanScc,
|
|
111
|
+
extractSpanLineageAndTopology,
|
|
112
|
+
getOntologyGraph,
|
|
113
|
+
loadTraceFaultLocusFromDb,
|
|
114
|
+
localizeTraceFaultLocus,
|
|
115
|
+
upsertOntologyFromSpans,
|
|
116
|
+
} from "./lib/autosre-ontology";
|
|
117
|
+
export {
|
|
118
|
+
acquireIncidentLease,
|
|
119
|
+
approveRemediationProposal,
|
|
120
|
+
claimRemediationGrant,
|
|
121
|
+
completeRemediationProposal,
|
|
122
|
+
dispatchIncidentWebhook,
|
|
123
|
+
evaluateRemediationWatchdogs,
|
|
124
|
+
failRemediationProposal,
|
|
125
|
+
getIncidentInvestigation,
|
|
126
|
+
getRemediationProposal,
|
|
127
|
+
listIncidentInvestigations,
|
|
128
|
+
listRemediationProposals,
|
|
129
|
+
openOrDeduplicateIncident,
|
|
130
|
+
proposeRemediationAction,
|
|
131
|
+
RemediationGateError,
|
|
132
|
+
rejectRemediationProposal,
|
|
133
|
+
type WatchdogEvaluationSummary,
|
|
134
|
+
} from "./lib/autosre-remediation-gate";
|
|
82
135
|
export {
|
|
83
136
|
type BlobListOptions,
|
|
84
137
|
type BlobListResult,
|
|
@@ -110,8 +163,10 @@ export { MetricsStore } from "./lib/metrics-store";
|
|
|
110
163
|
export { ProjectsStore } from "./lib/projects-store";
|
|
111
164
|
export { D1Adapter, type SqlDb, type SqlStatement } from "./lib/sql-db";
|
|
112
165
|
export {
|
|
166
|
+
configurePostgresTypeParsers,
|
|
113
167
|
PostgresAdapter,
|
|
114
168
|
type PostgresAdapterOptions,
|
|
169
|
+
type PostgresTypeParserRegistry,
|
|
115
170
|
} from "./lib/sql-db-postgres";
|
|
116
171
|
export {
|
|
117
172
|
actionRoutesPlugin,
|
|
@@ -120,6 +175,7 @@ export {
|
|
|
120
175
|
alertsRoutesPlugin,
|
|
121
176
|
analysesRoutesPlugin,
|
|
122
177
|
askRoutesPlugin,
|
|
178
|
+
autosreRoutesPlugin,
|
|
123
179
|
botFilterPlugin,
|
|
124
180
|
connectedRoutesPlugin,
|
|
125
181
|
createAlertEvaluatorHandler,
|
|
@@ -188,6 +244,7 @@ export const allPlugins = [
|
|
|
188
244
|
analysesRoutesPlugin,
|
|
189
245
|
actionRoutesPlugin,
|
|
190
246
|
evalCasesRoutesPlugin,
|
|
247
|
+
autosreRoutesPlugin,
|
|
191
248
|
evidenceRetrievalRoutesPlugin,
|
|
192
249
|
askRoutesPlugin,
|
|
193
250
|
tailRoutesPlugin,
|
|
@@ -209,3 +266,4 @@ export const createDefaultCollectorApp = (config?: Partial<CollectorConfig>) =>
|
|
|
209
266
|
});
|
|
210
267
|
|
|
211
268
|
export default createTelemetryCollectorApp(allPlugins);
|
|
269
|
+
export type { HostLookup } from "./lib/webhook-url";
|