Skip to content

Commit 188e4c8

Browse files
committed
ref(profiling): expose continuous profiler on Sentry.profiler
1 parent 8d9fe35 commit 188e4c8

File tree

8 files changed

+115
-24
lines changed

8 files changed

+115
-24
lines changed

packages/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ export { sessionTimingIntegration } from './integrations/sessiontiming';
100100
export { zodErrorsIntegration } from './integrations/zoderrors';
101101
export { thirdPartyErrorFilterIntegration } from './integrations/third-party-errors-filter';
102102
export { metrics } from './metrics/exports';
103+
export { profiler } from './profiling';
103104
export type { MetricData } from '@sentry/types';
104105
export { metricsDefault } from './metrics/exports-default';
105106
export { BrowserMetricsAggregator } from './metrics/browser-aggregator';

packages/core/src/profiling.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { logger } from '@sentry/utils';
2+
import { ProfilingIntegration, Profiler } from '@sentry/types';
3+
4+
import { DEBUG_BUILD } from "./debug-build";
5+
import { getClient } from "./currentScopes";
6+
7+
/**
8+
*
9+
*/
10+
function startProfiler() {
11+
const client = getClient()
12+
if (!client) {
13+
DEBUG_BUILD && logger.warn('No Sentry client available, profiling is not started');
14+
return;
15+
}
16+
17+
const integration = client.getIntegrationByName<ProfilingIntegration<any>>('ProfilingIntegration');
18+
if (!integration) {
19+
DEBUG_BUILD && logger.warn('ProfilingIntegration is not available');
20+
return
21+
}
22+
integration._profiler.start();
23+
}
24+
25+
/**
26+
*
27+
*/
28+
function stopProfiler() {
29+
const client = getClient()
30+
if (!client) {
31+
DEBUG_BUILD && logger.warn('No Sentry client available, profiling is not started');
32+
return;
33+
}
34+
35+
const integration = client.getIntegrationByName<ProfilingIntegration<any>>('ProfilingIntegration');
36+
if (!integration) {
37+
DEBUG_BUILD && logger.warn('ProfilingIntegration is not available');
38+
return
39+
}
40+
integration._profiler.stop();
41+
}
42+
43+
export const profiler: Profiler = {
44+
startProfiler,
45+
stopProfiler
46+
}

packages/node/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ export {
127127
spanToBaggageHeader,
128128
trpcMiddleware,
129129
zodErrorsIntegration,
130+
profiler,
130131
} from '@sentry/core';
131132

132133
export type {

packages/profiling-node/src/integration.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
spanToJSON,
1010
} from '@sentry/core';
1111
import type { NodeClient } from '@sentry/node';
12-
import type { Event, Integration, IntegrationFn, Profile, ProfileChunk, Span } from '@sentry/types';
12+
import type { Event, ProfilingIntegration, IntegrationFn, Profile, ProfileChunk, Span } from '@sentry/types';
1313

1414
import { LRUMap, logger, uuid4 } from '@sentry/utils';
1515

@@ -159,6 +159,7 @@ interface ChunkData {
159159
timer: NodeJS.Timeout | undefined;
160160
startTraceID: string;
161161
}
162+
162163
class ContinuousProfiler {
163164
private _profilerId = uuid4();
164165
private _client: NodeClient | undefined = undefined;
@@ -384,12 +385,8 @@ class ContinuousProfiler {
384385
}
385386
}
386387

