Skip to content

fix(console): Re-patch console in AWS Lambda runtimes#20337

Open
s1gr1d wants to merge 13 commits intodevelopfrom
sig/console-aws-lambda-fix
Open

fix(console): Re-patch console in AWS Lambda runtimes#20337
s1gr1d wants to merge 13 commits intodevelopfrom
sig/console-aws-lambda-fix

Conversation

@s1gr1d
Copy link
Copy Markdown
Member

@s1gr1d s1gr1d commented Apr 15, 2026

On AWS Lambda, the Node.js runtime replaces console.* methods with its own loggers. This means Sentry's console instrumentation gets silently overwritten, and integrations like consoleLoggingIntegration stop capturing console output entirely.

This PR fixes that by introducing a defineProperty-based patching strategy for Lambda environments. Instead of simply assigning a wrapper to console.log (which Lambda can overwrite), we define a getter/setter on the console property. When the Lambda runtime assigns its logger, the setter intercepts it, stores the new function as the underlying delegate, and keeps Sentry's wrapper in place. The handler continues to fire, and the Lambda logger still gets called underneath (I checked that manually - the log is still shown in the CloudWatch logs).

This behavior is guarded behind process.env.LAMBDA_TASK_ROOT, so non-Lambda environments continue to use the existing fill()-based patching with zero behavioral change. If defineProperty fails for any reason, it falls back to fill().

The setter also handles consoleSandbox correctly (recognizes when it restores the original method and allows it through), and defers to other Sentry wrappers by checking for __sentry_original__.

Closes #18238

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 15, 2026

size-limit report 📦

Path Size % Change Change
@sentry/browser 25.88 kB - -
@sentry/browser - with treeshaking flags 24.35 kB - -
@sentry/browser (incl. Tracing) 43.81 kB - -
@sentry/browser (incl. Tracing + Span Streaming) 45.5 kB - -
@sentry/browser (incl. Tracing, Profiling) 48.73 kB - -
@sentry/browser (incl. Tracing, Replay) 82.93 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 72.43 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 87.62 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 99.87 kB - -
@sentry/browser (incl. Feedback) 42.7 kB - -
@sentry/browser (incl. sendFeedback) 30.55 kB - -
@sentry/browser (incl. FeedbackAsync) 35.55 kB - -
@sentry/browser (incl. Metrics) 27.16 kB - -
@sentry/browser (incl. Logs) 27.29 kB - -
@sentry/browser (incl. Metrics & Logs) 27.98 kB - -
@sentry/react 27.62 kB - -
@sentry/react (incl. Tracing) 46.05 kB - -
@sentry/vue 30.71 kB - -
@sentry/vue (incl. Tracing) 45.62 kB - -
@sentry/svelte 25.89 kB - -
CDN Bundle 28.55 kB - -
CDN Bundle (incl. Tracing) 44.94 kB - -
CDN Bundle (incl. Logs, Metrics) 29.93 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 46.03 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) 68.83 kB - -
CDN Bundle (incl. Tracing, Replay) 81.9 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 82.97 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 87.41 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 88.49 kB - -
CDN Bundle - uncompressed 83.4 kB - -
CDN Bundle (incl. Tracing) - uncompressed 134.3 kB - -
CDN Bundle (incl. Logs, Metrics) - uncompressed 87.55 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 137.72 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 210.91 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 251.53 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 254.93 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 264.45 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 267.83 kB - -
@sentry/nextjs (client) 48.58 kB - -
@sentry/sveltekit (client) 44.22 kB - -
@sentry/node-core 58.24 kB +0.39% +221 B 🔺
@sentry/node 175.11 kB +0.14% +228 B 🔺
@sentry/node - without tracing 98.19 kB +0.24% +226 B 🔺
@sentry/aws-serverless 115.43 kB +0.21% +241 B 🔺

View base workflow run

Comment thread packages/core/src/instrument/console.ts Outdated
Comment thread .size-limit.js Outdated
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 249d85d. Configure here.

Comment thread packages/core/src/instrument/console.ts Outdated
Copy link
Copy Markdown
Member

@isaacs isaacs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly just some questions and edge cases to consider, but this is a very straightforward way around AWS's instrumentation thwarting ours.

Out of scope for this PR, but it does make me wonder if there's a way to abstract this into core/src/utils/object.ts wrapMethod or something. If we find ourselves in this position again, we can consider doing that. Probably premature otherwise, since just wrapping as a plain old method assignment is usually fine.

Tests look good (rubber-stamp LGTM, I did not run the tests.)

Comment on lines +66 to +72
if (isExecuting) {
// Re-entrant call: a third party captured `wrapper` via the getter and calls it
// from inside their replacement (e.g. `const prev = console.log; console.log = (...a) => { prev(...a); }`).
// Calling `consoleDelegate` here would recurse, so fall back to the native method.
nativeMethod.apply(GLOBAL_OBJ.console, args);
return;
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is fine; presumably if someone overrides the wrapper, and then calls it, they're trying to call the native function, not the wrapper. However, as written, if they do this, then it'll avoid the triggerHandlers call below.

Consider:

const actualMethod = isExecuting ? consoleDelegate : nativeMethod;
isExecuting = true;
try {
  triggerHandlers('console', { args, level } as HandlerDataConsole);
  // support `console.log.apply(someOtherConsole, args)`
  return actualMethod.apply(this, args);
} finally {
  isExecuting = false;
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current behavior is correct as skipping triggerHandlers on re-entrant calls avoids duplicate breadcrumbs. The user called console.log("hello") once...the re-entrant call is just the Lambda runtime forwarding that same message. We already captured it on the first pass.

And the ternary in the suggestion is flipped, was that on purpose? The first call would use nativeMethod (skipping the Lambda delegate entirely), and the re-entrant call would use consoleDelegate (infinite recursion). Did you maybe mean isExecuting ? nativeMethod : consoleDelegate?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yes, oops. Flipped the ternary.

If you have a test verifying the correct behavior, then that's fine. Might be worth a comment so no one comes alone making the same mistake I did and "fixes" it 😅

Comment thread packages/node-core/src/integrations/console.ts
triggerHandlers('console', { args, level } as HandlerDataConsole);

const log = originalConsoleMethods[level];
log?.apply(GLOBAL_OBJ.console, args);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
log?.apply(GLOBAL_OBJ.console, args);
return log?.apply(this, args);

typeof newValue === 'function' &&
newValue !== wrapper &&
newValue !== originalConsoleMethods[level] &&
!(newValue as WrappedFunction).__sentry_original__
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clobbering the wrapper if it's a sentry wrapped function makes sense (since that's likely ourselves doing it), but I'm unclear why you're allowing it to be set back to the originalConsoleMethods[level].

That would mean:

const original = console.log
// Sentry setup happens
console.log = someAwsThing;
// later...
console.log = original; // lose the Sentry instrumentation!

It seems like setting it to the original should just set the consoleDelegate, no?

Copy link
Copy Markdown
Member Author

@s1gr1d s1gr1d Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, that's probably a bug. I'm gonna look into that. Currently, it makes sure that the consoleSandbox still works.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Sentry Console Logging Integration not working in AWS Lambda

3 participants