Skip to content

Commit e3e3636

Browse files
committed
exprot consoleIntegration from node-core
1 parent 249d85d commit e3e3636

File tree

8 files changed

+146
-99
lines changed

8 files changed

+146
-99
lines changed

.size-limit.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ module.exports = [
131131
path: 'packages/browser/build/npm/esm/prod/index.js',
132132
import: createImport('init', 'metrics', 'logger'),
133133
gzip: true,
134-
limit: '30 KB',
134+
limit: '28 KB',
135135
},
136136
// React SDK (ESM)
137137
{
@@ -196,7 +196,7 @@ module.exports = [
196196
name: 'CDN Bundle (incl. Tracing, Logs, Metrics)',
197197
path: createCDNPath('bundle.tracing.logs.metrics.min.js'),
198198
gzip: true,
199-
limit: '47 KB',
199+
limit: '45 KB',
200200
},
201201
{
202202
name: 'CDN Bundle (incl. Replay, Logs, Metrics)',
@@ -234,14 +234,14 @@ module.exports = [
234234
path: createCDNPath('bundle.min.js'),
235235
gzip: false,
236236
brotli: false,
237-
limit: '84 KB',
237+
limit: '83.5 KB',
238238
},
239239
{
240240
name: 'CDN Bundle (incl. Tracing) - uncompressed',
241241
path: createCDNPath('bundle.tracing.min.js'),
242242
gzip: false,
243243
brotli: false,
244-
limit: '132 KB',
244+
limit: '130 KB',
245245
},
246246
{
247247
name: 'CDN Bundle (incl. Logs, Metrics) - uncompressed',
@@ -262,7 +262,7 @@ module.exports = [
262262
path: createCDNPath('bundle.replay.logs.metrics.min.js'),
263263
gzip: false,
264264
brotli: false,
265-
limit: '213 KB',
265+
limit: '211 KB',
266266
},
267267
{
268268
name: 'CDN Bundle (incl. Tracing, Replay) - uncompressed',
@@ -276,7 +276,7 @@ module.exports = [
276276
path: createCDNPath('bundle.tracing.replay.logs.metrics.min.js'),
277277
gzip: false,
278278
brotli: false,
279-
limit: '253 KB',
279+
limit: '251 KB',
280280
},
281281
{
282282
name: 'CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed',
Lines changed: 9 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any */
22
/* eslint-disable @typescript-eslint/ban-types */
33
import type { ConsoleLevel, HandlerDataConsole } from '../types-hoist/instrument';
4-
import type { WrappedFunction } from '../types-hoist/wrappedfunction';
54
import { CONSOLE_LEVELS, originalConsoleMethods } from '../utils/debug-logger';
6-
import { fill, markFunctionWrapped } from '../utils/object';
5+
import { fill } from '../utils/object';
76
import { GLOBAL_OBJ } from '../utils/worldwide';
87
import { addHandler, maybeInstrument, triggerHandlers } from './handlers';
98

@@ -29,83 +28,15 @@ function instrumentConsole(): void {
2928
return;
3029
}
3130

32-
if (typeof process !== 'undefined' && !!process.env.LAMBDA_TASK_ROOT) {
33-
// The AWS Lambda runtime replaces console methods AFTER our patch, which overwrites them.
34-
patchWithDefineProperty(level);
35-
} else {
36-
patchWithFill(level);
37-
}
38-
});
39-
}
40-
41-
function patchWithFill(level: ConsoleLevel): void {
42-
fill(GLOBAL_OBJ.console, level, function (originalConsoleMethod: () => any): Function {
43-
originalConsoleMethods[level] = originalConsoleMethod;
44-
45-
return function (...args: any[]): void {
46-
triggerHandlers('console', { args, level } as HandlerDataConsole);
47-
48-
const log = originalConsoleMethods[level];
49-
log?.apply(GLOBAL_OBJ.console, args);
50-
};
51-
});
52-
}
31+
fill(GLOBAL_OBJ.console, level, function (originalConsoleMethod: () => any): Function {
32+
originalConsoleMethods[level] = originalConsoleMethod;
5333

54-
function patchWithDefineProperty(level: ConsoleLevel): void {
55-
const nativeMethod = GLOBAL_OBJ.console[level] as (...args: unknown[]) => void;
56-
originalConsoleMethods[level] = nativeMethod;
34+
return function (...args: any[]): void {
35+
triggerHandlers('console', { args, level } as HandlerDataConsole);
5736

58-
let consoleDelegate: Function = nativeMethod;
59-
let isExecuting = false;
60-
61-
const wrapper = function (...args: any[]): void {
62-
if (isExecuting) {
63-
// Re-entrant call: a third party captured `wrapper` via the getter and calls it
64-
// from inside their replacement (e.g. `const prev = console.log; console.log = (...a) => { prev(...a); }`).
65-
// Calling `consoleDelegate` here would recurse, so fall back to the native method.
66-
nativeMethod.apply(GLOBAL_OBJ.console, args);
67-
return;
68-
}
69-
isExecuting = true;
70-
try {
71-
triggerHandlers('console', { args, level });
72-
consoleDelegate.apply(GLOBAL_OBJ.console, args);
73-
} finally {
74-
isExecuting = false;
75-
}
76-
};
77-
markFunctionWrapped(wrapper as unknown as WrappedFunction, nativeMethod as unknown as WrappedFunction);
78-
79-
try {
80-
let current: any = wrapper;
81-
82-
Object.defineProperty(GLOBAL_OBJ.console, level, {
83-
configurable: true,
84-
enumerable: true,
85-
get() {
86-
return current;
87-
},
88-
// When `console[level]` is set to a new value, we want to check if it's something not done by us but by e.g. the Lambda runtime.
89-
set(newValue) {
90-
if (
91-
typeof newValue === 'function' &&
92-
// Ignore if it's set to the wrapper (e.g. by our own patch or consoleSandbox), which would cause an infinite loop.
93-
newValue !== wrapper &&
94-
// Function is not one of our wrappers (which have __sentry_original__) and not the original (stored in originalConsoleMethods)
95-
newValue !== originalConsoleMethods[level] &&
96-
!(newValue as WrappedFunction).__sentry_original__
97-
) {
98-
// Absorb newly "set" function as the consoleDelegate but keep our wrapper as the active method.
99-
consoleDelegate = newValue;
100-
current = wrapper;
101-
} else {
102-
// Accept as-is: consoleSandbox restoring, other Sentry wrappers, or non-functions
103-
current = newValue;
104-
}
105-
},
37+
const log = originalConsoleMethods[level];
38+
log?.apply(GLOBAL_OBJ.console, args);
39+
};
10640
});
107-
} catch {
108-
// In case defineProperty fails (e.g. in older browsers), fall back to fill-style patching
109-
patchWithFill(level);
110-
}
41+
});
11142
}

packages/node-core/src/common-exports.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export { systemErrorIntegration } from './integrations/systemError';
2727
export { childProcessIntegration } from './integrations/childProcess';
2828
export { createSentryWinstonTransport } from './integrations/winston';
2929
export { pinoIntegration } from './integrations/pino';
30+
export { consoleIntegration } from './integrations/console';
3031

3132
// SDK utilities
3233
export { getSentryRelease, defaultStackParser } from './sdk/api';
@@ -117,7 +118,6 @@ export {
117118
profiler,
118119
consoleLoggingIntegration,
119120
createConsolaReporter,
120-
consoleIntegration,
121121
wrapMcpServerWithSentry,
122122
featureFlagsIntegration,
123123
spanStreamingIntegration,
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
/* eslint-disable @typescript-eslint/ban-types */
3+
import type { ConsoleLevel, HandlerDataConsole, WrappedFunction } from '@sentry/core';
4+
import {
5+
CONSOLE_LEVELS,
6+
GLOBAL_OBJ,
7+
consoleIntegration as coreConsoleIntegration,
8+
defineIntegration,
9+
fill,
10+
markFunctionWrapped,
11+
maybeInstrument,
12+
originalConsoleMethods,
13+
triggerHandlers,
14+
} from '@sentry/core';
15+
16+
interface ConsoleIntegrationOptions {
17+
levels: ConsoleLevel[];
18+
}
19+
20+
/**
21+
* Node-specific console integration that captures breadcrumbs and handles
22+
* the AWS Lambda runtime replacing console methods after our patch.
23+
*
24+
* In Lambda, console methods are patched via `Object.defineProperty` so that
25+
* external replacements (by the Lambda runtime) are absorbed as the delegate
26+
* while our wrapper stays in place. Outside Lambda, this delegates entirely
27+
* to the core `consoleIntegration` which uses the simpler `fill`-based patch.
28+
*/
29+
export const consoleIntegration = defineIntegration((options: Partial<ConsoleIntegrationOptions> = {}) => {
30+
return {
31+
name: 'Console',
32+
setup(client) {
33+
if (process.env.LAMBDA_TASK_ROOT) {
34+
maybeInstrument('console', instrumentConsoleLambda);
35+
}
36+
37+
// Delegate breadcrumb handling to the core console integration.
38+
const core = coreConsoleIntegration(options);
39+
core.setup?.(client);
40+
},
41+
};
42+
});
43+
44+
function instrumentConsoleLambda(): void {
45+
if (!('console' in GLOBAL_OBJ)) {
46+
return;
47+
}
48+
49+
CONSOLE_LEVELS.forEach(function (level: ConsoleLevel): void {
50+
if (!(level in GLOBAL_OBJ.console)) {
51+
return;
52+
}
53+
54+
patchWithDefineProperty(level);
55+
});
56+
}
57+
58+
function patchWithDefineProperty(level: ConsoleLevel): void {
59+
const nativeMethod = GLOBAL_OBJ.console[level] as (...args: unknown[]) => void;
60+
originalConsoleMethods[level] = nativeMethod;
61+
62+
let consoleDelegate: Function = nativeMethod;
63+
let isExecuting = false;
64+
65+
const wrapper = function (...args: any[]): void {
66+
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);
71+
return;
72+
}
73+
isExecuting = true;
74+
try {
75+
triggerHandlers('console', { args, level } as HandlerDataConsole);
76+
consoleDelegate.apply(GLOBAL_OBJ.console, args);
77+
} finally {
78+
isExecuting = false;
79+
}
80+
};
81+
markFunctionWrapped(wrapper as unknown as WrappedFunction, nativeMethod as unknown as WrappedFunction);
82+
83+
try {
84+
let current: any = wrapper;
85+
86+
Object.defineProperty(GLOBAL_OBJ.console, level, {
87+
configurable: true,
88+
enumerable: true,
89+
get() {
90+
return current;
91+
},
92+
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;
100+
current = wrapper;
101+
} else {
102+
current = newValue;
103+
}
104+
},
105+
});
106+
} catch {
107+
// Fall back to fill-based patching if defineProperty fails
108+
fill(GLOBAL_OBJ.console, level, function (originalConsoleMethod: () => any): Function {
109+
originalConsoleMethods[level] = originalConsoleMethod;
110+
111+
return function (...args: any[]): void {
112+
triggerHandlers('console', { args, level } as HandlerDataConsole);
113+
114+
const log = originalConsoleMethods[level];
115+
log?.apply(GLOBAL_OBJ.console, args);
116+
};
117+
});
118+
}
119+
}

