From 6ea7aeb2b0ddac17ddefb127e8fd084e4c9b6723 Mon Sep 17 00:00:00 2001 From: JonasBa Date: Tue, 11 Mar 2025 13:28:01 -0400 Subject: [PATCH 01/21] types: add startProfilerSession and stopProfilerSession --- packages/core/src/profiling.ts | 2 ++ packages/core/src/types-hoist/profiling.ts | 19 +++++++++++++++++++ packages/profiling-node/src/integration.ts | 1 - 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/core/src/profiling.ts b/packages/core/src/profiling.ts index 9f55a3879b8d..08d01f979801 100644 --- a/packages/core/src/profiling.ts +++ b/packages/core/src/profiling.ts @@ -18,6 +18,7 @@ function isProfilingIntegrationWithProfiler( * Starts the Sentry continuous profiler. * This mode is exclusive with the transaction profiler and will only work if the profilesSampleRate is set to a falsy value. * In continuous profiling mode, the profiler will keep reporting profile chunks to Sentry until it is stopped, which allows for continuous profiling of the application. + * @deprecated Use `startProfilerSession()` instead. */ function startProfiler(): void { const client = getClient(); @@ -44,6 +45,7 @@ function startProfiler(): void { /** * Stops the Sentry continuous profiler. * Calls to stop will stop the profiler and flush the currently collected profile data to Sentry. + * @deprecated Use `stopProfilerSession()` instead. */ function stopProfiler(): void { const client = getClient(); diff --git a/packages/core/src/types-hoist/profiling.ts b/packages/core/src/types-hoist/profiling.ts index 7f4f316a9d0b..5f9c47d6f409 100644 --- a/packages/core/src/types-hoist/profiling.ts +++ b/packages/core/src/types-hoist/profiling.ts @@ -14,8 +14,27 @@ export interface ProfilingIntegration extends Integration { } export interface Profiler { + /** + * Starts the profiler. + * @deprecated Use `startProfilerSession()` instead. + */ startProfiler(): void; + + /** + * Stops the profiler. + * @deprecated Use `stopProfilerSession()` instead. + */ stopProfiler(): void; + + /** + * Starts a new profiler session. + */ + startProfilerSession(): void; + + /** + * Stops the current profiler session. + */ + stopProfilerSession(): void; } export type ThreadId = string; diff --git a/packages/profiling-node/src/integration.ts b/packages/profiling-node/src/integration.ts index e134a272b929..9e241688166f 100644 --- a/packages/profiling-node/src/integration.ts +++ b/packages/profiling-node/src/integration.ts @@ -140,7 +140,6 @@ function setupAutomatedSpanProfiling(client: NodeClient): void { profilesToAddToEnvelope.push(profile); - // @ts-expect-error profile does not inherit from Event client.emit('preprocessEvent', profile, { event_id: profiledTransaction.event_id, }); From 8d3c390e98efd7204429a413e2cf56f1478c0866 Mon Sep 17 00:00:00 2001 From: JonasBa Date: Tue, 11 Mar 2025 13:29:30 -0400 Subject: [PATCH 02/21] profiler: add startgst --- packages/core/src/profiling.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/core/src/profiling.ts b/packages/core/src/profiling.ts index 08d01f979801..329dc4a07595 100644 --- a/packages/core/src/profiling.ts +++ b/packages/core/src/profiling.ts @@ -68,7 +68,21 @@ function stopProfiler(): void { integration._profiler.stop(); } +/** + * Starts a new profiler session. + */ +function startProfilerSession(): void { +} + +/** + * Stops the current profiler session. + */ +function stopProfilerSession(): void { +} + export const profiler: Profiler = { startProfiler, stopProfiler, + startProfilerSession, + stopProfilerSession, }; From afc305f14a44525158f2d18ddab2cb5df719675f Mon Sep 17 00:00:00 2001 From: JonasBa Date: Tue, 11 Mar 2025 13:56:46 -0400 Subject: [PATCH 03/21] profiler: deprecate profileSampler and profilesSampleRate --- packages/node/src/types.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/node/src/types.ts b/packages/node/src/types.ts index bf4913688470..a1272db69607 100644 --- a/packages/node/src/types.ts +++ b/packages/node/src/types.ts @@ -26,6 +26,8 @@ export interface BaseNodeOptions { /** * Sets profiling sample rate when @sentry/profiling-node is installed + * + * @deprecated */ profilesSampleRate?: number; @@ -39,6 +41,8 @@ export interface BaseNodeOptions { * * @returns A sample rate between 0 and 1 (0 drops the profile, 1 guarantees it will be sent). Returning `true` is * equivalent to returning 1 and returning `false` is equivalent to returning 0. + * + * @deprecated */ profilesSampler?: (samplingContext: SamplingContext) => number | boolean; From 80e64f80dc8158856f04ae179bbc0a93ed5a4c0c Mon Sep 17 00:00:00 2001 From: JonasBa Date: Tue, 11 Mar 2025 14:00:52 -0400 Subject: [PATCH 04/21] profiler: add profilelifecycle and profilesessionsamplerate --- packages/node/src/types.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/node/src/types.ts b/packages/node/src/types.ts index a1272db69607..9223619522e6 100644 --- a/packages/node/src/types.ts +++ b/packages/node/src/types.ts @@ -46,6 +46,22 @@ export interface BaseNodeOptions { */ profilesSampler?: (samplingContext: SamplingContext) => number | boolean; + /** + * Sets profiling session sample rate - only evaluated once per SDK initialization. + * @default 0 + */ + profileSessionSampleRate?: number; + + /** + * Set the lifecycle of the profiler. + * + * - `manual`: The profiler will be manually started and stopped. + * - `trace`: The profiler will be automatically started when when a span is sampled and stopped when there are no more sampled spans. + * + * @default 'manual' + */ + profileLifecycle?: 'manual' | 'trace'; + /** Sets an optional server name (device name) */ serverName?: string; From cbbba003998c360b5376e22469157cc226a65d88 Mon Sep 17 00:00:00 2001 From: JonasBa Date: Tue, 11 Mar 2025 14:33:22 -0400 Subject: [PATCH 05/21] profiler: infer profiling mode based of current options provided --- packages/profiling-node/src/integration.ts | 63 ++++++++++++++-------- 1 file changed, 40 insertions(+), 23 deletions(-) diff --git a/packages/profiling-node/src/integration.ts b/packages/profiling-node/src/integration.ts index 9e241688166f..be7fd7bd0da8 100644 --- a/packages/profiling-node/src/integration.ts +++ b/packages/profiling-node/src/integration.ts @@ -13,7 +13,7 @@ import { spanToJSON, uuid4, } from '@sentry/core'; -import type { NodeClient } from '@sentry/node'; +import type { NodeClient, NodeOptions } from '@sentry/node'; import { DEBUG_BUILD } from './debug-build'; import { NODE_MAJOR, NODE_VERSION } from './nodeVersion'; import { MAX_PROFILE_DURATION_MS, maybeProfileSpan, stopSpanProfile } from './spanProfileUtils'; @@ -445,34 +445,51 @@ export const _nodeProfilingIntegration = ((): ProfilingIntegration = DEBUG_BUILD && logger.log('[Profiling] Profiling integration setup.'); const options = client.getOptions(); - const mode = - (options.profilesSampleRate === undefined || - options.profilesSampleRate === null || - options.profilesSampleRate === 0) && - !options.profilesSampler - ? 'continuous' - : 'span'; - switch (mode) { - case 'continuous': { - DEBUG_BUILD && logger.log('[Profiling] Continuous profiler mode enabled.'); - this._profiler.initialize(client); - break; - } - // Default to span profiling when no mode profiler mode is set - case 'span': - case undefined: { - DEBUG_BUILD && logger.log('[Profiling] Span profiler mode enabled.'); - setupAutomatedSpanProfiling(client); - break; - } - default: { - DEBUG_BUILD && logger.warn(`[Profiling] Unknown profiler mode: ${mode}, profiler was not initialized`); + const profilingAPIVersion = getProfilingMode(options); + + if (profilingAPIVersion === 'legacy') { + const mode = + (options.profilesSampleRate === undefined || + options.profilesSampleRate === null || + options.profilesSampleRate === 0) && + !options.profilesSampler + ? 'continuous' + : 'span'; + switch (mode) { + case 'continuous': { + DEBUG_BUILD && logger.log('[Profiling] Continuous profiler mode enabled.'); + this._profiler.initialize(client); + break; + } + // Default to span profiling when no mode profiler mode is set + case 'span': + case undefined: { + DEBUG_BUILD && logger.log('[Profiling] Span profiler mode enabled.'); + setupAutomatedSpanProfiling(client); + break; + } + default: { + DEBUG_BUILD && logger.warn(`[Profiling] Unknown profiler mode: ${mode}, profiler was not initialized`); + } } } }, }; }) satisfies IntegrationFn; +/** + * Determines the profiling mode based on the options. + * @param options + * @returns 'legacy' if the options are using the legacy profiling API, 'current' if the options are using the current profiling API + */ +function getProfilingMode(options: NodeOptions): 'legacy' | 'current' { + if ('profilesSampleRate' in options || 'profilesSampler' in options) { + return 'legacy'; + } + + return 'current'; +} + /** * We need this integration in order to send data to Sentry. We hook into the event processor * and inspect each event to see if it is a transaction event and if that transaction event From 7bc561f0cfc1fa99fbea58217aa60d188762c480 Mon Sep 17 00:00:00 2001 From: JonasBa Date: Tue, 11 Mar 2025 15:03:14 -0400 Subject: [PATCH 06/21] profiler: simplify profiler check --- packages/profiling-node/src/integration.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/profiling-node/src/integration.ts b/packages/profiling-node/src/integration.ts index be7fd7bd0da8..2c1a5a709317 100644 --- a/packages/profiling-node/src/integration.ts +++ b/packages/profiling-node/src/integration.ts @@ -448,13 +448,8 @@ export const _nodeProfilingIntegration = ((): ProfilingIntegration = const profilingAPIVersion = getProfilingMode(options); if (profilingAPIVersion === 'legacy') { - const mode = - (options.profilesSampleRate === undefined || - options.profilesSampleRate === null || - options.profilesSampleRate === 0) && - !options.profilesSampler - ? 'continuous' - : 'span'; + const mode = !('profileSessionSampleRate' in options) && !options.profilesSampler ? 'continuous' : 'span'; + switch (mode) { case 'continuous': { DEBUG_BUILD && logger.log('[Profiling] Continuous profiler mode enabled.'); From 3d4942bf2aba236bc5115610d8ad990dde4fd5de Mon Sep 17 00:00:00 2001 From: JonasBa Date: Tue, 11 Mar 2025 15:18:46 -0400 Subject: [PATCH 07/21] profiling: expect error in preprocessevent --- packages/profiling-node/src/integration.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/profiling-node/src/integration.ts b/packages/profiling-node/src/integration.ts index 2c1a5a709317..501a5d896ced 100644 --- a/packages/profiling-node/src/integration.ts +++ b/packages/profiling-node/src/integration.ts @@ -140,6 +140,7 @@ function setupAutomatedSpanProfiling(client: NodeClient): void { profilesToAddToEnvelope.push(profile); + // @ts-expect-error profile does not inherit from Event client.emit('preprocessEvent', profile, { event_id: profiledTransaction.event_id, }); From b4d84880046899fdcfdf947be6a73d05e40fe8a6 Mon Sep 17 00:00:00 2001 From: JonasBa Date: Tue, 11 Mar 2025 16:00:01 -0400 Subject: [PATCH 08/21] profiling: fix tests --- packages/profiling-node/src/integration.ts | 26 +- .../test/spanProfileUtils.test.ts | 1135 +++++++++-------- .../test/spanProfileUtils.worker.test.ts | 1 - 3 files changed, 598 insertions(+), 564 deletions(-) diff --git a/packages/profiling-node/src/integration.ts b/packages/profiling-node/src/integration.ts index 501a5d896ced..a117d35fcbd9 100644 --- a/packages/profiling-node/src/integration.ts +++ b/packages/profiling-node/src/integration.ts @@ -445,30 +445,39 @@ export const _nodeProfilingIntegration = ((): ProfilingIntegration = setup(client: NodeClient) { DEBUG_BUILD && logger.log('[Profiling] Profiling integration setup.'); const options = client.getOptions(); - const profilingAPIVersion = getProfilingMode(options); + if (profilingAPIVersion === 'legacy') { - const mode = !('profileSessionSampleRate' in options) && !options.profilesSampler ? 'continuous' : 'span'; + const mode = ('profilesSampleRate' in options || 'profilesSampler' in options) ? 'span' : 'continuous'; switch (mode) { case 'continuous': { DEBUG_BUILD && logger.log('[Profiling] Continuous profiler mode enabled.'); this._profiler.initialize(client); - break; + return; } // Default to span profiling when no mode profiler mode is set case 'span': case undefined: { DEBUG_BUILD && logger.log('[Profiling] Span profiler mode enabled.'); setupAutomatedSpanProfiling(client); - break; + return; } default: { DEBUG_BUILD && logger.warn(`[Profiling] Unknown profiler mode: ${mode}, profiler was not initialized`); } } } + + else if(profilingAPIVersion === 'current') { + DEBUG_BUILD && logger.log('[Profiling] Continuous profiler mode enabled.'); + this._profiler.initialize(client); + return; + } + + DEBUG_BUILD && logger.log(['[Profiling] Profiling integration is added, but not enabled due to lack of SDK.init options.']) + return; }, }; }) satisfies IntegrationFn; @@ -478,12 +487,17 @@ export const _nodeProfilingIntegration = ((): ProfilingIntegration = * @param options * @returns 'legacy' if the options are using the legacy profiling API, 'current' if the options are using the current profiling API */ -function getProfilingMode(options: NodeOptions): 'legacy' | 'current' { +function getProfilingMode(options: NodeOptions): 'legacy' | 'current' | null { if ('profilesSampleRate' in options || 'profilesSampler' in options) { return 'legacy'; } - return 'current'; + if('profileSessionSampleRate' in options || 'profileLifecycle' in options){ + return 'current'; + } + + // If neither are set, we are in the legacy continuous profiling mode + return 'legacy'; } /** diff --git a/packages/profiling-node/test/spanProfileUtils.test.ts b/packages/profiling-node/test/spanProfileUtils.test.ts index 5aae42783c86..d7df77ccda20 100644 --- a/packages/profiling-node/test/spanProfileUtils.test.ts +++ b/packages/profiling-node/test/spanProfileUtils.test.ts @@ -36,7 +36,6 @@ function makeContinuousProfilingClient(): [Sentry.NodeClient, Transport] { const client = new Sentry.NodeClient({ stackParser: Sentry.defaultStackParser, tracesSampleRate: 1, - profilesSampleRate: undefined, debug: true, environment: 'test-environment', dsn: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', @@ -79,712 +78,734 @@ function makeClientOptions( const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); -describe('automated span instrumentation', () => { - beforeEach(() => { - vi.useRealTimers(); - // We will mock the carrier as if it has been initialized by the SDK, else everything is short circuited - getMainCarrier().__SENTRY__ = {}; - GLOBAL_OBJ._sentryDebugIds = undefined as any; - }); - afterEach(() => { - vi.clearAllMocks(); - vi.restoreAllMocks(); - delete getMainCarrier().__SENTRY__; - }); - - it('pulls environment from sdk init', async () => { - const [client, transport] = makeClientWithHooks(); - Sentry.setCurrentClient(client); - client.init(); - - const transportSpy = vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); - - const transaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); - await wait(500); - transaction.end(); +describe('ProfilingIntegration', () => { + describe('automated span instrumentation', () => { + beforeEach(() => { + vi.useRealTimers(); + // We will mock the carrier as if it has been initialized by the SDK, else everything is short circuited + getMainCarrier().__SENTRY__ = {}; + GLOBAL_OBJ._sentryDebugIds = undefined as any; + }); + afterEach(() => { + vi.clearAllMocks(); + vi.restoreAllMocks(); + delete getMainCarrier().__SENTRY__; + }); - await Sentry.flush(1000); - expect(transportSpy.mock.calls?.[0]?.[0]?.[1]?.[0]?.[1]).toMatchObject({ environment: 'test-environment' }); - }); + it('pulls environment from sdk init', async () => { + const [client, transport] = makeClientWithHooks(); + Sentry.setCurrentClient(client); + client.init(); - it('logger warns user if there are insufficient samples and discards the profile', async () => { - const logSpy = vi.spyOn(logger, 'log'); + const transportSpy = vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); - const [client, transport] = makeClientWithHooks(); - Sentry.setCurrentClient(client); - client.init(); + const transaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); + await wait(500); + transaction.end(); - // @ts-expect-error we just mock the return type and ignore the signature - vi.spyOn(CpuProfilerBindings, 'stopProfiling').mockImplementation(() => { - return { - samples: [ - { - stack_id: 0, - thread_id: '0', - elapsed_since_start_ns: '10', - }, - ], - measurements: {}, - stacks: [[0]], - frames: [], - resources: [], - profiler_logging_mode: 'lazy', - }; + await Sentry.flush(1000); + expect(transportSpy.mock.calls?.[0]?.[0]?.[1]?.[0]?.[1]).toMatchObject({ environment: 'test-environment' }); }); - vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); - - const transaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); - transaction.end(); - - await Sentry.flush(1000); + it('logger warns user if there are insufficient samples and discards the profile', async () => { + const logSpy = vi.spyOn(logger, 'log'); - expect(logSpy).toHaveBeenCalledWith('[Profiling] Discarding profile because it contains less than 2 samples'); + const [client, transport] = makeClientWithHooks(); + Sentry.setCurrentClient(client); + client.init(); - expect((transport.send as any).mock.calls[0][0][1][0][0]?.type).toBe('transaction'); - // eslint-disable-next-line @typescript-eslint/unbound-method - expect(transport.send).toHaveBeenCalledTimes(1); - }); + // @ts-expect-error we just mock the return type and ignore the signature + vi.spyOn(CpuProfilerBindings, 'stopProfiling').mockImplementation(() => { + return { + samples: [ + { + stack_id: 0, + thread_id: '0', + elapsed_since_start_ns: '10', + }, + ], + measurements: {}, + stacks: [[0]], + frames: [], + resources: [], + profiler_logging_mode: 'lazy', + }; + }); - it('logger warns user if traceId is invalid', async () => { - const logSpy = vi.spyOn(logger, 'log'); + vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); - const [client, transport] = makeClientWithHooks(); - Sentry.setCurrentClient(client); - client.init(); + const transaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); + transaction.end(); - // @ts-expect-error we just mock the return type and ignore the signature - vi.spyOn(CpuProfilerBindings, 'stopProfiling').mockImplementation(() => { - return { - samples: [ - { - stack_id: 0, - thread_id: '0', - elapsed_since_start_ns: '10', - }, - { - stack_id: 0, - thread_id: '0', - elapsed_since_start_ns: '10', - }, - ], - measurements: {}, - resources: [], - stacks: [[0]], - frames: [], - profiler_logging_mode: 'lazy', - }; - }); + await Sentry.flush(1000); - vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); + expect(logSpy).toHaveBeenCalledWith('[Profiling] Discarding profile because it contains less than 2 samples'); - Sentry.getCurrentScope().getPropagationContext().traceId = 'boop'; - const transaction = Sentry.startInactiveSpan({ - forceTransaction: true, - name: 'profile_hub', + expect((transport.send as any).mock.calls[0][0][1][0][0]?.type).toBe('transaction'); + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(transport.send).toHaveBeenCalledTimes(1); }); - await wait(500); - transaction.end(); - await Sentry.flush(1000); + it('logger warns user if traceId is invalid', async () => { + const logSpy = vi.spyOn(logger, 'log'); - expect(logSpy).toHaveBeenCalledWith('[Profiling] Invalid traceId: ' + 'boop' + ' on profiled event'); - }); - - describe('with hooks', () => { - it('calls profiler when transaction is started/stopped', async () => { const [client, transport] = makeClientWithHooks(); Sentry.setCurrentClient(client); client.init(); - const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); - const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); + // @ts-expect-error we just mock the return type and ignore the signature + vi.spyOn(CpuProfilerBindings, 'stopProfiling').mockImplementation(() => { + return { + samples: [ + { + stack_id: 0, + thread_id: '0', + elapsed_since_start_ns: '10', + }, + { + stack_id: 0, + thread_id: '0', + elapsed_since_start_ns: '10', + }, + ], + measurements: {}, + resources: [], + stacks: [[0]], + frames: [], + profiler_logging_mode: 'lazy', + }; + }); vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); - const transaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); + Sentry.getCurrentScope().getPropagationContext().traceId = 'boop'; + const transaction = Sentry.startInactiveSpan({ + forceTransaction: true, + name: 'profile_hub', + }); await wait(500); transaction.end(); await Sentry.flush(1000); - expect(startProfilingSpy).toHaveBeenCalledTimes(1); - expect((stopProfilingSpy.mock.calls[stopProfilingSpy.mock.calls.length - 1]?.[0] as string).length).toBe(32); + expect(logSpy).toHaveBeenCalledWith('[Profiling] Invalid traceId: ' + 'boop' + ' on profiled event'); }); - it('sends profile in the same envelope as transaction', async () => { - const [client, transport] = makeClientWithHooks(); - Sentry.setCurrentClient(client); - client.init(); + describe('with hooks', () => { + it('calls profiler when transaction is started/stopped', async () => { + const [client, transport] = makeClientWithHooks(); + Sentry.setCurrentClient(client); + client.init(); - const transportSpy = vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); - const transaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); - await wait(500); - transaction.end(); + vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); - await Sentry.flush(1000); + const transaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); + await wait(500); + transaction.end(); - // One for profile, the other for transaction - expect(transportSpy).toHaveBeenCalledTimes(1); - expect(transportSpy.mock.calls?.[0]?.[0]?.[1]?.[1]?.[0]).toMatchObject({ type: 'profile' }); - }); + await Sentry.flush(1000); - it('does not crash if transaction has no profile context or it is invalid', async () => { - const [client] = makeClientWithHooks(); - Sentry.setCurrentClient(client); - client.init(); + expect(startProfilingSpy).toHaveBeenCalledTimes(1); + expect((stopProfilingSpy.mock.calls[stopProfilingSpy.mock.calls.length - 1]?.[0] as string).length).toBe(32); + }); + + it('sends profile in the same envelope as transaction', async () => { + const [client, transport] = makeClientWithHooks(); + Sentry.setCurrentClient(client); + client.init(); + + const transportSpy = vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); + + const transaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); + await wait(500); + transaction.end(); + + await Sentry.flush(1000); + + // One for profile, the other for transaction + expect(transportSpy).toHaveBeenCalledTimes(1); + expect(transportSpy.mock.calls?.[0]?.[0]?.[1]?.[1]?.[0]).toMatchObject({ type: 'profile' }); + }); + + it('does not crash if transaction has no profile context or it is invalid', async () => { + const [client] = makeClientWithHooks(); + Sentry.setCurrentClient(client); + client.init(); - // @ts-expect-error transaction is partial - client.emit('beforeEnvelope', createEnvelope({ type: 'transaction' }, { type: 'transaction' })); - // @ts-expect-error transaction is partial - client.emit('beforeEnvelope', createEnvelope({ type: 'transaction' }, { type: 'transaction', contexts: {} })); - client.emit( - 'beforeEnvelope', // @ts-expect-error transaction is partial - createEnvelope({ type: 'transaction' }, { type: 'transaction', contexts: { profile: {} } }), - ); - client.emit( - 'beforeEnvelope', + client.emit('beforeEnvelope', createEnvelope({ type: 'transaction' }, { type: 'transaction' })); // @ts-expect-error transaction is partial - createEnvelope({ type: 'transaction' }, { type: 'transaction', contexts: { profile: { profile_id: null } } }), - ); + client.emit('beforeEnvelope', createEnvelope({ type: 'transaction' }, { type: 'transaction', contexts: {} })); + client.emit( + 'beforeEnvelope', + // @ts-expect-error transaction is partial + createEnvelope({ type: 'transaction' }, { type: 'transaction', contexts: { profile: {} } }), + ); + client.emit( + 'beforeEnvelope', + // @ts-expect-error transaction is partial + createEnvelope({ type: 'transaction' }, { type: 'transaction', contexts: { profile: { profile_id: null } } }), + ); + + // Emit is sync, so we can just assert that we got here + expect(true).toBe(true); + }); - // Emit is sync, so we can just assert that we got here - expect(true).toBe(true); - }); + it('if transaction was profiled, but profiler returned null', async () => { + const [client, transport] = makeClientWithHooks(); + Sentry.setCurrentClient(client); + client.init(); - it('if transaction was profiled, but profiler returned null', async () => { - const [client, transport] = makeClientWithHooks(); - Sentry.setCurrentClient(client); - client.init(); + vi.spyOn(CpuProfilerBindings, 'stopProfiling').mockReturnValue(null); + // Emit is sync, so we can just assert that we got here + const transportSpy = vi.spyOn(transport, 'send').mockImplementation(() => { + // Do nothing so we don't send events to Sentry + return Promise.resolve({}); + }); + + const transaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); + await wait(500); + transaction.end(); - vi.spyOn(CpuProfilerBindings, 'stopProfiling').mockReturnValue(null); - // Emit is sync, so we can just assert that we got here - const transportSpy = vi.spyOn(transport, 'send').mockImplementation(() => { - // Do nothing so we don't send events to Sentry - return Promise.resolve({}); + await Sentry.flush(1000); + + // Only transaction is sent + expect(transportSpy.mock.calls?.[0]?.[0]?.[1]?.[0]?.[0]).toMatchObject({ type: 'transaction' }); + expect(transportSpy.mock.calls?.[0]?.[0]?.[1][1]).toBeUndefined(); }); - const transaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); - await wait(500); - transaction.end(); + it('emits preprocessEvent for profile', async () => { + const [client] = makeClientWithHooks(); + Sentry.setCurrentClient(client); + client.init(); - await Sentry.flush(1000); + const onPreprocessEvent = vi.fn(); - // Only transaction is sent - expect(transportSpy.mock.calls?.[0]?.[0]?.[1]?.[0]?.[0]).toMatchObject({ type: 'transaction' }); - expect(transportSpy.mock.calls?.[0]?.[0]?.[1][1]).toBeUndefined(); - }); + client.on('preprocessEvent', onPreprocessEvent); - it('emits preprocessEvent for profile', async () => { - const [client] = makeClientWithHooks(); - Sentry.setCurrentClient(client); - client.init(); + const transaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); + await wait(500); + transaction.end(); - const onPreprocessEvent = vi.fn(); + await Sentry.flush(1000); - client.on('preprocessEvent', onPreprocessEvent); + expect(onPreprocessEvent.mock.calls[1]?.[0]).toMatchObject({ + profile: { + samples: expect.arrayContaining([expect.anything()]), + stacks: expect.arrayContaining([expect.anything()]), + }, + }); + }); - const transaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); - await wait(500); - transaction.end(); + it('automated span instrumentation does not support continuous profiling', () => { + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); - await Sentry.flush(1000); + const [client] = makeClientWithHooks(); + Sentry.setCurrentClient(client); + client.init(); - expect(onPreprocessEvent.mock.calls[1]?.[0]).toMatchObject({ - profile: { - samples: expect.arrayContaining([expect.anything()]), - stacks: expect.arrayContaining([expect.anything()]), - }, + const integration = + client.getIntegrationByName>('ProfilingIntegration'); + if (!integration) { + throw new Error('Profiling integration not found'); + } + integration._profiler.start(); + expect(startProfilingSpy).not.toHaveBeenCalled(); }); }); - it('automated span instrumentation does not support continuous profiling', () => { - const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + it('does not crash if stop is called multiple times', async () => { + const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); const [client] = makeClientWithHooks(); Sentry.setCurrentClient(client); client.init(); - const integration = client.getIntegrationByName>('ProfilingIntegration'); - if (!integration) { - throw new Error('Profiling integration not found'); - } - integration._profiler.start(); - expect(startProfilingSpy).not.toHaveBeenCalled(); + const transaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'txn' }); + transaction.end(); + transaction.end(); + expect(stopProfilingSpy).toHaveBeenCalledTimes(1); }); - }); - - it('does not crash if stop is called multiple times', async () => { - const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); - - const [client] = makeClientWithHooks(); - Sentry.setCurrentClient(client); - client.init(); - - const transaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'txn' }); - transaction.end(); - transaction.end(); - expect(stopProfilingSpy).toHaveBeenCalledTimes(1); - }); - it('enriches profile with debug_id', async () => { - GLOBAL_OBJ._sentryDebugIds = { - 'Error\n at filename.js (filename.js:36:15)': 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa', - 'Error\n at filename2.js (filename2.js:36:15)': 'bbbbbbbb-bbbb-4bbb-bbbb-bbbbbbbbbb', - 'Error\n at filename3.js (filename3.js:36:15)': 'bbbbbbbb-bbbb-4bbb-bbbb-bbbbbbbbbb', - }; - - // @ts-expect-error we just mock the return type and ignore the signature - vi.spyOn(CpuProfilerBindings, 'stopProfiling').mockImplementation(() => { - return { - samples: [ - { - stack_id: 0, - thread_id: '0', - elapsed_since_start_ns: '10', - }, - { - stack_id: 0, - thread_id: '0', - elapsed_since_start_ns: '10', - }, - ], - measurements: {}, - resources: ['filename.js', 'filename2.js'], - stacks: [[0]], - frames: [], - profiler_logging_mode: 'lazy', + it('enriches profile with debug_id', async () => { + GLOBAL_OBJ._sentryDebugIds = { + 'Error\n at filename.js (filename.js:36:15)': 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa', + 'Error\n at filename2.js (filename2.js:36:15)': 'bbbbbbbb-bbbb-4bbb-bbbb-bbbbbbbbbb', + 'Error\n at filename3.js (filename3.js:36:15)': 'bbbbbbbb-bbbb-4bbb-bbbb-bbbbbbbbbb', }; - }); - - const [client, transport] = makeClientWithHooks(); - Sentry.setCurrentClient(client); - client.init(); - - const transportSpy = vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); - const transaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); - await wait(500); - transaction.end(); + // @ts-expect-error we just mock the return type and ignore the signature + vi.spyOn(CpuProfilerBindings, 'stopProfiling').mockImplementation(() => { + return { + samples: [ + { + stack_id: 0, + thread_id: '0', + elapsed_since_start_ns: '10', + }, + { + stack_id: 0, + thread_id: '0', + elapsed_since_start_ns: '10', + }, + ], + measurements: {}, + resources: ['filename.js', 'filename2.js'], + stacks: [[0]], + frames: [], + profiler_logging_mode: 'lazy', + }; + }); - await Sentry.flush(1000); + const [client, transport] = makeClientWithHooks(); + Sentry.setCurrentClient(client); + client.init(); - expect(transportSpy.mock.calls?.[0]?.[0]?.[1]?.[1]?.[1]).toMatchObject({ - debug_meta: { - images: [ - { - type: 'sourcemap', - debug_id: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa', - code_file: 'filename.js', - }, - { - type: 'sourcemap', - debug_id: 'bbbbbbbb-bbbb-4bbb-bbbb-bbbbbbbbbb', - code_file: 'filename2.js', - }, - ], - }, - }); - }); -}); + const transportSpy = vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); -describe('continuous profiling', () => { - beforeEach(() => { - vi.useFakeTimers(); - // We will mock the carrier as if it has been initialized by the SDK, else everything is short circuited - getMainCarrier().__SENTRY__ = {}; - GLOBAL_OBJ._sentryDebugIds = undefined as any; - }); - afterEach(() => { - const client = Sentry.getClient(); - const integration = client?.getIntegrationByName>('ProfilingIntegration'); + const transaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); + await wait(500); + transaction.end(); - if (integration) { - Sentry.profiler.stopProfiler(); - } + await Sentry.flush(1000); - vi.clearAllMocks(); - vi.restoreAllMocks(); - vi.runAllTimers(); - delete getMainCarrier().__SENTRY__; + expect(transportSpy.mock.calls?.[0]?.[0]?.[1]?.[1]?.[1]).toMatchObject({ + debug_meta: { + images: [ + { + type: 'sourcemap', + debug_id: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa', + code_file: 'filename.js', + }, + { + type: 'sourcemap', + debug_id: 'bbbbbbbb-bbbb-4bbb-bbbb-bbbbbbbbbb', + code_file: 'filename2.js', + }, + ], + }, + }); + }); }); - it('attaches sdk metadata to chunks', () => { - // @ts-expect-error we just mock the return type and ignore the signature - vi.spyOn(CpuProfilerBindings, 'stopProfiling').mockImplementation(() => { - return { - samples: [ - { - stack_id: 0, - thread_id: '0', - elapsed_since_start_ns: '10', - }, - { - stack_id: 0, - thread_id: '0', - elapsed_since_start_ns: '10', + it('top level methods do not proxy to integration', () => { + const client = new Sentry.NodeClient({ + ...makeClientOptions({ profilesSampleRate: undefined }), + dsn: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', + tracesSampleRate: 1, + profilesSampleRate: 1, + transport: _opts => + Sentry.makeNodeTransport({ + url: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', + recordDroppedEvent: () => { + return undefined; }, - ], - measurements: {}, - stacks: [[0]], - frames: [], - resources: [], - profiler_logging_mode: 'lazy', - }; + }), + integrations: [_nodeProfilingIntegration()], }); - const [client, transport] = makeContinuousProfilingClient(); Sentry.setCurrentClient(client); client.init(); - const transportSpy = vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); + Sentry.profiler.startProfiler(); - vi.advanceTimersByTime(1000); + expect(startProfilingSpy).not.toHaveBeenCalled(); Sentry.profiler.stopProfiler(); - vi.advanceTimersByTime(1000); - - const profile = transportSpy.mock.calls?.[0]?.[0]?.[1]?.[0]?.[1] as ProfileChunk; - expect(profile.client_sdk.name).toBe('sentry.javascript.node'); - expect(profile.client_sdk.version).toEqual(expect.stringMatching(/\d+\.\d+\.\d+/)); + expect(stopProfilingSpy).not.toHaveBeenCalled(); }); - it('initializes the continuous profiler', () => { - const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + describe('continuous profiling', () => { + beforeEach(() => { + vi.useFakeTimers(); + // We will mock the carrier as if it has been initialized by the SDK, else everything is short circuited + getMainCarrier().__SENTRY__ = {}; + GLOBAL_OBJ._sentryDebugIds = undefined as any; + }); + afterEach(() => { + const client = Sentry.getClient(); + const integration = client?.getIntegrationByName>('ProfilingIntegration'); - const [client] = makeContinuousProfilingClient(); - Sentry.setCurrentClient(client); - client.init(); + if (integration) { + Sentry.profiler.stopProfiler(); + } - expect(startProfilingSpy).not.toHaveBeenCalledTimes(1); - Sentry.profiler.startProfiler(); + vi.clearAllMocks(); + vi.restoreAllMocks(); + vi.runAllTimers(); + delete getMainCarrier().__SENTRY__; + }); - const integration = client.getIntegrationByName>('ProfilingIntegration'); - expect(integration?._profiler).toBeDefined(); - }); + it('attaches sdk metadata to chunks', () => { + // @ts-expect-error we just mock the return type and ignore the signature + vi.spyOn(CpuProfilerBindings, 'stopProfiling').mockImplementation(() => { + return { + samples: [ + { + stack_id: 0, + thread_id: '0', + elapsed_since_start_ns: '10', + }, + { + stack_id: 0, + thread_id: '0', + elapsed_since_start_ns: '10', + }, + ], + measurements: {}, + stacks: [[0]], + frames: [], + resources: [], + profiler_logging_mode: 'lazy', + }; + }); - it('starts a continuous profile', () => { - const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + const [client, transport] = makeContinuousProfilingClient(); + Sentry.setCurrentClient(client); + client.init(); - const [client] = makeContinuousProfilingClient(); - Sentry.setCurrentClient(client); - client.init(); + const transportSpy = vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); + Sentry.profiler.startProfiler(); + vi.advanceTimersByTime(1000); + Sentry.profiler.stopProfiler(); + vi.advanceTimersByTime(1000); - expect(startProfilingSpy).not.toHaveBeenCalledTimes(1); - Sentry.profiler.startProfiler(); - expect(startProfilingSpy).toHaveBeenCalledTimes(1); - }); + const profile = transportSpy.mock.calls?.[0]?.[0]?.[1]?.[0]?.[1] as ProfileChunk; + expect(profile.client_sdk.name).toBe('sentry.javascript.node'); + expect(profile.client_sdk.version).toEqual(expect.stringMatching(/\d+\.\d+\.\d+/)); + }); - it('multiple calls to start abort previous profile', () => { - const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); - const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); + it('initializes the continuous profiler', () => { + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); - const [client] = makeContinuousProfilingClient(); - Sentry.setCurrentClient(client); - client.init(); + const [client] = makeContinuousProfilingClient(); + Sentry.setCurrentClient(client); + client.init(); - expect(startProfilingSpy).not.toHaveBeenCalledTimes(1); - Sentry.profiler.startProfiler(); - Sentry.profiler.startProfiler(); + expect(startProfilingSpy).not.toHaveBeenCalledTimes(1); + Sentry.profiler.startProfiler(); - expect(startProfilingSpy).toHaveBeenCalledTimes(2); - expect(stopProfilingSpy).toHaveBeenCalledTimes(1); - }); + const integration = client.getIntegrationByName>('ProfilingIntegration'); + expect(integration?._profiler).toBeDefined(); + }); - it('restarts a new chunk after previous', async () => { - const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); - const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); + it('starts a continuous profile', () => { + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); - const [client] = makeContinuousProfilingClient(); - Sentry.setCurrentClient(client); - client.init(); + const [client] = makeContinuousProfilingClient(); + Sentry.setCurrentClient(client); + client.init(); - expect(startProfilingSpy).not.toHaveBeenCalledTimes(1); - Sentry.profiler.startProfiler(); + expect(startProfilingSpy).not.toHaveBeenCalledTimes(1); + Sentry.profiler.startProfiler(); + expect(startProfilingSpy).toHaveBeenCalledTimes(1); + }); - vi.advanceTimersByTime(60_001); - expect(stopProfilingSpy).toHaveBeenCalledTimes(1); - expect(startProfilingSpy).toHaveBeenCalledTimes(2); - }); + it('multiple calls to start abort previous profile', () => { + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); - it('chunks share the same profilerId', async () => { - const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); - const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); + const [client] = makeContinuousProfilingClient(); + Sentry.setCurrentClient(client); + client.init(); - const [client] = makeContinuousProfilingClient(); - Sentry.setCurrentClient(client); - client.init(); + expect(startProfilingSpy).not.toHaveBeenCalledTimes(1); + Sentry.profiler.startProfiler(); + Sentry.profiler.startProfiler(); - expect(startProfilingSpy).not.toHaveBeenCalledTimes(1); - Sentry.profiler.startProfiler(); - const profilerId = getProfilerId(); + expect(startProfilingSpy).toHaveBeenCalledTimes(2); + expect(stopProfilingSpy).toHaveBeenCalledTimes(1); + }); - vi.advanceTimersByTime(60_001); - expect(stopProfilingSpy).toHaveBeenCalledTimes(1); - expect(startProfilingSpy).toHaveBeenCalledTimes(2); - expect(getProfilerId()).toBe(profilerId); - }); + it('restarts a new chunk after previous', async () => { + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); - it('explicit calls to stop clear profilerId', async () => { - const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + const [client] = makeContinuousProfilingClient(); + Sentry.setCurrentClient(client); + client.init(); - const [client] = makeContinuousProfilingClient(); - Sentry.setCurrentClient(client); - client.init(); + expect(startProfilingSpy).not.toHaveBeenCalledTimes(1); + Sentry.profiler.startProfiler(); - expect(startProfilingSpy).not.toHaveBeenCalledTimes(1); - Sentry.profiler.startProfiler(); - const profilerId = getProfilerId(); - Sentry.profiler.stopProfiler(); - Sentry.profiler.startProfiler(); + vi.advanceTimersByTime(60_001); + expect(stopProfilingSpy).toHaveBeenCalledTimes(1); + expect(startProfilingSpy).toHaveBeenCalledTimes(2); + }); - expect(getProfilerId()).toEqual(expect.any(String)); - expect(getProfilerId()).not.toBe(profilerId); - }); + it('chunks share the same profilerId', async () => { + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); - it('stops a continuous profile after interval', async () => { - const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); - const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); + const [client] = makeContinuousProfilingClient(); + Sentry.setCurrentClient(client); + client.init(); - const [client] = makeContinuousProfilingClient(); - Sentry.setCurrentClient(client); - client.init(); + expect(startProfilingSpy).not.toHaveBeenCalledTimes(1); + Sentry.profiler.startProfiler(); + const profilerId = getProfilerId(); - expect(startProfilingSpy).not.toHaveBeenCalledTimes(1); - Sentry.profiler.startProfiler(); + vi.advanceTimersByTime(60_001); + expect(stopProfilingSpy).toHaveBeenCalledTimes(1); + expect(startProfilingSpy).toHaveBeenCalledTimes(2); + expect(getProfilerId()).toBe(profilerId); + }); - vi.advanceTimersByTime(60_001); - expect(stopProfilingSpy).toHaveBeenCalledTimes(1); - }); + it('explicit calls to stop clear profilerId', async () => { + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); - it('manually stopping a chunk doesnt restart the profiler', async () => { - const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); - const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); + const [client] = makeContinuousProfilingClient(); + Sentry.setCurrentClient(client); + client.init(); - const [client] = makeContinuousProfilingClient(); - Sentry.setCurrentClient(client); - client.init(); + expect(startProfilingSpy).not.toHaveBeenCalledTimes(1); + Sentry.profiler.startProfiler(); + const profilerId = getProfilerId(); + Sentry.profiler.stopProfiler(); + Sentry.profiler.startProfiler(); - expect(startProfilingSpy).not.toHaveBeenCalledTimes(1); - Sentry.profiler.startProfiler(); + expect(getProfilerId()).toEqual(expect.any(String)); + expect(getProfilerId()).not.toBe(profilerId); + }); + + it('stops a continuous profile after interval', async () => { + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); - vi.advanceTimersByTime(1000); + const [client] = makeContinuousProfilingClient(); + Sentry.setCurrentClient(client); + client.init(); - Sentry.profiler.stopProfiler(); - expect(stopProfilingSpy).toHaveBeenCalledTimes(1); + expect(startProfilingSpy).not.toHaveBeenCalledTimes(1); + Sentry.profiler.startProfiler(); - vi.advanceTimersByTime(1000); - expect(startProfilingSpy).toHaveBeenCalledTimes(1); - }); + vi.advanceTimersByTime(60_001); + expect(stopProfilingSpy).toHaveBeenCalledTimes(1); + }); - it('continuous mode does not instrument spans', () => { - const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + it('manually stopping a chunk doesnt restart the profiler', async () => { + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); - const [client] = makeContinuousProfilingClient(); - Sentry.setCurrentClient(client); - client.init(); + const [client] = makeContinuousProfilingClient(); + Sentry.setCurrentClient(client); + client.init(); - Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); - expect(startProfilingSpy).not.toHaveBeenCalled(); - }); + expect(startProfilingSpy).not.toHaveBeenCalledTimes(1); + Sentry.profiler.startProfiler(); - it('sends as profile_chunk envelope type', async () => { - // @ts-expect-error we just mock the return type and ignore the signature - vi.spyOn(CpuProfilerBindings, 'stopProfiling').mockImplementation(() => { - return { - samples: [ - { - stack_id: 0, - thread_id: '0', - elapsed_since_start_ns: '10', - }, - { - stack_id: 0, - thread_id: '0', - elapsed_since_start_ns: '10', - }, - ], - measurements: {}, - stacks: [[0]], - frames: [], - resources: [], - profiler_logging_mode: 'lazy', - }; + vi.advanceTimersByTime(1000); + + Sentry.profiler.stopProfiler(); + expect(stopProfilingSpy).toHaveBeenCalledTimes(1); + + vi.advanceTimersByTime(1000); + expect(startProfilingSpy).toHaveBeenCalledTimes(1); }); - const [client, transport] = makeContinuousProfilingClient(); - Sentry.setCurrentClient(client); - client.init(); + it('continuous mode does not instrument spans', () => { + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); - const transportSpy = vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); - Sentry.profiler.startProfiler(); - vi.advanceTimersByTime(1000); - Sentry.profiler.stopProfiler(); - vi.advanceTimersByTime(1000); + const [client] = makeContinuousProfilingClient(); + Sentry.setCurrentClient(client); + client.init(); - expect(transportSpy.mock.calls?.[0]?.[0]?.[1]?.[0]?.[0]?.type).toBe('profile_chunk'); - }); + Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); + expect(startProfilingSpy).not.toHaveBeenCalled(); + }); - it('sets global profile context', async () => { - const [client, transport] = makeContinuousProfilingClient(); - Sentry.setCurrentClient(client); - client.init(); + it('sends as profile_chunk envelope type', async () => { + // @ts-expect-error we just mock the return type and ignore the signature + vi.spyOn(CpuProfilerBindings, 'stopProfiling').mockImplementation(() => { + return { + samples: [ + { + stack_id: 0, + thread_id: '0', + elapsed_since_start_ns: '10', + }, + { + stack_id: 0, + thread_id: '0', + elapsed_since_start_ns: '10', + }, + ], + measurements: {}, + stacks: [[0]], + frames: [], + resources: [], + profiler_logging_mode: 'lazy', + }; + }); - const transportSpy = vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); + const [client, transport] = makeContinuousProfilingClient(); + Sentry.setCurrentClient(client); + client.init(); - const nonProfiledTransaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); - nonProfiledTransaction.end(); + const transportSpy = vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); + Sentry.profiler.startProfiler(); + vi.advanceTimersByTime(1000); + Sentry.profiler.stopProfiler(); + vi.advanceTimersByTime(1000); - expect(transportSpy.mock.calls?.[0]?.[0]?.[1]?.[0]?.[1]).not.toMatchObject({ - contexts: { - profile: {}, - }, + expect(transportSpy.mock.calls?.[0]?.[0]?.[1]?.[0]?.[0]?.type).toBe('profile_chunk'); }); - const integration = client.getIntegrationByName>('ProfilingIntegration'); - if (!integration) { - throw new Error('Profiling integration not found'); - } + it('sets global profile context', async () => { + const [client, transport] = makeContinuousProfilingClient(); + Sentry.setCurrentClient(client); + client.init(); + + const transportSpy = vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); - integration._profiler.start(); - const profiledTransaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); - profiledTransaction.end(); - Sentry.profiler.stopProfiler(); + const nonProfiledTransaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); + nonProfiledTransaction.end(); - expect(transportSpy.mock.calls?.[1]?.[0]?.[1]?.[0]?.[1]).toMatchObject({ - contexts: { - trace: { - data: expect.objectContaining({ - ['thread.id']: expect.any(String), - ['thread.name']: expect.any(String), - }), + expect(transportSpy.mock.calls?.[0]?.[0]?.[1]?.[0]?.[1]).not.toMatchObject({ + contexts: { + profile: {}, }, - profile: { - profiler_id: expect.any(String), + }); + + const integration = client.getIntegrationByName>('ProfilingIntegration'); + if (!integration) { + throw new Error('Profiling integration not found'); + } + + integration._profiler.start(); + const profiledTransaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); + profiledTransaction.end(); + Sentry.profiler.stopProfiler(); + + expect(transportSpy.mock.calls?.[1]?.[0]?.[1]?.[0]?.[1]).toMatchObject({ + contexts: { + trace: { + data: expect.objectContaining({ + ['thread.id']: expect.any(String), + ['thread.name']: expect.any(String), + }), + }, + profile: { + profiler_id: expect.any(String), + }, }, - }, + }); }); }); -}); -describe('continuous profiling does not start in span profiling mode', () => { - it.each([ - ['profilesSampleRate=1', makeClientOptions({ profilesSampleRate: 1 })], - ['profilesSampler is defined', makeClientOptions({ profilesSampler: () => 1 })], - ])('%s', async (_label, options) => { - const logSpy = vi.spyOn(logger, 'log'); - const client = new Sentry.NodeClient({ - ...options, - dsn: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', - tracesSampleRate: 1, - transport: _opts => - Sentry.makeNodeTransport({ - url: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', - recordDroppedEvent: () => { - return undefined; - }, - }), - integrations: [_nodeProfilingIntegration()], - }); + describe('continuous profiling does not start in span profiling mode', () => { + it.each([ + ['profilesSampleRate=1', makeClientOptions({ profilesSampleRate: 1 })], + ['profilesSampler is defined', makeClientOptions({ profilesSampler: () => 1 })], + ])('%s', async (_label, options) => { + const logSpy = vi.spyOn(logger, 'log'); + const client = new Sentry.NodeClient({ + ...options, + dsn: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', + tracesSampleRate: 1, + transport: _opts => + Sentry.makeNodeTransport({ + url: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', + recordDroppedEvent: () => { + return undefined; + }, + }), + integrations: [_nodeProfilingIntegration()], + }); - Sentry.setCurrentClient(client); - client.init(); + Sentry.setCurrentClient(client); + client.init(); - const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); - const transport = client.getTransport(); + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + const transport = client.getTransport(); - if (!transport) { - throw new Error('Transport not found'); - } + if (!transport) { + throw new Error('Transport not found'); + } - vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); - Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); + vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); + Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); - expect(startProfilingSpy).toHaveBeenCalled(); - const integration = client.getIntegrationByName>('ProfilingIntegration'); + expect(startProfilingSpy).toHaveBeenCalled(); + const integration = client.getIntegrationByName>('ProfilingIntegration'); - if (!integration) { - throw new Error('Profiling integration not found'); - } + if (!integration) { + throw new Error('Profiling integration not found'); + } - integration._profiler.start(); - expect(logSpy).toHaveBeenLastCalledWith( - '[Profiling] Failed to start, sentry client was never attached to the profiler.', - ); - }); -}); -describe('continuous profiling mode', () => { - beforeEach(() => { - vi.clearAllMocks(); + integration._profiler.start(); + expect(logSpy).toHaveBeenLastCalledWith( + '[Profiling] Failed to start, sentry client was never attached to the profiler.', + ); + }); }); - - it.each([ - ['profilesSampleRate=0', makeClientOptions({ profilesSampleRate: 0 })], - ['profilesSampleRate=undefined', makeClientOptions({ profilesSampleRate: undefined })], - // @ts-expect-error test invalid value - ['profilesSampleRate=null', makeClientOptions({ profilesSampleRate: null })], - [ - 'profilesSampler is not defined and profilesSampleRate is not set', - makeClientOptions({ profilesSampler: undefined, profilesSampleRate: 0 }), - ], - ])('%s', async (_label, options) => { - const client = new Sentry.NodeClient({ - ...options, - dsn: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', - tracesSampleRate: 1, - transport: _opts => - Sentry.makeNodeTransport({ - url: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', - recordDroppedEvent: () => { - return undefined; - }, - }), - integrations: [_nodeProfilingIntegration()], + describe('continuous profiling mode', () => { + beforeEach(() => { + vi.clearAllMocks(); }); - Sentry.setCurrentClient(client); - client.init(); + it.each([['no option is set', makeClientOptions({})]])('%s', async (_label, options) => { + const client = new Sentry.NodeClient({ + ...options, + dsn: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', + tracesSampleRate: 1, + transport: _opts => + Sentry.makeNodeTransport({ + url: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', + recordDroppedEvent: () => { + return undefined; + }, + }), + integrations: [_nodeProfilingIntegration()], + }); - const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); - const transport = client.getTransport(); + Sentry.setCurrentClient(client); + client.init(); - if (!transport) { - throw new Error('Transport not found'); - } + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + const transport = client.getTransport(); - vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); - Sentry.profiler.startProfiler(); - const callCount = startProfilingSpy.mock.calls.length; - expect(startProfilingSpy).toHaveBeenCalled(); + if (!transport) { + throw new Error('Transport not found'); + } - Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); - expect(startProfilingSpy).toHaveBeenCalledTimes(callCount); - }); + vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); + Sentry.profiler.startProfiler(); + const callCount = startProfilingSpy.mock.calls.length; + expect(startProfilingSpy).toHaveBeenCalled(); - it('top level methods proxy to integration', () => { - const client = new Sentry.NodeClient({ - ...makeClientOptions({ profilesSampleRate: undefined }), - dsn: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', - tracesSampleRate: 1, - transport: _opts => - Sentry.makeNodeTransport({ - url: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', - recordDroppedEvent: () => { - return undefined; - }, - }), - integrations: [_nodeProfilingIntegration()], + Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); + expect(startProfilingSpy).toHaveBeenCalledTimes(callCount); }); - Sentry.setCurrentClient(client); - client.init(); + it('top level methods proxy to integration', () => { + const client = new Sentry.NodeClient({ + ...makeClientOptions({}), + dsn: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', + tracesSampleRate: 1, + transport: _opts => + Sentry.makeNodeTransport({ + url: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', + recordDroppedEvent: () => { + return undefined; + }, + }), + integrations: [_nodeProfilingIntegration()], + }); - const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); - const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); + Sentry.setCurrentClient(client); + client.init(); - Sentry.profiler.startProfiler(); - expect(startProfilingSpy).toHaveBeenCalledTimes(1); - Sentry.profiler.stopProfiler(); - expect(stopProfilingSpy).toHaveBeenCalledTimes(1); + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); + + Sentry.profiler.startProfiler(); + expect(startProfilingSpy).toHaveBeenCalledTimes(1); + Sentry.profiler.stopProfiler(); + expect(stopProfilingSpy).toHaveBeenCalledTimes(1); + }); }); }); diff --git a/packages/profiling-node/test/spanProfileUtils.worker.test.ts b/packages/profiling-node/test/spanProfileUtils.worker.test.ts index 95b1a7bfad3f..6ce62f018c01 100644 --- a/packages/profiling-node/test/spanProfileUtils.worker.test.ts +++ b/packages/profiling-node/test/spanProfileUtils.worker.test.ts @@ -18,7 +18,6 @@ function makeContinuousProfilingClient(): [Sentry.NodeClient, Transport] { const client = new Sentry.NodeClient({ stackParser: Sentry.defaultStackParser, tracesSampleRate: 1, - profilesSampleRate: undefined, debug: true, environment: 'test-environment', dsn: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', From 7c694b8126639250d2e7e66fca21049bfdae6b57 Mon Sep 17 00:00:00 2001 From: JonasBa Date: Tue, 11 Mar 2025 16:01:22 -0400 Subject: [PATCH 09/21] test: rename spanProfileUtils to integration.test.ts --- .../profiling-node/test/integration.test.ts | 849 ++++++++++++++++-- ...ker.test.ts => integration.worker.test.ts} | 0 .../test/spanProfileUtils.test.ts | 811 ----------------- 3 files changed, 792 insertions(+), 868 deletions(-) rename packages/profiling-node/test/{spanProfileUtils.worker.test.ts => integration.worker.test.ts} (100%) delete mode 100644 packages/profiling-node/test/spanProfileUtils.test.ts diff --git a/packages/profiling-node/test/integration.test.ts b/packages/profiling-node/test/integration.test.ts index 392e02ed7704..d7df77ccda20 100644 --- a/packages/profiling-node/test/integration.test.ts +++ b/packages/profiling-node/test/integration.test.ts @@ -1,76 +1,811 @@ -import { EventEmitter } from 'events'; +import * as Sentry from '@sentry/node'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import type { Transport } from '@sentry/core'; - -import type { NodeClient } from '@sentry/node'; -import { afterEach, describe, expect, it, vi } from 'vitest'; +import { CpuProfilerBindings } from '@sentry-internal/node-cpu-profiler'; +import { getMainCarrier } from '@sentry/core'; +import { GLOBAL_OBJ, createEnvelope, logger } from '@sentry/core'; +import type { ProfilingIntegration } from '@sentry/core'; +import type { ProfileChunk, Transport } from '@sentry/core'; +import type { NodeClientOptions } from '@sentry/node/build/types/types'; import { _nodeProfilingIntegration } from '../src/integration'; -describe('ProfilingIntegration', () => { - afterEach(() => { - vi.clearAllMocks(); +function makeClientWithHooks(): [Sentry.NodeClient, Transport] { + const integration = _nodeProfilingIntegration(); + const client = new Sentry.NodeClient({ + stackParser: Sentry.defaultStackParser, + tracesSampleRate: 1, + profilesSampleRate: 1, + debug: true, + environment: 'test-environment', + dsn: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', + integrations: [integration], + transport: _opts => + Sentry.makeNodeTransport({ + url: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', + recordDroppedEvent: () => { + return undefined; + }, + }), }); - it('has a name', () => { - expect(_nodeProfilingIntegration().name).toBe('ProfilingIntegration'); + + return [client, client.getTransport() as Transport]; +} + +function makeContinuousProfilingClient(): [Sentry.NodeClient, Transport] { + const integration = _nodeProfilingIntegration(); + const client = new Sentry.NodeClient({ + stackParser: Sentry.defaultStackParser, + tracesSampleRate: 1, + debug: true, + environment: 'test-environment', + dsn: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', + integrations: [integration], + transport: _opts => + Sentry.makeNodeTransport({ + url: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', + recordDroppedEvent: () => { + return undefined; + }, + }), }); - it('does not call transporter if null profile is received', () => { - const transport: Transport = { - send: vi.fn().mockImplementation(() => Promise.resolve()), - flush: vi.fn().mockImplementation(() => Promise.resolve()), - }; - const integration = _nodeProfilingIntegration(); - const emitter = new EventEmitter(); - - const client = { - on: emitter.on.bind(emitter), - emit: emitter.emit.bind(emitter), - getOptions: () => { + return [client, client.getTransport() as Transport]; +} + +function getProfilerId(): string { + return ( + Sentry.getClient()?.getIntegrationByName>('ProfilingIntegration') as any + )?._profiler?._profilerId; +} + +function makeClientOptions( + options: Omit, +): NodeClientOptions { + return { + stackParser: Sentry.defaultStackParser, + integrations: [_nodeProfilingIntegration()], + debug: true, + transport: _opts => + Sentry.makeNodeTransport({ + url: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', + recordDroppedEvent: () => { + return undefined; + }, + }), + ...options, + }; +} + +const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + +describe('ProfilingIntegration', () => { + describe('automated span instrumentation', () => { + beforeEach(() => { + vi.useRealTimers(); + // We will mock the carrier as if it has been initialized by the SDK, else everything is short circuited + getMainCarrier().__SENTRY__ = {}; + GLOBAL_OBJ._sentryDebugIds = undefined as any; + }); + afterEach(() => { + vi.clearAllMocks(); + vi.restoreAllMocks(); + delete getMainCarrier().__SENTRY__; + }); + + it('pulls environment from sdk init', async () => { + const [client, transport] = makeClientWithHooks(); + Sentry.setCurrentClient(client); + client.init(); + + const transportSpy = vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); + + const transaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); + await wait(500); + transaction.end(); + + await Sentry.flush(1000); + expect(transportSpy.mock.calls?.[0]?.[0]?.[1]?.[0]?.[1]).toMatchObject({ environment: 'test-environment' }); + }); + + it('logger warns user if there are insufficient samples and discards the profile', async () => { + const logSpy = vi.spyOn(logger, 'log'); + + const [client, transport] = makeClientWithHooks(); + Sentry.setCurrentClient(client); + client.init(); + + // @ts-expect-error we just mock the return type and ignore the signature + vi.spyOn(CpuProfilerBindings, 'stopProfiling').mockImplementation(() => { + return { + samples: [ + { + stack_id: 0, + thread_id: '0', + elapsed_since_start_ns: '10', + }, + ], + measurements: {}, + stacks: [[0]], + frames: [], + resources: [], + profiler_logging_mode: 'lazy', + }; + }); + + vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); + + const transaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); + transaction.end(); + + await Sentry.flush(1000); + + expect(logSpy).toHaveBeenCalledWith('[Profiling] Discarding profile because it contains less than 2 samples'); + + expect((transport.send as any).mock.calls[0][0][1][0][0]?.type).toBe('transaction'); + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(transport.send).toHaveBeenCalledTimes(1); + }); + + it('logger warns user if traceId is invalid', async () => { + const logSpy = vi.spyOn(logger, 'log'); + + const [client, transport] = makeClientWithHooks(); + Sentry.setCurrentClient(client); + client.init(); + + // @ts-expect-error we just mock the return type and ignore the signature + vi.spyOn(CpuProfilerBindings, 'stopProfiling').mockImplementation(() => { + return { + samples: [ + { + stack_id: 0, + thread_id: '0', + elapsed_since_start_ns: '10', + }, + { + stack_id: 0, + thread_id: '0', + elapsed_since_start_ns: '10', + }, + ], + measurements: {}, + resources: [], + stacks: [[0]], + frames: [], + profiler_logging_mode: 'lazy', + }; + }); + + vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); + + Sentry.getCurrentScope().getPropagationContext().traceId = 'boop'; + const transaction = Sentry.startInactiveSpan({ + forceTransaction: true, + name: 'profile_hub', + }); + await wait(500); + transaction.end(); + + await Sentry.flush(1000); + + expect(logSpy).toHaveBeenCalledWith('[Profiling] Invalid traceId: ' + 'boop' + ' on profiled event'); + }); + + describe('with hooks', () => { + it('calls profiler when transaction is started/stopped', async () => { + const [client, transport] = makeClientWithHooks(); + Sentry.setCurrentClient(client); + client.init(); + + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); + + vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); + + const transaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); + await wait(500); + transaction.end(); + + await Sentry.flush(1000); + + expect(startProfilingSpy).toHaveBeenCalledTimes(1); + expect((stopProfilingSpy.mock.calls[stopProfilingSpy.mock.calls.length - 1]?.[0] as string).length).toBe(32); + }); + + it('sends profile in the same envelope as transaction', async () => { + const [client, transport] = makeClientWithHooks(); + Sentry.setCurrentClient(client); + client.init(); + + const transportSpy = vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); + + const transaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); + await wait(500); + transaction.end(); + + await Sentry.flush(1000); + + // One for profile, the other for transaction + expect(transportSpy).toHaveBeenCalledTimes(1); + expect(transportSpy.mock.calls?.[0]?.[0]?.[1]?.[1]?.[0]).toMatchObject({ type: 'profile' }); + }); + + it('does not crash if transaction has no profile context or it is invalid', async () => { + const [client] = makeClientWithHooks(); + Sentry.setCurrentClient(client); + client.init(); + + // @ts-expect-error transaction is partial + client.emit('beforeEnvelope', createEnvelope({ type: 'transaction' }, { type: 'transaction' })); + // @ts-expect-error transaction is partial + client.emit('beforeEnvelope', createEnvelope({ type: 'transaction' }, { type: 'transaction', contexts: {} })); + client.emit( + 'beforeEnvelope', + // @ts-expect-error transaction is partial + createEnvelope({ type: 'transaction' }, { type: 'transaction', contexts: { profile: {} } }), + ); + client.emit( + 'beforeEnvelope', + // @ts-expect-error transaction is partial + createEnvelope({ type: 'transaction' }, { type: 'transaction', contexts: { profile: { profile_id: null } } }), + ); + + // Emit is sync, so we can just assert that we got here + expect(true).toBe(true); + }); + + it('if transaction was profiled, but profiler returned null', async () => { + const [client, transport] = makeClientWithHooks(); + Sentry.setCurrentClient(client); + client.init(); + + vi.spyOn(CpuProfilerBindings, 'stopProfiling').mockReturnValue(null); + // Emit is sync, so we can just assert that we got here + const transportSpy = vi.spyOn(transport, 'send').mockImplementation(() => { + // Do nothing so we don't send events to Sentry + return Promise.resolve({}); + }); + + const transaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); + await wait(500); + transaction.end(); + + await Sentry.flush(1000); + + // Only transaction is sent + expect(transportSpy.mock.calls?.[0]?.[0]?.[1]?.[0]?.[0]).toMatchObject({ type: 'transaction' }); + expect(transportSpy.mock.calls?.[0]?.[0]?.[1][1]).toBeUndefined(); + }); + + it('emits preprocessEvent for profile', async () => { + const [client] = makeClientWithHooks(); + Sentry.setCurrentClient(client); + client.init(); + + const onPreprocessEvent = vi.fn(); + + client.on('preprocessEvent', onPreprocessEvent); + + const transaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); + await wait(500); + transaction.end(); + + await Sentry.flush(1000); + + expect(onPreprocessEvent.mock.calls[1]?.[0]).toMatchObject({ + profile: { + samples: expect.arrayContaining([expect.anything()]), + stacks: expect.arrayContaining([expect.anything()]), + }, + }); + }); + + it('automated span instrumentation does not support continuous profiling', () => { + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + + const [client] = makeClientWithHooks(); + Sentry.setCurrentClient(client); + client.init(); + + const integration = + client.getIntegrationByName>('ProfilingIntegration'); + if (!integration) { + throw new Error('Profiling integration not found'); + } + integration._profiler.start(); + expect(startProfilingSpy).not.toHaveBeenCalled(); + }); + }); + + it('does not crash if stop is called multiple times', async () => { + const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); + + const [client] = makeClientWithHooks(); + Sentry.setCurrentClient(client); + client.init(); + + const transaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'txn' }); + transaction.end(); + transaction.end(); + expect(stopProfilingSpy).toHaveBeenCalledTimes(1); + }); + it('enriches profile with debug_id', async () => { + GLOBAL_OBJ._sentryDebugIds = { + 'Error\n at filename.js (filename.js:36:15)': 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa', + 'Error\n at filename2.js (filename2.js:36:15)': 'bbbbbbbb-bbbb-4bbb-bbbb-bbbbbbbbbb', + 'Error\n at filename3.js (filename3.js:36:15)': 'bbbbbbbb-bbbb-4bbb-bbbb-bbbbbbbbbb', + }; + + // @ts-expect-error we just mock the return type and ignore the signature + vi.spyOn(CpuProfilerBindings, 'stopProfiling').mockImplementation(() => { return { - _metadata: {}, + samples: [ + { + stack_id: 0, + thread_id: '0', + elapsed_since_start_ns: '10', + }, + { + stack_id: 0, + thread_id: '0', + elapsed_since_start_ns: '10', + }, + ], + measurements: {}, + resources: ['filename.js', 'filename2.js'], + stacks: [[0]], + frames: [], + profiler_logging_mode: 'lazy', }; - }, - getDsn: () => { - return {}; - }, - getTransport: () => transport, - } as unknown as NodeClient; + }); + + const [client, transport] = makeClientWithHooks(); + Sentry.setCurrentClient(client); + client.init(); + + const transportSpy = vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); + + const transaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); + await wait(500); + transaction.end(); + + await Sentry.flush(1000); + + expect(transportSpy.mock.calls?.[0]?.[0]?.[1]?.[1]?.[1]).toMatchObject({ + debug_meta: { + images: [ + { + type: 'sourcemap', + debug_id: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa', + code_file: 'filename.js', + }, + { + type: 'sourcemap', + debug_id: 'bbbbbbbb-bbbb-4bbb-bbbb-bbbbbbbbbb', + code_file: 'filename2.js', + }, + ], + }, + }); + }); + }); + + it('top level methods do not proxy to integration', () => { + const client = new Sentry.NodeClient({ + ...makeClientOptions({ profilesSampleRate: undefined }), + dsn: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', + tracesSampleRate: 1, + profilesSampleRate: 1, + transport: _opts => + Sentry.makeNodeTransport({ + url: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', + recordDroppedEvent: () => { + return undefined; + }, + }), + integrations: [_nodeProfilingIntegration()], + }); + + Sentry.setCurrentClient(client); + client.init(); - integration?.setup?.(client); + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); - // eslint-disable-next-line @typescript-eslint/unbound-method - expect(transport.send).not.toHaveBeenCalled(); + Sentry.profiler.startProfiler(); + expect(startProfilingSpy).not.toHaveBeenCalled(); + Sentry.profiler.stopProfiler(); + expect(stopProfilingSpy).not.toHaveBeenCalled(); }); - it('binds to spanStart, spanEnd and beforeEnvelope', () => { - const transport: Transport = { - send: vi.fn().mockImplementation(() => Promise.resolve()), - flush: vi.fn().mockImplementation(() => Promise.resolve()), - }; - const integration = _nodeProfilingIntegration(); - - const client = { - on: vi.fn(), - emit: vi.fn(), - getOptions: () => { + describe('continuous profiling', () => { + beforeEach(() => { + vi.useFakeTimers(); + // We will mock the carrier as if it has been initialized by the SDK, else everything is short circuited + getMainCarrier().__SENTRY__ = {}; + GLOBAL_OBJ._sentryDebugIds = undefined as any; + }); + afterEach(() => { + const client = Sentry.getClient(); + const integration = client?.getIntegrationByName>('ProfilingIntegration'); + + if (integration) { + Sentry.profiler.stopProfiler(); + } + + vi.clearAllMocks(); + vi.restoreAllMocks(); + vi.runAllTimers(); + delete getMainCarrier().__SENTRY__; + }); + + it('attaches sdk metadata to chunks', () => { + // @ts-expect-error we just mock the return type and ignore the signature + vi.spyOn(CpuProfilerBindings, 'stopProfiling').mockImplementation(() => { + return { + samples: [ + { + stack_id: 0, + thread_id: '0', + elapsed_since_start_ns: '10', + }, + { + stack_id: 0, + thread_id: '0', + elapsed_since_start_ns: '10', + }, + ], + measurements: {}, + stacks: [[0]], + frames: [], + resources: [], + profiler_logging_mode: 'lazy', + }; + }); + + const [client, transport] = makeContinuousProfilingClient(); + Sentry.setCurrentClient(client); + client.init(); + + const transportSpy = vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); + Sentry.profiler.startProfiler(); + vi.advanceTimersByTime(1000); + Sentry.profiler.stopProfiler(); + vi.advanceTimersByTime(1000); + + const profile = transportSpy.mock.calls?.[0]?.[0]?.[1]?.[0]?.[1] as ProfileChunk; + expect(profile.client_sdk.name).toBe('sentry.javascript.node'); + expect(profile.client_sdk.version).toEqual(expect.stringMatching(/\d+\.\d+\.\d+/)); + }); + + it('initializes the continuous profiler', () => { + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + + const [client] = makeContinuousProfilingClient(); + Sentry.setCurrentClient(client); + client.init(); + + expect(startProfilingSpy).not.toHaveBeenCalledTimes(1); + Sentry.profiler.startProfiler(); + + const integration = client.getIntegrationByName>('ProfilingIntegration'); + expect(integration?._profiler).toBeDefined(); + }); + + it('starts a continuous profile', () => { + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + + const [client] = makeContinuousProfilingClient(); + Sentry.setCurrentClient(client); + client.init(); + + expect(startProfilingSpy).not.toHaveBeenCalledTimes(1); + Sentry.profiler.startProfiler(); + expect(startProfilingSpy).toHaveBeenCalledTimes(1); + }); + + it('multiple calls to start abort previous profile', () => { + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); + + const [client] = makeContinuousProfilingClient(); + Sentry.setCurrentClient(client); + client.init(); + + expect(startProfilingSpy).not.toHaveBeenCalledTimes(1); + Sentry.profiler.startProfiler(); + Sentry.profiler.startProfiler(); + + expect(startProfilingSpy).toHaveBeenCalledTimes(2); + expect(stopProfilingSpy).toHaveBeenCalledTimes(1); + }); + + it('restarts a new chunk after previous', async () => { + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); + + const [client] = makeContinuousProfilingClient(); + Sentry.setCurrentClient(client); + client.init(); + + expect(startProfilingSpy).not.toHaveBeenCalledTimes(1); + Sentry.profiler.startProfiler(); + + vi.advanceTimersByTime(60_001); + expect(stopProfilingSpy).toHaveBeenCalledTimes(1); + expect(startProfilingSpy).toHaveBeenCalledTimes(2); + }); + + it('chunks share the same profilerId', async () => { + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); + + const [client] = makeContinuousProfilingClient(); + Sentry.setCurrentClient(client); + client.init(); + + expect(startProfilingSpy).not.toHaveBeenCalledTimes(1); + Sentry.profiler.startProfiler(); + const profilerId = getProfilerId(); + + vi.advanceTimersByTime(60_001); + expect(stopProfilingSpy).toHaveBeenCalledTimes(1); + expect(startProfilingSpy).toHaveBeenCalledTimes(2); + expect(getProfilerId()).toBe(profilerId); + }); + + it('explicit calls to stop clear profilerId', async () => { + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + + const [client] = makeContinuousProfilingClient(); + Sentry.setCurrentClient(client); + client.init(); + + expect(startProfilingSpy).not.toHaveBeenCalledTimes(1); + Sentry.profiler.startProfiler(); + const profilerId = getProfilerId(); + Sentry.profiler.stopProfiler(); + Sentry.profiler.startProfiler(); + + expect(getProfilerId()).toEqual(expect.any(String)); + expect(getProfilerId()).not.toBe(profilerId); + }); + + it('stops a continuous profile after interval', async () => { + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); + + const [client] = makeContinuousProfilingClient(); + Sentry.setCurrentClient(client); + client.init(); + + expect(startProfilingSpy).not.toHaveBeenCalledTimes(1); + Sentry.profiler.startProfiler(); + + vi.advanceTimersByTime(60_001); + expect(stopProfilingSpy).toHaveBeenCalledTimes(1); + }); + + it('manually stopping a chunk doesnt restart the profiler', async () => { + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); + + const [client] = makeContinuousProfilingClient(); + Sentry.setCurrentClient(client); + client.init(); + + expect(startProfilingSpy).not.toHaveBeenCalledTimes(1); + Sentry.profiler.startProfiler(); + + vi.advanceTimersByTime(1000); + + Sentry.profiler.stopProfiler(); + expect(stopProfilingSpy).toHaveBeenCalledTimes(1); + + vi.advanceTimersByTime(1000); + expect(startProfilingSpy).toHaveBeenCalledTimes(1); + }); + + it('continuous mode does not instrument spans', () => { + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + + const [client] = makeContinuousProfilingClient(); + Sentry.setCurrentClient(client); + client.init(); + + Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); + expect(startProfilingSpy).not.toHaveBeenCalled(); + }); + + it('sends as profile_chunk envelope type', async () => { + // @ts-expect-error we just mock the return type and ignore the signature + vi.spyOn(CpuProfilerBindings, 'stopProfiling').mockImplementation(() => { return { - _metadata: {}, - profilesSampleRate: 1, + samples: [ + { + stack_id: 0, + thread_id: '0', + elapsed_since_start_ns: '10', + }, + { + stack_id: 0, + thread_id: '0', + elapsed_since_start_ns: '10', + }, + ], + measurements: {}, + stacks: [[0]], + frames: [], + resources: [], + profiler_logging_mode: 'lazy', }; - }, - getDsn: () => { - return {}; - }, - getTransport: () => transport, - } as unknown as NodeClient; + }); + + const [client, transport] = makeContinuousProfilingClient(); + Sentry.setCurrentClient(client); + client.init(); + + const transportSpy = vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); + Sentry.profiler.startProfiler(); + vi.advanceTimersByTime(1000); + Sentry.profiler.stopProfiler(); + vi.advanceTimersByTime(1000); + + expect(transportSpy.mock.calls?.[0]?.[0]?.[1]?.[0]?.[0]?.type).toBe('profile_chunk'); + }); + + it('sets global profile context', async () => { + const [client, transport] = makeContinuousProfilingClient(); + Sentry.setCurrentClient(client); + client.init(); + + const transportSpy = vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); + + const nonProfiledTransaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); + nonProfiledTransaction.end(); + + expect(transportSpy.mock.calls?.[0]?.[0]?.[1]?.[0]?.[1]).not.toMatchObject({ + contexts: { + profile: {}, + }, + }); + + const integration = client.getIntegrationByName>('ProfilingIntegration'); + if (!integration) { + throw new Error('Profiling integration not found'); + } + + integration._profiler.start(); + const profiledTransaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); + profiledTransaction.end(); + Sentry.profiler.stopProfiler(); + + expect(transportSpy.mock.calls?.[1]?.[0]?.[1]?.[0]?.[1]).toMatchObject({ + contexts: { + trace: { + data: expect.objectContaining({ + ['thread.id']: expect.any(String), + ['thread.name']: expect.any(String), + }), + }, + profile: { + profiler_id: expect.any(String), + }, + }, + }); + }); + }); + + describe('continuous profiling does not start in span profiling mode', () => { + it.each([ + ['profilesSampleRate=1', makeClientOptions({ profilesSampleRate: 1 })], + ['profilesSampler is defined', makeClientOptions({ profilesSampler: () => 1 })], + ])('%s', async (_label, options) => { + const logSpy = vi.spyOn(logger, 'log'); + const client = new Sentry.NodeClient({ + ...options, + dsn: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', + tracesSampleRate: 1, + transport: _opts => + Sentry.makeNodeTransport({ + url: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', + recordDroppedEvent: () => { + return undefined; + }, + }), + integrations: [_nodeProfilingIntegration()], + }); + + Sentry.setCurrentClient(client); + client.init(); + + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + const transport = client.getTransport(); + + if (!transport) { + throw new Error('Transport not found'); + } + + vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); + Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); + + expect(startProfilingSpy).toHaveBeenCalled(); + const integration = client.getIntegrationByName>('ProfilingIntegration'); + + if (!integration) { + throw new Error('Profiling integration not found'); + } + + integration._profiler.start(); + expect(logSpy).toHaveBeenLastCalledWith( + '[Profiling] Failed to start, sentry client was never attached to the profiler.', + ); + }); + }); + describe('continuous profiling mode', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it.each([['no option is set', makeClientOptions({})]])('%s', async (_label, options) => { + const client = new Sentry.NodeClient({ + ...options, + dsn: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', + tracesSampleRate: 1, + transport: _opts => + Sentry.makeNodeTransport({ + url: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', + recordDroppedEvent: () => { + return undefined; + }, + }), + integrations: [_nodeProfilingIntegration()], + }); + + Sentry.setCurrentClient(client); + client.init(); + + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + const transport = client.getTransport(); + + if (!transport) { + throw new Error('Transport not found'); + } + + vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); + Sentry.profiler.startProfiler(); + const callCount = startProfilingSpy.mock.calls.length; + expect(startProfilingSpy).toHaveBeenCalled(); + + Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); + expect(startProfilingSpy).toHaveBeenCalledTimes(callCount); + }); + + it('top level methods proxy to integration', () => { + const client = new Sentry.NodeClient({ + ...makeClientOptions({}), + dsn: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', + tracesSampleRate: 1, + transport: _opts => + Sentry.makeNodeTransport({ + url: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', + recordDroppedEvent: () => { + return undefined; + }, + }), + integrations: [_nodeProfilingIntegration()], + }); - const spy = vi.spyOn(client, 'on'); + Sentry.setCurrentClient(client); + client.init(); - integration?.setup?.(client); + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); - expect(spy).toHaveBeenCalledTimes(3); - expect(spy).toHaveBeenCalledWith('spanStart', expect.any(Function)); - expect(spy).toHaveBeenCalledWith('spanEnd', expect.any(Function)); - expect(spy).toHaveBeenCalledWith('beforeEnvelope', expect.any(Function)); + Sentry.profiler.startProfiler(); + expect(startProfilingSpy).toHaveBeenCalledTimes(1); + Sentry.profiler.stopProfiler(); + expect(stopProfilingSpy).toHaveBeenCalledTimes(1); + }); }); }); diff --git a/packages/profiling-node/test/spanProfileUtils.worker.test.ts b/packages/profiling-node/test/integration.worker.test.ts similarity index 100% rename from packages/profiling-node/test/spanProfileUtils.worker.test.ts rename to packages/profiling-node/test/integration.worker.test.ts diff --git a/packages/profiling-node/test/spanProfileUtils.test.ts b/packages/profiling-node/test/spanProfileUtils.test.ts deleted file mode 100644 index d7df77ccda20..000000000000 --- a/packages/profiling-node/test/spanProfileUtils.test.ts +++ /dev/null @@ -1,811 +0,0 @@ -import * as Sentry from '@sentry/node'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; - -import { CpuProfilerBindings } from '@sentry-internal/node-cpu-profiler'; -import { getMainCarrier } from '@sentry/core'; -import { GLOBAL_OBJ, createEnvelope, logger } from '@sentry/core'; -import type { ProfilingIntegration } from '@sentry/core'; -import type { ProfileChunk, Transport } from '@sentry/core'; -import type { NodeClientOptions } from '@sentry/node/build/types/types'; -import { _nodeProfilingIntegration } from '../src/integration'; - -function makeClientWithHooks(): [Sentry.NodeClient, Transport] { - const integration = _nodeProfilingIntegration(); - const client = new Sentry.NodeClient({ - stackParser: Sentry.defaultStackParser, - tracesSampleRate: 1, - profilesSampleRate: 1, - debug: true, - environment: 'test-environment', - dsn: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', - integrations: [integration], - transport: _opts => - Sentry.makeNodeTransport({ - url: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', - recordDroppedEvent: () => { - return undefined; - }, - }), - }); - - return [client, client.getTransport() as Transport]; -} - -function makeContinuousProfilingClient(): [Sentry.NodeClient, Transport] { - const integration = _nodeProfilingIntegration(); - const client = new Sentry.NodeClient({ - stackParser: Sentry.defaultStackParser, - tracesSampleRate: 1, - debug: true, - environment: 'test-environment', - dsn: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', - integrations: [integration], - transport: _opts => - Sentry.makeNodeTransport({ - url: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', - recordDroppedEvent: () => { - return undefined; - }, - }), - }); - - return [client, client.getTransport() as Transport]; -} - -function getProfilerId(): string { - return ( - Sentry.getClient()?.getIntegrationByName>('ProfilingIntegration') as any - )?._profiler?._profilerId; -} - -function makeClientOptions( - options: Omit, -): NodeClientOptions { - return { - stackParser: Sentry.defaultStackParser, - integrations: [_nodeProfilingIntegration()], - debug: true, - transport: _opts => - Sentry.makeNodeTransport({ - url: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', - recordDroppedEvent: () => { - return undefined; - }, - }), - ...options, - }; -} - -const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); - -describe('ProfilingIntegration', () => { - describe('automated span instrumentation', () => { - beforeEach(() => { - vi.useRealTimers(); - // We will mock the carrier as if it has been initialized by the SDK, else everything is short circuited - getMainCarrier().__SENTRY__ = {}; - GLOBAL_OBJ._sentryDebugIds = undefined as any; - }); - afterEach(() => { - vi.clearAllMocks(); - vi.restoreAllMocks(); - delete getMainCarrier().__SENTRY__; - }); - - it('pulls environment from sdk init', async () => { - const [client, transport] = makeClientWithHooks(); - Sentry.setCurrentClient(client); - client.init(); - - const transportSpy = vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); - - const transaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); - await wait(500); - transaction.end(); - - await Sentry.flush(1000); - expect(transportSpy.mock.calls?.[0]?.[0]?.[1]?.[0]?.[1]).toMatchObject({ environment: 'test-environment' }); - }); - - it('logger warns user if there are insufficient samples and discards the profile', async () => { - const logSpy = vi.spyOn(logger, 'log'); - - const [client, transport] = makeClientWithHooks(); - Sentry.setCurrentClient(client); - client.init(); - - // @ts-expect-error we just mock the return type and ignore the signature - vi.spyOn(CpuProfilerBindings, 'stopProfiling').mockImplementation(() => { - return { - samples: [ - { - stack_id: 0, - thread_id: '0', - elapsed_since_start_ns: '10', - }, - ], - measurements: {}, - stacks: [[0]], - frames: [], - resources: [], - profiler_logging_mode: 'lazy', - }; - }); - - vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); - - const transaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); - transaction.end(); - - await Sentry.flush(1000); - - expect(logSpy).toHaveBeenCalledWith('[Profiling] Discarding profile because it contains less than 2 samples'); - - expect((transport.send as any).mock.calls[0][0][1][0][0]?.type).toBe('transaction'); - // eslint-disable-next-line @typescript-eslint/unbound-method - expect(transport.send).toHaveBeenCalledTimes(1); - }); - - it('logger warns user if traceId is invalid', async () => { - const logSpy = vi.spyOn(logger, 'log'); - - const [client, transport] = makeClientWithHooks(); - Sentry.setCurrentClient(client); - client.init(); - - // @ts-expect-error we just mock the return type and ignore the signature - vi.spyOn(CpuProfilerBindings, 'stopProfiling').mockImplementation(() => { - return { - samples: [ - { - stack_id: 0, - thread_id: '0', - elapsed_since_start_ns: '10', - }, - { - stack_id: 0, - thread_id: '0', - elapsed_since_start_ns: '10', - }, - ], - measurements: {}, - resources: [], - stacks: [[0]], - frames: [], - profiler_logging_mode: 'lazy', - }; - }); - - vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); - - Sentry.getCurrentScope().getPropagationContext().traceId = 'boop'; - const transaction = Sentry.startInactiveSpan({ - forceTransaction: true, - name: 'profile_hub', - }); - await wait(500); - transaction.end(); - - await Sentry.flush(1000); - - expect(logSpy).toHaveBeenCalledWith('[Profiling] Invalid traceId: ' + 'boop' + ' on profiled event'); - }); - - describe('with hooks', () => { - it('calls profiler when transaction is started/stopped', async () => { - const [client, transport] = makeClientWithHooks(); - Sentry.setCurrentClient(client); - client.init(); - - const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); - const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); - - vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); - - const transaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); - await wait(500); - transaction.end(); - - await Sentry.flush(1000); - - expect(startProfilingSpy).toHaveBeenCalledTimes(1); - expect((stopProfilingSpy.mock.calls[stopProfilingSpy.mock.calls.length - 1]?.[0] as string).length).toBe(32); - }); - - it('sends profile in the same envelope as transaction', async () => { - const [client, transport] = makeClientWithHooks(); - Sentry.setCurrentClient(client); - client.init(); - - const transportSpy = vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); - - const transaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); - await wait(500); - transaction.end(); - - await Sentry.flush(1000); - - // One for profile, the other for transaction - expect(transportSpy).toHaveBeenCalledTimes(1); - expect(transportSpy.mock.calls?.[0]?.[0]?.[1]?.[1]?.[0]).toMatchObject({ type: 'profile' }); - }); - - it('does not crash if transaction has no profile context or it is invalid', async () => { - const [client] = makeClientWithHooks(); - Sentry.setCurrentClient(client); - client.init(); - - // @ts-expect-error transaction is partial - client.emit('beforeEnvelope', createEnvelope({ type: 'transaction' }, { type: 'transaction' })); - // @ts-expect-error transaction is partial - client.emit('beforeEnvelope', createEnvelope({ type: 'transaction' }, { type: 'transaction', contexts: {} })); - client.emit( - 'beforeEnvelope', - // @ts-expect-error transaction is partial - createEnvelope({ type: 'transaction' }, { type: 'transaction', contexts: { profile: {} } }), - ); - client.emit( - 'beforeEnvelope', - // @ts-expect-error transaction is partial - createEnvelope({ type: 'transaction' }, { type: 'transaction', contexts: { profile: { profile_id: null } } }), - ); - - // Emit is sync, so we can just assert that we got here - expect(true).toBe(true); - }); - - it('if transaction was profiled, but profiler returned null', async () => { - const [client, transport] = makeClientWithHooks(); - Sentry.setCurrentClient(client); - client.init(); - - vi.spyOn(CpuProfilerBindings, 'stopProfiling').mockReturnValue(null); - // Emit is sync, so we can just assert that we got here - const transportSpy = vi.spyOn(transport, 'send').mockImplementation(() => { - // Do nothing so we don't send events to Sentry - return Promise.resolve({}); - }); - - const transaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); - await wait(500); - transaction.end(); - - await Sentry.flush(1000); - - // Only transaction is sent - expect(transportSpy.mock.calls?.[0]?.[0]?.[1]?.[0]?.[0]).toMatchObject({ type: 'transaction' }); - expect(transportSpy.mock.calls?.[0]?.[0]?.[1][1]).toBeUndefined(); - }); - - it('emits preprocessEvent for profile', async () => { - const [client] = makeClientWithHooks(); - Sentry.setCurrentClient(client); - client.init(); - - const onPreprocessEvent = vi.fn(); - - client.on('preprocessEvent', onPreprocessEvent); - - const transaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); - await wait(500); - transaction.end(); - - await Sentry.flush(1000); - - expect(onPreprocessEvent.mock.calls[1]?.[0]).toMatchObject({ - profile: { - samples: expect.arrayContaining([expect.anything()]), - stacks: expect.arrayContaining([expect.anything()]), - }, - }); - }); - - it('automated span instrumentation does not support continuous profiling', () => { - const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); - - const [client] = makeClientWithHooks(); - Sentry.setCurrentClient(client); - client.init(); - - const integration = - client.getIntegrationByName>('ProfilingIntegration'); - if (!integration) { - throw new Error('Profiling integration not found'); - } - integration._profiler.start(); - expect(startProfilingSpy).not.toHaveBeenCalled(); - }); - }); - - it('does not crash if stop is called multiple times', async () => { - const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); - - const [client] = makeClientWithHooks(); - Sentry.setCurrentClient(client); - client.init(); - - const transaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'txn' }); - transaction.end(); - transaction.end(); - expect(stopProfilingSpy).toHaveBeenCalledTimes(1); - }); - it('enriches profile with debug_id', async () => { - GLOBAL_OBJ._sentryDebugIds = { - 'Error\n at filename.js (filename.js:36:15)': 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa', - 'Error\n at filename2.js (filename2.js:36:15)': 'bbbbbbbb-bbbb-4bbb-bbbb-bbbbbbbbbb', - 'Error\n at filename3.js (filename3.js:36:15)': 'bbbbbbbb-bbbb-4bbb-bbbb-bbbbbbbbbb', - }; - - // @ts-expect-error we just mock the return type and ignore the signature - vi.spyOn(CpuProfilerBindings, 'stopProfiling').mockImplementation(() => { - return { - samples: [ - { - stack_id: 0, - thread_id: '0', - elapsed_since_start_ns: '10', - }, - { - stack_id: 0, - thread_id: '0', - elapsed_since_start_ns: '10', - }, - ], - measurements: {}, - resources: ['filename.js', 'filename2.js'], - stacks: [[0]], - frames: [], - profiler_logging_mode: 'lazy', - }; - }); - - const [client, transport] = makeClientWithHooks(); - Sentry.setCurrentClient(client); - client.init(); - - const transportSpy = vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); - - const transaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); - await wait(500); - transaction.end(); - - await Sentry.flush(1000); - - expect(transportSpy.mock.calls?.[0]?.[0]?.[1]?.[1]?.[1]).toMatchObject({ - debug_meta: { - images: [ - { - type: 'sourcemap', - debug_id: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa', - code_file: 'filename.js', - }, - { - type: 'sourcemap', - debug_id: 'bbbbbbbb-bbbb-4bbb-bbbb-bbbbbbbbbb', - code_file: 'filename2.js', - }, - ], - }, - }); - }); - }); - - it('top level methods do not proxy to integration', () => { - const client = new Sentry.NodeClient({ - ...makeClientOptions({ profilesSampleRate: undefined }), - dsn: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', - tracesSampleRate: 1, - profilesSampleRate: 1, - transport: _opts => - Sentry.makeNodeTransport({ - url: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', - recordDroppedEvent: () => { - return undefined; - }, - }), - integrations: [_nodeProfilingIntegration()], - }); - - Sentry.setCurrentClient(client); - client.init(); - - const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); - const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); - - Sentry.profiler.startProfiler(); - expect(startProfilingSpy).not.toHaveBeenCalled(); - Sentry.profiler.stopProfiler(); - expect(stopProfilingSpy).not.toHaveBeenCalled(); - }); - - describe('continuous profiling', () => { - beforeEach(() => { - vi.useFakeTimers(); - // We will mock the carrier as if it has been initialized by the SDK, else everything is short circuited - getMainCarrier().__SENTRY__ = {}; - GLOBAL_OBJ._sentryDebugIds = undefined as any; - }); - afterEach(() => { - const client = Sentry.getClient(); - const integration = client?.getIntegrationByName>('ProfilingIntegration'); - - if (integration) { - Sentry.profiler.stopProfiler(); - } - - vi.clearAllMocks(); - vi.restoreAllMocks(); - vi.runAllTimers(); - delete getMainCarrier().__SENTRY__; - }); - - it('attaches sdk metadata to chunks', () => { - // @ts-expect-error we just mock the return type and ignore the signature - vi.spyOn(CpuProfilerBindings, 'stopProfiling').mockImplementation(() => { - return { - samples: [ - { - stack_id: 0, - thread_id: '0', - elapsed_since_start_ns: '10', - }, - { - stack_id: 0, - thread_id: '0', - elapsed_since_start_ns: '10', - }, - ], - measurements: {}, - stacks: [[0]], - frames: [], - resources: [], - profiler_logging_mode: 'lazy', - }; - }); - - const [client, transport] = makeContinuousProfilingClient(); - Sentry.setCurrentClient(client); - client.init(); - - const transportSpy = vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); - Sentry.profiler.startProfiler(); - vi.advanceTimersByTime(1000); - Sentry.profiler.stopProfiler(); - vi.advanceTimersByTime(1000); - - const profile = transportSpy.mock.calls?.[0]?.[0]?.[1]?.[0]?.[1] as ProfileChunk; - expect(profile.client_sdk.name).toBe('sentry.javascript.node'); - expect(profile.client_sdk.version).toEqual(expect.stringMatching(/\d+\.\d+\.\d+/)); - }); - - it('initializes the continuous profiler', () => { - const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); - - const [client] = makeContinuousProfilingClient(); - Sentry.setCurrentClient(client); - client.init(); - - expect(startProfilingSpy).not.toHaveBeenCalledTimes(1); - Sentry.profiler.startProfiler(); - - const integration = client.getIntegrationByName>('ProfilingIntegration'); - expect(integration?._profiler).toBeDefined(); - }); - - it('starts a continuous profile', () => { - const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); - - const [client] = makeContinuousProfilingClient(); - Sentry.setCurrentClient(client); - client.init(); - - expect(startProfilingSpy).not.toHaveBeenCalledTimes(1); - Sentry.profiler.startProfiler(); - expect(startProfilingSpy).toHaveBeenCalledTimes(1); - }); - - it('multiple calls to start abort previous profile', () => { - const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); - const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); - - const [client] = makeContinuousProfilingClient(); - Sentry.setCurrentClient(client); - client.init(); - - expect(startProfilingSpy).not.toHaveBeenCalledTimes(1); - Sentry.profiler.startProfiler(); - Sentry.profiler.startProfiler(); - - expect(startProfilingSpy).toHaveBeenCalledTimes(2); - expect(stopProfilingSpy).toHaveBeenCalledTimes(1); - }); - - it('restarts a new chunk after previous', async () => { - const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); - const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); - - const [client] = makeContinuousProfilingClient(); - Sentry.setCurrentClient(client); - client.init(); - - expect(startProfilingSpy).not.toHaveBeenCalledTimes(1); - Sentry.profiler.startProfiler(); - - vi.advanceTimersByTime(60_001); - expect(stopProfilingSpy).toHaveBeenCalledTimes(1); - expect(startProfilingSpy).toHaveBeenCalledTimes(2); - }); - - it('chunks share the same profilerId', async () => { - const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); - const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); - - const [client] = makeContinuousProfilingClient(); - Sentry.setCurrentClient(client); - client.init(); - - expect(startProfilingSpy).not.toHaveBeenCalledTimes(1); - Sentry.profiler.startProfiler(); - const profilerId = getProfilerId(); - - vi.advanceTimersByTime(60_001); - expect(stopProfilingSpy).toHaveBeenCalledTimes(1); - expect(startProfilingSpy).toHaveBeenCalledTimes(2); - expect(getProfilerId()).toBe(profilerId); - }); - - it('explicit calls to stop clear profilerId', async () => { - const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); - - const [client] = makeContinuousProfilingClient(); - Sentry.setCurrentClient(client); - client.init(); - - expect(startProfilingSpy).not.toHaveBeenCalledTimes(1); - Sentry.profiler.startProfiler(); - const profilerId = getProfilerId(); - Sentry.profiler.stopProfiler(); - Sentry.profiler.startProfiler(); - - expect(getProfilerId()).toEqual(expect.any(String)); - expect(getProfilerId()).not.toBe(profilerId); - }); - - it('stops a continuous profile after interval', async () => { - const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); - const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); - - const [client] = makeContinuousProfilingClient(); - Sentry.setCurrentClient(client); - client.init(); - - expect(startProfilingSpy).not.toHaveBeenCalledTimes(1); - Sentry.profiler.startProfiler(); - - vi.advanceTimersByTime(60_001); - expect(stopProfilingSpy).toHaveBeenCalledTimes(1); - }); - - it('manually stopping a chunk doesnt restart the profiler', async () => { - const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); - const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); - - const [client] = makeContinuousProfilingClient(); - Sentry.setCurrentClient(client); - client.init(); - - expect(startProfilingSpy).not.toHaveBeenCalledTimes(1); - Sentry.profiler.startProfiler(); - - vi.advanceTimersByTime(1000); - - Sentry.profiler.stopProfiler(); - expect(stopProfilingSpy).toHaveBeenCalledTimes(1); - - vi.advanceTimersByTime(1000); - expect(startProfilingSpy).toHaveBeenCalledTimes(1); - }); - - it('continuous mode does not instrument spans', () => { - const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); - - const [client] = makeContinuousProfilingClient(); - Sentry.setCurrentClient(client); - client.init(); - - Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); - expect(startProfilingSpy).not.toHaveBeenCalled(); - }); - - it('sends as profile_chunk envelope type', async () => { - // @ts-expect-error we just mock the return type and ignore the signature - vi.spyOn(CpuProfilerBindings, 'stopProfiling').mockImplementation(() => { - return { - samples: [ - { - stack_id: 0, - thread_id: '0', - elapsed_since_start_ns: '10', - }, - { - stack_id: 0, - thread_id: '0', - elapsed_since_start_ns: '10', - }, - ], - measurements: {}, - stacks: [[0]], - frames: [], - resources: [], - profiler_logging_mode: 'lazy', - }; - }); - - const [client, transport] = makeContinuousProfilingClient(); - Sentry.setCurrentClient(client); - client.init(); - - const transportSpy = vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); - Sentry.profiler.startProfiler(); - vi.advanceTimersByTime(1000); - Sentry.profiler.stopProfiler(); - vi.advanceTimersByTime(1000); - - expect(transportSpy.mock.calls?.[0]?.[0]?.[1]?.[0]?.[0]?.type).toBe('profile_chunk'); - }); - - it('sets global profile context', async () => { - const [client, transport] = makeContinuousProfilingClient(); - Sentry.setCurrentClient(client); - client.init(); - - const transportSpy = vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); - - const nonProfiledTransaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); - nonProfiledTransaction.end(); - - expect(transportSpy.mock.calls?.[0]?.[0]?.[1]?.[0]?.[1]).not.toMatchObject({ - contexts: { - profile: {}, - }, - }); - - const integration = client.getIntegrationByName>('ProfilingIntegration'); - if (!integration) { - throw new Error('Profiling integration not found'); - } - - integration._profiler.start(); - const profiledTransaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); - profiledTransaction.end(); - Sentry.profiler.stopProfiler(); - - expect(transportSpy.mock.calls?.[1]?.[0]?.[1]?.[0]?.[1]).toMatchObject({ - contexts: { - trace: { - data: expect.objectContaining({ - ['thread.id']: expect.any(String), - ['thread.name']: expect.any(String), - }), - }, - profile: { - profiler_id: expect.any(String), - }, - }, - }); - }); - }); - - describe('continuous profiling does not start in span profiling mode', () => { - it.each([ - ['profilesSampleRate=1', makeClientOptions({ profilesSampleRate: 1 })], - ['profilesSampler is defined', makeClientOptions({ profilesSampler: () => 1 })], - ])('%s', async (_label, options) => { - const logSpy = vi.spyOn(logger, 'log'); - const client = new Sentry.NodeClient({ - ...options, - dsn: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', - tracesSampleRate: 1, - transport: _opts => - Sentry.makeNodeTransport({ - url: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', - recordDroppedEvent: () => { - return undefined; - }, - }), - integrations: [_nodeProfilingIntegration()], - }); - - Sentry.setCurrentClient(client); - client.init(); - - const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); - const transport = client.getTransport(); - - if (!transport) { - throw new Error('Transport not found'); - } - - vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); - Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); - - expect(startProfilingSpy).toHaveBeenCalled(); - const integration = client.getIntegrationByName>('ProfilingIntegration'); - - if (!integration) { - throw new Error('Profiling integration not found'); - } - - integration._profiler.start(); - expect(logSpy).toHaveBeenLastCalledWith( - '[Profiling] Failed to start, sentry client was never attached to the profiler.', - ); - }); - }); - describe('continuous profiling mode', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it.each([['no option is set', makeClientOptions({})]])('%s', async (_label, options) => { - const client = new Sentry.NodeClient({ - ...options, - dsn: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', - tracesSampleRate: 1, - transport: _opts => - Sentry.makeNodeTransport({ - url: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', - recordDroppedEvent: () => { - return undefined; - }, - }), - integrations: [_nodeProfilingIntegration()], - }); - - Sentry.setCurrentClient(client); - client.init(); - - const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); - const transport = client.getTransport(); - - if (!transport) { - throw new Error('Transport not found'); - } - - vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); - Sentry.profiler.startProfiler(); - const callCount = startProfilingSpy.mock.calls.length; - expect(startProfilingSpy).toHaveBeenCalled(); - - Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); - expect(startProfilingSpy).toHaveBeenCalledTimes(callCount); - }); - - it('top level methods proxy to integration', () => { - const client = new Sentry.NodeClient({ - ...makeClientOptions({}), - dsn: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', - tracesSampleRate: 1, - transport: _opts => - Sentry.makeNodeTransport({ - url: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', - recordDroppedEvent: () => { - return undefined; - }, - }), - integrations: [_nodeProfilingIntegration()], - }); - - Sentry.setCurrentClient(client); - client.init(); - - const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); - const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); - - Sentry.profiler.startProfiler(); - expect(startProfilingSpy).toHaveBeenCalledTimes(1); - Sentry.profiler.stopProfiler(); - expect(stopProfilingSpy).toHaveBeenCalledTimes(1); - }); - }); -}); From 9d2a44e55d9c10445c74c10728423f9baf9eff86 Mon Sep 17 00:00:00 2001 From: JonasBa Date: Tue, 11 Mar 2025 16:13:39 -0400 Subject: [PATCH 10/21] fix formatting --- packages/core/src/profiling.ts | 6 ++---- packages/profiling-node/src/integration.ts | 12 +++++------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/packages/core/src/profiling.ts b/packages/core/src/profiling.ts index 329dc4a07595..1cd33bae8135 100644 --- a/packages/core/src/profiling.ts +++ b/packages/core/src/profiling.ts @@ -71,14 +71,12 @@ function stopProfiler(): void { /** * Starts a new profiler session. */ -function startProfilerSession(): void { -} +function startProfilerSession(): void {} /** * Stops the current profiler session. */ -function stopProfilerSession(): void { -} +function stopProfilerSession(): void {} export const profiler: Profiler = { startProfiler, diff --git a/packages/profiling-node/src/integration.ts b/packages/profiling-node/src/integration.ts index a117d35fcbd9..7bc51611095f 100644 --- a/packages/profiling-node/src/integration.ts +++ b/packages/profiling-node/src/integration.ts @@ -447,9 +447,8 @@ export const _nodeProfilingIntegration = ((): ProfilingIntegration = const options = client.getOptions(); const profilingAPIVersion = getProfilingMode(options); - if (profilingAPIVersion === 'legacy') { - const mode = ('profilesSampleRate' in options || 'profilesSampler' in options) ? 'span' : 'continuous'; + const mode = 'profilesSampleRate' in options || 'profilesSampler' in options ? 'span' : 'continuous'; switch (mode) { case 'continuous': { @@ -468,15 +467,14 @@ export const _nodeProfilingIntegration = ((): ProfilingIntegration = DEBUG_BUILD && logger.warn(`[Profiling] Unknown profiler mode: ${mode}, profiler was not initialized`); } } - } - - else if(profilingAPIVersion === 'current') { + } else if (profilingAPIVersion === 'current') { DEBUG_BUILD && logger.log('[Profiling] Continuous profiler mode enabled.'); this._profiler.initialize(client); return; } - DEBUG_BUILD && logger.log(['[Profiling] Profiling integration is added, but not enabled due to lack of SDK.init options.']) + DEBUG_BUILD && + logger.log(['[Profiling] Profiling integration is added, but not enabled due to lack of SDK.init options.']); return; }, }; @@ -492,7 +490,7 @@ function getProfilingMode(options: NodeOptions): 'legacy' | 'current' | null { return 'legacy'; } - if('profileSessionSampleRate' in options || 'profileLifecycle' in options){ + if ('profileSessionSampleRate' in options || 'profileLifecycle' in options) { return 'current'; } From 05c6cf841bdbd8b7aac94461b362e602669fab20 Mon Sep 17 00:00:00 2001 From: Jonas Date: Wed, 12 Mar 2025 19:14:02 -0400 Subject: [PATCH 11/21] profiling: implement new profiling API spec (#15636) Implements https://www.notion.so/sentry/Continuous-UI-Profiling-SDK-API-Spec-17e8b10e4b5d80c59a40c6e114470934 --- packages/core/src/profiling.ts | 48 +- packages/core/src/types-hoist/profiling.ts | 8 +- packages/profiling-node/src/integration.ts | 469 ++++++++---- .../profiling-node/src/spanProfileUtils.ts | 1 + .../profiling-node/test/integration.test.ts | 711 +++++++++++------- 5 files changed, 808 insertions(+), 429 deletions(-) diff --git a/packages/core/src/profiling.ts b/packages/core/src/profiling.ts index 1cd33bae8135..7330bf8b55f8 100644 --- a/packages/core/src/profiling.ts +++ b/packages/core/src/profiling.ts @@ -18,7 +18,7 @@ function isProfilingIntegrationWithProfiler( * Starts the Sentry continuous profiler. * This mode is exclusive with the transaction profiler and will only work if the profilesSampleRate is set to a falsy value. * In continuous profiling mode, the profiler will keep reporting profile chunks to Sentry until it is stopped, which allows for continuous profiling of the application. - * @deprecated Use `startProfilerSession()` instead. + * @deprecated Use `startProfileSession()` instead. */ function startProfiler(): void { const client = getClient(); @@ -71,16 +71,54 @@ function stopProfiler(): void { /** * Starts a new profiler session. */ -function startProfilerSession(): void {} +function startProfileSession(): void { + const client = getClient(); + if (!client) { + DEBUG_BUILD && logger.warn('No Sentry client available, profiling is not started'); + return; + } + + const integration = client.getIntegrationByName>('ProfilingIntegration'); + if (!integration) { + DEBUG_BUILD && logger.warn('ProfilingIntegration is not available'); + return; + } + + if (!isProfilingIntegrationWithProfiler(integration)) { + DEBUG_BUILD && logger.warn('Profiler is not available on profiling integration.'); + return; + } + + integration._profiler.startProfileSession(); +} /** * Stops the current profiler session. */ -function stopProfilerSession(): void {} +function stopProfileSession(): void { + const client = getClient(); + if (!client) { + DEBUG_BUILD && logger.warn('No Sentry client available, profiling is not started'); + return; + } + + const integration = client.getIntegrationByName>('ProfilingIntegration'); + if (!integration) { + DEBUG_BUILD && logger.warn('ProfilingIntegration is not available'); + return; + } + + if (!isProfilingIntegrationWithProfiler(integration)) { + DEBUG_BUILD && logger.warn('Profiler is not available on profiling integration.'); + return; + } + + integration._profiler.stopProfileSession(); +} export const profiler: Profiler = { startProfiler, stopProfiler, - startProfilerSession, - stopProfilerSession, + startProfileSession, + stopProfileSession, }; diff --git a/packages/core/src/types-hoist/profiling.ts b/packages/core/src/types-hoist/profiling.ts index 5f9c47d6f409..0df93e835a3c 100644 --- a/packages/core/src/types-hoist/profiling.ts +++ b/packages/core/src/types-hoist/profiling.ts @@ -7,6 +7,8 @@ export interface ContinuousProfiler { initialize(client: T): void; start(): void; stop(): void; + startProfileSession(): void; + stopProfileSession(): void; } export interface ProfilingIntegration extends Integration { @@ -16,7 +18,7 @@ export interface ProfilingIntegration extends Integration { export interface Profiler { /** * Starts the profiler. - * @deprecated Use `startProfilerSession()` instead. + * @deprecated Use `startProfileSession()` instead. */ startProfiler(): void; @@ -29,12 +31,12 @@ export interface Profiler { /** * Starts a new profiler session. */ - startProfilerSession(): void; + startProfileSession(): void; /** * Stops the current profiler session. */ - stopProfilerSession(): void; + stopProfileSession(): void; } export type ThreadId = string; diff --git a/packages/profiling-node/src/integration.ts b/packages/profiling-node/src/integration.ts index 7bc51611095f..9afa098561be 100644 --- a/packages/profiling-node/src/integration.ts +++ b/packages/profiling-node/src/integration.ts @@ -41,120 +41,6 @@ function takeFromProfileQueue(profile_id: string): RawThreadCpuProfile | undefin return profile; } -/** - * Instruments the client to automatically invoke the profiler on span start and stop events. - * @param client - */ -function setupAutomatedSpanProfiling(client: NodeClient): void { - const spanToProfileIdMap = new WeakMap(); - - client.on('spanStart', span => { - if (span !== getRootSpan(span)) { - return; - } - - const profile_id = maybeProfileSpan(client, span); - - if (profile_id) { - const options = client.getOptions(); - // Not intended for external use, hence missing types, but we want to profile a couple of things at Sentry that - // currently exceed the default timeout set by the SDKs. - const maxProfileDurationMs = options._experiments?.maxProfileDurationMs || MAX_PROFILE_DURATION_MS; - - if (PROFILE_TIMEOUTS[profile_id]) { - global.clearTimeout(PROFILE_TIMEOUTS[profile_id]); - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete PROFILE_TIMEOUTS[profile_id]; - } - - // Enqueue a timeout to prevent profiles from running over max duration. - const timeout = global.setTimeout(() => { - DEBUG_BUILD && - logger.log('[Profiling] max profile duration elapsed, stopping profiling for:', spanToJSON(span).description); - - const profile = stopSpanProfile(span, profile_id); - if (profile) { - addToProfileQueue(profile_id, profile); - } - }, maxProfileDurationMs); - - // Unref timeout so it doesn't keep the process alive. - timeout.unref(); - - getIsolationScope().setContext('profile', { profile_id }); - spanToProfileIdMap.set(span, profile_id); - } - }); - - client.on('spanEnd', span => { - const profile_id = spanToProfileIdMap.get(span); - - if (profile_id) { - if (PROFILE_TIMEOUTS[profile_id]) { - global.clearTimeout(PROFILE_TIMEOUTS[profile_id]); - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete PROFILE_TIMEOUTS[profile_id]; - } - const profile = stopSpanProfile(span, profile_id); - - if (profile) { - addToProfileQueue(profile_id, profile); - } - } - }); - - client.on('beforeEnvelope', (envelope): void => { - // if not profiles are in queue, there is nothing to add to the envelope. - if (!PROFILE_MAP.size) { - return; - } - - const profiledTransactionEvents = findProfiledTransactionsFromEnvelope(envelope); - if (!profiledTransactionEvents.length) { - return; - } - - const profilesToAddToEnvelope: Profile[] = []; - - for (const profiledTransaction of profiledTransactionEvents) { - const profileContext = profiledTransaction.contexts?.profile; - const profile_id = profileContext?.profile_id; - - if (!profile_id) { - throw new TypeError('[Profiling] cannot find profile for a transaction without a profile context'); - } - - // Remove the profile from the transaction context before sending, relay will take care of the rest. - if (profileContext) { - delete profiledTransaction.contexts?.profile; - } - - const cpuProfile = takeFromProfileQueue(profile_id); - if (!cpuProfile) { - DEBUG_BUILD && logger.log(`[Profiling] Could not retrieve profile for transaction: ${profile_id}`); - continue; - } - - const profile = createProfilingEvent(client, cpuProfile, profiledTransaction); - if (!profile) return; - - profilesToAddToEnvelope.push(profile); - - // @ts-expect-error profile does not inherit from Event - client.emit('preprocessEvent', profile, { - event_id: profiledTransaction.event_id, - }); - - // @ts-expect-error profile does not inherit from Event - client.emit('postprocessEvent', profile, { - event_id: profiledTransaction.event_id, - }); - } - - addProfilesToEnvelope(envelope, profilesToAddToEnvelope); - }); -} - interface ChunkData { id: string; timer: NodeJS.Timeout | undefined; @@ -165,7 +51,11 @@ class ContinuousProfiler { private _profilerId: string | undefined; private _client: NodeClient | undefined = undefined; private _chunkData: ChunkData | undefined = undefined; - + private _mode: 'legacy' | 'current' | undefined = undefined; + private _legacyProfilerMode: 'span' | 'continuous' | undefined = undefined; + private _profileLifecycle: 'manual' | 'trace' | undefined = undefined; + private _sampled: boolean | undefined = undefined; + private _sessionSamplingRate: number | undefined = undefined; /** * Called when the profiler is attached to the client (continuous mode is enabled). If of the profiler * methods called before the profiler is initialized will result in a noop action with debug logs. @@ -173,6 +63,61 @@ class ContinuousProfiler { */ public initialize(client: NodeClient): void { this._client = client; + const options = client.getOptions(); + + this._mode = getProfilingMode(options); + this._sessionSamplingRate = Math.random(); + this._sampled = this._sessionSamplingRate < (options.profileSessionSampleRate ?? 0); + this._profileLifecycle = options.profileLifecycle ?? 'manual'; + + switch (this._mode) { + case 'legacy': { + this._legacyProfilerMode = + 'profilesSampleRate' in options || 'profilesSampler' in options ? 'span' : 'continuous'; + + switch (this._legacyProfilerMode) { + case 'span': { + this._setupAutomaticSpanProfiling(); + break; + } + case 'continuous': { + // Continous mode requires manual calls to profiler.start() and profiler.stop() + break; + } + default: { + DEBUG_BUILD && + logger.warn( + `[Profiling] Unknown profiler mode: ${this._legacyProfilerMode}, profiler was not initialized`, + ); + break; + } + } + break; + } + + case 'current': { + switch (this._profileLifecycle) { + case 'trace': { + this._startTraceLifecycleProfiling(); + break; + } + case 'manual': { + // Manual mode requires manual calls to profiler.startProfileSession() and profiler.stopProfileSession() + break; + } + default: { + DEBUG_BUILD && + logger.warn(`[Profiling] Unknown profiler mode: ${this._profileLifecycle}, profiler was not initialized`); + break; + } + } + break; + } + default: { + DEBUG_BUILD && logger.warn(`[Profiling] Unknown profiler mode: ${this._mode}, profiler was not initialized`); + break; + } + } // Attaches a listener to beforeSend which will add the threadId data to the event being sent. // This adds a constant overhead to all events being sent which could be improved to only attach @@ -190,12 +135,22 @@ class ContinuousProfiler { return; } + if (this._mode !== 'legacy') { + DEBUG_BUILD && logger.log('[Profiling] Continuous profiling is not supported in the current mode.'); + return; + } + + if (this._legacyProfilerMode === 'span') { + DEBUG_BUILD && logger.log('[Profiling] Calls to profiler.start() are not supported in span profiling mode.'); + return; + } + // Flush any existing chunks before starting a new one. - this._chunkStop(); + this._stopChunkProfiling(); // Restart the profiler session this._setupSpanChunkInstrumentation(); - this._chunkStart(); + this._restartChunkProfiling(); } /** @@ -207,14 +162,229 @@ class ContinuousProfiler { DEBUG_BUILD && logger.log('[Profiling] Failed to stop, sentry client was never attached to the profiler.'); return; } - this._chunkStop(); + + if (this._mode !== 'legacy') { + DEBUG_BUILD && logger.log('[Profiling] Continuous profiling is not supported in the current mode.'); + return; + } + + if (this._legacyProfilerMode === 'span') { + DEBUG_BUILD && logger.log('[Profiling] Calls to profiler.stop() are not supported in span profiling mode.'); + return; + } + + this._stopChunkProfiling(); this._teardownSpanChunkInstrumentation(); } + public startProfileSession(): void { + if (this._mode !== 'current') { + DEBUG_BUILD && logger.log('[Profiling] Continuous profiling is not supported in the current mode.'); + return; + } + + if (this._chunkData !== undefined) { + DEBUG_BUILD && logger.log('[Profiling] Profile session already running, no-op.'); + return; + } + + if (this._mode === 'current') { + if (!this._sampled) { + DEBUG_BUILD && logger.log('[Profiling] Profile session not sampled, no-op.'); + return; + } + } + + if (this._profileLifecycle === 'trace') { + DEBUG_BUILD && + logger.log( + '[Profiling] You are using the trace profile lifecycle, manual calls to profiler.startProfileSession() and profiler.stopProfileSession() will be ignored.', + ); + return; + } + + this._startChunkProfiling(); + } + + public stopProfileSession(): void { + if (this._mode !== 'current') { + DEBUG_BUILD && logger.log('[Profiling] Continuous profiling is not supported in the current mode.'); + return; + } + + if (this._profileLifecycle === 'trace') { + DEBUG_BUILD && + logger.log( + '[Profiling] You are using the trace profile lifecycle, manual calls to profiler.startProfileSession() and profiler.stopProfileSession() will be ignored.', + ); + return; + } + + if (!this._chunkData) { + DEBUG_BUILD && logger.log('[Profiling] No profile session running, no-op.'); + return; + } + + this._stopChunkProfiling(); + } + + /** + * Starts trace lifecycle profiling. Profiling will remain active as long as there is an active span. + */ + private _startTraceLifecycleProfiling(): void { + if (!this._client) { + DEBUG_BUILD && + logger.log( + '[Profiling] Failed to start trace lifecycle profiling, sentry client was never attached to the profiler.', + ); + return; + } + + let activeSpanCounter = 0; + this._client.on('spanStart', _span => { + if (activeSpanCounter === 0) { + this._startChunkProfiling(); + } + activeSpanCounter++; + }); + + this._client.on('spanEnd', _span => { + if (activeSpanCounter === 1) { + this._stopChunkProfiling(); + } + activeSpanCounter--; + }); + } + + private _setupAutomaticSpanProfiling(): void { + if (!this._client) { + DEBUG_BUILD && + logger.log( + '[Profiling] Failed to setup automatic span profiling, sentry client was never attached to the profiler.', + ); + return; + } + + const spanToProfileIdMap = new WeakMap(); + + this._client.on('spanStart', span => { + if (span !== getRootSpan(span)) { + return; + } + + const profile_id = maybeProfileSpan(this._client, span); + + if (profile_id) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const options = this._client!.getOptions(); + // Not intended for external use, hence missing types, but we want to profile a couple of things at Sentry that + // currently exceed the default timeout set by the SDKs. + const maxProfileDurationMs = options._experiments?.maxProfileDurationMs || MAX_PROFILE_DURATION_MS; + + if (PROFILE_TIMEOUTS[profile_id]) { + global.clearTimeout(PROFILE_TIMEOUTS[profile_id]); + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete PROFILE_TIMEOUTS[profile_id]; + } + + // Enqueue a timeout to prevent profiles from running over max duration. + const timeout = global.setTimeout(() => { + DEBUG_BUILD && + logger.log( + '[Profiling] max profile duration elapsed, stopping profiling for:', + spanToJSON(span).description, + ); + + const profile = stopSpanProfile(span, profile_id); + if (profile) { + addToProfileQueue(profile_id, profile); + } + }, maxProfileDurationMs); + + // Unref timeout so it doesn't keep the process alive. + timeout.unref(); + + getIsolationScope().setContext('profile', { profile_id }); + spanToProfileIdMap.set(span, profile_id); + } + }); + + this._client.on('spanEnd', span => { + const profile_id = spanToProfileIdMap.get(span); + + if (profile_id) { + if (PROFILE_TIMEOUTS[profile_id]) { + global.clearTimeout(PROFILE_TIMEOUTS[profile_id]); + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete PROFILE_TIMEOUTS[profile_id]; + } + const profile = stopSpanProfile(span, profile_id); + + if (profile) { + addToProfileQueue(profile_id, profile); + } + } + }); + + this._client.on('beforeEnvelope', (envelope): void => { + // if not profiles are in queue, there is nothing to add to the envelope. + if (!PROFILE_MAP.size) { + return; + } + + const profiledTransactionEvents = findProfiledTransactionsFromEnvelope(envelope); + if (!profiledTransactionEvents.length) { + return; + } + + const profilesToAddToEnvelope: Profile[] = []; + + for (const profiledTransaction of profiledTransactionEvents) { + const profileContext = profiledTransaction.contexts?.profile; + const profile_id = profileContext?.profile_id; + + if (!profile_id) { + throw new TypeError('[Profiling] cannot find profile for a transaction without a profile context'); + } + + // Remove the profile from the transaction context before sending, relay will take care of the rest. + if (profileContext) { + delete profiledTransaction.contexts?.profile; + } + + const cpuProfile = takeFromProfileQueue(profile_id); + if (!cpuProfile) { + DEBUG_BUILD && logger.log(`[Profiling] Could not retrieve profile for transaction: ${profile_id}`); + continue; + } + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const profile = createProfilingEvent(this._client!, cpuProfile, profiledTransaction); + if (!profile) return; + + profilesToAddToEnvelope.push(profile); + + // @ts-expect-error profile does not inherit from Event + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this._client!.emit('preprocessEvent', profile, { + event_id: profiledTransaction.event_id, + }); + + // @ts-expect-error profile does not inherit from Event + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this._client!.emit('postprocessEvent', profile, { + event_id: profiledTransaction.event_id, + }); + } + + addProfilesToEnvelope(envelope, profilesToAddToEnvelope); + }); + } + /** * Stop profiler and initializes profiling of the next chunk */ - private _chunkStart(): void { + private _restartChunkProfiling(): void { if (!this._client) { // The client is not attached to the profiler if the user has not enabled continuous profiling. // In this case, calling start() and stop() is a noop action.The reason this exists is because @@ -222,12 +392,13 @@ class ContinuousProfiler { DEBUG_BUILD && logger.log('[Profiling] Profiler was never attached to the client.'); return; } + if (this._chunkData) { DEBUG_BUILD && logger.log( `[Profiling] Chunk with chunk_id ${this._chunkData.id} is still running, current chunk will be stopped a new chunk will be started.`, ); - this._chunkStop(); + this._stopChunkProfiling(); } this._startChunkProfiling(); @@ -236,32 +407,42 @@ class ContinuousProfiler { /** * Stops profiling of the current chunks and flushes the profile to Sentry */ - private _chunkStop(): void { + private _stopChunkProfiling(): void { + if (!this._chunkData) { + DEBUG_BUILD && logger.log('[Profiling] No chunk data found, no-op.'); + return; + } + if (this._chunkData?.timer) { global.clearTimeout(this._chunkData.timer); this._chunkData.timer = undefined; DEBUG_BUILD && logger.log(`[Profiling] Stopping profiling chunk: ${this._chunkData.id}`); } + if (!this._client) { DEBUG_BUILD && logger.log('[Profiling] Failed to collect profile, sentry client was never attached to the profiler.'); + this._resetChunkData(); return; } if (!this._chunkData?.id) { DEBUG_BUILD && logger.log(`[Profiling] Failed to collect profile for: ${this._chunkData?.id}, the chunk_id is missing.`); + this._resetChunkData(); return; } const profile = CpuProfilerBindings.stopProfiling(this._chunkData.id, ProfileFormat.CHUNK); if (!profile) { - DEBUG_BUILD && logger.log(`[Profiling] _chunkiledStartTraceID to collect profile for: ${this._chunkData.id}`); + DEBUG_BUILD && logger.log(`[Profiling] Failed to collect profile for: ${this._chunkData.id}`); + this._resetChunkData(); return; } if (!this._profilerId) { DEBUG_BUILD && logger.log('[Profiling] Profile chunk does not contain a valid profiler_id, this is a bug in the SDK'); + this._resetChunkData(); return; } if (profile) { @@ -327,6 +508,11 @@ class ContinuousProfiler { * @param chunk */ private _startChunkProfiling(): void { + if (this._chunkData) { + DEBUG_BUILD && logger.log('[Profiling] Chunk is already running, no-op.'); + return; + } + const traceId = getCurrentScope().getPropagationContext().traceId || getIsolationScope().getPropagationContext().traceId; const chunk = this._initializeChunk(traceId); @@ -336,9 +522,9 @@ class ContinuousProfiler { chunk.timer = global.setTimeout(() => { DEBUG_BUILD && logger.log(`[Profiling] Stopping profiling chunk: ${chunk.id}`); - this._chunkStop(); + this._stopChunkProfiling(); DEBUG_BUILD && logger.log('[Profiling] Starting new profiling chunk.'); - setImmediate(this._chunkStart.bind(this)); + setImmediate(this._restartChunkProfiling.bind(this)); }, CHUNK_INTERVAL_MS); // Unref timeout so it doesn't keep the process alive. @@ -444,37 +630,7 @@ export const _nodeProfilingIntegration = ((): ProfilingIntegration = _profiler: new ContinuousProfiler(), setup(client: NodeClient) { DEBUG_BUILD && logger.log('[Profiling] Profiling integration setup.'); - const options = client.getOptions(); - const profilingAPIVersion = getProfilingMode(options); - - if (profilingAPIVersion === 'legacy') { - const mode = 'profilesSampleRate' in options || 'profilesSampler' in options ? 'span' : 'continuous'; - - switch (mode) { - case 'continuous': { - DEBUG_BUILD && logger.log('[Profiling] Continuous profiler mode enabled.'); - this._profiler.initialize(client); - return; - } - // Default to span profiling when no mode profiler mode is set - case 'span': - case undefined: { - DEBUG_BUILD && logger.log('[Profiling] Span profiler mode enabled.'); - setupAutomatedSpanProfiling(client); - return; - } - default: { - DEBUG_BUILD && logger.warn(`[Profiling] Unknown profiler mode: ${mode}, profiler was not initialized`); - } - } - } else if (profilingAPIVersion === 'current') { - DEBUG_BUILD && logger.log('[Profiling] Continuous profiler mode enabled.'); - this._profiler.initialize(client); - return; - } - - DEBUG_BUILD && - logger.log(['[Profiling] Profiling integration is added, but not enabled due to lack of SDK.init options.']); + this._profiler.initialize(client); return; }, }; @@ -485,7 +641,8 @@ export const _nodeProfilingIntegration = ((): ProfilingIntegration = * @param options * @returns 'legacy' if the options are using the legacy profiling API, 'current' if the options are using the current profiling API */ -function getProfilingMode(options: NodeOptions): 'legacy' | 'current' | null { +function getProfilingMode(options: NodeOptions): 'legacy' | 'current' { + // Legacy mode takes precedence over current mode if ('profilesSampleRate' in options || 'profilesSampler' in options) { return 'legacy'; } diff --git a/packages/profiling-node/src/spanProfileUtils.ts b/packages/profiling-node/src/spanProfileUtils.ts index 9ff20816895c..4bd28f8e9531 100644 --- a/packages/profiling-node/src/spanProfileUtils.ts +++ b/packages/profiling-node/src/spanProfileUtils.ts @@ -1,3 +1,4 @@ +/* eslint-disable deprecation/deprecation */ import { CpuProfilerBindings, type RawThreadCpuProfile } from '@sentry-internal/node-cpu-profiler'; import type { CustomSamplingContext, Span } from '@sentry/core'; import { logger, spanIsSampled, spanToJSON, uuid4 } from '@sentry/core'; diff --git a/packages/profiling-node/test/integration.test.ts b/packages/profiling-node/test/integration.test.ts index d7df77ccda20..9f534083b27b 100644 --- a/packages/profiling-node/test/integration.test.ts +++ b/packages/profiling-node/test/integration.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable deprecation/deprecation */ import * as Sentry from '@sentry/node'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; @@ -9,7 +10,7 @@ import type { ProfileChunk, Transport } from '@sentry/core'; import type { NodeClientOptions } from '@sentry/node/build/types/types'; import { _nodeProfilingIntegration } from '../src/integration'; -function makeClientWithHooks(): [Sentry.NodeClient, Transport] { +function makeLegacySpanProfilingClient(): [Sentry.NodeClient, Transport] { const integration = _nodeProfilingIntegration(); const client = new Sentry.NodeClient({ stackParser: Sentry.defaultStackParser, @@ -31,7 +32,7 @@ function makeClientWithHooks(): [Sentry.NodeClient, Transport] { return [client, client.getTransport() as Transport]; } -function makeContinuousProfilingClient(): [Sentry.NodeClient, Transport] { +function makeLegacyContinuousProfilingClient(): [Sentry.NodeClient, Transport] { const integration = _nodeProfilingIntegration(); const client = new Sentry.NodeClient({ stackParser: Sentry.defaultStackParser, @@ -52,6 +53,28 @@ function makeContinuousProfilingClient(): [Sentry.NodeClient, Transport] { return [client, client.getTransport() as Transport]; } +function makeCurrentSpanProfilingClient(options: Partial = {}): [Sentry.NodeClient, Transport] { + const integration = _nodeProfilingIntegration(); + const client = new Sentry.NodeClient({ + stackParser: Sentry.defaultStackParser, + tracesSampleRate: 1, + debug: true, + environment: 'test-environment', + dsn: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', + integrations: [integration], + transport: _opts => + Sentry.makeNodeTransport({ + url: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', + recordDroppedEvent: () => { + return undefined; + }, + }), + ...options, + }); + + return [client, client.getTransport() as Transport]; +} + function getProfilerId(): string { return ( Sentry.getClient()?.getIntegrationByName>('ProfilingIntegration') as any @@ -79,7 +102,7 @@ function makeClientOptions( const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); describe('ProfilingIntegration', () => { - describe('automated span instrumentation', () => { + describe('legacy automated span instrumentation', () => { beforeEach(() => { vi.useRealTimers(); // We will mock the carrier as if it has been initialized by the SDK, else everything is short circuited @@ -93,7 +116,7 @@ describe('ProfilingIntegration', () => { }); it('pulls environment from sdk init', async () => { - const [client, transport] = makeClientWithHooks(); + const [client, transport] = makeLegacySpanProfilingClient(); Sentry.setCurrentClient(client); client.init(); @@ -110,7 +133,7 @@ describe('ProfilingIntegration', () => { it('logger warns user if there are insufficient samples and discards the profile', async () => { const logSpy = vi.spyOn(logger, 'log'); - const [client, transport] = makeClientWithHooks(); + const [client, transport] = makeLegacySpanProfilingClient(); Sentry.setCurrentClient(client); client.init(); @@ -149,7 +172,7 @@ describe('ProfilingIntegration', () => { it('logger warns user if traceId is invalid', async () => { const logSpy = vi.spyOn(logger, 'log'); - const [client, transport] = makeClientWithHooks(); + const [client, transport] = makeLegacySpanProfilingClient(); Sentry.setCurrentClient(client); client.init(); @@ -191,179 +214,99 @@ describe('ProfilingIntegration', () => { expect(logSpy).toHaveBeenCalledWith('[Profiling] Invalid traceId: ' + 'boop' + ' on profiled event'); }); - describe('with hooks', () => { - it('calls profiler when transaction is started/stopped', async () => { - const [client, transport] = makeClientWithHooks(); - Sentry.setCurrentClient(client); - client.init(); + it('calls profiler when transaction is started/stopped', async () => { + const [client, transport] = makeLegacySpanProfilingClient(); + Sentry.setCurrentClient(client); + client.init(); - const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); - const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); - vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); + vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); - const transaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); - await wait(500); - transaction.end(); + const transaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); + await wait(500); + transaction.end(); - await Sentry.flush(1000); + await Sentry.flush(1000); - expect(startProfilingSpy).toHaveBeenCalledTimes(1); - expect((stopProfilingSpy.mock.calls[stopProfilingSpy.mock.calls.length - 1]?.[0] as string).length).toBe(32); - }); + expect(startProfilingSpy).toHaveBeenCalledTimes(1); + expect((stopProfilingSpy.mock.calls[stopProfilingSpy.mock.calls.length - 1]?.[0] as string).length).toBe(32); + }); - it('sends profile in the same envelope as transaction', async () => { - const [client, transport] = makeClientWithHooks(); - Sentry.setCurrentClient(client); - client.init(); + it('sends profile in the same envelope as transaction', async () => { + const [client, transport] = makeLegacySpanProfilingClient(); + Sentry.setCurrentClient(client); + client.init(); - const transportSpy = vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); + const transportSpy = vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); - const transaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); - await wait(500); - transaction.end(); + const transaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); + await wait(500); + transaction.end(); - await Sentry.flush(1000); + await Sentry.flush(1000); - // One for profile, the other for transaction - expect(transportSpy).toHaveBeenCalledTimes(1); - expect(transportSpy.mock.calls?.[0]?.[0]?.[1]?.[1]?.[0]).toMatchObject({ type: 'profile' }); - }); + // One for profile, the other for transaction + expect(transportSpy).toHaveBeenCalledTimes(1); + expect(transportSpy.mock.calls?.[0]?.[0]?.[1]?.[1]?.[0]).toMatchObject({ type: 'profile' }); + }); - it('does not crash if transaction has no profile context or it is invalid', async () => { - const [client] = makeClientWithHooks(); - Sentry.setCurrentClient(client); - client.init(); + it('does not crash if transaction has no profile context or it is invalid', async () => { + const [client] = makeLegacySpanProfilingClient(); + Sentry.setCurrentClient(client); + client.init(); + // @ts-expect-error transaction is partial + client.emit('beforeEnvelope', createEnvelope({ type: 'transaction' }, { type: 'transaction' })); + // @ts-expect-error transaction is partial + client.emit('beforeEnvelope', createEnvelope({ type: 'transaction' }, { type: 'transaction', contexts: {} })); + client.emit( + 'beforeEnvelope', // @ts-expect-error transaction is partial - client.emit('beforeEnvelope', createEnvelope({ type: 'transaction' }, { type: 'transaction' })); + createEnvelope({ type: 'transaction' }, { type: 'transaction', contexts: { profile: {} } }), + ); + client.emit( + 'beforeEnvelope', // @ts-expect-error transaction is partial - client.emit('beforeEnvelope', createEnvelope({ type: 'transaction' }, { type: 'transaction', contexts: {} })); - client.emit( - 'beforeEnvelope', - // @ts-expect-error transaction is partial - createEnvelope({ type: 'transaction' }, { type: 'transaction', contexts: { profile: {} } }), - ); - client.emit( - 'beforeEnvelope', - // @ts-expect-error transaction is partial - createEnvelope({ type: 'transaction' }, { type: 'transaction', contexts: { profile: { profile_id: null } } }), - ); - - // Emit is sync, so we can just assert that we got here - expect(true).toBe(true); - }); - - it('if transaction was profiled, but profiler returned null', async () => { - const [client, transport] = makeClientWithHooks(); - Sentry.setCurrentClient(client); - client.init(); - - vi.spyOn(CpuProfilerBindings, 'stopProfiling').mockReturnValue(null); - // Emit is sync, so we can just assert that we got here - const transportSpy = vi.spyOn(transport, 'send').mockImplementation(() => { - // Do nothing so we don't send events to Sentry - return Promise.resolve({}); - }); - - const transaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); - await wait(500); - transaction.end(); - - await Sentry.flush(1000); - - // Only transaction is sent - expect(transportSpy.mock.calls?.[0]?.[0]?.[1]?.[0]?.[0]).toMatchObject({ type: 'transaction' }); - expect(transportSpy.mock.calls?.[0]?.[0]?.[1][1]).toBeUndefined(); - }); - - it('emits preprocessEvent for profile', async () => { - const [client] = makeClientWithHooks(); - Sentry.setCurrentClient(client); - client.init(); - - const onPreprocessEvent = vi.fn(); - - client.on('preprocessEvent', onPreprocessEvent); + createEnvelope({ type: 'transaction' }, { type: 'transaction', contexts: { profile: { profile_id: null } } }), + ); - const transaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); - await wait(500); - transaction.end(); + // Emit is sync, so we can just assert that we got here + expect(true).toBe(true); + }); - await Sentry.flush(1000); + it('if transaction was profiled, but profiler returned null', async () => { + const [client, transport] = makeLegacySpanProfilingClient(); + Sentry.setCurrentClient(client); + client.init(); - expect(onPreprocessEvent.mock.calls[1]?.[0]).toMatchObject({ - profile: { - samples: expect.arrayContaining([expect.anything()]), - stacks: expect.arrayContaining([expect.anything()]), - }, - }); + vi.spyOn(CpuProfilerBindings, 'stopProfiling').mockReturnValue(null); + // Emit is sync, so we can just assert that we got here + const transportSpy = vi.spyOn(transport, 'send').mockImplementation(() => { + // Do nothing so we don't send events to Sentry + return Promise.resolve({}); }); - it('automated span instrumentation does not support continuous profiling', () => { - const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + const transaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); + await wait(500); + transaction.end(); - const [client] = makeClientWithHooks(); - Sentry.setCurrentClient(client); - client.init(); + await Sentry.flush(1000); - const integration = - client.getIntegrationByName>('ProfilingIntegration'); - if (!integration) { - throw new Error('Profiling integration not found'); - } - integration._profiler.start(); - expect(startProfilingSpy).not.toHaveBeenCalled(); - }); + // Only transaction is sent + expect(transportSpy.mock.calls?.[0]?.[0]?.[1]?.[0]?.[0]).toMatchObject({ type: 'transaction' }); + expect(transportSpy.mock.calls?.[0]?.[0]?.[1][1]).toBeUndefined(); }); - it('does not crash if stop is called multiple times', async () => { - const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); - - const [client] = makeClientWithHooks(); + it('emits preprocessEvent for profile', async () => { + const [client] = makeLegacySpanProfilingClient(); Sentry.setCurrentClient(client); client.init(); - const transaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'txn' }); - transaction.end(); - transaction.end(); - expect(stopProfilingSpy).toHaveBeenCalledTimes(1); - }); - it('enriches profile with debug_id', async () => { - GLOBAL_OBJ._sentryDebugIds = { - 'Error\n at filename.js (filename.js:36:15)': 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa', - 'Error\n at filename2.js (filename2.js:36:15)': 'bbbbbbbb-bbbb-4bbb-bbbb-bbbbbbbbbb', - 'Error\n at filename3.js (filename3.js:36:15)': 'bbbbbbbb-bbbb-4bbb-bbbb-bbbbbbbbbb', - }; - - // @ts-expect-error we just mock the return type and ignore the signature - vi.spyOn(CpuProfilerBindings, 'stopProfiling').mockImplementation(() => { - return { - samples: [ - { - stack_id: 0, - thread_id: '0', - elapsed_since_start_ns: '10', - }, - { - stack_id: 0, - thread_id: '0', - elapsed_since_start_ns: '10', - }, - ], - measurements: {}, - resources: ['filename.js', 'filename2.js'], - stacks: [[0]], - frames: [], - profiler_logging_mode: 'lazy', - }; - }); - - const [client, transport] = makeClientWithHooks(); - Sentry.setCurrentClient(client); - client.init(); + const onPreprocessEvent = vi.fn(); - const transportSpy = vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); + client.on('preprocessEvent', onPreprocessEvent); const transaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); await wait(500); @@ -371,54 +314,104 @@ describe('ProfilingIntegration', () => { await Sentry.flush(1000); - expect(transportSpy.mock.calls?.[0]?.[0]?.[1]?.[1]?.[1]).toMatchObject({ - debug_meta: { - images: [ - { - type: 'sourcemap', - debug_id: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa', - code_file: 'filename.js', - }, - { - type: 'sourcemap', - debug_id: 'bbbbbbbb-bbbb-4bbb-bbbb-bbbbbbbbbb', - code_file: 'filename2.js', - }, - ], + expect(onPreprocessEvent.mock.calls[1]?.[0]).toMatchObject({ + profile: { + samples: expect.arrayContaining([expect.anything()]), + stacks: expect.arrayContaining([expect.anything()]), }, }); }); + + it('automated span instrumentation does not support continuous profiling', () => { + const [client] = makeLegacySpanProfilingClient(); + Sentry.setCurrentClient(client); + client.init(); + + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + const integration = client.getIntegrationByName>('ProfilingIntegration'); + if (!integration) { + throw new Error('Profiling integration not found'); + } + + Sentry.profiler.startProfiler(); + expect(startProfilingSpy).not.toHaveBeenCalled(); + }); }); - it('top level methods do not proxy to integration', () => { - const client = new Sentry.NodeClient({ - ...makeClientOptions({ profilesSampleRate: undefined }), - dsn: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', - tracesSampleRate: 1, - profilesSampleRate: 1, - transport: _opts => - Sentry.makeNodeTransport({ - url: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', - recordDroppedEvent: () => { - return undefined; + it('does not crash if stop is called multiple times', async () => { + const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); + + const [client] = makeLegacySpanProfilingClient(); + Sentry.setCurrentClient(client); + client.init(); + + const transaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'txn' }); + transaction.end(); + transaction.end(); + expect(stopProfilingSpy).toHaveBeenCalledTimes(1); + }); + + it('enriches profile with debug_id', async () => { + GLOBAL_OBJ._sentryDebugIds = { + 'Error\n at filename.js (filename.js:36:15)': 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa', + 'Error\n at filename2.js (filename2.js:36:15)': 'bbbbbbbb-bbbb-4bbb-bbbb-bbbbbbbbbb', + 'Error\n at filename3.js (filename3.js:36:15)': 'bbbbbbbb-bbbb-4bbb-bbbb-bbbbbbbbbb', + }; + + // @ts-expect-error we just mock the return type and ignore the signature + vi.spyOn(CpuProfilerBindings, 'stopProfiling').mockImplementation(() => { + return { + samples: [ + { + stack_id: 0, + thread_id: '0', + elapsed_since_start_ns: '10', + }, + { + stack_id: 0, + thread_id: '0', + elapsed_since_start_ns: '10', }, - }), - integrations: [_nodeProfilingIntegration()], + ], + measurements: {}, + resources: ['filename.js', 'filename2.js'], + stacks: [[0]], + frames: [], + profiler_logging_mode: 'lazy', + }; }); + const [client, transport] = makeLegacySpanProfilingClient(); Sentry.setCurrentClient(client); client.init(); - const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); - const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); + const transportSpy = vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); - Sentry.profiler.startProfiler(); - expect(startProfilingSpy).not.toHaveBeenCalled(); - Sentry.profiler.stopProfiler(); - expect(stopProfilingSpy).not.toHaveBeenCalled(); + const transaction = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); + await wait(500); + transaction.end(); + + await Sentry.flush(1000); + + expect(transportSpy.mock.calls?.[0]?.[0]?.[1]?.[1]?.[1]).toMatchObject({ + debug_meta: { + images: [ + { + type: 'sourcemap', + debug_id: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa', + code_file: 'filename.js', + }, + { + type: 'sourcemap', + debug_id: 'bbbbbbbb-bbbb-4bbb-bbbb-bbbbbbbbbb', + code_file: 'filename2.js', + }, + ], + }, + }); }); - describe('continuous profiling', () => { + describe('legacy continuous profiling', () => { beforeEach(() => { vi.useFakeTimers(); // We will mock the carrier as if it has been initialized by the SDK, else everything is short circuited @@ -463,7 +456,7 @@ describe('ProfilingIntegration', () => { }; }); - const [client, transport] = makeContinuousProfilingClient(); + const [client, transport] = makeLegacyContinuousProfilingClient(); Sentry.setCurrentClient(client); client.init(); @@ -481,7 +474,7 @@ describe('ProfilingIntegration', () => { it('initializes the continuous profiler', () => { const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); - const [client] = makeContinuousProfilingClient(); + const [client] = makeLegacyContinuousProfilingClient(); Sentry.setCurrentClient(client); client.init(); @@ -495,7 +488,7 @@ describe('ProfilingIntegration', () => { it('starts a continuous profile', () => { const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); - const [client] = makeContinuousProfilingClient(); + const [client] = makeLegacyContinuousProfilingClient(); Sentry.setCurrentClient(client); client.init(); @@ -508,7 +501,7 @@ describe('ProfilingIntegration', () => { const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); - const [client] = makeContinuousProfilingClient(); + const [client] = makeLegacyContinuousProfilingClient(); Sentry.setCurrentClient(client); client.init(); @@ -524,7 +517,7 @@ describe('ProfilingIntegration', () => { const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); - const [client] = makeContinuousProfilingClient(); + const [client] = makeLegacyContinuousProfilingClient(); Sentry.setCurrentClient(client); client.init(); @@ -540,7 +533,7 @@ describe('ProfilingIntegration', () => { const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); - const [client] = makeContinuousProfilingClient(); + const [client] = makeLegacyContinuousProfilingClient(); Sentry.setCurrentClient(client); client.init(); @@ -557,7 +550,7 @@ describe('ProfilingIntegration', () => { it('explicit calls to stop clear profilerId', async () => { const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); - const [client] = makeContinuousProfilingClient(); + const [client] = makeLegacyContinuousProfilingClient(); Sentry.setCurrentClient(client); client.init(); @@ -575,7 +568,7 @@ describe('ProfilingIntegration', () => { const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); - const [client] = makeContinuousProfilingClient(); + const [client] = makeLegacyContinuousProfilingClient(); Sentry.setCurrentClient(client); client.init(); @@ -590,7 +583,7 @@ describe('ProfilingIntegration', () => { const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); - const [client] = makeContinuousProfilingClient(); + const [client] = makeLegacyContinuousProfilingClient(); Sentry.setCurrentClient(client); client.init(); @@ -609,7 +602,7 @@ describe('ProfilingIntegration', () => { it('continuous mode does not instrument spans', () => { const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); - const [client] = makeContinuousProfilingClient(); + const [client] = makeLegacyContinuousProfilingClient(); Sentry.setCurrentClient(client); client.init(); @@ -641,7 +634,7 @@ describe('ProfilingIntegration', () => { }; }); - const [client, transport] = makeContinuousProfilingClient(); + const [client, transport] = makeLegacyContinuousProfilingClient(); Sentry.setCurrentClient(client); client.init(); @@ -655,7 +648,7 @@ describe('ProfilingIntegration', () => { }); it('sets global profile context', async () => { - const [client, transport] = makeContinuousProfilingClient(); + const [client, transport] = makeLegacyContinuousProfilingClient(); Sentry.setCurrentClient(client); client.init(); @@ -694,14 +687,8 @@ describe('ProfilingIntegration', () => { }, }); }); - }); - describe('continuous profiling does not start in span profiling mode', () => { - it.each([ - ['profilesSampleRate=1', makeClientOptions({ profilesSampleRate: 1 })], - ['profilesSampler is defined', makeClientOptions({ profilesSampler: () => 1 })], - ])('%s', async (_label, options) => { - const logSpy = vi.spyOn(logger, 'log'); + it.each([['no option is set', makeClientOptions({})]])('%s', async (_label, options) => { const client = new Sentry.NodeClient({ ...options, dsn: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', @@ -727,85 +714,279 @@ describe('ProfilingIntegration', () => { } vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); + Sentry.profiler.startProfiler(); + const callCount = startProfilingSpy.mock.calls.length; + expect(startProfilingSpy).toHaveBeenCalled(); + Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); + expect(startProfilingSpy).toHaveBeenCalledTimes(callCount); + }); + }); +}); - expect(startProfilingSpy).toHaveBeenCalled(); - const integration = client.getIntegrationByName>('ProfilingIntegration'); +describe('current manual continuous profiling', () => { + it('start and stops a profile session', () => { + const [client] = makeCurrentSpanProfilingClient({ + profileLifecycle: 'manual', + profileSessionSampleRate: 1, + }); + Sentry.setCurrentClient(client); + client.init(); - if (!integration) { - throw new Error('Profiling integration not found'); - } + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); - integration._profiler.start(); - expect(logSpy).toHaveBeenLastCalledWith( - '[Profiling] Failed to start, sentry client was never attached to the profiler.', - ); + Sentry.profiler.startProfileSession(); + Sentry.profiler.stopProfileSession(); + + expect(startProfilingSpy).toHaveBeenCalled(); + expect(stopProfilingSpy).toHaveBeenCalled(); + }); + + it('calling start and stop while profile session is running does nothing', () => { + const [client] = makeCurrentSpanProfilingClient({ + profileLifecycle: 'manual', + profileSessionSampleRate: 1, }); + Sentry.setCurrentClient(client); + client.init(); + + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); + + Sentry.profiler.startProfileSession(); + Sentry.profiler.startProfileSession(); + + expect(startProfilingSpy).toHaveBeenCalledTimes(1); + + Sentry.profiler.stopProfileSession(); + Sentry.profiler.stopProfileSession(); + + expect(stopProfilingSpy).toHaveBeenCalledTimes(1); }); - describe('continuous profiling mode', () => { - beforeEach(() => { - vi.clearAllMocks(); + + it('profileSessionSamplingRate is respected', () => { + const [client] = makeCurrentSpanProfilingClient({ + profileSessionSampleRate: 0, + profileLifecycle: 'manual', }); + Sentry.setCurrentClient(client); + client.init(); - it.each([['no option is set', makeClientOptions({})]])('%s', async (_label, options) => { - const client = new Sentry.NodeClient({ - ...options, - dsn: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', - tracesSampleRate: 1, - transport: _opts => - Sentry.makeNodeTransport({ - url: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', - recordDroppedEvent: () => { - return undefined; - }, - }), - integrations: [_nodeProfilingIntegration()], - }); + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); - Sentry.setCurrentClient(client); - client.init(); + Sentry.profiler.startProfileSession(); + Sentry.profiler.stopProfileSession(); - const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); - const transport = client.getTransport(); + expect(startProfilingSpy).not.toHaveBeenCalled(); + expect(stopProfilingSpy).not.toHaveBeenCalled(); + }); +}); - if (!transport) { - throw new Error('Transport not found'); - } +describe('trace profile lifecycle', () => { + it('trace profile lifecycle ignores manual calls to start and stop', () => { + const [client] = makeCurrentSpanProfilingClient({ + profileLifecycle: 'trace', + }); - vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); - Sentry.profiler.startProfiler(); - const callCount = startProfilingSpy.mock.calls.length; - expect(startProfilingSpy).toHaveBeenCalled(); + Sentry.setCurrentClient(client); + client.init(); - Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); - expect(startProfilingSpy).toHaveBeenCalledTimes(callCount); + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); + + Sentry.profiler.startProfileSession(); + Sentry.profiler.stopProfileSession(); + + expect(startProfilingSpy).not.toHaveBeenCalled(); + expect(stopProfilingSpy).not.toHaveBeenCalled(); + }); + + it('starts profiler when first span is created', () => { + const [client] = makeCurrentSpanProfilingClient({ + profileLifecycle: 'trace', }); - it('top level methods proxy to integration', () => { - const client = new Sentry.NodeClient({ - ...makeClientOptions({}), - dsn: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', - tracesSampleRate: 1, - transport: _opts => - Sentry.makeNodeTransport({ - url: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', - recordDroppedEvent: () => { - return undefined; - }, - }), - integrations: [_nodeProfilingIntegration()], + Sentry.setCurrentClient(client); + client.init(); + + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); + + const span = Sentry.startInactiveSpan({ forceTransaction: true, name: 'test' }); + + expect(startProfilingSpy).toHaveBeenCalled(); + expect(stopProfilingSpy).not.toHaveBeenCalled(); + + span.end(); + expect(stopProfilingSpy).toHaveBeenCalled(); + }); + + it('waits for the tail span to end before stopping the profiler', () => { + const [client] = makeCurrentSpanProfilingClient({ + profileLifecycle: 'trace', + }); + + Sentry.setCurrentClient(client); + client.init(); + + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); + + const first = Sentry.startInactiveSpan({ forceTransaction: true, name: 'test' }); + const second = Sentry.startInactiveSpan({ forceTransaction: true, name: 'child' }); + + expect(startProfilingSpy).toHaveBeenCalled(); + expect(stopProfilingSpy).not.toHaveBeenCalled(); + + first.end(); + expect(stopProfilingSpy).not.toHaveBeenCalled(); + + second.end(); + expect(stopProfilingSpy).toHaveBeenCalled(); + }); + + it('ending last span does not stop the profiler if first span is not ended', () => { + const [client] = makeCurrentSpanProfilingClient({ + profileLifecycle: 'trace', + }); + + Sentry.setCurrentClient(client); + client.init(); + + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); + + const first = Sentry.startInactiveSpan({ forceTransaction: true, name: 'test' }); + const second = Sentry.startInactiveSpan({ forceTransaction: true, name: 'child' }); + + expect(startProfilingSpy).toHaveBeenCalled(); + + second.end(); + expect(stopProfilingSpy).not.toHaveBeenCalled(); + + first.end(); + expect(stopProfilingSpy).toHaveBeenCalled(); + }); + it('multiple calls to span.end do not restart the profiler', () => { + const [client] = makeCurrentSpanProfilingClient({ + profileLifecycle: 'trace', + }); + + Sentry.setCurrentClient(client); + client.init(); + + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); + + const first = Sentry.startInactiveSpan({ forceTransaction: true, name: 'test' }); + const second = Sentry.startInactiveSpan({ forceTransaction: true, name: 'child' }); + + expect(startProfilingSpy).toHaveBeenCalled(); + + first.end(); + first.end(); + expect(stopProfilingSpy).not.toHaveBeenCalled(); + + second.end(); + expect(stopProfilingSpy).toHaveBeenCalled(); + }); +}); + +describe('Legacy vs Current API compat', () => { + describe('legacy', () => { + describe('span profiling', () => { + it('profiler.start, profiler.stop, profiler.startProfileSession, profiler.stopProfileSession void in automated span profiling mode', () => { + const [client] = makeLegacySpanProfilingClient(); + Sentry.setCurrentClient(client); + client.init(); + + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); + + // Profiler calls void + Sentry.profiler.startProfiler(); + Sentry.profiler.stopProfiler(); + + expect(startProfilingSpy).not.toHaveBeenCalled(); + expect(stopProfilingSpy).not.toHaveBeenCalled(); + + // This API is not supported in legacy mode + Sentry.profiler.startProfileSession(); + Sentry.profiler.stopProfileSession(); + + expect(startProfilingSpy).not.toHaveBeenCalled(); + expect(stopProfilingSpy).not.toHaveBeenCalled(); + + // Only starting and stopping the profiler is supported in legacy mode + const span = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); + span.end(); + + expect(startProfilingSpy).toHaveBeenCalled(); + expect(stopProfilingSpy).toHaveBeenCalled(); }); + }); - Sentry.setCurrentClient(client); - client.init(); + describe('continuous profiling', () => { + it('profiler.start and profiler.stop start and stop the profiler, calls to profiler.startProfileSession and profiler.stopProfileSession are ignored', () => { + const [client] = makeLegacyContinuousProfilingClient(); + Sentry.setCurrentClient(client); + client.init(); - const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); - const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); - Sentry.profiler.startProfiler(); - expect(startProfilingSpy).toHaveBeenCalledTimes(1); - Sentry.profiler.stopProfiler(); - expect(stopProfilingSpy).toHaveBeenCalledTimes(1); + // Creating a span will not invoke the profiler + const span = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); + span.end(); + + expect(startProfilingSpy).not.toHaveBeenCalled(); + expect(stopProfilingSpy).not.toHaveBeenCalled(); + + // This API is not supported in legacy mode + Sentry.profiler.startProfileSession(); + Sentry.profiler.stopProfileSession(); + + expect(startProfilingSpy).not.toHaveBeenCalled(); + expect(stopProfilingSpy).not.toHaveBeenCalled(); + + // Only the old signature is supported + Sentry.profiler.startProfiler(); + Sentry.profiler.stopProfiler(); + + expect(startProfilingSpy).toHaveBeenCalled(); + expect(stopProfilingSpy).toHaveBeenCalled(); + }); + }); + }); + + describe('current', () => { + describe('span profiling', () => { + it('profiler.start, profiler.stop, profiler.startProfileSession, profiler.stopProfileSession void in automated span profiling mode', () => { + const [client] = makeCurrentSpanProfilingClient({ + profileLifecycle: 'trace', + }); + Sentry.setCurrentClient(client); + client.init(); + + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); + + // Legacy mode is not supported under the new API + Sentry.profiler.startProfiler(); + Sentry.profiler.stopProfiler(); + + expect(startProfilingSpy).not.toHaveBeenCalled(); + expect(stopProfilingSpy).not.toHaveBeenCalled(); + + // This API is not supported in trace mode + Sentry.profiler.startProfileSession(); + Sentry.profiler.stopProfileSession(); + + expect(startProfilingSpy).not.toHaveBeenCalled(); + expect(stopProfilingSpy).not.toHaveBeenCalled(); + }); }); }); }); From 08e12ead4707a471c48f30865b2cc92bc5670c0d Mon Sep 17 00:00:00 2001 From: JonasBa Date: Thu, 13 Mar 2025 13:02:33 -0400 Subject: [PATCH 12/21] fix formatting --- packages/core/src/profiling.ts | 36 +++++++++++++++++----------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/core/src/profiling.ts b/packages/core/src/profiling.ts index 7330bf8b55f8..cc88c6842ee2 100644 --- a/packages/core/src/profiling.ts +++ b/packages/core/src/profiling.ts @@ -96,24 +96,24 @@ function startProfileSession(): void { * Stops the current profiler session. */ function stopProfileSession(): void { - const client = getClient(); - if (!client) { - DEBUG_BUILD && logger.warn('No Sentry client available, profiling is not started'); - return; - } - - const integration = client.getIntegrationByName>('ProfilingIntegration'); - if (!integration) { - DEBUG_BUILD && logger.warn('ProfilingIntegration is not available'); - return; - } - - if (!isProfilingIntegrationWithProfiler(integration)) { - DEBUG_BUILD && logger.warn('Profiler is not available on profiling integration.'); - return; - } - - integration._profiler.stopProfileSession(); + const client = getClient(); + if (!client) { + DEBUG_BUILD && logger.warn('No Sentry client available, profiling is not started'); + return; + } + + const integration = client.getIntegrationByName>('ProfilingIntegration'); + if (!integration) { + DEBUG_BUILD && logger.warn('ProfilingIntegration is not available'); + return; + } + + if (!isProfilingIntegrationWithProfiler(integration)) { + DEBUG_BUILD && logger.warn('Profiler is not available on profiling integration.'); + return; + } + + integration._profiler.stopProfileSession(); } export const profiler: Profiler = { From 43b8aec81e49aa16b5434fe7dfa2899bd9a92fc2 Mon Sep 17 00:00:00 2001 From: JonasBa Date: Thu, 13 Mar 2025 13:47:42 -0400 Subject: [PATCH 13/21] setup context --- packages/profiling-node/src/integration.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/profiling-node/src/integration.ts b/packages/profiling-node/src/integration.ts index 9afa098561be..65346815bdf0 100644 --- a/packages/profiling-node/src/integration.ts +++ b/packages/profiling-node/src/integration.ts @@ -96,6 +96,8 @@ class ContinuousProfiler { } case 'current': { + this._setupSpanChunkInstrumentation(); + switch (this._profileLifecycle) { case 'trace': { this._startTraceLifecycleProfiling(); From 6d1670476a6b44d420fe2e6e4834dc3a6f333555 Mon Sep 17 00:00:00 2001 From: JonasBa Date: Thu, 13 Mar 2025 14:33:56 -0400 Subject: [PATCH 14/21] add envelope sending test --- packages/profiling-node/src/integration.ts | 3 + .../profiling-node/test/integration.test.ts | 323 +++++++++++------- 2 files changed, 211 insertions(+), 115 deletions(-) diff --git a/packages/profiling-node/src/integration.ts b/packages/profiling-node/src/integration.ts index 65346815bdf0..c1b59ec213f9 100644 --- a/packages/profiling-node/src/integration.ts +++ b/packages/profiling-node/src/integration.ts @@ -148,6 +148,7 @@ class ContinuousProfiler { } // Flush any existing chunks before starting a new one. + this._stopChunkProfiling(); // Restart the profiler session @@ -427,6 +428,7 @@ class ContinuousProfiler { this._resetChunkData(); return; } + if (!this._chunkData?.id) { DEBUG_BUILD && logger.log(`[Profiling] Failed to collect profile for: ${this._chunkData?.id}, the chunk_id is missing.`); @@ -441,6 +443,7 @@ class ContinuousProfiler { this._resetChunkData(); return; } + if (!this._profilerId) { DEBUG_BUILD && logger.log('[Profiling] Profile chunk does not contain a valid profiler_id, this is a bug in the SDK'); diff --git a/packages/profiling-node/test/integration.test.ts b/packages/profiling-node/test/integration.test.ts index 9f534083b27b..8ff5ee09013e 100644 --- a/packages/profiling-node/test/integration.test.ts +++ b/packages/profiling-node/test/integration.test.ts @@ -722,175 +722,268 @@ describe('ProfilingIntegration', () => { expect(startProfilingSpy).toHaveBeenCalledTimes(callCount); }); }); -}); -describe('current manual continuous profiling', () => { - it('start and stops a profile session', () => { - const [client] = makeCurrentSpanProfilingClient({ - profileLifecycle: 'manual', - profileSessionSampleRate: 1, + describe('current manual continuous profiling', () => { + it('start and stops a profile session', () => { + const [client] = makeCurrentSpanProfilingClient({ + profileLifecycle: 'manual', + profileSessionSampleRate: 1, + }); + Sentry.setCurrentClient(client); + client.init(); + + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); + + Sentry.profiler.startProfileSession(); + Sentry.profiler.stopProfileSession(); + + expect(startProfilingSpy).toHaveBeenCalled(); + expect(stopProfilingSpy).toHaveBeenCalled(); }); - Sentry.setCurrentClient(client); - client.init(); - const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); - const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); + it('calling start and stop while profile session is running does nothing', () => { + const [client] = makeCurrentSpanProfilingClient({ + profileLifecycle: 'manual', + profileSessionSampleRate: 1, + }); + Sentry.setCurrentClient(client); + client.init(); - Sentry.profiler.startProfileSession(); - Sentry.profiler.stopProfileSession(); + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); - expect(startProfilingSpy).toHaveBeenCalled(); - expect(stopProfilingSpy).toHaveBeenCalled(); - }); + Sentry.profiler.startProfileSession(); + Sentry.profiler.startProfileSession(); + + expect(startProfilingSpy).toHaveBeenCalledTimes(1); - it('calling start and stop while profile session is running does nothing', () => { - const [client] = makeCurrentSpanProfilingClient({ - profileLifecycle: 'manual', - profileSessionSampleRate: 1, + Sentry.profiler.stopProfileSession(); + Sentry.profiler.stopProfileSession(); + + expect(stopProfilingSpy).toHaveBeenCalledTimes(1); }); - Sentry.setCurrentClient(client); - client.init(); - const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); - const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); + it('profileSessionSamplingRate is required', () => { + const [client] = makeCurrentSpanProfilingClient({ + profileLifecycle: 'manual', + }); + Sentry.setCurrentClient(client); + client.init(); + + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); + + Sentry.profiler.startProfileSession(); + Sentry.profiler.stopProfileSession(); - Sentry.profiler.startProfileSession(); - Sentry.profiler.startProfileSession(); + expect(startProfilingSpy).not.toHaveBeenCalled(); + expect(stopProfilingSpy).not.toHaveBeenCalled(); + }); - expect(startProfilingSpy).toHaveBeenCalledTimes(1); + it('profileSessionSamplingRate is respected', () => { + const [client] = makeCurrentSpanProfilingClient({ + profileSessionSampleRate: 0, + profileLifecycle: 'manual', + }); + Sentry.setCurrentClient(client); + client.init(); - Sentry.profiler.stopProfileSession(); - Sentry.profiler.stopProfileSession(); + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); - expect(stopProfilingSpy).toHaveBeenCalledTimes(1); - }); + Sentry.profiler.startProfileSession(); + Sentry.profiler.stopProfileSession(); - it('profileSessionSamplingRate is respected', () => { - const [client] = makeCurrentSpanProfilingClient({ - profileSessionSampleRate: 0, - profileLifecycle: 'manual', + expect(startProfilingSpy).not.toHaveBeenCalled(); + expect(stopProfilingSpy).not.toHaveBeenCalled(); }); - Sentry.setCurrentClient(client); - client.init(); - const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); - const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); + describe('envelope', () => { + beforeEach(() => { + vi.useRealTimers(); + }); - Sentry.profiler.startProfileSession(); - Sentry.profiler.stopProfileSession(); + it('sends a profile_chunk envelope type', async () => { + const [client, transport] = makeCurrentSpanProfilingClient({ + profileLifecycle: 'manual', + profileSessionSampleRate: 1, + }); - expect(startProfilingSpy).not.toHaveBeenCalled(); - expect(stopProfilingSpy).not.toHaveBeenCalled(); + Sentry.setCurrentClient(client); + client.init(); + + const transportSpy = vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); + + Sentry.profiler.startProfileSession(); + await wait(1000); + Sentry.profiler.stopProfileSession(); + + await Sentry.flush(1000); + + console.log(JSON.stringify(transportSpy.mock.calls[0], null, 2)); + expect(transportSpy.mock.calls?.[0]?.[0]?.[1]?.[0]?.[0]).toMatchObject({ + type: 'profile_chunk', + }); + + expect(transportSpy.mock.calls?.[0]?.[0]?.[1]?.[0]?.[1]).toMatchObject({ + profiler_id: expect.any(String), + chunk_id: expect.any(String), + profile: expect.objectContaining({ + stacks: expect.any(Array), + }), + }); + }); + }); }); -}); -describe('trace profile lifecycle', () => { - it('trace profile lifecycle ignores manual calls to start and stop', () => { - const [client] = makeCurrentSpanProfilingClient({ - profileLifecycle: 'trace', + describe('trace profile lifecycle', () => { + it('trace profile lifecycle ignores manual calls to start and stop', () => { + const [client] = makeCurrentSpanProfilingClient({ + profileLifecycle: 'trace', + }); + + Sentry.setCurrentClient(client); + client.init(); + + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); + + Sentry.profiler.startProfileSession(); + Sentry.profiler.stopProfileSession(); + + expect(startProfilingSpy).not.toHaveBeenCalled(); + expect(stopProfilingSpy).not.toHaveBeenCalled(); }); - Sentry.setCurrentClient(client); - client.init(); + it('starts profiler when first span is created', () => { + const [client] = makeCurrentSpanProfilingClient({ + profileLifecycle: 'trace', + }); - const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); - const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); + Sentry.setCurrentClient(client); + client.init(); - Sentry.profiler.startProfileSession(); - Sentry.profiler.stopProfileSession(); + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); - expect(startProfilingSpy).not.toHaveBeenCalled(); - expect(stopProfilingSpy).not.toHaveBeenCalled(); - }); + const span = Sentry.startInactiveSpan({ forceTransaction: true, name: 'test' }); - it('starts profiler when first span is created', () => { - const [client] = makeCurrentSpanProfilingClient({ - profileLifecycle: 'trace', + expect(startProfilingSpy).toHaveBeenCalled(); + expect(stopProfilingSpy).not.toHaveBeenCalled(); + + span.end(); + expect(stopProfilingSpy).toHaveBeenCalled(); }); - Sentry.setCurrentClient(client); - client.init(); + it('waits for the tail span to end before stopping the profiler', () => { + const [client] = makeCurrentSpanProfilingClient({ + profileLifecycle: 'trace', + }); - const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); - const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); + Sentry.setCurrentClient(client); + client.init(); + + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); - const span = Sentry.startInactiveSpan({ forceTransaction: true, name: 'test' }); + const first = Sentry.startInactiveSpan({ forceTransaction: true, name: 'test' }); + const second = Sentry.startInactiveSpan({ forceTransaction: true, name: 'child' }); - expect(startProfilingSpy).toHaveBeenCalled(); - expect(stopProfilingSpy).not.toHaveBeenCalled(); + expect(startProfilingSpy).toHaveBeenCalled(); + expect(stopProfilingSpy).not.toHaveBeenCalled(); - span.end(); - expect(stopProfilingSpy).toHaveBeenCalled(); - }); + first.end(); + expect(stopProfilingSpy).not.toHaveBeenCalled(); - it('waits for the tail span to end before stopping the profiler', () => { - const [client] = makeCurrentSpanProfilingClient({ - profileLifecycle: 'trace', + second.end(); + expect(stopProfilingSpy).toHaveBeenCalled(); }); - Sentry.setCurrentClient(client); - client.init(); + it('ending last span does not stop the profiler if first span is not ended', () => { + const [client] = makeCurrentSpanProfilingClient({ + profileLifecycle: 'trace', + }); - const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); - const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); + Sentry.setCurrentClient(client); + client.init(); - const first = Sentry.startInactiveSpan({ forceTransaction: true, name: 'test' }); - const second = Sentry.startInactiveSpan({ forceTransaction: true, name: 'child' }); + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); - expect(startProfilingSpy).toHaveBeenCalled(); - expect(stopProfilingSpy).not.toHaveBeenCalled(); + const first = Sentry.startInactiveSpan({ forceTransaction: true, name: 'test' }); + const second = Sentry.startInactiveSpan({ forceTransaction: true, name: 'child' }); - first.end(); - expect(stopProfilingSpy).not.toHaveBeenCalled(); + expect(startProfilingSpy).toHaveBeenCalled(); - second.end(); - expect(stopProfilingSpy).toHaveBeenCalled(); - }); + second.end(); + expect(stopProfilingSpy).not.toHaveBeenCalled(); - it('ending last span does not stop the profiler if first span is not ended', () => { - const [client] = makeCurrentSpanProfilingClient({ - profileLifecycle: 'trace', + first.end(); + expect(stopProfilingSpy).toHaveBeenCalled(); }); + it('multiple calls to span.end do not restart the profiler', () => { + const [client] = makeCurrentSpanProfilingClient({ + profileLifecycle: 'trace', + }); - Sentry.setCurrentClient(client); - client.init(); + Sentry.setCurrentClient(client); + client.init(); - const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); - const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); + const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); + const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); - const first = Sentry.startInactiveSpan({ forceTransaction: true, name: 'test' }); - const second = Sentry.startInactiveSpan({ forceTransaction: true, name: 'child' }); + const first = Sentry.startInactiveSpan({ forceTransaction: true, name: 'test' }); + const second = Sentry.startInactiveSpan({ forceTransaction: true, name: 'child' }); - expect(startProfilingSpy).toHaveBeenCalled(); + expect(startProfilingSpy).toHaveBeenCalled(); - second.end(); - expect(stopProfilingSpy).not.toHaveBeenCalled(); + first.end(); + first.end(); + expect(stopProfilingSpy).not.toHaveBeenCalled(); - first.end(); - expect(stopProfilingSpy).toHaveBeenCalled(); - }); - it('multiple calls to span.end do not restart the profiler', () => { - const [client] = makeCurrentSpanProfilingClient({ - profileLifecycle: 'trace', + second.end(); + expect(stopProfilingSpy).toHaveBeenCalled(); }); - Sentry.setCurrentClient(client); - client.init(); + describe('envelope', () => { + beforeEach(() => { + vi.useRealTimers(); + }); - const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); - const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); + it('sends a profile_chunk envelope type', async () => { + const [client, transport] = makeCurrentSpanProfilingClient({ + profileLifecycle: 'trace', + profileSessionSampleRate: 1, + }); + + Sentry.setCurrentClient(client); + client.init(); - const first = Sentry.startInactiveSpan({ forceTransaction: true, name: 'test' }); - const second = Sentry.startInactiveSpan({ forceTransaction: true, name: 'child' }); + const transportSpy = vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); - expect(startProfilingSpy).toHaveBeenCalled(); + const span = Sentry.startInactiveSpan({ forceTransaction: true, name: 'test' }); + await wait(1000); + span.end(); + + await Sentry.flush(1000); - first.end(); - first.end(); - expect(stopProfilingSpy).not.toHaveBeenCalled(); + expect(transportSpy.mock.calls?.[1]?.[0]?.[1]?.[0]?.[0]).toMatchObject({ + type: 'transaction', + }); + expect(transportSpy.mock.calls?.[0]?.[0]?.[1]?.[0]?.[0]).toMatchObject({ + type: 'profile_chunk', + }); - second.end(); - expect(stopProfilingSpy).toHaveBeenCalled(); + expect(transportSpy.mock.calls?.[0]?.[0]?.[1]?.[0]?.[1]).toMatchObject({ + profiler_id: expect.any(String), + chunk_id: expect.any(String), + profile: expect.objectContaining({ + stacks: expect.any(Array), + }), + }); + }); + }); }); }); From e6fced876384c8d83b589c21581fcd7bc26a5708 Mon Sep 17 00:00:00 2001 From: JonasBa Date: Fri, 14 Mar 2025 10:04:14 -0400 Subject: [PATCH 15/21] remove log --- packages/profiling-node/test/integration.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/profiling-node/test/integration.test.ts b/packages/profiling-node/test/integration.test.ts index 8ff5ee09013e..daee2da6260a 100644 --- a/packages/profiling-node/test/integration.test.ts +++ b/packages/profiling-node/test/integration.test.ts @@ -821,7 +821,6 @@ describe('ProfilingIntegration', () => { await Sentry.flush(1000); - console.log(JSON.stringify(transportSpy.mock.calls[0], null, 2)); expect(transportSpy.mock.calls?.[0]?.[0]?.[1]?.[0]?.[0]).toMatchObject({ type: 'profile_chunk', }); From da77c989d67ff55c1e96228a3cab043eb8a6a002 Mon Sep 17 00:00:00 2001 From: JonasBa Date: Tue, 18 Mar 2025 09:43:28 -0400 Subject: [PATCH 16/21] profiling: add debug log for mode --- packages/profiling-node/src/integration.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/profiling-node/src/integration.ts b/packages/profiling-node/src/integration.ts index c1b59ec213f9..03b121f85377 100644 --- a/packages/profiling-node/src/integration.ts +++ b/packages/profiling-node/src/integration.ts @@ -75,6 +75,8 @@ class ContinuousProfiler { this._legacyProfilerMode = 'profilesSampleRate' in options || 'profilesSampler' in options ? 'span' : 'continuous'; + DEBUG_BUILD && logger.log(`[Profiling] Profiling mode is ${this._legacyProfilerMode}.`); + switch (this._legacyProfilerMode) { case 'span': { this._setupAutomaticSpanProfiling(); @@ -98,6 +100,8 @@ class ContinuousProfiler { case 'current': { this._setupSpanChunkInstrumentation(); + DEBUG_BUILD && logger.log(`[Profiling] Profiling mode is ${this._profileLifecycle}.`); + switch (this._profileLifecycle) { case 'trace': { this._startTraceLifecycleProfiling(); From 757bb6ba5005beede97c39e1874acfc0f6de0d8f Mon Sep 17 00:00:00 2001 From: JonasBa Date: Tue, 18 Mar 2025 10:07:16 -0400 Subject: [PATCH 17/21] profiling: add migration doc --- docs/migration/continuous-profiling.md | 37 ++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 docs/migration/continuous-profiling.md diff --git a/docs/migration/continuous-profiling.md b/docs/migration/continuous-profiling.md new file mode 100644 index 000000000000..c5fae55ae7b6 --- /dev/null +++ b/docs/migration/continuous-profiling.md @@ -0,0 +1,37 @@ +# Continuous Profiling API Changes + +The continuous profiling API has been redesigned to give developers more explicit control over profiling sessions while maintaining ease of use. This guide outlines the key changes. + +## New Profiling Modes + +### profileLifecycle Option + +We've introduced a new `profileLifecycle` option that allows you to explicitly set how profiling sessions are managed: + +- `manual` (default) - You control profiling sessions using the API methods +- `trace` - Profiling sessions are automatically tied to traces + +Previously, the profiling mode was implicitly determined by initialization options. Now you can clearly specify your intended behavior. + +## Renamed API Methods + +The main profiling control methods have been renamed to better reflect their purpose: + +- `Sentry.profiler.start()` → `Sentry.profiler.startProfileSession()` +- `Sentry.profiler.stop()` → `Sentry.profiler.stopProfileSession()` + +The new names emphasize that these methods control profiling sessions and are subject to session sampling. + +## New Sampling Controls + +### profileSessionSampleRate + +We've introduced `profileSessionSampleRate` to control what percentage of SDK instances will collect profiles. This is evaluated once during SDK initialization. This is particularly useful for: + +- Controlling profiling costs across distributed services +- Managing profiling in serverless environments where you may only want to profile a subset of instances + +### Deprecations + +The `profilesSampleRate` option has been deprecated in favor of the new sampling controls. +The `profilesSampler` option hsa been deprecated in favor of manual profiler control. From 8c325f30a205ee54e25decc5621e75c94611c811 Mon Sep 17 00:00:00 2001 From: JonasBa Date: Tue, 18 Mar 2025 16:41:45 -0400 Subject: [PATCH 18/21] profiling: rename api --- docs/migration/continuous-profiling.md | 4 +- packages/core/src/profiling.ts | 52 ------------------- packages/core/src/types-hoist/profiling.ts | 14 ----- packages/profiling-node/src/integration.ts | 20 +++++-- .../profiling-node/test/integration.test.ts | 46 ++++++++-------- 5 files changed, 40 insertions(+), 96 deletions(-) diff --git a/docs/migration/continuous-profiling.md b/docs/migration/continuous-profiling.md index c5fae55ae7b6..ea6349cdb369 100644 --- a/docs/migration/continuous-profiling.md +++ b/docs/migration/continuous-profiling.md @@ -17,8 +17,8 @@ Previously, the profiling mode was implicitly determined by initialization optio The main profiling control methods have been renamed to better reflect their purpose: -- `Sentry.profiler.start()` → `Sentry.profiler.startProfileSession()` -- `Sentry.profiler.stop()` → `Sentry.profiler.stopProfileSession()` +- `Sentry.profiler.start()` → `Sentry.profiler.startProfiler()` +- `Sentry.profiler.stop()` → `Sentry.profiler.stopProfiler()` The new names emphasize that these methods control profiling sessions and are subject to session sampling. diff --git a/packages/core/src/profiling.ts b/packages/core/src/profiling.ts index cc88c6842ee2..9f55a3879b8d 100644 --- a/packages/core/src/profiling.ts +++ b/packages/core/src/profiling.ts @@ -18,7 +18,6 @@ function isProfilingIntegrationWithProfiler( * Starts the Sentry continuous profiler. * This mode is exclusive with the transaction profiler and will only work if the profilesSampleRate is set to a falsy value. * In continuous profiling mode, the profiler will keep reporting profile chunks to Sentry until it is stopped, which allows for continuous profiling of the application. - * @deprecated Use `startProfileSession()` instead. */ function startProfiler(): void { const client = getClient(); @@ -45,7 +44,6 @@ function startProfiler(): void { /** * Stops the Sentry continuous profiler. * Calls to stop will stop the profiler and flush the currently collected profile data to Sentry. - * @deprecated Use `stopProfilerSession()` instead. */ function stopProfiler(): void { const client = getClient(); @@ -68,57 +66,7 @@ function stopProfiler(): void { integration._profiler.stop(); } -/** - * Starts a new profiler session. - */ -function startProfileSession(): void { - const client = getClient(); - if (!client) { - DEBUG_BUILD && logger.warn('No Sentry client available, profiling is not started'); - return; - } - - const integration = client.getIntegrationByName>('ProfilingIntegration'); - if (!integration) { - DEBUG_BUILD && logger.warn('ProfilingIntegration is not available'); - return; - } - - if (!isProfilingIntegrationWithProfiler(integration)) { - DEBUG_BUILD && logger.warn('Profiler is not available on profiling integration.'); - return; - } - - integration._profiler.startProfileSession(); -} - -/** - * Stops the current profiler session. - */ -function stopProfileSession(): void { - const client = getClient(); - if (!client) { - DEBUG_BUILD && logger.warn('No Sentry client available, profiling is not started'); - return; - } - - const integration = client.getIntegrationByName>('ProfilingIntegration'); - if (!integration) { - DEBUG_BUILD && logger.warn('ProfilingIntegration is not available'); - return; - } - - if (!isProfilingIntegrationWithProfiler(integration)) { - DEBUG_BUILD && logger.warn('Profiler is not available on profiling integration.'); - return; - } - - integration._profiler.stopProfileSession(); -} - export const profiler: Profiler = { startProfiler, stopProfiler, - startProfileSession, - stopProfileSession, }; diff --git a/packages/core/src/types-hoist/profiling.ts b/packages/core/src/types-hoist/profiling.ts index 0df93e835a3c..2c0c439450bb 100644 --- a/packages/core/src/types-hoist/profiling.ts +++ b/packages/core/src/types-hoist/profiling.ts @@ -7,8 +7,6 @@ export interface ContinuousProfiler { initialize(client: T): void; start(): void; stop(): void; - startProfileSession(): void; - stopProfileSession(): void; } export interface ProfilingIntegration extends Integration { @@ -18,25 +16,13 @@ export interface ProfilingIntegration extends Integration { export interface Profiler { /** * Starts the profiler. - * @deprecated Use `startProfileSession()` instead. */ startProfiler(): void; /** * Stops the profiler. - * @deprecated Use `stopProfilerSession()` instead. */ stopProfiler(): void; - - /** - * Starts a new profiler session. - */ - startProfileSession(): void; - - /** - * Stops the current profiler session. - */ - stopProfileSession(): void; } export type ThreadId = string; diff --git a/packages/profiling-node/src/integration.ts b/packages/profiling-node/src/integration.ts index 03b121f85377..18ab2f0c7ff1 100644 --- a/packages/profiling-node/src/integration.ts +++ b/packages/profiling-node/src/integration.ts @@ -108,7 +108,7 @@ class ContinuousProfiler { break; } case 'manual': { - // Manual mode requires manual calls to profiler.startProfileSession() and profiler.stopProfileSession() + // Manual mode requires manual calls to profiler.startProfiler() and profiler.stopProfiler() break; } default: { @@ -136,6 +136,11 @@ class ContinuousProfiler { * @returns void */ public start(): void { + if (this._mode === 'current') { + this.startProfiler(); + return; + } + if (!this._client) { DEBUG_BUILD && logger.log('[Profiling] Failed to start, sentry client was never attached to the profiler.'); return; @@ -165,6 +170,11 @@ class ContinuousProfiler { * @returns void */ public stop(): void { + if (this._mode === 'current') { + this.stopProfiler(); + return; + } + if (!this._client) { DEBUG_BUILD && logger.log('[Profiling] Failed to stop, sentry client was never attached to the profiler.'); return; @@ -184,7 +194,7 @@ class ContinuousProfiler { this._teardownSpanChunkInstrumentation(); } - public startProfileSession(): void { + private startProfiler(): void { if (this._mode !== 'current') { DEBUG_BUILD && logger.log('[Profiling] Continuous profiling is not supported in the current mode.'); return; @@ -205,7 +215,7 @@ class ContinuousProfiler { if (this._profileLifecycle === 'trace') { DEBUG_BUILD && logger.log( - '[Profiling] You are using the trace profile lifecycle, manual calls to profiler.startProfileSession() and profiler.stopProfileSession() will be ignored.', + '[Profiling] You are using the trace profile lifecycle, manual calls to profiler.startProfiler() and profiler.stopProfiler() will be ignored.', ); return; } @@ -213,7 +223,7 @@ class ContinuousProfiler { this._startChunkProfiling(); } - public stopProfileSession(): void { + private stopProfiler(): void { if (this._mode !== 'current') { DEBUG_BUILD && logger.log('[Profiling] Continuous profiling is not supported in the current mode.'); return; @@ -222,7 +232,7 @@ class ContinuousProfiler { if (this._profileLifecycle === 'trace') { DEBUG_BUILD && logger.log( - '[Profiling] You are using the trace profile lifecycle, manual calls to profiler.startProfileSession() and profiler.stopProfileSession() will be ignored.', + '[Profiling] You are using the trace profile lifecycle, manual calls to profiler.startProfiler() and profiler.stopProfiler() will be ignored.', ); return; } diff --git a/packages/profiling-node/test/integration.test.ts b/packages/profiling-node/test/integration.test.ts index daee2da6260a..1ab11dba105a 100644 --- a/packages/profiling-node/test/integration.test.ts +++ b/packages/profiling-node/test/integration.test.ts @@ -735,8 +735,8 @@ describe('ProfilingIntegration', () => { const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); - Sentry.profiler.startProfileSession(); - Sentry.profiler.stopProfileSession(); + Sentry.profiler.startProfiler(); + Sentry.profiler.stopProfiler(); expect(startProfilingSpy).toHaveBeenCalled(); expect(stopProfilingSpy).toHaveBeenCalled(); @@ -753,13 +753,13 @@ describe('ProfilingIntegration', () => { const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); - Sentry.profiler.startProfileSession(); - Sentry.profiler.startProfileSession(); + Sentry.profiler.startProfiler(); + Sentry.profiler.startProfiler(); expect(startProfilingSpy).toHaveBeenCalledTimes(1); - Sentry.profiler.stopProfileSession(); - Sentry.profiler.stopProfileSession(); + Sentry.profiler.stopProfiler(); + Sentry.profiler.stopProfiler(); expect(stopProfilingSpy).toHaveBeenCalledTimes(1); }); @@ -774,8 +774,8 @@ describe('ProfilingIntegration', () => { const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); - Sentry.profiler.startProfileSession(); - Sentry.profiler.stopProfileSession(); + Sentry.profiler.startProfiler(); + Sentry.profiler.stopProfiler(); expect(startProfilingSpy).not.toHaveBeenCalled(); expect(stopProfilingSpy).not.toHaveBeenCalled(); @@ -792,8 +792,8 @@ describe('ProfilingIntegration', () => { const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); - Sentry.profiler.startProfileSession(); - Sentry.profiler.stopProfileSession(); + Sentry.profiler.startProfiler(); + Sentry.profiler.stopProfiler(); expect(startProfilingSpy).not.toHaveBeenCalled(); expect(stopProfilingSpy).not.toHaveBeenCalled(); @@ -815,9 +815,9 @@ describe('ProfilingIntegration', () => { const transportSpy = vi.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); - Sentry.profiler.startProfileSession(); + Sentry.profiler.startProfiler(); await wait(1000); - Sentry.profiler.stopProfileSession(); + Sentry.profiler.stopProfiler(); await Sentry.flush(1000); @@ -848,8 +848,8 @@ describe('ProfilingIntegration', () => { const startProfilingSpy = vi.spyOn(CpuProfilerBindings, 'startProfiling'); const stopProfilingSpy = vi.spyOn(CpuProfilerBindings, 'stopProfiling'); - Sentry.profiler.startProfileSession(); - Sentry.profiler.stopProfileSession(); + Sentry.profiler.startProfiler(); + Sentry.profiler.stopProfiler(); expect(startProfilingSpy).not.toHaveBeenCalled(); expect(stopProfilingSpy).not.toHaveBeenCalled(); @@ -989,7 +989,7 @@ describe('ProfilingIntegration', () => { describe('Legacy vs Current API compat', () => { describe('legacy', () => { describe('span profiling', () => { - it('profiler.start, profiler.stop, profiler.startProfileSession, profiler.stopProfileSession void in automated span profiling mode', () => { + it('profiler.start, profiler.stop, profiler.startProfiler, profiler.stopProfiler void in automated span profiling mode', () => { const [client] = makeLegacySpanProfilingClient(); Sentry.setCurrentClient(client); client.init(); @@ -1005,8 +1005,8 @@ describe('Legacy vs Current API compat', () => { expect(stopProfilingSpy).not.toHaveBeenCalled(); // This API is not supported in legacy mode - Sentry.profiler.startProfileSession(); - Sentry.profiler.stopProfileSession(); + Sentry.profiler.startProfiler(); + Sentry.profiler.stopProfiler(); expect(startProfilingSpy).not.toHaveBeenCalled(); expect(stopProfilingSpy).not.toHaveBeenCalled(); @@ -1021,7 +1021,7 @@ describe('Legacy vs Current API compat', () => { }); describe('continuous profiling', () => { - it('profiler.start and profiler.stop start and stop the profiler, calls to profiler.startProfileSession and profiler.stopProfileSession are ignored', () => { + it('profiler.start and profiler.stop start and stop the profiler, calls to profiler.startProfiler and profiler.stopProfiler are ignored', () => { const [client] = makeLegacyContinuousProfilingClient(); Sentry.setCurrentClient(client); client.init(); @@ -1037,8 +1037,8 @@ describe('Legacy vs Current API compat', () => { expect(stopProfilingSpy).not.toHaveBeenCalled(); // This API is not supported in legacy mode - Sentry.profiler.startProfileSession(); - Sentry.profiler.stopProfileSession(); + Sentry.profiler.startProfiler(); + Sentry.profiler.stopProfiler(); expect(startProfilingSpy).not.toHaveBeenCalled(); expect(stopProfilingSpy).not.toHaveBeenCalled(); @@ -1055,7 +1055,7 @@ describe('Legacy vs Current API compat', () => { describe('current', () => { describe('span profiling', () => { - it('profiler.start, profiler.stop, profiler.startProfileSession, profiler.stopProfileSession void in automated span profiling mode', () => { + it('profiler.start, profiler.stop, profiler.startProfiler, profiler.stopProfiler void in automated span profiling mode', () => { const [client] = makeCurrentSpanProfilingClient({ profileLifecycle: 'trace', }); @@ -1073,8 +1073,8 @@ describe('Legacy vs Current API compat', () => { expect(stopProfilingSpy).not.toHaveBeenCalled(); // This API is not supported in trace mode - Sentry.profiler.startProfileSession(); - Sentry.profiler.stopProfileSession(); + Sentry.profiler.startProfiler(); + Sentry.profiler.stopProfiler(); expect(startProfilingSpy).not.toHaveBeenCalled(); expect(stopProfilingSpy).not.toHaveBeenCalled(); From dc8807c43fc7fdff21b323e14f1fa5e5102b0076 Mon Sep 17 00:00:00 2001 From: JonasBa Date: Tue, 18 Mar 2025 16:45:07 -0400 Subject: [PATCH 19/21] profiling: rename api --- packages/profiling-node/test/integration.test.ts | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/packages/profiling-node/test/integration.test.ts b/packages/profiling-node/test/integration.test.ts index 1ab11dba105a..8bce73e8d33b 100644 --- a/packages/profiling-node/test/integration.test.ts +++ b/packages/profiling-node/test/integration.test.ts @@ -1004,13 +1004,6 @@ describe('Legacy vs Current API compat', () => { expect(startProfilingSpy).not.toHaveBeenCalled(); expect(stopProfilingSpy).not.toHaveBeenCalled(); - // This API is not supported in legacy mode - Sentry.profiler.startProfiler(); - Sentry.profiler.stopProfiler(); - - expect(startProfilingSpy).not.toHaveBeenCalled(); - expect(stopProfilingSpy).not.toHaveBeenCalled(); - // Only starting and stopping the profiler is supported in legacy mode const span = Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' }); span.end(); @@ -1036,14 +1029,6 @@ describe('Legacy vs Current API compat', () => { expect(startProfilingSpy).not.toHaveBeenCalled(); expect(stopProfilingSpy).not.toHaveBeenCalled(); - // This API is not supported in legacy mode - Sentry.profiler.startProfiler(); - Sentry.profiler.stopProfiler(); - - expect(startProfilingSpy).not.toHaveBeenCalled(); - expect(stopProfilingSpy).not.toHaveBeenCalled(); - - // Only the old signature is supported Sentry.profiler.startProfiler(); Sentry.profiler.stopProfiler(); From 4be5973e31abb602fccc0f0a5d56aca0dca45d82 Mon Sep 17 00:00:00 2001 From: JonasBa Date: Tue, 18 Mar 2025 16:45:24 -0400 Subject: [PATCH 20/21] profiling: rename api --- docs/migration/continuous-profiling.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/docs/migration/continuous-profiling.md b/docs/migration/continuous-profiling.md index ea6349cdb369..59f7a0824b08 100644 --- a/docs/migration/continuous-profiling.md +++ b/docs/migration/continuous-profiling.md @@ -13,15 +13,6 @@ We've introduced a new `profileLifecycle` option that allows you to explicitly s Previously, the profiling mode was implicitly determined by initialization options. Now you can clearly specify your intended behavior. -## Renamed API Methods - -The main profiling control methods have been renamed to better reflect their purpose: - -- `Sentry.profiler.start()` → `Sentry.profiler.startProfiler()` -- `Sentry.profiler.stop()` → `Sentry.profiler.stopProfiler()` - -The new names emphasize that these methods control profiling sessions and are subject to session sampling. - ## New Sampling Controls ### profileSessionSampleRate From bceec4a92a8f1bcb3261cb34b9143616f46109bf Mon Sep 17 00:00:00 2001 From: JonasBa Date: Tue, 18 Mar 2025 19:27:32 -0400 Subject: [PATCH 21/21] fix linters --- packages/profiling-node/src/integration.ts | 8 ++++---- packages/profiling-node/test/integration.test.ts | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/profiling-node/src/integration.ts b/packages/profiling-node/src/integration.ts index 18ab2f0c7ff1..35243f6da396 100644 --- a/packages/profiling-node/src/integration.ts +++ b/packages/profiling-node/src/integration.ts @@ -137,7 +137,7 @@ class ContinuousProfiler { */ public start(): void { if (this._mode === 'current') { - this.startProfiler(); + this._startProfiler(); return; } @@ -171,7 +171,7 @@ class ContinuousProfiler { */ public stop(): void { if (this._mode === 'current') { - this.stopProfiler(); + this._stopProfiler(); return; } @@ -194,7 +194,7 @@ class ContinuousProfiler { this._teardownSpanChunkInstrumentation(); } - private startProfiler(): void { + private _startProfiler(): void { if (this._mode !== 'current') { DEBUG_BUILD && logger.log('[Profiling] Continuous profiling is not supported in the current mode.'); return; @@ -223,7 +223,7 @@ class ContinuousProfiler { this._startChunkProfiling(); } - private stopProfiler(): void { + private _stopProfiler(): void { if (this._mode !== 'current') { DEBUG_BUILD && logger.log('[Profiling] Continuous profiling is not supported in the current mode.'); return; diff --git a/packages/profiling-node/test/integration.test.ts b/packages/profiling-node/test/integration.test.ts index 8bce73e8d33b..99142651312e 100644 --- a/packages/profiling-node/test/integration.test.ts +++ b/packages/profiling-node/test/integration.test.ts @@ -1,4 +1,3 @@ -/* eslint-disable deprecation/deprecation */ import * as Sentry from '@sentry/node'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';