@obsunified/collector 2.0.1 → 2.1.1

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.
Files changed (107) hide show
  1. package/dist/auth.d.ts +1 -1
  2. package/dist/auth.js +1 -1
  3. package/dist/{chunk-UGWDOGGH.js → chunk-KMEZKGSO.js} +22 -10
  4. package/dist/chunk-KMEZKGSO.js.map +1 -0
  5. package/dist/index.d.ts +322 -26
  6. package/dist/index.js +1478 -415
  7. package/dist/index.js.map +1 -1
  8. package/dist/{ingest-auth-HFVO1zIp.d.ts → ingest-auth-C5-8Mly5.d.ts} +9 -0
  9. package/package.json +5 -5
  10. package/src/analyses/derive.ts +26 -25
  11. package/src/analyses/investigations.ts +8 -8
  12. package/src/analyses/tier0.ts +22 -22
  13. package/src/auth/dashboard-auth.test.ts +791 -0
  14. package/src/auth/dashboard-auth.ts +187 -38
  15. package/src/auth/login-rate-limit.test.ts +129 -0
  16. package/src/auth/login-rate-limit.ts +143 -0
  17. package/src/framework/body-limit.test.ts +165 -0
  18. package/src/framework/collector.ts +355 -115
  19. package/src/framework/env.ts +9 -0
  20. package/src/framework/limits.ts +31 -0
  21. package/src/framework/scheduled-handlers.test.ts +342 -0
  22. package/src/index.ts +16 -1
  23. package/src/lib/action-retention.test.ts +472 -0
  24. package/src/lib/action-retention.ts +420 -0
  25. package/src/lib/actions-store.ts +6 -12
  26. package/src/lib/ai-store/calls.ts +2 -2
  27. package/src/lib/ai-store/sessions.ts +3 -3
  28. package/src/lib/ai-store/spans.ts +1 -1
  29. package/src/lib/ai-store/types.ts +2 -1
  30. package/src/lib/alerts-store.test.ts +153 -0
  31. package/src/lib/alerts-store.ts +122 -21
  32. package/src/lib/analyses-runner.test.ts +75 -0
  33. package/src/lib/analyses-runner.ts +60 -3
  34. package/src/lib/analyses-store-latest.test.ts +61 -0
  35. package/src/lib/analyses-store.ts +17 -10
  36. package/src/lib/blob-store.ts +1 -1
  37. package/src/lib/housekeeping.test.ts +73 -0
  38. package/src/lib/housekeeping.ts +93 -0
  39. package/src/lib/identity-index/action-lookups.ts +6 -6
  40. package/src/lib/identity-index/key-lookups.ts +2 -2
  41. package/src/lib/identity-index/user-lookup.ts +8 -5
  42. package/src/lib/identity-index.test.ts +2 -3
  43. package/src/lib/metrics-store-series.test.ts +99 -0
  44. package/src/lib/metrics-store.test.ts +23 -0
  45. package/src/lib/metrics-store.ts +27 -3
  46. package/src/lib/projects-store.test.ts +25 -0
  47. package/src/lib/projects-store.ts +4 -3
  48. package/src/lib/sql-db-dialect.test.ts +12 -3
  49. package/src/lib/sql-db-postgres.integration.test.ts +1225 -0
  50. package/src/lib/sql-db-postgres.test.ts +314 -0
  51. package/src/lib/sql-db-postgres.ts +392 -54
  52. package/src/lib/sql-db-sqlite-time-windows.test.ts +194 -0
  53. package/src/lib/sql-db.ts +40 -6
  54. package/src/lib/test-utils/action-graph-fixtures.ts +307 -0
  55. package/src/lib/test-utils/node-sqlite-db.ts +188 -0
  56. package/src/lib/webhook-url.test.ts +122 -0
  57. package/src/lib/webhook-url.ts +155 -0
  58. package/src/migrations/041_ingest_key_hash_text.sql +5 -0
  59. package/src/migrations/042_session_replay_project_composite_pk.sql +52 -0
  60. package/src/migrations/043_metric_exemplars_text_timestamps.sql +5 -0
  61. package/src/migrations/044_actions_started_at_index.sql +10 -0
  62. package/src/migrations/045_user_profiles_project_composite_pk.sql +40 -0
  63. package/src/migrations/046_action_graph_project_composite_pk.sql +255 -0
  64. package/src/migrations/047_ai_span_payloads_project_composite_pk.sql +61 -0
  65. package/src/migrations/048_telemetry_spans_project_composite_pk.sql +92 -0
  66. package/src/migrations/049_alert_state_notified.sql +17 -0
  67. package/src/migrations/alert-state-notified.test.ts +111 -0
  68. package/src/migrations/parity.test.ts +64 -0
  69. package/src/migrations/project-scoped-keys.test.ts +432 -0
  70. package/src/migrations-postgres/009_projects_and_keys.sql +4 -4
  71. package/src/migrations-postgres/026_obs_dashboard_project_seed.sql +1 -1
  72. package/src/migrations-postgres/035_metric_exemplars.sql +5 -2
  73. package/src/migrations-postgres/041_ingest_key_hash_text.sql +32 -0
  74. package/src/migrations-postgres/042_session_replay_project_composite_pk.sql +16 -0
  75. package/src/migrations-postgres/043_metric_exemplars_text_timestamps.sql +47 -0
  76. package/src/migrations-postgres/044_actions_started_at_index.sql +8 -0
  77. package/src/migrations-postgres/045_user_profiles_project_composite_pk.sql +33 -0
  78. package/src/migrations-postgres/046_action_graph_project_composite_pk.sql +39 -0
  79. package/src/migrations-postgres/047_ai_span_payloads_project_composite_pk.sql +30 -0
  80. package/src/migrations-postgres/048_telemetry_spans_project_composite_pk.sql +37 -0
  81. package/src/migrations-postgres/049_alert_state_notified.sql +10 -0
  82. package/src/migrations-postgres/README.md +58 -5
  83. package/src/otlp/decode/body.test.ts +76 -0
  84. package/src/otlp/decode/body.ts +67 -6
  85. package/src/plugins/_json.ts +21 -0
  86. package/src/plugins/action-graph-processor.ts +6 -12
  87. package/src/plugins/ai-receiver.ts +5 -12
  88. package/src/plugins/ai-span-payloads-processor.ts +1 -2
  89. package/src/plugins/alerts-evaluator-retention.test.ts +170 -0
  90. package/src/plugins/alerts-evaluator.test.ts +464 -3
  91. package/src/plugins/alerts-evaluator.ts +165 -41
  92. package/src/plugins/alerts-routes.ts +10 -6
  93. package/src/plugins/ask-routes.ts +4 -5
  94. package/src/plugins/connected-routes.test.ts +106 -0
  95. package/src/plugins/connected-routes.ts +14 -5
  96. package/src/plugins/dashboard-routes.test.ts +120 -0
  97. package/src/plugins/dashboard-routes.ts +11 -2
  98. package/src/plugins/eval-cases-routes.ts +8 -17
  99. package/src/plugins/identity-receiver.ts +9 -11
  100. package/src/plugins/json-body.test.ts +91 -0
  101. package/src/plugins/onboarding-routes.test.ts +105 -0
  102. package/src/plugins/onboarding-routes.ts +26 -11
  103. package/src/plugins/projects-routes.ts +2 -1
  104. package/src/plugins/replay-isolation.test.ts +922 -0
  105. package/src/plugins/replay-receiver.ts +8 -8
  106. package/src/plugins/usage-receiver.ts +3 -6
  107. package/dist/chunk-UGWDOGGH.js.map +0 -1
