Skip to content

Commit a1e84f0

Browse files
ericallamTrigger.dev RepoOps
authored andcommitted
fix(webapp,core): don't fail trace ingest on exception span events without exception attributes
Fix trace ingestion failing for a whole batch of spans when one span had an exception event without exception details. Mono-RevId: 9bd62ccadeedce0621a9382a46799f4b324cb941
1 parent 47cf81f commit a1e84f0

4 files changed

Lines changed: 113 additions & 4 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: fix
4+
---
5+
6+
Fix run traces occasionally going missing when a span recorded an exception with no message, type or stack trace.

‎apps/webapp/app/v3/eventRepository/clickhouseEventRepository.server.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1064,9 +1064,9 @@ export class ClickhouseEventRepository implements IEventRepository {
10641064
status: "OK",
10651065
attributes: {
10661066
error: {
1067-
message: spanEvent.properties.exception.message,
1068-
name: spanEvent.properties.exception.type,
1069-
stackTrace: spanEvent.properties.exception.stacktrace,
1067+
message: spanEvent.properties.exception?.message,
1068+
name: spanEvent.properties.exception?.type,
1069+
stackTrace: spanEvent.properties.exception?.stacktrace,
10701070
},
10711071
},
10721072
metadata: JSON.stringify(spanEvent.properties),
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { ClickHouse } from "@internal/clickhouse";
2+
import { clickhouseTest } from "@internal/testcontainers";
3+
import { describe, expect, vi } from "vitest";
4+
import { z } from "zod";
5+
import { ClickhouseEventRepository } from "~/v3/eventRepository/clickhouseEventRepository.server";
6+
import type { CreateEventInput } from "~/v3/eventRepository/eventRepository.types";
7+
8+
const ENV_ID = "env_malformed_span_events";
9+
const RUN_ID = "run_malformed_span_events";
10+
11+
function span(spanId: string, events?: CreateEventInput["events"]): CreateEventInput {
12+
const startMs = Date.now();
13+
return {
14+
message: `span ${spanId}`,
15+
traceId: "c".repeat(32),
16+
spanId,
17+
startTime: BigInt(startMs) * 1_000_000n,
18+
duration: 1_000_000,
19+
isPartial: false,
20+
environmentId: ENV_ID,
21+
environmentType: "PRODUCTION",
22+
organizationId: "org_malformed_span_events",
23+
projectId: "proj_malformed_span_events",
24+
runId: RUN_ID,
25+
taskSlug: "malformed-span-events-task",
26+
properties: {},
27+
metadata: undefined,
28+
style: undefined,
29+
events,
30+
};
31+
}
32+
33+
describe("ClickhouseEventRepository span events", () => {
34+
clickhouseTest(
35+
"stores exception and attempt_failed span events without exception attributes instead of failing the batch, keeping attempt_failed as an override",
36+
async ({ clickhouseContainer }) => {
37+
const clickhouse = new ClickHouse({
38+
url: clickhouseContainer.getConnectionUrl(),
39+
logLevel: "error",
40+
});
41+
42+
const repository = new ClickhouseEventRepository({
43+
clickhouse,
44+
version: "v2",
45+
insertStrategy: "insert",
46+
batchSize: 100,
47+
flushInterval: 200,
48+
});
49+
50+
const eventTime = new Date(Date.now() + 10);
51+
52+
try {
53+
expect(() =>
54+
repository.insertMany([
55+
span("aaaaaaaaaaaaaaaa", [
56+
{ name: "exception", time: eventTime, properties: {} },
57+
{
58+
name: "attempt_failed",
59+
time: eventTime,
60+
properties: { attemptNumber: 1, runId: RUN_ID },
61+
},
62+
]),
63+
span("bbbbbbbbbbbbbbbb"),
64+
])
65+
).not.toThrow();
66+
67+
const queryRows = clickhouse.reader.query({
68+
name: "malformed-span-events-check",
69+
query:
70+
"SELECT span_id, kind, message FROM trigger_dev.task_events_v2 WHERE run_id = {runId: String}",
71+
schema: z.object({ span_id: z.string(), kind: z.string(), message: z.string() }),
72+
params: z.object({ runId: z.string() }),
73+
});
74+
75+
const rows = await vi.waitFor(
76+
async () => {
77+
const [error, result] = await queryRows({ runId: RUN_ID });
78+
expect(error).toBeNull();
79+
expect(result).toHaveLength(4);
80+
return result!;
81+
},
82+
{ timeout: 30_000, interval: 250 }
83+
);
84+
85+
const kindByMessage = Object.fromEntries(rows.map((r) => [r.message, r.kind]));
86+
expect(kindByMessage.exception).toBe("SPAN_EVENT");
87+
expect(kindByMessage.attempt_failed).toBe("ANCESTOR_OVERRIDE");
88+
} finally {
89+
await Promise.all([
90+
(repository as any)._flushScheduler.shutdown(),
91+
(repository as any)._llmMetricsFlushScheduler.shutdown(),
92+
(repository as any)._otlpMetricsFlushScheduler.shutdown(),
93+
]);
94+
}
95+
},
96+
60_000
97+
);
98+
});

‎packages/core/src/v3/schemas/openTelemetry.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,13 @@ export const SpanEvents = z.array(SpanEvent);
6161

6262
export type SpanEvents = z.infer<typeof SpanEvents>;
6363

64+
function hasExceptionProperties(event: SpanEvent): boolean {
65+
const exception = (event.properties as Record<string, unknown> | undefined)?.exception;
66+
return typeof exception === "object" && exception !== null;
67+
}
68+
6469
export function isExceptionSpanEvent(event: SpanEvent): event is ExceptionSpanEvent {
65-
return event.name === "exception";
70+
return event.name === "exception" && hasExceptionProperties(event);
6671
}
6772

6873
export function isCancellationSpanEvent(event: SpanEvent): event is CancellationSpanEvent {

0 commit comments

Comments
 (0)