@autter/otlp-ingester 1.0.0 → 1.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/sink.d.ts ADDED
@@ -0,0 +1,103 @@
1
+ import type { IngesterConfig } from "./config.js";
2
+ import type { IngestContext, RuntimeLlmCall, RuntimeMetricPoint, RuntimeOccurrence } from "./types.js";
3
+ /**
4
+ * At-least-once delivery to the sink webhook.
5
+ *
6
+ * The sink feeds the consumer's issue grouping and incident detection, so a
7
+ * lost batch means silently missing error occurrences — a single
8
+ * fire-and-forget POST is not enough (a routine consumer deploy is longer
9
+ * than one request timeout). Batches therefore queue in memory and retry
10
+ * with exponential backoff until delivered, permanently rejected, or the
11
+ * bounded buffer overflows.
12
+ *
13
+ * Durability boundary: the queue is in-memory only. Everything forwarded
14
+ * here was already written to ClickHouse (ingest 503s otherwise), so after
15
+ * a process crash or an overflow/permanent drop the consumer recovers by
16
+ * replaying the logged time range from ClickHouse — see
17
+ * docs/ARCHITECTURE.md "Sink webhook". Every batch carries a unique
18
+ * `batchId` so consumers can deduplicate retried deliveries.
19
+ */
20
+ /** Delivery timing/concurrency knobs; overridable for tests. */
21
+ export interface SinkTuning {
22
+ /** Base delay before the second attempt; doubles per attempt to the cap. */
23
+ retryBaseMs: number;
24
+ retryCapMs: number;
25
+ requestTimeoutMs: number;
26
+ /** Deliveries run concurrently while healthy, serially while failing. */
27
+ healthyConcurrency: number;
28
+ }
29
+ /**
30
+ * Operational counters for /healthz. Failure detail is reduced to a fixed
31
+ * category (`timeout`, `connection_error`, `http_<status>`, `error`) — the
32
+ * health endpoint is unauthenticated, so raw transport/exception text stays
33
+ * in server-side logs only.
34
+ */
35
+ export interface SinkStats {
36
+ queued: number;
37
+ queuedBytes: number;
38
+ inFlight: number;
39
+ delivered: number;
40
+ retried: number;
41
+ droppedOverflow: number;
42
+ droppedPermanent: number;
43
+ consecutiveFailures: number;
44
+ lastFailureAt: string | null;
45
+ lastFailureReason: string | null;
46
+ oldestQueuedSince: string | null;
47
+ }
48
+ export declare class SinkForwarder {
49
+ private readonly config;
50
+ private readonly fetchImpl;
51
+ /** Always sorted by `seq`: queue[0] is the oldest batch. */
52
+ private readonly queue;
53
+ private queuedBytes;
54
+ /** Per-org share of queuedBytes — overflow evicts from the heaviest org. */
55
+ private readonly queuedBytesByOrg;
56
+ private inFlight;
57
+ private timer;
58
+ private stopped;
59
+ private seqCounter;
60
+ private delivered;
61
+ private retried;
62
+ private droppedOverflow;
63
+ private droppedPermanent;
64
+ private consecutiveFailures;
65
+ /**
66
+ * Bumped on every failure. A success only clears consecutiveFailures if
67
+ * no failure happened after that request STARTED — a concurrent success
68
+ * that overlapped a failure proves nothing about current sink health and
69
+ * must not reopen full concurrency mid-outage.
70
+ */
71
+ private failureEpoch;
72
+ private lastFailureAt;
73
+ private lastFailureReason;
74
+ private readonly tuning;
75
+ constructor(config: IngesterConfig, fetchImpl?: typeof fetch, tuning?: Partial<SinkTuning>);
76
+ /** Queue a batch for delivery. No-op when there is nothing to send. */
77
+ enqueue(ctx: IngestContext, occurrences: RuntimeOccurrence[], metricPoints?: RuntimeMetricPoint[], llmCalls?: RuntimeLlmCall[]): void;
78
+ stats(): SinkStats;
79
+ pendingCount(): number;
80
+ /** Stop scheduling new deliveries (shutdown). Queued batches are logged
81
+ * by the caller — they are recoverable from ClickHouse, not from here. */
82
+ stop(): void;
83
+ /** Serialize one delivery payload; `seq` pins its age for ordering. */
84
+ private buildBatch;
85
+ private maxBufferedBytes;
86
+ private track;
87
+ private untrack;
88
+ /** Re-insert a retrying batch at its age position (queue is seq-sorted),
89
+ * so overflow eviction still drops the genuinely oldest signals first. */
90
+ private insertBySeq;
91
+ /**
92
+ * Oldest-first eviction, charged to the heaviest tenant: the freshest
93
+ * signals matter most to grouping, and one org flooding the shared
94
+ * buffer must not evict everyone else's batches.
95
+ */
96
+ private enforceBounds;
97
+ /** The oldest batch of the org holding the most buffered bytes. */
98
+ private evictOne;
99
+ private currentLimit;
100
+ private pump;
101
+ private scheduleWake;
102
+ private send;
103
+ }
package/dist/sink.js ADDED
@@ -0,0 +1,314 @@
1
+ import { randomUUID } from "node:crypto";
2
+ const DEFAULT_TUNING = {
3
+ retryBaseMs: 1000,
4
+ retryCapMs: 60_000,
5
+ requestTimeoutMs: 10_000,
6
+ healthyConcurrency: 4,
7
+ };
8
+ /** Fixed failure category — safe for the unauthenticated health response. */
9
+ function failureReason(err) {
10
+ if (err instanceof Error) {
11
+ if (err.name === "TimeoutError" || err.name === "AbortError") {
12
+ return "timeout";
13
+ }
14
+ // fetch surfaces DNS/TLS/socket failures as TypeError.
15
+ if (err instanceof TypeError)
16
+ return "connection_error";
17
+ }
18
+ return "error";
19
+ }
20
+ export class SinkForwarder {
21
+ config;
22
+ fetchImpl;
23
+ /** Always sorted by `seq`: queue[0] is the oldest batch. */
24
+ queue = [];
25
+ queuedBytes = 0;
26
+ /** Per-org share of queuedBytes — overflow evicts from the heaviest org. */
27
+ queuedBytesByOrg = new Map();
28
+ inFlight = 0;
29
+ timer = null;
30
+ stopped = false;
31
+ seqCounter = 0;
32
+ delivered = 0;
33
+ retried = 0;
34
+ droppedOverflow = 0;
35
+ droppedPermanent = 0;
36
+ consecutiveFailures = 0;
37
+ /**
38
+ * Bumped on every failure. A success only clears consecutiveFailures if
39
+ * no failure happened after that request STARTED — a concurrent success
40
+ * that overlapped a failure proves nothing about current sink health and
41
+ * must not reopen full concurrency mid-outage.
42
+ */
43
+ failureEpoch = 0;
44
+ lastFailureAt = null;
45
+ lastFailureReason = null;
46
+ tuning;
47
+ constructor(config, fetchImpl = fetch, tuning = {}) {
48
+ this.config = config;
49
+ this.fetchImpl = fetchImpl;
50
+ this.tuning = { ...DEFAULT_TUNING, ...tuning };
51
+ }
52
+ /** Queue a batch for delivery. No-op when there is nothing to send. */
53
+ enqueue(ctx, occurrences, metricPoints = [], llmCalls = []) {
54
+ if (!this.config.sinkUrl || this.stopped)
55
+ return;
56
+ if (occurrences.length === 0 &&
57
+ metricPoints.length === 0 &&
58
+ llmCalls.length === 0) {
59
+ return;
60
+ }
61
+ const batch = this.buildBatch(ctx, occurrences, metricPoints, llmCalls);
62
+ // A batch bigger than the whole buffer could never be admitted without
63
+ // evicting everyone else — and the consumer's body cap would reject it
64
+ // anyway. Drop it alone instead of letting it flush the queue.
65
+ if (batch.bytes > this.maxBufferedBytes()) {
66
+ this.droppedOverflow += 1;
67
+ console.warn(`sink batch ${batch.batchId} (org ${batch.orgId}) exceeds the buffer cap (${batch.bytes} bytes) — dropped; replay signals ${batch.signalsFrom ?? "?"} .. ${batch.signalsTo ?? "?"} from ClickHouse`);
68
+ return;
69
+ }
70
+ this.queue.push(batch);
71
+ this.track(batch);
72
+ this.enforceBounds();
73
+ this.pump();
74
+ }
75
+ stats() {
76
+ const oldest = this.queue[0]?.enqueuedAt ?? null;
77
+ return {
78
+ queued: this.queue.length,
79
+ queuedBytes: this.queuedBytes,
80
+ inFlight: this.inFlight,
81
+ delivered: this.delivered,
82
+ retried: this.retried,
83
+ droppedOverflow: this.droppedOverflow,
84
+ droppedPermanent: this.droppedPermanent,
85
+ consecutiveFailures: this.consecutiveFailures,
86
+ lastFailureAt: this.lastFailureAt,
87
+ lastFailureReason: this.lastFailureReason,
88
+ oldestQueuedSince: oldest ? new Date(oldest).toISOString() : null,
89
+ };
90
+ }
91
+ pendingCount() {
92
+ return this.queue.length + this.inFlight;
93
+ }
94
+ /** Stop scheduling new deliveries (shutdown). Queued batches are logged
95
+ * by the caller — they are recoverable from ClickHouse, not from here. */
96
+ stop() {
97
+ this.stopped = true;
98
+ if (this.timer) {
99
+ clearTimeout(this.timer);
100
+ this.timer = null;
101
+ }
102
+ }
103
+ /** Serialize one delivery payload; `seq` pins its age for ordering. */
104
+ buildBatch(ctx, occurrences, metricPoints, llmCalls) {
105
+ const batchId = randomUUID();
106
+ const body = JSON.stringify({
107
+ version: 1,
108
+ batchId,
109
+ orgId: ctx.orgId,
110
+ repositoryId: ctx.repositoryId,
111
+ occurrences: occurrences.map((o) => ({
112
+ ...o,
113
+ occurredAt: o.occurredAt.toISOString(),
114
+ })),
115
+ metrics: metricPoints.map((p) => ({
116
+ ...p,
117
+ bucketAt: p.bucketAt.toISOString(),
118
+ })),
119
+ llmCalls: llmCalls.map((c) => ({
120
+ ...c,
121
+ startedAt: c.startedAt.toISOString(),
122
+ })),
123
+ });
124
+ const range = signalRange(occurrences, metricPoints, llmCalls);
125
+ return {
126
+ batchId,
127
+ orgId: ctx.orgId,
128
+ body,
129
+ bytes: Buffer.byteLength(body),
130
+ seq: ++this.seqCounter,
131
+ attempts: 0,
132
+ nextAttemptAt: Date.now(),
133
+ signalsFrom: range.from,
134
+ signalsTo: range.to,
135
+ enqueuedAt: Date.now(),
136
+ };
137
+ }
138
+ maxBufferedBytes() {
139
+ return this.config.sinkMaxBufferedMb * 1024 * 1024;
140
+ }
141
+ track(batch) {
142
+ this.queuedBytes += batch.bytes;
143
+ this.queuedBytesByOrg.set(batch.orgId, (this.queuedBytesByOrg.get(batch.orgId) ?? 0) + batch.bytes);
144
+ }
145
+ untrack(batch) {
146
+ this.queuedBytes -= batch.bytes;
147
+ const left = (this.queuedBytesByOrg.get(batch.orgId) ?? 0) - batch.bytes;
148
+ if (left > 0)
149
+ this.queuedBytesByOrg.set(batch.orgId, left);
150
+ else
151
+ this.queuedBytesByOrg.delete(batch.orgId);
152
+ }
153
+ /** Re-insert a retrying batch at its age position (queue is seq-sorted),
154
+ * so overflow eviction still drops the genuinely oldest signals first. */
155
+ insertBySeq(batch) {
156
+ const at = this.queue.findIndex((b) => b.seq > batch.seq);
157
+ if (at === -1)
158
+ this.queue.push(batch);
159
+ else
160
+ this.queue.splice(at, 0, batch);
161
+ }
162
+ /**
163
+ * Oldest-first eviction, charged to the heaviest tenant: the freshest
164
+ * signals matter most to grouping, and one org flooding the shared
165
+ * buffer must not evict everyone else's batches.
166
+ */
167
+ enforceBounds() {
168
+ const maxBytes = this.maxBufferedBytes();
169
+ while (this.queue.length > this.config.sinkMaxBufferedBatches ||
170
+ (this.queuedBytes > maxBytes && this.queue.length > 1)) {
171
+ const dropped = this.evictOne();
172
+ if (!dropped)
173
+ break;
174
+ this.droppedOverflow += 1;
175
+ console.warn(`sink buffer overflow: dropped batch ${dropped.batchId} (org ${dropped.orgId}, signals ${dropped.signalsFrom ?? "?"} .. ${dropped.signalsTo ?? "?"}) — replay this range from ClickHouse`);
176
+ }
177
+ }
178
+ /** The oldest batch of the org holding the most buffered bytes. */
179
+ evictOne() {
180
+ let heaviest = null;
181
+ let heaviestBytes = -1;
182
+ for (const [orgId, bytes] of this.queuedBytesByOrg) {
183
+ if (bytes > heaviestBytes) {
184
+ heaviest = orgId;
185
+ heaviestBytes = bytes;
186
+ }
187
+ }
188
+ const at = heaviest
189
+ ? this.queue.findIndex((b) => b.orgId === heaviest)
190
+ : 0;
191
+ const [dropped] = this.queue.splice(at === -1 ? 0 : at, 1);
192
+ if (!dropped)
193
+ return null;
194
+ this.untrack(dropped);
195
+ return dropped;
196
+ }
197
+ currentLimit() {
198
+ return this.consecutiveFailures > 0 ? 1 : this.tuning.healthyConcurrency;
199
+ }
200
+ pump() {
201
+ if (this.stopped || !this.config.sinkUrl)
202
+ return;
203
+ const limit = this.currentLimit();
204
+ const now = Date.now();
205
+ while (this.inFlight < limit) {
206
+ const index = this.queue.findIndex((b) => b.nextAttemptAt <= now);
207
+ if (index === -1)
208
+ break;
209
+ const [batch] = this.queue.splice(index, 1);
210
+ if (!batch)
211
+ break;
212
+ this.untrack(batch);
213
+ this.inFlight += 1;
214
+ void this.send(batch).finally(() => {
215
+ this.inFlight -= 1;
216
+ this.pump();
217
+ });
218
+ }
219
+ this.scheduleWake();
220
+ }
221
+ scheduleWake() {
222
+ if (this.timer) {
223
+ clearTimeout(this.timer);
224
+ this.timer = null;
225
+ }
226
+ if (this.stopped || this.queue.length === 0)
227
+ return;
228
+ // At capacity there is nothing to wake for: every completion pumps
229
+ // again anyway, and a 0 ms timer here would spin the event loop while
230
+ // a ready batch waits on a slow in-flight request.
231
+ if (this.inFlight >= this.currentLimit())
232
+ return;
233
+ const next = Math.min(...this.queue.map((b) => b.nextAttemptAt));
234
+ this.timer = setTimeout(() => {
235
+ this.timer = null;
236
+ this.pump();
237
+ }, Math.max(0, next - Date.now()));
238
+ this.timer.unref();
239
+ }
240
+ async send(batch) {
241
+ batch.attempts += 1;
242
+ const epochAtStart = this.failureEpoch;
243
+ let reason = null;
244
+ /** Raw failure text — logged server-side, never surfaced in stats. */
245
+ let detail = null;
246
+ let permanent = false;
247
+ try {
248
+ const res = await this.fetchImpl(this.config.sinkUrl, {
249
+ method: "POST",
250
+ headers: {
251
+ "content-type": "application/json",
252
+ ...(this.config.sinkToken
253
+ ? { authorization: `Bearer ${this.config.sinkToken}` }
254
+ : {}),
255
+ },
256
+ body: batch.body,
257
+ signal: AbortSignal.timeout(this.tuning.requestTimeoutMs),
258
+ });
259
+ // Drain the body so keep-alive sockets are reusable; its content is
260
+ // irrelevant to delivery.
261
+ await res.text().catch(() => { });
262
+ if (res.ok) {
263
+ this.delivered += 1;
264
+ if (epochAtStart === this.failureEpoch) {
265
+ this.consecutiveFailures = 0;
266
+ }
267
+ return;
268
+ }
269
+ reason = `http_${res.status}`;
270
+ detail = `sink responded ${res.status}`;
271
+ // 4xx (except timeout/rate-limit) means the consumer rejected the
272
+ // batch — retrying the same body cannot succeed.
273
+ permanent = res.status < 500 && res.status !== 408 && res.status !== 429;
274
+ }
275
+ catch (err) {
276
+ reason = failureReason(err);
277
+ detail = err instanceof Error ? err.message : String(err);
278
+ }
279
+ this.failureEpoch += 1;
280
+ this.consecutiveFailures += 1;
281
+ this.lastFailureAt = new Date().toISOString();
282
+ this.lastFailureReason = reason;
283
+ if (permanent || batch.attempts >= this.config.sinkMaxAttempts) {
284
+ this.droppedPermanent += 1;
285
+ console.error(`sink delivery gave up after ${batch.attempts} attempt(s) (${detail}): batch ${batch.batchId} (signals ${batch.signalsFrom ?? "?"} .. ${batch.signalsTo ?? "?"}) — replay this range from ClickHouse`);
286
+ return;
287
+ }
288
+ this.retried += 1;
289
+ // Full jitter avoids retry stampedes when the consumer comes back.
290
+ const backoff = Math.min(this.tuning.retryCapMs, this.tuning.retryBaseMs * 2 ** (batch.attempts - 1));
291
+ batch.nextAttemptAt = Date.now() + backoff / 2 + Math.random() * (backoff / 2);
292
+ this.insertBySeq(batch);
293
+ this.track(batch);
294
+ if (batch.attempts === 1 || batch.attempts % 5 === 0) {
295
+ console.warn(`sink delivery failed (attempt ${batch.attempts}/${this.config.sinkMaxAttempts}, will retry): ${detail}`);
296
+ }
297
+ this.enforceBounds();
298
+ this.scheduleWake();
299
+ }
300
+ }
301
+ /** ISO range across every signal in the batch — the replay hint on drops. */
302
+ function signalRange(occurrences, metricPoints, llmCalls) {
303
+ const timestamps = [
304
+ ...occurrences.map((o) => o.occurredAt),
305
+ ...metricPoints.map((p) => p.bucketAt),
306
+ ...llmCalls.map((c) => c.startedAt),
307
+ ].map((d) => d.getTime());
308
+ if (timestamps.length === 0)
309
+ return { from: null, to: null };
310
+ return {
311
+ from: new Date(Math.min(...timestamps)).toISOString(),
312
+ to: new Date(Math.max(...timestamps)).toISOString(),
313
+ };
314
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Representative raw stack traces for every officially-supported runtime,
3
+ * exactly as their OpenTelemetry SDKs put them on the `exception.stacktrace`
4
+ * span attribute (or the browser relay's `stack` field). These drive the
5
+ * golden fingerprint tests in fingerprint.test.ts.
6
+ *
7
+ * Each language provides three variants of the SAME family of errors:
8
+ * - `primary` — the defect under test.
9
+ * - `sibling` — a DIFFERENT defect (different top function/file) that
10
+ * carries the SAME error message. It must fingerprint separately: this is
11
+ * the regression the pipeline exists to prevent (before per-language
12
+ * parsing, Go/Rust frames were dropped and these collided into one issue).
13
+ * - `redeploy` — the primary defect after a rebuild that shifted every line
14
+ * number (and, for Go, pointer offsets/addresses/goroutine ids). It must
15
+ * fingerprint IDENTICALLY to `primary`, proving grouping is stable across
16
+ * re-deploys and repeated ingestion.
17
+ */
18
+ export interface StackFixture {
19
+ primary: string;
20
+ sibling: string;
21
+ redeploy: string;
22
+ /** Golden normalised top frames for `primary` (locks the parser output). */
23
+ primaryFrames: string[];
24
+ }
25
+ export declare const STACK_FIXTURES: Record<string, StackFixture>;
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Representative raw stack traces for every officially-supported runtime,
3
+ * exactly as their OpenTelemetry SDKs put them on the `exception.stacktrace`
4
+ * span attribute (or the browser relay's `stack` field). These drive the
5
+ * golden fingerprint tests in fingerprint.test.ts.
6
+ *
7
+ * Each language provides three variants of the SAME family of errors:
8
+ * - `primary` — the defect under test.
9
+ * - `sibling` — a DIFFERENT defect (different top function/file) that
10
+ * carries the SAME error message. It must fingerprint separately: this is
11
+ * the regression the pipeline exists to prevent (before per-language
12
+ * parsing, Go/Rust frames were dropped and these collided into one issue).
13
+ * - `redeploy` — the primary defect after a rebuild that shifted every line
14
+ * number (and, for Go, pointer offsets/addresses/goroutine ids). It must
15
+ * fingerprint IDENTICALLY to `primary`, proving grouping is stable across
16
+ * re-deploys and repeated ingestion.
17
+ */
18
+ // ── Go ──────────────────────────────────────────────────────────────────────
19
+ const GO_PRIMARY = `panic: runtime error: invalid memory address or nil pointer dereference
20
+ [signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x1a2b3c]
21
+
22
+ goroutine 42 [running]:
23
+ main.(*OrderService).Process(0xc0000b4000, 0xc0000d2000)
24
+ /app/orders/service.go:128 +0x1a5
25
+ main.(*Handler).ServeHTTP(0xc0000a2000, {0x8f2a40, 0xc0000b0000})
26
+ /app/web/handler.go:64 +0x2c8
27
+ net/http.(*conn).serve(0xc0001a4000, {0x8f2b20, 0xc0000c2000})
28
+ /usr/local/go/src/net/http/server.go:2092 +0x1a5
29
+ created by net/http.(*Server).Serve in goroutine 1
30
+ /usr/local/go/src/net/http/server.go:3285 +0x33e`;
31
+ const GO_SIBLING = `panic: runtime error: invalid memory address or nil pointer dereference
32
+ [signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x4d5e6f]
33
+
34
+ goroutine 88 [running]:
35
+ main.(*PaymentService).Charge(0xc0000f4000, 0xc000102000)
36
+ /app/payments/service.go:212 +0x9c
37
+ main.(*Handler).ServeHTTP(0xc0000a2000, {0x8f2a40, 0xc0000b0000})
38
+ /app/web/handler.go:64 +0x2c8
39
+ net/http.(*conn).serve(0xc0001a4000, {0x8f2b20, 0xc0000c2000})
40
+ /usr/local/go/src/net/http/server.go:2092 +0x1a5`;
41
+ const GO_REDEPLOY = `panic: runtime error: invalid memory address or nil pointer dereference
42
+ [signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x7a8b9c]
43
+
44
+ goroutine 15 [running]:
45
+ main.(*OrderService).Process(0xc000200000, 0xc000210000)
46
+ /app/orders/service.go:140 +0x1f2
47
+ main.(*Handler).ServeHTTP(0xc000202000, {0x8f2a40, 0xc000208000})
48
+ /app/web/handler.go:71 +0x300
49
+ net/http.(*conn).serve(0xc000300000, {0x8f2b20, 0xc000310000})
50
+ /usr/local/go/src/net/http/server.go:2092 +0x1a5
51
+ created by net/http.(*Server).Serve in goroutine 1
52
+ /usr/local/go/src/net/http/server.go:3285 +0x33e`;
53
+ // ── Rust ────────────────────────────────────────────────────────────────────
54
+ const RUST_PRIMARY = `thread 'actix-rt|system:0|arbiter:1' panicked at src/orders/service.rs:88:21:
55
+ called \`Result::unwrap()\` on an \`Err\` value: PoolTimedOut
56
+ stack backtrace:
57
+ 0: rust_begin_unwind
58
+ at /rustc/abc123/library/std/src/panicking.rs:665:5
59
+ 1: core::panicking::panic_fmt
60
+ at /rustc/abc123/library/core/src/panicking.rs:74:14
61
+ 2: core::result::unwrap_failed
62
+ at /rustc/abc123/library/core/src/result.rs:1679:5
63
+ 3: myapp::orders::service::OrderService::process
64
+ at ./src/orders/service.rs:88:21
65
+ 4: myapp::web::handler::handle_request
66
+ at ./src/web/handler.rs:42:9`;
67
+ const RUST_SIBLING = `thread 'main' panicked at src/payments/service.rs:143:10:
68
+ called \`Result::unwrap()\` on an \`Err\` value: PoolTimedOut
69
+ stack backtrace:
70
+ 0: rust_begin_unwind
71
+ at /rustc/abc123/library/std/src/panicking.rs:665:5
72
+ 1: core::panicking::panic_fmt
73
+ at /rustc/abc123/library/core/src/panicking.rs:74:14
74
+ 2: core::result::unwrap_failed
75
+ at /rustc/abc123/library/core/src/result.rs:1679:5
76
+ 3: myapp::payments::service::PaymentService::charge
77
+ at ./src/payments/service.rs:143:10
78
+ 4: myapp::web::handler::handle_request
79
+ at ./src/web/handler.rs:42:9`;
80
+ const RUST_REDEPLOY = `thread 'main' panicked at src/orders/service.rs:95:21:
81
+ called \`Result::unwrap()\` on an \`Err\` value: PoolTimedOut
82
+ stack backtrace:
83
+ 0: rust_begin_unwind
84
+ at /rustc/abc123/library/std/src/panicking.rs:665:5
85
+ 1: core::panicking::panic_fmt
86
+ at /rustc/abc123/library/core/src/panicking.rs:74:14
87
+ 2: core::result::unwrap_failed
88
+ at /rustc/abc123/library/core/src/result.rs:1679:5
89
+ 3: myapp::orders::service::OrderService::process
90
+ at ./src/orders/service.rs:95:21
91
+ 4: myapp::web::handler::handle_request
92
+ at ./src/web/handler.rs:47:9`;
93
+ // ── Java / JVM ──────────────────────────────────────────────────────────────
94
+ const JAVA_PRIMARY = `java.lang.NullPointerException: Cannot invoke "com.example.model.Order.total()" because "order" is null
95
+ at com.example.orders.OrderService.process(OrderService.java:88)
96
+ at com.example.web.RequestHandler.handle(RequestHandler.java:42)
97
+ at com.example.web.RequestHandler.doGet(RequestHandler.java:31)
98
+ at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:687)
99
+ at java.base/java.lang.Thread.run(Thread.java:1583)
100
+ Caused by: java.lang.IllegalStateException: order not loaded
101
+ at com.example.orders.OrderLoader.require(OrderLoader.java:55)
102
+ ... 4 more`;
103
+ const JAVA_SIBLING = `java.lang.NullPointerException: Cannot invoke "com.example.model.Order.total()" because "order" is null
104
+ at com.example.billing.InvoiceService.render(InvoiceService.java:140)
105
+ at com.example.web.RequestHandler.handle(RequestHandler.java:42)
106
+ at com.example.web.RequestHandler.doGet(RequestHandler.java:31)
107
+ at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:687)
108
+ at java.base/java.lang.Thread.run(Thread.java:1583)`;
109
+ const JAVA_REDEPLOY = `java.lang.NullPointerException: Cannot invoke "com.example.model.Order.total()" because "order" is null
110
+ at com.example.orders.OrderService.process(OrderService.java:92)
111
+ at com.example.web.RequestHandler.handle(RequestHandler.java:45)
112
+ at com.example.web.RequestHandler.doGet(RequestHandler.java:33)
113
+ at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:690)
114
+ at java.base/java.lang.Thread.run(Thread.java:1589)`;
115
+ // ── .NET ────────────────────────────────────────────────────────────────────
116
+ const DOTNET_PRIMARY = `System.NullReferenceException: Object reference not set to an instance of an object.
117
+ at MyApp.Orders.OrderService.Process(Order order) in C:\\src\\MyApp\\Orders\\OrderService.cs:line 88
118
+ at MyApp.Web.RequestHandler.HandleAsync(HttpContext context) in C:\\src\\MyApp\\Web\\RequestHandler.cs:line 42
119
+ at MyApp.Web.RequestHandler.<HandleAsync>d__4.MoveNext() in C:\\src\\MyApp\\Web\\RequestHandler.cs:line 39
120
+ at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](ref TStateMachine stateMachine)
121
+ at System.Threading.Tasks.Task.ExecuteWithThreadLocal(ref Task currentTaskSlot)`;
122
+ const DOTNET_SIBLING = `System.NullReferenceException: Object reference not set to an instance of an object.
123
+ at MyApp.Billing.InvoiceService.Render(Invoice invoice) in C:\\src\\MyApp\\Billing\\InvoiceService.cs:line 205
124
+ at MyApp.Web.RequestHandler.HandleAsync(HttpContext context) in C:\\src\\MyApp\\Web\\RequestHandler.cs:line 42
125
+ at MyApp.Web.RequestHandler.<HandleAsync>d__4.MoveNext() in C:\\src\\MyApp\\Web\\RequestHandler.cs:line 39
126
+ at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](ref TStateMachine stateMachine)
127
+ at System.Threading.Tasks.Task.ExecuteWithThreadLocal(ref Task currentTaskSlot)`;
128
+ const DOTNET_REDEPLOY = `System.NullReferenceException: Object reference not set to an instance of an object.
129
+ at MyApp.Orders.OrderService.Process(Order order) in C:\\src\\MyApp\\Orders\\OrderService.cs:line 94
130
+ at MyApp.Web.RequestHandler.HandleAsync(HttpContext context) in C:\\src\\MyApp\\Web\\RequestHandler.cs:line 47
131
+ at MyApp.Web.RequestHandler.<HandleAsync>d__4.MoveNext() in C:\\src\\MyApp\\Web\\RequestHandler.cs:line 44
132
+ at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](ref TStateMachine stateMachine)
133
+ at System.Threading.Tasks.Task.ExecuteWithThreadLocal(ref Task currentTaskSlot)`;
134
+ // ── JavaScript / Node (V8) ──────────────────────────────────────────────────
135
+ const NODE_PRIMARY = `TypeError: Cannot read properties of undefined (reading 'total')
136
+ at OrderService.process (/app/dist/orders/service.js:128:35)
137
+ at RequestHandler.handle (/app/dist/web/handler.js:64:20)
138
+ at /app/dist/web/router.js:22:9
139
+ at processTicksAndRejections (node:internal/process/task_queues:95:5)`;
140
+ const NODE_SIBLING = `TypeError: Cannot read properties of undefined (reading 'total')
141
+ at PaymentService.charge (/app/dist/payments/service.js:212:18)
142
+ at RequestHandler.handle (/app/dist/web/handler.js:64:20)
143
+ at /app/dist/web/router.js:22:9
144
+ at processTicksAndRejections (node:internal/process/task_queues:95:5)`;
145
+ const NODE_REDEPLOY = `TypeError: Cannot read properties of undefined (reading 'total')
146
+ at OrderService.process (/app/dist/orders/service.js:131:35)
147
+ at RequestHandler.handle (/app/dist/web/handler.js:70:20)
148
+ at /app/dist/web/router.js:25:9
149
+ at processTicksAndRejections (node:internal/process/task_queues:95:5)`;
150
+ // ── Python ──────────────────────────────────────────────────────────────────
151
+ const PYTHON_PRIMARY = `Traceback (most recent call last):
152
+ File "/app/web/handler.py", line 42, in handle
153
+ return self.service.process(order)
154
+ File "/app/orders/service.py", line 88, in process
155
+ raise ValueError(f"order {order_id} is invalid")
156
+ ValueError: order 4821 is invalid`;
157
+ const PYTHON_SIBLING = `Traceback (most recent call last):
158
+ File "/app/web/handler.py", line 42, in handle
159
+ return self.service.process(order)
160
+ File "/app/billing/invoice.py", line 205, in render
161
+ raise ValueError(f"order {order_id} is invalid")
162
+ ValueError: order 7734 is invalid`;
163
+ export const STACK_FIXTURES = {
164
+ go: {
165
+ primary: GO_PRIMARY,
166
+ sibling: GO_SIBLING,
167
+ redeploy: GO_REDEPLOY,
168
+ primaryFrames: [
169
+ "main.(*OrderService).Process (/app/orders/service.go)",
170
+ "main.(*Handler).ServeHTTP (/app/web/handler.go)",
171
+ "net/http.(*conn).serve (/usr/local/go/src/net/http/server.go)",
172
+ "net/http.(*Server).Serve (/usr/local/go/src/net/http/server.go)",
173
+ ],
174
+ },
175
+ rust: {
176
+ primary: RUST_PRIMARY,
177
+ sibling: RUST_SIBLING,
178
+ redeploy: RUST_REDEPLOY,
179
+ primaryFrames: [
180
+ "rust_begin_unwind (/rustc/abc123/library/std/src/panicking.rs)",
181
+ "core::panicking::panic_fmt (/rustc/abc123/library/core/src/panicking.rs)",
182
+ "core::result::unwrap_failed (/rustc/abc123/library/core/src/result.rs)",
183
+ "myapp::orders::service::OrderService::process (./src/orders/service.rs)",
184
+ "myapp::web::handler::handle_request (./src/web/handler.rs)",
185
+ ],
186
+ },
187
+ java: {
188
+ primary: JAVA_PRIMARY,
189
+ sibling: JAVA_SIBLING,
190
+ redeploy: JAVA_REDEPLOY,
191
+ primaryFrames: [
192
+ "com.example.orders.OrderService.process(OrderService.java)",
193
+ "com.example.web.RequestHandler.handle(RequestHandler.java)",
194
+ "com.example.web.RequestHandler.doGet(RequestHandler.java)",
195
+ "jakarta.servlet.http.HttpServlet.service(HttpServlet.java)",
196
+ "java.base/java.lang.Thread.run(Thread.java)",
197
+ ],
198
+ },
199
+ dotnet: {
200
+ primary: DOTNET_PRIMARY,
201
+ sibling: DOTNET_SIBLING,
202
+ redeploy: DOTNET_REDEPLOY,
203
+ primaryFrames: [
204
+ "MyApp.Orders.OrderService.Process (C:\\src\\MyApp\\Orders\\OrderService.cs)",
205
+ "MyApp.Web.RequestHandler.HandleAsync (C:\\src\\MyApp\\Web\\RequestHandler.cs)",
206
+ "MyApp.Web.RequestHandler.<HandleAsync>d__4.MoveNext (C:\\src\\MyApp\\Web\\RequestHandler.cs)",
207
+ "System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine]",
208
+ "System.Threading.Tasks.Task.ExecuteWithThreadLocal",
209
+ ],
210
+ },
211
+ node: {
212
+ primary: NODE_PRIMARY,
213
+ sibling: NODE_SIBLING,
214
+ redeploy: NODE_REDEPLOY,
215
+ primaryFrames: [
216
+ "at OrderService.process (/app/dist/orders/service.js",
217
+ "at RequestHandler.handle (/app/dist/web/handler.js",
218
+ "at /app/dist/web/router.js",
219
+ "at processTicksAndRejections (node:internal/process/task_queues",
220
+ ],
221
+ },
222
+ python: {
223
+ primary: PYTHON_PRIMARY,
224
+ sibling: PYTHON_SIBLING,
225
+ redeploy: PYTHON_PRIMARY,
226
+ primaryFrames: [
227
+ 'File "/app/web/handler.py", line 42, in handle',
228
+ 'File "/app/orders/service.py", line 88, in process',
229
+ ],
230
+ },
231
+ };