Skip to content

Commit b33352b

Browse files
committed
fix(node): Defensively check inheritableHandlers in LangChain dedupe
In practice CallbackManager keeps `inheritableHandlers ⊆ handlers` (both `addHandler` and `setHandlers` maintain the invariant), so checking `handlers` alone suffices. But an externally-constructed manager subclass could in theory leak the Sentry handler onto `inheritableHandlers` without mirroring it. Checking both lists costs nothing and pre-empts the duplicate-span class of bug Sentry's own reviewer flagged.
1 parent 4c74bfa commit b33352b

2 files changed

Lines changed: 31 additions & 6 deletions

File tree

packages/node/src/integrations/tracing/langchain/instrumentation.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,16 @@ function augmentCallbackHandlers(handlers: unknown, sentryHandler: unknown): unk
9494
const copied = handlers.copy() as {
9595
addHandler: (handler: unknown, inherit?: boolean) => void;
9696
handlers?: unknown[];
97+
inheritableHandlers?: unknown[];
9798
};
98-
// Avoid double-registering if the caller already added us.
99-
const existing = copied.handlers ?? [];
100-
if (!existing.includes(sentryHandler)) {
99+
// Avoid double-registering on nested invocations. CallbackManager's own
100+
// `addHandler` keeps `inheritableHandlers ⊆ handlers`, so checking
101+
// `handlers` alone is normally enough — but we check both as a defensive
102+
// guard against externally-constructed managers that bypass `addHandler`.
103+
const alreadyRegistered =
104+
(copied.handlers?.includes(sentryHandler) ?? false) ||
105+
(copied.inheritableHandlers?.includes(sentryHandler) ?? false);
106+
if (!alreadyRegistered) {
101107
copied.addHandler(sentryHandler, true);
102108
}
103109
return copied;

packages/node/test/integrations/tracing/langchain.test.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,18 @@ const sentryHandler = { name: 'SentryCallbackHandler' };
88
* (`addHandler` + `copy`) for the production code to recognize this as a
99
* `CallbackManager` rather than fall through to the "unknown" branch.
1010
*/
11-
function makeFakeCallbackManager(existingHandlers: unknown[] = []) {
11+
function makeFakeCallbackManager(existingHandlers: unknown[] = [], existingInheritableHandlers?: unknown[]) {
1212
const manager = {
1313
handlers: [...existingHandlers],
14-
addHandler: vi.fn(function (this: any, handler: unknown, _inherit?: boolean) {
14+
inheritableHandlers: [...(existingInheritableHandlers ?? existingHandlers)],
15+
addHandler: vi.fn(function (this: any, handler: unknown, inherit?: boolean) {
1516
this.handlers.push(handler);
17+
if (inherit !== false) {
18+
this.inheritableHandlers.push(handler);
19+
}
1620
}),
1721
copy: vi.fn(function (this: any) {
18-
return makeFakeCallbackManager(this.handlers);
22+
return makeFakeCallbackManager(this.handlers, this.inheritableHandlers);
1923
}),
2024
};
2125
return manager;
@@ -79,6 +83,21 @@ describe('augmentCallbackHandlers', () => {
7983
expect(result.addHandler).not.toHaveBeenCalled();
8084
});
8185

86+
test('does not double-register when the handler lives only on inheritableHandlers', () => {
87+
// Defensive: a CallbackManager subclass or externally-constructed
88+
// instance might keep the Sentry handler on `inheritableHandlers`
89+
// without mirroring it onto `handlers`. We must still recognize it
90+
// as already-registered to avoid duplicate spans on nested calls.
91+
const manager = makeFakeCallbackManager([], [sentryHandler]);
92+
const result = _INTERNAL_augmentCallbackHandlers(manager, sentryHandler) as {
93+
handlers: unknown[];
94+
inheritableHandlers: unknown[];
95+
addHandler: ReturnType<typeof vi.fn>;
96+
};
97+
expect(result.addHandler).not.toHaveBeenCalled();
98+
expect(result.inheritableHandlers).toEqual([sentryHandler]);
99+
});
100+
82101
test('returns the value unchanged when it is neither an array nor a CallbackManager', () => {
83102
const opaque = { name: 'NotAManager' };
84103
const result = _INTERNAL_augmentCallbackHandlers(opaque, sentryHandler);

0 commit comments

Comments
 (0)