Skip to content

Commit b1b6ed6

Browse files
committed
better handle sync errors
1 parent af762bf commit b1b6ed6

1 file changed

Lines changed: 27 additions & 14 deletions

File tree

  • packages/node/src/integrations/tracing-channel

packages/node/src/integrations/tracing-channel/mysql.ts

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,17 @@ const _mysqlChannelIntegration = (() => {
4444
// asyncEnd for some reason.
4545
const spans = new WeakMap<object, Span>();
4646

47-
// `subscribe()` requires all five lifecycle hooks. For callback-style mysql:
48-
// - `start` — synchronous entry to `Connection.query`. Start span here.
49-
// - `end` — synchronous return from `Connection.query` (BEFORE the
50-
// network round-trip completes). Not the right span end.
51-
// - `error` — the query callback fired with an error.
52-
// - `asyncStart` — about to invoke the user callback (query result is ready).
53-
// - `asyncEnd` — user callback returned. This is the moment we want to
54-
// end the span — it captures the full query latency.
47+
// `subscribe()` requires all five lifecycle hooks. The orchestrion
48+
// `wrapCallback` transform fires them in one of three orders:
49+
// - sync throw from `query()` : start → error → end (NO asyncEnd)
50+
// - async error from callback : start → end → error → asyncStart → asyncEnd
51+
// - async success : start → end → asyncStart → asyncEnd
52+
// We end the span on `asyncEnd` for the two async paths (so the span
53+
// covers the full network round-trip + callback duration), and fall back
54+
// to `end` for the sync-throw path so the span isn't left unfinished.
55+
// The discriminator between "end fired before any error" and "end fired
56+
// after a sync throw" is whether `ctx.error` is set when `end` runs —
57+
// orchestrion populates it before publishing `error`.
5558
queryCh.subscribe({
5659
start(rawCtx) {
5760
const ctx = rawCtx as MysqlQueryChannelContext;
@@ -71,8 +74,14 @@ const _mysqlChannelIntegration = (() => {
7174
spans.set(rawCtx, span);
7275
},
7376

74-
end() {
75-
// No-op: span ends in `asyncEnd` once the network round-trip completes.
77+
end(rawCtx) {
78+
// Only acts for sync throws: `end` fires AFTER `error` (both inside
79+
// the wrapper's `try/catch/finally`), so `ctx.error` is already set.
80+
// For async paths `end` fires before `error`, so `ctx.error` is still
81+
// undefined here and we leave the span open for `asyncEnd` to close.
82+
const ctx = rawCtx as MysqlQueryChannelContext;
83+
if (ctx.error === undefined) return;
84+
finishSpan(rawCtx);
7685
},
7786

7887
error(rawCtx) {
@@ -90,12 +99,16 @@ const _mysqlChannelIntegration = (() => {
9099
},
91100

92101
asyncEnd(rawCtx) {
93-
const span = spans.get(rawCtx);
94-
if (!span) return;
95-
span.end();
96-
spans.delete(rawCtx);
102+
finishSpan(rawCtx);
97103
},
98104
});
105+
106+
function finishSpan(rawCtx: object): void {
107+
const span = spans.get(rawCtx);
108+
if (!span) return;
109+
span.end();
110+
spans.delete(rawCtx);
111+
}
99112
},
100113
};
101114
}) satisfies IntegrationFn;

0 commit comments

Comments
 (0)