From 36c5d8de1f9e2fa2a6ad9b98ac5d40507fc8e270 Mon Sep 17 00:00:00 2001 From: Thomas Bertet Date: Thu, 12 Feb 2026 18:27:57 +0100 Subject: [PATCH 1/3] [PROF-13743] Fix user stop() being ignored after session expiration stopProfilerInstance did not update stateReason when the profiler was already in stopped state. This meant calling stop() after SESSION_EXPIRED left stateReason as 'session-expired', causing SESSION_RENEWED to restart the profiler against the user's intent. --- .../rum/src/domain/profiling/profiler.spec.ts | 32 +++++++++++++++++++ packages/rum/src/domain/profiling/profiler.ts | 5 +++ 2 files changed, 37 insertions(+) diff --git a/packages/rum/src/domain/profiling/profiler.spec.ts b/packages/rum/src/domain/profiling/profiler.spec.ts index 1e69b1b92b..06ef188489 100644 --- a/packages/rum/src/domain/profiling/profiler.spec.ts +++ b/packages/rum/src/domain/profiling/profiler.spec.ts @@ -631,6 +631,38 @@ describe('profiler', () => { expect(profiler.isStopped()).toBe(true) }) + it('should not restart profiling on session renewal if user called stop after session expiration', async () => { + const { profiler, profilingContextManager } = setupProfiler() + + profiler.start() + + // Wait for start of collection. + await waitForBoolean(() => profiler.isRunning()) + + expect(profilingContextManager.get()?.status).toBe('running') + + // Session expires (sync - state changes immediately) + lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED) + + expect(profiler.isStopped()).toBe(true) + expect(profilingContextManager.get()?.status).toBe('stopped') + + // User explicitly stops the profiler after session expiration + profiler.stop() + + expect(profiler.isStopped()).toBe(true) + + // Session is renewed + lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED) + + // Wait a bit to ensure profiler doesn't restart + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Profiler should remain stopped - user's explicit stop should take priority over session expiration + expect(profiler.isStopped()).toBe(true) + expect(profilingContextManager.get()?.status).toBe('stopped') + }) + it('should restart profiling when session expires while paused and then renews', async () => { const { profiler, profilingContextManager } = setupProfiler() diff --git a/packages/rum/src/domain/profiling/profiler.ts b/packages/rum/src/domain/profiling/profiler.ts index aed29e78e2..a716ef6c78 100644 --- a/packages/rum/src/domain/profiling/profiler.ts +++ b/packages/rum/src/domain/profiling/profiler.ts @@ -260,6 +260,11 @@ export function createRumProfiler( return } if (instance.state !== 'running') { + // Update stateReason when already stopped and the user explicitly stops the profiler, + // so that SESSION_RENEWED does not override the user's intent. + if (instance.state === 'stopped' && stateReason === 'stopped-by-user') { + instance = { state: 'stopped', stateReason } + } return } From d1b7c242b785e3619852e9689220cbf806581860 Mon Sep 17 00:00:00 2001 From: Thomas Bertet Date: Thu, 12 Feb 2026 18:28:14 +0100 Subject: [PATCH 2/3] [PROF-13743] Clear globalCleanupTasks array after cleanup The array was never reset after forEach, causing stale cleanup function references to accumulate across start/stop cycles. --- packages/rum/src/domain/profiling/profiler.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/rum/src/domain/profiling/profiler.ts b/packages/rum/src/domain/profiling/profiler.ts index a716ef6c78..b89e2b5e72 100644 --- a/packages/rum/src/domain/profiling/profiler.ts +++ b/packages/rum/src/domain/profiling/profiler.ts @@ -111,8 +111,9 @@ export function createRumProfiler( // Stop current profiler instance (data collection happens async in background) stopProfilerInstance(reason) - // Cleanup global listeners + // Cleanup global listeners and reset the array to prevent accumulation across start/stop cycles globalCleanupTasks.forEach((task) => task()) + globalCleanupTasks.length = 0 // Update Profiling status once the Profiler has been stopped. profilingContextManager.set({ status: 'stopped', error_reason: undefined }) From 9b7dcb89a787c1d61270409862cb252709095684 Mon Sep 17 00:00:00 2001 From: Thomas Bertet Date: Thu, 19 Feb 2026 16:06:38 +0100 Subject: [PATCH 3/3] [PROF-13743] Address review feedback Refactor stopProfilerInstance to merge paused and stopped early-return branches into a single guard. Remove arbitrary setTimeout wait in test since start() is synchronous. --- .../rum/src/domain/profiling/profiler.spec.ts | 5 +---- packages/rum/src/domain/profiling/profiler.ts | 16 ++++++++-------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/packages/rum/src/domain/profiling/profiler.spec.ts b/packages/rum/src/domain/profiling/profiler.spec.ts index 06ef188489..67f520cea7 100644 --- a/packages/rum/src/domain/profiling/profiler.spec.ts +++ b/packages/rum/src/domain/profiling/profiler.spec.ts @@ -652,12 +652,9 @@ describe('profiler', () => { expect(profiler.isStopped()).toBe(true) - // Session is renewed + // Session is renewed — start() is called synchronously, so no need to wait lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED) - // Wait a bit to ensure profiler doesn't restart - await new Promise((resolve) => setTimeout(resolve, 100)) - // Profiler should remain stopped - user's explicit stop should take priority over session expiration expect(profiler.isStopped()).toBe(true) expect(profilingContextManager.get()?.status).toBe('stopped') diff --git a/packages/rum/src/domain/profiling/profiler.ts b/packages/rum/src/domain/profiling/profiler.ts index b89e2b5e72..61b82213c2 100644 --- a/packages/rum/src/domain/profiling/profiler.ts +++ b/packages/rum/src/domain/profiling/profiler.ts @@ -255,17 +255,17 @@ export function createRumProfiler( } function stopProfilerInstance(stateReason: RumProfilerStoppedInstance['stateReason']) { - if (instance.state === 'paused') { - // If paused, profiler data was already collected during pause, just update state - instance = { state: 'stopped', stateReason } - return - } if (instance.state !== 'running') { - // Update stateReason when already stopped and the user explicitly stops the profiler, - // so that SESSION_RENEWED does not override the user's intent. - if (instance.state === 'stopped' && stateReason === 'stopped-by-user') { + if ( + // If paused, profiler data was already collected during pause, just update state + instance.state === 'paused' || + // Update stateReason when already stopped and the user explicitly stops the profiler, + // so that SESSION_RENEWED does not override the user's intent. + (instance.state === 'stopped' && stateReason === 'stopped-by-user') + ) { instance = { state: 'stopped', stateReason } } + return }