Skip to content

Commit ef9eaa6

Browse files
committed
improve implementation
1 parent 3268548 commit ef9eaa6

1 file changed

Lines changed: 36 additions & 28 deletions

File tree

packages/node-core/src/integrations/console.ts

Lines changed: 36 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any */
2-
/* eslint-disable @typescript-eslint/ban-types */
32
import type { ConsoleLevel, HandlerDataConsole, WrappedFunction } from '@sentry/core';
43
import {
54
CONSOLE_LEVELS,
@@ -42,61 +41,72 @@ export const consoleIntegration = defineIntegration((options: Partial<ConsoleInt
4241
});
4342

4443
function instrumentConsoleLambda(): void {
45-
if (!('console' in GLOBAL_OBJ)) {
44+
const consoleObj = GLOBAL_OBJ?.console;
45+
if (!consoleObj) {
4646
return;
4747
}
4848

49-
CONSOLE_LEVELS.forEach(function (level: ConsoleLevel): void {
50-
if (!(level in GLOBAL_OBJ.console)) {
51-
return;
49+
CONSOLE_LEVELS.forEach((level: ConsoleLevel) => {
50+
if (level in consoleObj) {
51+
patchWithDefineProperty(consoleObj, level);
5252
}
53-
54-
patchWithDefineProperty(level);
5553
});
5654
}
5755

58-
function patchWithDefineProperty(level: ConsoleLevel): void {
59-
const nativeMethod = GLOBAL_OBJ.console[level] as (...args: unknown[]) => void;
56+
function patchWithDefineProperty(consoleObj: Console, level: ConsoleLevel): void {
57+
const nativeMethod = consoleObj[level] as (...args: unknown[]) => void;
6058
originalConsoleMethods[level] = nativeMethod;
6159

62-
let consoleDelegate: Function = nativeMethod;
60+
let delegate: Function = nativeMethod;
61+
let savedDelegate: Function | undefined;
6362
let isExecuting = false;
6463

6564
const wrapper = function (...args: any[]): void {
6665
if (isExecuting) {
67-
// Re-entrant call: a third party captured `wrapper` via the getter and calls it
68-
// from inside their replacement (e.g. `const prev = console.log; console.log = (...a) => { prev(...a); }`).
69-
// Calling `consoleDelegate` here would recurse, so fall back to the native method.
70-
nativeMethod.apply(GLOBAL_OBJ.console, args);
66+
// Re-entrant call: a third party captured `wrapper` via the getter and calls it from inside their replacement. We must
67+
// use `nativeMethod` (not `delegate`) to break the cycle, and we intentionally skip `triggerHandlers` to avoid duplicate
68+
// breadcrumbs. The outer invocation already triggered the handlers for this console call.
69+
nativeMethod.apply(consoleObj, args);
7170
return;
7271
}
7372
isExecuting = true;
7473
try {
7574
triggerHandlers('console', { args, level } as HandlerDataConsole);
76-
consoleDelegate.apply(GLOBAL_OBJ.console, args);
75+
delegate.apply(consoleObj, args);
7776
} finally {
7877
isExecuting = false;
7978
}
8079
};
8180
markFunctionWrapped(wrapper as unknown as WrappedFunction, nativeMethod as unknown as WrappedFunction);
8281

82+
// consoleSandbox reads originalConsoleMethods[level] to temporarily bypass instrumentation. We replace it with a distinct reference (.bind creates a
83+
// new function identity) so the setter can tell apart "consoleSandbox bypass" from "external code restoring a native method captured before Sentry init."
84+
const sandboxBypass = nativeMethod.bind(consoleObj);
85+
originalConsoleMethods[level] = sandboxBypass;
86+
8387
try {
8488
let current: any = wrapper;
8589

86-
Object.defineProperty(GLOBAL_OBJ.console, level, {
90+
Object.defineProperty(consoleObj, level, {
8791
configurable: true,
8892
enumerable: true,
8993
get() {
9094
return current;
9195
},
9296
set(newValue) {
93-
if (
94-
typeof newValue === 'function' &&
95-
newValue !== wrapper &&
96-
newValue !== originalConsoleMethods[level] &&
97-
!(newValue as WrappedFunction).__sentry_original__
98-
) {
99-
consoleDelegate = newValue;
97+
if (newValue === wrapper) {
98+
// consoleSandbox restoring the wrapper: recover the saved delegate.
99+
if (savedDelegate !== undefined) {
100+
delegate = savedDelegate;
101+
savedDelegate = undefined;
102+
}
103+
current = wrapper;
104+
} else if (newValue === sandboxBypass) {
105+
// consoleSandbox entering bypass: save delegate, let getter return sandboxBypass directly so calls skip the wrapper entirely.
106+
savedDelegate = delegate;
107+
current = sandboxBypass;
108+
} else if (typeof newValue === 'function' && !(newValue as WrappedFunction).__sentry_original__) {
109+
delegate = newValue;
100110
current = wrapper;
101111
} else {
102112
current = newValue;
@@ -105,14 +115,12 @@ function patchWithDefineProperty(level: ConsoleLevel): void {
105115
});
106116
} catch {
107117
// Fall back to fill-based patching if defineProperty fails
108-
fill(GLOBAL_OBJ.console, level, function (originalConsoleMethod: () => any): Function {
118+
fill(consoleObj, level, function (originalConsoleMethod: () => any): Function {
109119
originalConsoleMethods[level] = originalConsoleMethod;
110120

111-
return function (...args: any[]): void {
121+
return function (this: Console, ...args: any[]): void {
112122
triggerHandlers('console', { args, level } as HandlerDataConsole);
113-
114-
const log = originalConsoleMethods[level];
115-
log?.apply(GLOBAL_OBJ.console, args);
123+
originalConsoleMethods[level]?.apply(this, args);
116124
};
117125
});
118126
}

0 commit comments

Comments
 (0)