@@ -0,0 +1,165 @@
1
+ /**
2
+ * The framework caps request bodies on `/v1/*` and `/internal/*` before any
3
+ * handler buffers them. Both the Content-Length fast path and the streamed
4
+ * (chunked) path must refuse oversized bodies with a JSON 413.
5
+ */
6
+
7
+ import { describe, expect, it } from "vitest";
8
+ import { createDashboardAuth } from "../auth/dashboard-auth";
9
+ import type { CollectorPlugin } from "./collector";
10
+ import { createTelemetryCollectorApp } from "./collector";
11
+ import type { CollectorEnv } from "./env";
12
+
13
+ const echoPlugin: CollectorPlugin = {
14
+ name: "echo",
15
+ register(app) {
16
+ const echo = async (c: {
17
+ req: { arrayBuffer(): Promise<ArrayBuffer> };
18
+ json: (v: unknown) => Response;
19
+ }) => c.json({ received: (await c.req.arrayBuffer()).byteLength });
20
+ app.post("/v1/echo", echo);
21
+ app.post("/internal/echo", echo);
22
+ },
23
+ };
24
+
25
+ const streamOf = (bytes: number, chunk = 256): ReadableStream<Uint8Array> =>
26
+ new ReadableStream({
27
+ start(controller) {
28
+ let sent = 0;
29
+ while (sent < bytes) {
30
+ const n = Math.min(chunk, bytes - sent);
31
+ controller.enqueue(new Uint8Array(n).fill(120));
32
+ sent += n;
33
+ }
34
+ controller.close();
35
+ },
36
+ });
37
+
38
+ const post = (
39
+ app: ReturnType<typeof createTelemetryCollectorApp>,
40
+ path: string,
41
+ body: BodyInit,
42
+ env: Partial<CollectorEnv> = {},
43
+ ) =>
44
+ app.request(
45
+ path,
46
+ {
47
+ method: "POST",
48
+ body,
49
+ headers: { "content-type": "application/octet-stream" },
50
+ // Required by undici for stream bodies; harmless for strings.
51
+ duplex: "half",
52
+ } as RequestInit,
53
+ env as CollectorEnv,
54
+ );
55
+
56
+ describe("request body cap", () => {
57
+ it("refuses oversized bodies by Content-Length and when streamed", async () => {
58
+ const app = createTelemetryCollectorApp({
59
+ plugins: [echoPlugin],
60
+ maxBodyBytes: 1024,
61
+ // /internal/* fails closed without dashboard auth; a pass-through
62
+ // keeps the cap, not the auth, under test.
63
+ dashboardAuth: {
64
+ middleware: async (_c, next) => next(),
65
+ registerRoutes: () => {},
66
+ },
67
+ });
68
+ for (const path of ["/v1/echo", "/internal/echo"]) {
69
+ const byLength = await post(app, path, "x".repeat(2048));
70
+ expect(byLength.status, path).toBe(413);
71
+ await expect(byLength.json()).resolves.toEqual({
72
+ error: "Payload too large (max 1024 bytes)",
73
+ });
74
+
75
+ const streamed = await post(app, path, streamOf(2048));
76
+ expect(streamed.status, path).toBe(413);
77
+
78
+ const ok = await post(app, path, "x".repeat(512));
79
+ expect(ok.status, path).toBe(200);
80
+ await expect(ok.json()).resolves.toEqual({ received: 512 });
81
+ }
82
+ });
83
+
84
+ it("reads the cap from MAX_BODY_BYTES when the config leaves it unset", async () => {
85
+ const app = createTelemetryCollectorApp({ plugins: [echoPlugin] });
86
+ const env = { MAX_BODY_BYTES: "100" };
87
+ expect((await post(app, "/v1/echo", "x".repeat(101), env)).status).toBe(
88
+ 413,
89
+ );
90
+ expect((await post(app, "/v1/echo", "x".repeat(100), env)).status).toBe(
91
+ 200,
92
+ );
93
+ // Garbage values fall back to the 10 MiB default.
94
+ expect(
95
+ (await post(app, "/v1/echo", "x".repeat(101), { MAX_BODY_BYTES: "lots" }))
96
+ .status,
97
+ ).toBe(200);
98
+ });
99
+
100
+ it("caps unauthenticated /auth/login bodies independently of the ingest cap", async () => {
101
+ const app = createTelemetryCollectorApp({
102
+ plugins: [],
103
+ // A generous ingest cap must not widen what an anonymous client
104
+ // can make the host buffer on the login route.
105
+ maxBodyBytes: 1024 * 1024,
106
+ maxAuthBodyBytes: 256,
107
+ dashboardAuth: createDashboardAuth({
108
+ password: "correct-horse-battery-staple",
109
+ loginRateLimit: false,
110
+ }),
111
+ });
112
+ const login = (body: BodyInit) =>
113
+ app.request(
114
+ "/auth/login",
115
+ {
116
+ method: "POST",
117
+ body,
118
+ headers: { "content-type": "application/json" },
119
+ duplex: "half",
120
+ } as RequestInit,
121
+ {} as CollectorEnv,
122
+ );
123
+
124
+ const padded = JSON.stringify({ password: "x".repeat(512) });
125
+ const byLength = await login(padded);
126
+ expect(byLength.status).toBe(413);
127
+ await expect(byLength.json()).resolves.toEqual({
128
+ error: "Payload too large (max 256 bytes)",
129
+ });
130
+ expect((await login(streamOf(512))).status).toBe(413);
131
+
132
+ // Normal-sized bodies still reach the handler.
133
+ const wrong = await login(JSON.stringify({ password: "nope" }));
134
+ expect(wrong.status).toBe(401);
135
+ const right = await login(
136
+ JSON.stringify({ password: "correct-horse-battery-staple" }),
137
+ );
138
+ expect(right.status).toBe(200);
139
+ });
140
+
141
+ it("applies the 16 KiB default to /auth/* when the config leaves it unset", async () => {
142
+ const app = createTelemetryCollectorApp({
143
+ plugins: [],
144
+ dashboardAuth: createDashboardAuth({
145
+ password: "pw",
146
+ loginRateLimit: false,
147
+ }),
148
+ });
149
+ const login = (body: string) =>
150
+ app.request(
151
+ "/auth/login",
152
+ {
153
+ method: "POST",
154
+ body,
155
+ headers: { "content-type": "application/json" },
156
+ },
157
+ // MAX_BODY_BYTES only governs ingest/admin routes.
158
+ { MAX_BODY_BYTES: String(10 * 1024 * 1024) } as CollectorEnv,
159
+ );
160
+ expect(
161
+ (await login(JSON.stringify({ password: "x".repeat(16 * 1024) }))).status,
162
+ ).toBe(413);
163
+ expect((await login(JSON.stringify({ password: "x" }))).status).toBe(401);
164
+ });
165
+ });