@@ -12,6 +12,11 @@ import {
1212} from '@sentry/core' ;
1313import { consoleIntegration } from '../../src/integrations/console' ;
1414
15+ // Capture the real native method before any patches are installed.
16+ // This simulates external code doing `const log = console.log` before Sentry init.
17+ // oxlint-disable-next-line no-console
18+ const nativeConsoleLog = console . log ;
19+
1520afterAll ( ( ) => {
1621 delete process . env . LAMBDA_TASK_ROOT ;
1722} ) ;
@@ -183,6 +188,38 @@ describe('consoleIntegration in Lambda (patchWithDefineProperty)', () => {
183188 expect ( handler ) . toHaveBeenCalledWith ( expect . objectContaining ( { args : [ 'should not overflow' ] , level : 'log' } ) ) ;
184189 } ) ;
185190
191+ it ( 'fires the handler exactly once on re-entrant calls' , ( ) => {
192+ const handler = vi . fn ( ) ;
193+ addConsoleInstrumentationHandler ( handler ) ;
194+ handler . mockClear ( ) ;
195+
196+ const callOrder : string [ ] = [ ] ;
197+
198+ const prevLog = GLOBAL_OBJ . console . log ;
199+ GLOBAL_OBJ . console . log = ( ...args : any [ ] ) => {
200+ callOrder . push ( 'delegate-before-prev' ) ;
201+ prevLog ( ...args ) ;
202+ callOrder . push ( 'delegate-after-prev' ) ;
203+ } ;
204+
205+ handler . mockImplementation ( ( ) => {
206+ callOrder . push ( 'handler' ) ;
207+ } ) ;
208+
209+ GLOBAL_OBJ . console . log ( 're-entrant test' ) ;
210+
211+ // The handler fires exactly once — on the first (outer) entry.
212+ // The re-entrant call through prev() must NOT trigger it a second time.
213+ expect ( handler ) . toHaveBeenCalledTimes ( 1 ) ;
214+
215+ // Verify the full call order:
216+ // 1. wrapper enters → triggerHandlers → handler fires
217+ // 2. wrapper calls consoleDelegate (third-party fn)
218+ // 3. third-party fn calls prev() → re-enters wrapper → nativeMethod (no handler)
219+ // 4. third-party fn continues after prev()
220+ expect ( callOrder ) . toEqual ( [ 'handler' , 'delegate-before-prev' , 'delegate-after-prev' ] ) ;
221+ } ) ;
222+
186223 it ( 'consoleSandbox still bypasses the handler after third-party wrapping' , ( ) => {
187224 const handler = vi . fn ( ) ;
188225 addConsoleInstrumentationHandler ( handler ) ;
@@ -199,5 +236,24 @@ describe('consoleIntegration in Lambda (patchWithDefineProperty)', () => {
199236
200237 expect ( handler ) . not . toHaveBeenCalled ( ) ;
201238 } ) ;
239+
240+ it ( 'keeps firing the handler when console.log is set back to the original native method' , ( ) => {
241+ const handler = vi . fn ( ) ;
242+ addConsoleInstrumentationHandler ( handler ) ;
243+
244+ // Simulate Lambda-style replacement
245+ GLOBAL_OBJ . console . log = vi . fn ( ) ;
246+ handler . mockClear ( ) ;
247+
248+ // Simulate external code restoring a native method reference it captured
249+ // before Sentry init — this should NOT clobber the wrapper.
250+ GLOBAL_OBJ . console . log = nativeConsoleLog ;
251+
252+ GLOBAL_OBJ . console . log ( 'after restore to original' ) ;
253+
254+ expect ( handler ) . toHaveBeenCalledWith (
255+ expect . objectContaining ( { args : [ 'after restore to original' ] , level : 'log' } ) ,
256+ ) ;
257+ } ) ;
202258 } ) ;
203259} ) ;
0 commit comments