-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Expand file tree
/
Copy pathconsole.ts
More file actions
127 lines (115 loc) · 4.46 KB
/
console.ts
File metadata and controls
127 lines (115 loc) · 4.46 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { ConsoleLevel, HandlerDataConsole, WrappedFunction } from '@sentry/core';
import {
CONSOLE_LEVELS,
GLOBAL_OBJ,
consoleIntegration as coreConsoleIntegration,
defineIntegration,
fill,
markFunctionWrapped,
maybeInstrument,
originalConsoleMethods,
triggerHandlers,
} from '@sentry/core';
interface ConsoleIntegrationOptions {
levels: ConsoleLevel[];
}
/**
* Node-specific console integration that captures breadcrumbs and handles
* the AWS Lambda runtime replacing console methods after our patch.
*
* In Lambda, console methods are patched via `Object.defineProperty` so that
* external replacements (by the Lambda runtime) are absorbed as the delegate
* while our wrapper stays in place. Outside Lambda, this delegates entirely
* to the core `consoleIntegration` which uses the simpler `fill`-based patch.
*/
export const consoleIntegration = defineIntegration((options: Partial<ConsoleIntegrationOptions> = {}) => {
return {
name: 'Console',
setup(client) {
if (process.env.LAMBDA_TASK_ROOT) {
maybeInstrument('console', instrumentConsoleLambda);
}
// Delegate breadcrumb handling to the core console integration.
const core = coreConsoleIntegration(options);
core.setup?.(client);
},
};
});
function instrumentConsoleLambda(): void {
const consoleObj = GLOBAL_OBJ?.console;
if (!consoleObj) {
return;
}
CONSOLE_LEVELS.forEach((level: ConsoleLevel) => {
if (level in consoleObj) {
patchWithDefineProperty(consoleObj, level);
}
});
}
function patchWithDefineProperty(consoleObj: Console, level: ConsoleLevel): void {
const nativeMethod = consoleObj[level] as (...args: unknown[]) => void;
originalConsoleMethods[level] = nativeMethod;
let delegate: Function = nativeMethod;
let savedDelegate: Function | undefined;
let isExecuting = false;
const wrapper = function (...args: any[]): void {
if (isExecuting) {
// Re-entrant call: a third party captured `wrapper` via the getter and calls it from inside their replacement. We must
// use `nativeMethod` (not `delegate`) to break the cycle, and we intentionally skip `triggerHandlers` to avoid duplicate
// breadcrumbs. The outer invocation already triggered the handlers for this console call.
nativeMethod.apply(consoleObj, args);
return;
}
isExecuting = true;
try {
triggerHandlers('console', { args, level } as HandlerDataConsole);
delegate.apply(consoleObj, args);
} finally {
isExecuting = false;
}
};
markFunctionWrapped(wrapper as unknown as WrappedFunction, nativeMethod as unknown as WrappedFunction);
// consoleSandbox reads originalConsoleMethods[level] to temporarily bypass instrumentation. We replace it with a distinct reference (.bind creates a
// new function identity) so the setter can tell apart "consoleSandbox bypass" from "external code restoring a native method captured before Sentry init."
const sandboxBypass = nativeMethod.bind(consoleObj);
originalConsoleMethods[level] = sandboxBypass;
try {
let current: any = wrapper;
Object.defineProperty(consoleObj, level, {
configurable: true,
enumerable: true,
get() {
return current;
},
set(newValue) {
if (newValue === wrapper) {
// consoleSandbox restoring the wrapper: recover the saved delegate.
if (savedDelegate !== undefined) {
delegate = savedDelegate;
savedDelegate = undefined;
}
current = wrapper;
} else if (newValue === sandboxBypass) {
// consoleSandbox entering bypass: save delegate, let getter return sandboxBypass directly so calls skip the wrapper entirely.
savedDelegate = delegate;
current = sandboxBypass;
} else if (typeof newValue === 'function' && !(newValue as WrappedFunction).__sentry_original__) {
delegate = newValue;
current = wrapper;
} else {
current = newValue;
}
},
});
} catch {
// Fall back to fill-based patching if defineProperty fails
fill(consoleObj, level, function (originalConsoleMethod: () => any): Function {
originalConsoleMethods[level] = originalConsoleMethod;
return function (this: Console, ...args: any[]): void {
triggerHandlers('console', { args, level } as HandlerDataConsole);
originalConsoleMethods[level]?.apply(this, args);
};
});
}
}