387-
export interface ProfilingIntegration extends Integration {
388-
_profiler: ContinuousProfiler;
389-
}
390-
391388
/** Exported only for tests. */
392-
export const _nodeProfilingIntegration = ((): ProfilingIntegration => {
389+
export const _nodeProfilingIntegration = ((): ProfilingIntegration<NodeClient> => {
393390
if (DEBUG_BUILD && ![16, 18, 20, 22].includes(NODE_MAJOR)) {
394391
logger.warn(
395392
`[Profiling] You are using a Node.js version that does not have prebuilt binaries (${NODE_VERSION}).`,

packages/profiling-node/test/spanProfileUtils.test.ts

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import * as Sentry from '@sentry/node';
22

33
import { getMainCarrier } from '@sentry/core';
4+
import { ProfilingIntegration } from '@sentry/types';
45
import type { NodeClientOptions } from '@sentry/node/build/types/types';
56
import type { ProfileChunk, Transport } from '@sentry/types';
67
import { GLOBAL_OBJ, createEnvelope, logger } from '@sentry/utils';
78
import { CpuProfilerBindings } from '../src/cpu_profiler';
8-
import { type ProfilingIntegration, _nodeProfilingIntegration } from '../src/integration';
9+
import { _nodeProfilingIntegration } from '../src/integration';
910

1011
function makeClientWithHooks(): [Sentry.NodeClient, Transport] {
1112
const integration = _nodeProfilingIntegration();
@@ -299,7 +300,7 @@ describe('automated span instrumentation', () => {
299300
Sentry.setCurrentClient(client);
300301
client.init();
301302

302-
const integration = client.getIntegrationByName<ProfilingIntegration>('ProfilingIntegration');
303+
const integration = client.getIntegrationByName<ProfilingIntegration<Sentry.NodeClient>>('ProfilingIntegration');
303304
if (!integration) {
304305
throw new Error('Profiling integration not found');
305306
}
@@ -390,7 +391,7 @@ describe('continuous profiling', () => {
390391
});
391392
afterEach(() => {
392393
const client = Sentry.getClient();
393-
const integration = client?.getIntegrationByName<ProfilingIntegration>('ProfilingIntegration');
394+
const integration = client?.getIntegrationByName<ProfilingIntegration<Sentry.NodeClient>>('ProfilingIntegration');
394395

395396
if (integration) {
396397
integration._profiler.stop();
@@ -432,7 +433,7 @@ describe('continuous profiling', () => {
432433

433434
const transportSpy = jest.spyOn(transport, 'send').mockReturnValue(Promise.resolve({}));
434435

435-
const integration = client.getIntegrationByName<ProfilingIntegration>('ProfilingIntegration');
436+
const integration = client.getIntegrationByName<ProfilingIntegration<Sentry.NodeClient>>('ProfilingIntegration');
436437
if (!integration) {
437438
throw new Error('Profiling integration not found');
438439
}
@@ -446,7 +447,7 @@ describe('continuous profiling', () => {
446447
expect(profile.client_sdk.version).toEqual(expect.stringMatching(/\d+\.\d+\.\d+/));
447448
});
448449

449-
it('initializes the continuous profiler and binds the sentry client', () => {
450+
it('initializes the continuous profiler', () => {
450451
const startProfilingSpy = jest.spyOn(CpuProfilerBindings, 'startProfiling');
451452

452453
const [client] = makeContinuousProfilingClient();
@@ -455,14 +456,13 @@ describe('continuous profiling', () => {
455456

456457
expect(startProfilingSpy).not.toHaveBeenCalledTimes(1);
457458

458-
const integration = client.getIntegrationByName<ProfilingIntegration>('ProfilingIntegration');
459+
const integration = client.getIntegrationByName<ProfilingIntegration<Sentry.NodeClient>>('ProfilingIntegration');
459460
if (!integration) {
460461
throw new Error('Profiling integration not found');
461462
}
462463
integration._profiler.start();
463464

464465
expect(integration._profiler).toBeDefined();
465-
expect(integration._profiler['_client']).toBe(client);
466466
});
467467

468468
it('starts a continuous profile', () => {
@@ -473,7 +473,7 @@ describe('continuous profiling', () => {
473473
client.init();
474474

475475
expect(startProfilingSpy).not.toHaveBeenCalledTimes(1);
476-
const integration = client.getIntegrationByName<ProfilingIntegration>('ProfilingIntegration');
476+
const integration = client.getIntegrationByName<ProfilingIntegration<Sentry.NodeClient>>('ProfilingIntegration');
477477
if (!integration) {
478478
throw new Error('Profiling integration not found');
479479
}
@@ -490,7 +490,7 @@ describe('continuous profiling', () => {
490490
client.init();
491491

492492
expect(startProfilingSpy).not.toHaveBeenCalledTimes(1);
493-
const integration = client.getIntegrationByName<ProfilingIntegration>('ProfilingIntegration');
493+
const integration = client.getIntegrationByName<ProfilingIntegration<Sentry.NodeClient>>('ProfilingIntegration');
494494
if (!integration) {
495495
throw new Error('Profiling integration not found');
496496
}
@@ -509,7 +509,7 @@ describe('continuous profiling', () => {
509509
client.init();
510510

511511
expect(startProfilingSpy).not.toHaveBeenCalledTimes(1);
512-
const integration = client.getIntegrationByName<ProfilingIntegration>('ProfilingIntegration');
512+
const integration = client.getIntegrationByName<ProfilingIntegration<Sentry.NodeClient>>('ProfilingIntegration');
513513
if (!integration) {
514514
throw new Error('Profiling integration not found');
515515
}
@@ -529,7 +529,7 @@ describe('continuous profiling', () => {
529529
client.init();
530530

531531
expect(startProfilingSpy).not.toHaveBeenCalledTimes(1);
532-
const integration = client.getIntegrationByName<ProfilingIntegration>('ProfilingIntegration');
532+
const integration = client.getIntegrationByName<ProfilingIntegration<Sentry.NodeClient>>('ProfilingIntegration');
533533
if (!integration) {
534534
throw new Error('Profiling integration not found');
535535
}
@@ -548,7 +548,7 @@ describe('continuous profiling', () => {
548548
client.init();
549549

550550
expect(startProfilingSpy).not.toHaveBeenCalledTimes(1);
551-
const integration = client.getIntegrationByName<ProfilingIntegration>('ProfilingIntegration');
551+
const integration = client.getIntegrationByName<ProfilingIntegration<Sentry.NodeClient>>('ProfilingIntegration');
552552
if (!integration) {
553553
throw new Error('Profiling integration not found');
554554
}
@@ -604,7 +604,7 @@ describe('continuous profiling', () => {
604604

605605
const transportSpy = jest.spyOn(transport, 'send').mockReturnValue(Promise.resolve({}));
606606

607-
const integration = client.getIntegrationByName<ProfilingIntegration>('ProfilingIntegration');
607+
const integration = client.getIntegrationByName<ProfilingIntegration<Sentry.NodeClient>>('ProfilingIntegration');
608608
if (!integration) {
609609
throw new Error('Profiling integration not found');
610610
}
@@ -632,7 +632,7 @@ describe('continuous profiling', () => {
632632
},
633633
});
634634

635-
const integration = client.getIntegrationByName<ProfilingIntegration>('ProfilingIntegration');
635+
const integration = client.getIntegrationByName<ProfilingIntegration<Sentry.NodeClient>>('ProfilingIntegration');
636636
if (!integration) {
637637
throw new Error('Profiling integration not found');
638638
}
@@ -692,7 +692,7 @@ describe('span profiling mode', () => {
692692
Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' });
693693

694694
expect(startProfilingSpy).toHaveBeenCalled();
695-
const integration = client.getIntegrationByName<ProfilingIntegration>('ProfilingIntegration');
695+
const integration = client.getIntegrationByName<ProfilingIntegration<Sentry.NodeClient>>('ProfilingIntegration');
696696

697697
if (!integration) {
698698
throw new Error('Profiling integration not found');
@@ -739,7 +739,7 @@ describe('continuous profiling mode', () => {
739739

740740
jest.spyOn(transport, 'send').mockReturnValue(Promise.resolve({}));
741741

742-
const integration = client.getIntegrationByName<ProfilingIntegration>('ProfilingIntegration');
742+
const integration = client.getIntegrationByName<ProfilingIntegration<Sentry.NodeClient>>('ProfilingIntegration');
743743
if (!integration) {
744744
throw new Error('Profiling integration not found');
745745
}
@@ -750,4 +750,31 @@ describe('continuous profiling mode', () => {
750750
Sentry.startInactiveSpan({ forceTransaction: true, name: 'profile_hub' });
751751
expect(startProfilingSpy).toHaveBeenCalledTimes(callCount);
752752
});
753+
754+
it('top level methods proxy to integration', () => {
755+
const client = new Sentry.NodeClient({
756+
...makeClientOptions({ profilesSampleRate: undefined }),
757+
dsn: 'https://[email protected]/6625302',
758+
tracesSampleRate: 1,
759+
transport: _opts =>
760+
Sentry.makeNodeTransport({
761+
url: 'https://[email protected]/6625302',
762+
recordDroppedEvent: () => {
763+
return undefined;
764+
},
765+
}),
766+
integrations: [_nodeProfilingIntegration()],
767+
});
768+
769+
Sentry.setCurrentClient(client);
770+
client.init();
771+
772+
const startProfilingSpy = jest.spyOn(CpuProfilerBindings, 'startProfiling');
773+
const stopProfilingSpy = jest.spyOn(CpuProfilerBindings, 'stopProfiling');
774+
775+
Sentry.profiler.startProfiler();
776+
expect(startProfilingSpy).toHaveBeenCalledTimes(1);
777+
Sentry.profiler.stopProfiler();
778+
expect(stopProfilingSpy).toHaveBeenCalledTimes(1);
779+
})
753780
});

packages/profiling-node/test/spanProfileUtils.worker.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ jest.setTimeout(10000);
99

1010
import * as Sentry from '@sentry/node';
1111
import type { Transport } from '@sentry/types';
12-
import { type ProfilingIntegration, _nodeProfilingIntegration } from '../src/integration';
12+
import { type ProfilingIntegration } from '@sentry/types';
13+
import { _nodeProfilingIntegration } from '../src/integration';
1314

1415
function makeContinuousProfilingClient(): [Sentry.NodeClient, Transport] {
1516
const integration = _nodeProfilingIntegration();
@@ -49,7 +50,7 @@ it('worker threads context', () => {
4950
},
5051
});
5152

52-
const integration = client.getIntegrationByName<ProfilingIntegration>('ProfilingIntegration');
53+
const integration = client.getIntegrationByName<ProfilingIntegration<any>>('ProfilingIntegration');
5354
if (!integration) {
5455
throw new Error('Profiling integration not found');
5556
}

packages/types/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,4 +171,5 @@ export type {
171171
Metrics,
172172
} from './metrics';
173173
export type { ParameterizedString } from './parameterize';
174+
export type { ContinuousProfiler, ProfilingIntegration, Profiler } from './profiling'
174175
export type { ViewHierarchyData, ViewHierarchyWindow } from './view-hierarchy';

packages/types/src/profiling.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,22 @@
11
import type { DebugImage } from './debugMeta';
22
import type { MeasurementUnit } from './measurement';
3+
import type { Integration } from './integration';
4+
import { Client } from './client';
5+
6+
export interface ContinuousProfiler<T extends Client> {
7+
initialize(client: T): void;
8+
start(): void;
9+
stop(): void;
10+
}
11+
12+
export interface ProfilingIntegration<T extends Client> extends Integration {
13+
_profiler: ContinuousProfiler<T>;
14+
}
15+
16+
export interface Profiler {
17+
startProfiler(): void;
18+
stopProfiler(): void;
19+
}
320

421
export type ThreadId = string;
522
export type FrameId = number;

0 commit comments

Comments
 (0)