packages/node-core/src/light/sdk.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import type { Integration, Options } from '@sentry/core';
22
import {
33
applySdkMetadata,
4-
consoleIntegration,
54
consoleSandbox,
65
debug,
76
envToBool,
@@ -25,6 +24,7 @@ import { onUncaughtExceptionIntegration } from '../integrations/onuncaughtexcept
2524
import { onUnhandledRejectionIntegration } from '../integrations/onunhandledrejection';
2625
import { processSessionIntegration } from '../integrations/processSession';
2726
import { INTEGRATION_NAME as SPOTLIGHT_INTEGRATION_NAME, spotlightIntegration } from '../integrations/spotlight';
27+
import { consoleIntegration } from '../integrations/console';
2828
import { systemErrorIntegration } from '../integrations/systemError';
2929
import { defaultStackParser, getSentryRelease } from '../sdk/api';
3030
import { makeNodeTransport } from '../transports';

packages/node-core/src/sdk/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import type { Integration, Options } from '@sentry/core';
22
import {
33
applySdkMetadata,
4-
consoleIntegration,
54
consoleSandbox,
65
conversationIdIntegration,
76
debug,
@@ -35,6 +34,7 @@ import { onUncaughtExceptionIntegration } from '../integrations/onuncaughtexcept
3534
import { onUnhandledRejectionIntegration } from '../integrations/onunhandledrejection';
3635
import { processSessionIntegration } from '../integrations/processSession';
3736
import { INTEGRATION_NAME as SPOTLIGHT_INTEGRATION_NAME, spotlightIntegration } from '../integrations/spotlight';
37+
import { consoleIntegration } from '../integrations/console';
3838
import { systemErrorIntegration } from '../integrations/systemError';
3939
import { makeNodeTransport } from '../transports';
4040
import type { NodeClientOptions, NodeOptions } from '../types';

packages/core/test/lib/instrument/console-lambda.test.ts renamed to packages/node-core/test/integrations/console.test.ts

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
1-
// Set LAMBDA_TASK_ROOT before any imports so instrumentConsole uses patchWithDefineProperty
1+
// Set LAMBDA_TASK_ROOT before any imports so consoleIntegration uses patchWithDefineProperty
22
process.env.LAMBDA_TASK_ROOT = '/var/task';
33

44
import { afterAll, describe, expect, it, vi } from 'vitest';
5-
import { addConsoleInstrumentationHandler } from '../../../src/instrument/console';
6-
import type { WrappedFunction } from '../../../src/types-hoist/wrappedfunction';
7-
import { consoleSandbox, originalConsoleMethods } from '../../../src/utils/debug-logger';
8-
import { markFunctionWrapped } from '../../../src/utils/object';
9-
import { GLOBAL_OBJ } from '../../../src/utils/worldwide';
5+
import type { WrappedFunction } from '@sentry/core';
6+
import { addConsoleInstrumentationHandler, consoleSandbox, markFunctionWrapped, originalConsoleMethods, GLOBAL_OBJ } from '@sentry/core';
7+
import { consoleIntegration } from '../../src/integrations/console';
108

119
afterAll(() => {
1210
delete process.env.LAMBDA_TASK_ROOT;
1311
});
1412

15-
describe('addConsoleInstrumentationHandler in Lambda (patchWithDefineProperty)', () => {
13+
describe('consoleIntegration in Lambda (patchWithDefineProperty)', () => {
1614
it('calls registered handler when console.log is called', () => {
1715
const handler = vi.fn();
16+
// Setup the integration so it calls maybeInstrument with the Lambda strategy
17+
consoleIntegration().setup?.({ on: vi.fn() } as any);
18+
1819
addConsoleInstrumentationHandler(handler);
1920

2021
GLOBAL_OBJ.console.log('test');
@@ -162,17 +163,13 @@ describe('addConsoleInstrumentationHandler in Lambda (patchWithDefineProperty)',
162163
addConsoleInstrumentationHandler(handler);
163164
handler.mockClear();
164165

165-
// This is the extremely common pattern used by logging libraries, test frameworks, etc:
166-
// const prevLog = console.log;
167-
// console.log = (...args) => { prevLog(...args); doSomethingElse(); }
168166
const prevLog = GLOBAL_OBJ.console.log;
169167
const thirdPartyExtra = vi.fn();
170168
GLOBAL_OBJ.console.log = (...args: any[]) => {
171169
prevLog(...args);
172170
thirdPartyExtra(...args);
173171
};
174172

175-
// With the bug, this causes "Maximum call stack size exceeded"
176173
expect(() => GLOBAL_OBJ.console.log('should not overflow')).not.toThrow();
177174

178175
expect(thirdPartyExtra).toHaveBeenCalledWith('should not overflow');

packages/node/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,6 @@ export {
137137
profiler,
138138
consoleLoggingIntegration,
139139
createConsolaReporter,
140-
consoleIntegration,
141140
wrapMcpServerWithSentry,
142141
featureFlagsIntegration,
143142
spanStreamingIntegration,
@@ -192,6 +191,7 @@ export {
192191
processSessionIntegration,
193192
nodeRuntimeMetricsIntegration,
194193
type NodeRuntimeMetricsOptions,
194+
consoleIntegration,
195195
pinoIntegration,
196196
createSentryWinstonTransport,
197197
SentryContextManager,

0 commit comments

Comments
 (0)