@sentry/profiling-node 8.0.0-alpha.4 → 8.0.0-alpha.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,763 @@
1
+ import { _nullishCoalesce, _optionalChain, _optionalChainDelete } from '@sentry/utils';
2
+ import { spanIsSampled, spanToJSON, defineIntegration, getRootSpan, getCurrentScope } from '@sentry/core';
3
+ import { logger, GLOBAL_OBJ, forEachEnvelopeItem, uuid4 } from '@sentry/utils';
4
+ import * as os from 'os';
5
+ import { platform as platform$1, arch as arch$1 } from 'os';
6
+ import { resolve, join } from 'path';
7
+ import { familySync } from 'detect-libc';
8
+ import { getAbi } from 'node-abi';
9
+ import { versions, env } from 'process';
10
+ import { threadId, isMainThread } from 'worker_threads';
11
+
12
+
13
+ // -- Shims --
14
+ import cjsUrl from 'node:url';
15
+ import cjsPath from 'node:path';
16
+ import cjsModule from 'node:module';
17
+ const __filename = cjsUrl.fileURLToPath(import.meta.url);
18
+ const __dirname = cjsPath.dirname(__filename);
19
+ const require = cjsModule.createRequire(import.meta.url);
20
+ /**
21
+ * This serves as a build time flag that will be true by default, but false in non-debug builds or if users replace `__SENTRY_DEBUG__` in their generated code.
22
+ *
23
+ * ATTENTION: This constant must never cross package boundaries (i.e. be exported) to guarantee that it can be used for tree shaking.
24
+ */
25
+ const DEBUG_BUILD = (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__);
26
+
27
+ const stdlib = familySync();
28
+ const platform = process.env['BUILD_PLATFORM'] || platform$1();
29
+ const arch = process.env['BUILD_ARCH'] || arch$1();
30
+ const abi = getAbi(versions.node, 'node');
31
+ const identifier = [platform, arch, stdlib, abi].filter(c => c !== undefined && c !== null).join('-');
32
+
33
+ const built_from_source_path = resolve(__dirname, '..', `./sentry_cpu_profiler-${identifier}`);
34
+
35
+ /**
36
+ * Imports cpp bindings based on the current platform and architecture.
37
+ */
38
+ // eslint-disable-next-line complexity
39
+ function importCppBindingsModule() {
40
+ // If a binary path is specified, use that.
41
+ if (env['SENTRY_PROFILER_BINARY_PATH']) {
42
+ const envPath = env['SENTRY_PROFILER_BINARY_PATH'];
43
+ return require(envPath);
44
+ }
45
+
46
+ // If a user specifies a different binary dir, they are in control of the binaries being moved there
47
+ if (env['SENTRY_PROFILER_BINARY_DIR']) {
48
+ const binaryPath = join(resolve(env['SENTRY_PROFILER_BINARY_DIR']), `sentry_cpu_profiler-${identifier}`);
49
+ return require(`${binaryPath}.node`);
50
+ }
51
+
52
+ // We need the fallthrough so that in the end, we can fallback to the require dynamice require.
53
+ // This is for cases where precompiled binaries were not provided, but may have been compiled from source.
54
+ if (platform === 'darwin') {
55
+ if (arch === 'x64') {
56
+ if (abi === '93') {
57
+ return require('../sentry_cpu_profiler-darwin-x64-93.node');
58
+ }
59
+ if (abi === '108') {
60
+ return require('../sentry_cpu_profiler-darwin-x64-108.node');
61
+ }
62
+ if (abi === '115') {
63
+ return require('../sentry_cpu_profiler-darwin-x64-115.node');
64
+ }
65
+ }
66
+
67
+ if (arch === 'arm64') {
68
+ if (abi === '93') {
69
+ return require('../sentry_cpu_profiler-darwin-arm64-93.node');
70
+ }
71
+ if (abi === '108') {
72
+ return require('../sentry_cpu_profiler-darwin-arm64-108.node');
73
+ }
74
+ if (abi === '115') {
75
+ return require('../sentry_cpu_profiler-darwin-arm64-115.node');
76
+ }
77
+ }
78
+ }
79
+
80
+ if (platform === 'win32') {
81
+ if (arch === 'x64') {
82
+ if (abi === '93') {
83
+ return require('../sentry_cpu_profiler-win32-x64-93.node');
84
+ }
85
+ if (abi === '108') {
86
+ return require('../sentry_cpu_profiler-win32-x64-108.node');
87
+ }
88
+ if (abi === '115') {
89
+ return require('../sentry_cpu_profiler-win32-x64-115.node');
90
+ }
91
+ }
92
+ }
93
+
94
+ if (platform === 'linux') {
95
+ if (arch === 'x64') {
96
+ if (stdlib === 'musl') {
97
+ if (abi === '93') {
98
+ return require('../sentry_cpu_profiler-linux-x64-musl-93.node');
99
+ }
100
+ if (abi === '108') {
101
+ return require('../sentry_cpu_profiler-linux-x64-musl-108.node');
102
+ }
103
+ if (abi === '115') {
104
+ return require('../sentry_cpu_profiler-linux-x64-musl-115.node');
105
+ }
106
+ }
107
+ if (stdlib === 'glibc') {
108
+ if (abi === '93') {
109
+ return require('../sentry_cpu_profiler-linux-x64-glibc-93.node');
110
+ }
111
+ if (abi === '108') {
112
+ return require('../sentry_cpu_profiler-linux-x64-glibc-108.node');
113
+ }
114
+ if (abi === '115') {
115
+ return require('../sentry_cpu_profiler-linux-x64-glibc-115.node');
116
+ }
117
+ }
118
+ }
119
+ if (arch === 'arm64') {
120
+ if (stdlib === 'musl') {
121
+ if (abi === '93') {
122
+ return require('../sentry_cpu_profiler-linux-arm64-musl-93.node');
123
+ }
124
+ if (abi === '108') {
125
+ return require('../sentry_cpu_profiler-linux-arm64-musl-108.node');
126
+ }
127
+ if (abi === '115') {
128
+ return require('../sentry_cpu_profiler-linux-arm64-musl-115.node');
129
+ }
130
+ }
131
+ if (stdlib === 'glibc') {
132
+ if (abi === '93') {
133
+ return require('../sentry_cpu_profiler-linux-arm64-glibc-93.node');
134
+ }
135
+ if (abi === '108') {
136
+ return require('../sentry_cpu_profiler-linux-arm64-glibc-108.node');
137
+ }
138
+ if (abi === '115') {
139
+ return require('../sentry_cpu_profiler-linux-arm64-glibc-115.node');
140
+ }
141
+ }
142
+ }
143
+ }
144
+ return require(`${built_from_source_path}.node`);
145
+ }
146
+
147
+ const PrivateCpuProfilerBindings = importCppBindingsModule();
148
+ const CpuProfilerBindings = {
149
+ startProfiling(name) {
150
+ if (!PrivateCpuProfilerBindings) {
151
+ DEBUG_BUILD && logger.log('[Profiling] Bindings not loaded, ignoring call to startProfiling.');
152
+ return;
153
+ }
154
+
155
+ return PrivateCpuProfilerBindings.startProfiling(name);
156
+ },
157
+ stopProfiling(name) {
158
+ if (!PrivateCpuProfilerBindings) {
159
+ DEBUG_BUILD &&
160
+ logger.log('[Profiling] Bindings not loaded or profile was never started, ignoring call to stopProfiling.');
161
+ return null;
162
+ }
163
+ return PrivateCpuProfilerBindings.stopProfiling(name, threadId, !!GLOBAL_OBJ._sentryDebugIds);
164
+ },
165
+ };
166
+
167
+ function _optionalChain$1(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
168
+ // We require the file because if we import it, it will be included in the bundle.
169
+ // I guess tsc does not check file contents when it's imported.
170
+ const THREAD_ID_STRING = String(threadId);
171
+ const THREAD_NAME = isMainThread ? 'main' : 'worker';
172
+ const FORMAT_VERSION = '1';
173
+
174
+ // Os machine was backported to 16.18, but this was not reflected in the types
175
+ // @ts-expect-error ignore missing
176
+ const machine = typeof os.machine === 'function' ? os.machine() : os.arch();
177
+
178
+ // Machine properties (eval only once)
179
+ const PLATFORM = os.platform();
180
+ const RELEASE = os.release();
181
+ const VERSION = os.version();
182
+ const TYPE = os.type();
183
+ const MODEL = machine;
184
+ const ARCH = os.arch();
185
+
186
+ /**
187
+ * Checks if the profile is a raw profile or a profile enriched with thread information.
188
+ * @param {ThreadCpuProfile | RawThreadCpuProfile} profile
189
+ * @returns {boolean}
190
+ */
191
+ function isRawThreadCpuProfile(profile) {
192
+ return !('thread_metadata' in profile);
193
+ }
194
+
195
+ /**
196
+ * Enriches the profile with threadId of the current thread.
197
+ * This is done in node as we seem to not be able to get the info from C native code.
198
+ *
199
+ * @param {ThreadCpuProfile | RawThreadCpuProfile} profile
200
+ * @returns {ThreadCpuProfile}
201
+ */
202
+ function enrichWithThreadInformation(profile) {
203
+ if (!isRawThreadCpuProfile(profile)) {
204
+ return profile;
205
+ }
206
+
207
+ return {
208
+ samples: profile.samples,
209
+ frames: profile.frames,
210
+ stacks: profile.stacks,
211
+ thread_metadata: {
212
+ [THREAD_ID_STRING]: {
213
+ name: THREAD_NAME,
214
+ },
215
+ },
216
+ };
217
+ }
218
+
219
+ /**
220
+ * Creates a profiling envelope item, if the profile does not pass validation, returns null.
221
+ * @param {RawThreadCpuProfile}
222
+ * @param {Event}
223
+ * @returns {Profile | null}
224
+ */
225
+ function createProfilingEvent(client, profile, event) {
226
+ if (!isValidProfile(profile)) {
227
+ return null;
228
+ }
229
+
230
+ return createProfilePayload(client, profile, {
231
+ release: _nullishCoalesce(event.release, () => ( '')),
232
+ environment: _nullishCoalesce(event.environment, () => ( '')),
233
+ event_id: _nullishCoalesce(event.event_id, () => ( '')),
234
+ transaction: _nullishCoalesce(event.transaction, () => ( '')),
235
+ start_timestamp: event.start_timestamp ? event.start_timestamp * 1000 : Date.now(),
236
+ trace_id: _nullishCoalesce(_optionalChain$1([event, 'access', _ => _.contexts, 'optionalAccess', _2 => _2['trace'], 'optionalAccess', _3 => _3['trace_id']]), () => ( '')),
237
+ profile_id: profile.profile_id,
238
+ });
239
+ }
240
+
241
+ /**
242
+ * Create a profile
243
+ * @param {RawThreadCpuProfile} cpuProfile
244
+ * @param {options}
245
+ * @returns {Profile}
246
+ */
247
+
248
+ function createProfilePayload(
249
+ client,
250
+ cpuProfile,
251
+ {
252
+ release,
253
+ environment,
254
+ event_id,
255
+ transaction,
256
+ start_timestamp,
257
+ trace_id,
258
+ profile_id,
259
+ }
260
+
261
+ ,
262
+ ) {
263
+ // Log a warning if the profile has an invalid traceId (should be uuidv4).
264
+ // All profiles and transactions are rejected if this is the case and we want to
265
+ // warn users that this is happening if they enable debug flag
266
+ if (trace_id && trace_id.length !== 32) {
267
+ DEBUG_BUILD && logger.log(`[Profiling] Invalid traceId: ${trace_id} on profiled event`);
268
+ }
269
+
270
+ const enrichedThreadProfile = enrichWithThreadInformation(cpuProfile);
271
+
272
+ const profile = {
273
+ event_id: profile_id,
274
+ timestamp: new Date(start_timestamp).toISOString(),
275
+ platform: 'node',
276
+ version: FORMAT_VERSION,
277
+ release: release,
278
+ environment: environment,
279
+ measurements: cpuProfile.measurements,
280
+ runtime: {
281
+ name: 'node',
282
+ version: versions.node || '',
283
+ },
284
+ os: {
285
+ name: PLATFORM,
286
+ version: RELEASE,
287
+ build_number: VERSION,
288
+ },
289
+ device: {
290
+ locale: env['LC_ALL'] || env['LC_MESSAGES'] || env['LANG'] || env['LANGUAGE'] || '',
291
+ model: MODEL,
292
+ manufacturer: TYPE,
293
+ architecture: ARCH,
294
+ is_emulator: false,
295
+ },
296
+ debug_meta: {
297
+ images: applyDebugMetadata(client, cpuProfile.resources),
298
+ },
299
+ profile: enrichedThreadProfile,
300
+ transaction: {
301
+ name: transaction,
302
+ id: event_id,
303
+ trace_id: trace_id || '',
304
+ active_thread_id: THREAD_ID_STRING,
305
+ },
306
+ };
307
+
308
+ return profile;
309
+ }
310
+
311
+ /**
312
+ * Checks the given sample rate to make sure it is valid type and value (a boolean, or a number between 0 and 1).
313
+ * @param {unknown} rate
314
+ * @returns {boolean}
315
+ */
316
+ function isValidSampleRate(rate) {
317
+ // we need to check NaN explicitly because it's of type 'number' and therefore wouldn't get caught by this typecheck
318
+ if ((typeof rate !== 'number' && typeof rate !== 'boolean') || (typeof rate === 'number' && isNaN(rate))) {
319
+ DEBUG_BUILD &&
320
+ logger.warn(
321
+ `[Profiling] Invalid sample rate. Sample rate must be a boolean or a number between 0 and 1. Got ${JSON.stringify(
322
+ rate,
323
+ )} of type ${JSON.stringify(typeof rate)}.`,
324
+ );
325
+ return false;
326
+ }
327
+
328
+ // Boolean sample rates are always valid
329
+ if (rate === true || rate === false) {
330
+ return true;
331
+ }
332
+
333
+ // in case sampleRate is a boolean, it will get automatically cast to 1 if it's true and 0 if it's false
334
+ if (rate < 0 || rate > 1) {
335
+ DEBUG_BUILD && logger.warn(`[Profiling] Invalid sample rate. Sample rate must be between 0 and 1. Got ${rate}.`);
336
+ return false;
337
+ }
338
+ return true;
339
+ }
340
+
341
+ /**
342
+ * Checks if the profile is valid and can be sent to Sentry.
343
+ * @param {RawThreadCpuProfile} profile
344
+ * @returns {boolean}
345
+ */
346
+ function isValidProfile(profile) {
347
+ if (profile.samples.length <= 1) {
348
+ DEBUG_BUILD &&
349
+ // Log a warning if the profile has less than 2 samples so users can know why
350
+ // they are not seeing any profiling data and we cant avoid the back and forth
351
+ // of asking them to provide us with a dump of the profile data.
352
+ logger.log('[Profiling] Discarding profile because it contains less than 2 samples');
353
+ return false;
354
+ }
355
+
356
+ if (!profile.profile_id) {
357
+ return false;
358
+ }
359
+
360
+ return true;
361
+ }
362
+
363
+ /**
364
+ * Adds items to envelope if they are not already present - mutates the envelope.
365
+ * @param {Envelope} envelope
366
+ * @param {Profile[]} profiles
367
+ * @returns {Envelope}
368
+ */
369
+ function addProfilesToEnvelope(envelope, profiles) {
370
+ if (!profiles.length) {
371
+ return envelope;
372
+ }
373
+
374
+ for (const profile of profiles) {
375
+ // @ts-expect-error untyped envelope
376
+ envelope[1].push([{ type: 'profile' }, profile]);
377
+ }
378
+ return envelope;
379
+ }
380
+
381
+ /**
382
+ * Finds transactions with profile_id context in the envelope
383
+ * @param {Envelope} envelope
384
+ * @returns {Event[]}
385
+ */
386
+ function findProfiledTransactionsFromEnvelope(envelope) {
387
+ const events = [];
388
+
389
+ forEachEnvelopeItem(envelope, (item, type) => {
390
+ if (type !== 'transaction') {
391
+ return;
392
+ }
393
+
394
+ // First item is the type, so we can skip it, everything else is an event
395
+ for (let j = 1; j < item.length; j++) {
396
+ const event = item[j];
397
+
398
+ if (!event) {
399
+ // Shouldnt happen, but lets be safe
400
+ continue;
401
+ }
402
+
403
+ // @ts-expect-error profile_id is not part of the metadata type
404
+ const profile_id = _optionalChain$1([(event.contexts ), 'optionalAccess', _4 => _4['profile'], 'optionalAccess', _5 => _5['profile_id']]);
405
+
406
+ if (event && profile_id) {
407
+ events.push(item[j] );
408
+ }
409
+ }
410
+ });
411
+
412
+ return events;
413
+ }
414
+
415
+ const debugIdStackParserCache = new WeakMap();
416
+
417
+ /**
418
+ * Cross reference profile collected resources with debug_ids and return a list of debug images.
419
+ * @param {string[]} resource_paths
420
+ * @returns {DebugImage[]}
421
+ */
422
+ function applyDebugMetadata(client, resource_paths) {
423
+ const debugIdMap = GLOBAL_OBJ._sentryDebugIds;
424
+ if (!debugIdMap) {
425
+ return [];
426
+ }
427
+
428
+ const options = client.getOptions();
429
+
430
+ if (!options || !options.stackParser) {
431
+ return [];
432
+ }
433
+
434
+ let debugIdStackFramesCache;
435
+ const cachedDebugIdStackFrameCache = debugIdStackParserCache.get(options.stackParser);
436
+ if (cachedDebugIdStackFrameCache) {
437
+ debugIdStackFramesCache = cachedDebugIdStackFrameCache;
438
+ } else {
439
+ debugIdStackFramesCache = new Map();
440
+ debugIdStackParserCache.set(options.stackParser, debugIdStackFramesCache);
441
+ }
442
+
443
+ // Build a map of filename -> debug_id.
444
+ const filenameDebugIdMap = Object.keys(debugIdMap).reduce((acc, debugIdStackTrace) => {
445
+ let parsedStack;
446
+
447
+ const cachedParsedStack = debugIdStackFramesCache.get(debugIdStackTrace);
448
+ if (cachedParsedStack) {
449
+ parsedStack = cachedParsedStack;
450
+ } else {
451
+ parsedStack = options.stackParser(debugIdStackTrace);
452
+ debugIdStackFramesCache.set(debugIdStackTrace, parsedStack);
453
+ }
454
+
455
+ for (let i = parsedStack.length - 1; i >= 0; i--) {
456
+ const stackFrame = parsedStack[i];
457
+ const file = stackFrame && stackFrame.filename;
458
+
459
+ if (stackFrame && file) {
460
+ acc[file] = debugIdMap[debugIdStackTrace] ;
461
+ break;
462
+ }
463
+ }
464
+ return acc;
465
+ }, {});
466
+
467
+ const images = [];
468
+
469
+ for (const resource of resource_paths) {
470
+ if (resource && filenameDebugIdMap[resource]) {
471
+ images.push({
472
+ type: 'sourcemap',
473
+ code_file: resource,
474
+ debug_id: filenameDebugIdMap[resource] ,
475
+ });
476
+ }
477
+ }
478
+
479
+ return images;
480
+ }
481
+
482
+ const MAX_PROFILE_DURATION_MS = 30 * 1000;
483
+
484
+ /**
485
+ * Takes a transaction and determines if it should be profiled or not. If it should be profiled, it returns the
486
+ * profile_id, otherwise returns undefined. Takes care of setting profile context on transaction as well
487
+ */
488
+ function maybeProfileSpan(
489
+ client,
490
+ span,
491
+ customSamplingContext,
492
+ ) {
493
+ // profilesSampleRate is multiplied with tracesSampleRate to get the final sampling rate. We dont perform
494
+ // the actual multiplication to get the final rate, but we discard the profile if the span was sampled,
495
+ // so anything after this block from here is based on the span sampling.
496
+ if (!spanIsSampled(span)) {
497
+ return;
498
+ }
499
+
500
+ // Client and options are required for profiling
501
+ if (!client) {
502
+ DEBUG_BUILD && logger.log('[Profiling] Profiling disabled, no client found.');
503
+ return;
504
+ }
505
+
506
+ const options = client.getOptions();
507
+ if (!options) {
508
+ DEBUG_BUILD && logger.log('[Profiling] Profiling disabled, no options found.');
509
+ return;
510
+ }
511
+
512
+ const profilesSampler = options.profilesSampler;
513
+ let profilesSampleRate = options.profilesSampleRate;
514
+
515
+ // Prefer sampler to sample rate if both are provided.
516
+ if (typeof profilesSampler === 'function') {
517
+ const { description: spanName = '<unknown>', data } = spanToJSON(span);
518
+ // We bail out early if that is not the case
519
+ const parentSampled = true;
520
+
521
+ profilesSampleRate = profilesSampler({
522
+ name: spanName,
523
+ attributes: data,
524
+ transactionContext: {
525
+ name: spanName,
526
+ parentSampled,
527
+ },
528
+ parentSampled,
529
+ ...customSamplingContext,
530
+ });
531
+ }
532
+
533
+ // Since this is coming from the user (or from a function provided by the user), who knows what we might get. (The
534
+ // only valid values are booleans or numbers between 0 and 1.)
535
+ if (!isValidSampleRate(profilesSampleRate)) {
536
+ DEBUG_BUILD && logger.warn('[Profiling] Discarding profile because of invalid sample rate.');
537
+ return;
538
+ }
539
+
540
+ // if the function returned 0 (or false), or if `profileSampleRate` is 0, it's a sign the profile should be dropped
541
+ if (!profilesSampleRate) {
542
+ DEBUG_BUILD &&
543
+ logger.log(
544
+ `[Profiling] Discarding profile because ${
545
+ typeof profilesSampler === 'function'
546
+ ? 'profileSampler returned 0 or false'
547
+ : 'a negative sampling decision was inherited or profileSampleRate is set to 0'
548
+ }`,
549
+ );
550
+ return;
551
+ }
552
+
553
+ // Now we roll the dice. Math.random is inclusive of 0, but not of 1, so strict < is safe here. In case sampleRate is
554
+ // a boolean, the < comparison will cause it to be automatically cast to 1 if it's true and 0 if it's false.
555
+ const sampled = profilesSampleRate === true ? true : Math.random() < profilesSampleRate;
556
+ // Check if we should sample this profile
557
+ if (!sampled) {
558
+ DEBUG_BUILD &&
559
+ logger.log(
560
+ `[Profiling] Discarding profile because it's not included in the random sample (sampling rate = ${Number(
561
+ profilesSampleRate,
562
+ )})`,
563
+ );
564
+ return;
565
+ }
566
+
567
+ const profile_id = uuid4();
568
+ CpuProfilerBindings.startProfiling(profile_id);
569
+ DEBUG_BUILD && logger.log(`[Profiling] started profiling transaction: ${spanToJSON(span).description}`);
570
+
571
+ // set transaction context - do this regardless if profiling fails down the line
572
+ // so that we can still see the profile_id in the transaction context
573
+ return profile_id;
574
+ }
575
+
576
+ /**
577
+ * Stops the profiler for profile_id and returns the profile
578
+ * @param transaction
579
+ * @param profile_id
580
+ * @returns
581
+ */
582
+ function stopSpanProfile(
583
+ span,
584
+ profile_id,
585
+ ) {
586
+ // Should not happen, but satisfy the type checker and be safe regardless.
587
+ if (!profile_id) {
588
+ return null;
589
+ }
590
+
591
+ const profile = CpuProfilerBindings.stopProfiling(profile_id);
592
+
593
+ DEBUG_BUILD && logger.log(`[Profiling] stopped profiling of transaction: ${spanToJSON(span).description}`);
594
+
595
+ // In case of an overlapping span, stopProfiling may return null and silently ignore the overlapping profile.
596
+ if (!profile) {
597
+ DEBUG_BUILD &&
598
+ logger.log(
599
+ `[Profiling] profiler returned null profile for: ${spanToJSON(span).description}`,
600
+ 'this may indicate an overlapping span or a call to stopProfiling with a profile title that was never started',
601
+ );
602
+ return null;
603
+ }
604
+
605
+ // Assign profile_id to the profile
606
+ profile.profile_id = profile_id;
607
+ return profile;
608
+ }
609
+
610
+ const MAX_PROFILE_QUEUE_LENGTH = 50;
611
+ const PROFILE_QUEUE = [];
612
+ const PROFILE_TIMEOUTS = {};
613
+
614
+ function addToProfileQueue(profile) {
615
+ PROFILE_QUEUE.push(profile);
616
+
617
+ // We only want to keep the last n profiles in the queue.
618
+ if (PROFILE_QUEUE.length > MAX_PROFILE_QUEUE_LENGTH) {
619
+ PROFILE_QUEUE.shift();
620
+ }
621
+ }
622
+
623
+ /** Exported only for tests. */
624
+ const _nodeProfilingIntegration = (() => {
625
+ return {
626
+ name: 'ProfilingIntegration',
627
+ setup(client) {
628
+ const spanToProfileIdMap = new WeakMap();
629
+
630
+ client.on('spanStart', span => {
631
+ if (span !== getRootSpan(span)) {
632
+ return;
633
+ }
634
+
635
+ const profile_id = maybeProfileSpan(client, span, undefined);
636
+
637
+ if (profile_id) {
638
+ const options = client.getOptions();
639
+ // Not intended for external use, hence missing types, but we want to profile a couple of things at Sentry that
640
+ // currently exceed the default timeout set by the SDKs.
641
+ const maxProfileDurationMs =
642
+ (options._experiments && options._experiments['maxProfileDurationMs']) || MAX_PROFILE_DURATION_MS;
643
+
644
+ if (PROFILE_TIMEOUTS[profile_id]) {
645
+ global.clearTimeout(PROFILE_TIMEOUTS[profile_id]);
646
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
647
+ delete PROFILE_TIMEOUTS[profile_id];
648
+ }
649
+
650
+ // Enqueue a timeout to prevent profiles from running over max duration.
651
+ PROFILE_TIMEOUTS[profile_id] = global.setTimeout(() => {
652
+ DEBUG_BUILD &&
653
+ logger.log(
654
+ '[Profiling] max profile duration elapsed, stopping profiling for:',
655
+ spanToJSON(span).description,
656
+ );
657
+
658
+ const profile = stopSpanProfile(span, profile_id);
659
+ if (profile) {
660
+ addToProfileQueue(profile);
661
+ }
662
+ }, maxProfileDurationMs);
663
+
664
+ getCurrentScope().setContext('profile', { profile_id });
665
+
666
+ spanToProfileIdMap.set(span, profile_id);
667
+ }
668
+ });
669
+
670
+ client.on('spanEnd', span => {
671
+ const profile_id = spanToProfileIdMap.get(span);
672
+
673
+ if (profile_id) {
674
+ if (PROFILE_TIMEOUTS[profile_id]) {
675
+ global.clearTimeout(PROFILE_TIMEOUTS[profile_id]);
676
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
677
+ delete PROFILE_TIMEOUTS[profile_id];
678
+ }
679
+ const profile = stopSpanProfile(span, profile_id);
680
+
681
+ if (profile) {
682
+ addToProfileQueue(profile);
683
+ }
684
+ }
685
+ });
686
+
687
+ client.on('beforeEnvelope', (envelope) => {
688
+ // if not profiles are in queue, there is nothing to add to the envelope.
689
+ if (!PROFILE_QUEUE.length) {
690
+ return;
691
+ }
692
+
693
+ const profiledTransactionEvents = findProfiledTransactionsFromEnvelope(envelope);
694
+ if (!profiledTransactionEvents.length) {
695
+ return;
696
+ }
697
+
698
+ const profilesToAddToEnvelope = [];
699
+
700
+ for (const profiledTransaction of profiledTransactionEvents) {
701
+ const profileContext = _optionalChain([profiledTransaction, 'access', _ => _.contexts, 'optionalAccess', _2 => _2['profile']]);
702
+ const profile_id = _optionalChain([profileContext, 'optionalAccess', _3 => _3['profile_id']]);
703
+
704
+ if (!profile_id) {
705
+ throw new TypeError('[Profiling] cannot find profile for a transaction without a profile context');
706
+ }
707
+
708
+ // Remove the profile from the transaction context before sending, relay will take care of the rest.
709
+ if (profileContext) {
710
+ _optionalChainDelete([profiledTransaction, 'access', _4 => _4.contexts, 'optionalAccess', _5 => delete _5['profile']]);
711
+ }
712
+
713
+ // We need to find both a profile and a transaction event for the same profile_id.
714
+ const profileIndex = PROFILE_QUEUE.findIndex(p => p.profile_id === profile_id);
715
+ if (profileIndex === -1) {
716
+ DEBUG_BUILD && logger.log(`[Profiling] Could not retrieve profile for transaction: ${profile_id}`);
717
+ continue;
718
+ }
719
+
720
+ const cpuProfile = PROFILE_QUEUE[profileIndex];
721
+ if (!cpuProfile) {
722
+ DEBUG_BUILD && logger.log(`[Profiling] Could not retrieve profile for transaction: ${profile_id}`);
723
+ continue;
724
+ }
725
+
726
+ // Remove the profile from the queue.
727
+ PROFILE_QUEUE.splice(profileIndex, 1);
728
+ const profile = createProfilingEvent(client, cpuProfile, profiledTransaction);
729
+
730
+ if (client.emit && profile) {
731
+ const integrations =
732
+ client['_integrations'] && client['_integrations'] !== null && !Array.isArray(client['_integrations'])
733
+ ? Object.keys(client['_integrations'])
734
+ : undefined;
735
+
736
+ // @ts-expect-error bad overload due to unknown event
737
+ client.emit('preprocessEvent', profile, {
738
+ event_id: profiledTransaction.event_id,
739
+ integrations,
740
+ });
741
+ }
742
+
743
+ if (profile) {
744
+ profilesToAddToEnvelope.push(profile);
745
+ }
746
+ }
747
+
748
+ addProfilesToEnvelope(envelope, profilesToAddToEnvelope);
749
+ });
750
+ },
751
+ };
752
+ }) ;
753
+
754
+ /**
755
+ * We need this integration in order to send data to Sentry. We hook into the event processor
756
+ * and inspect each event to see if it is a transaction event and if that transaction event
757
+ * contains a profile on it's metadata. If that is the case, we create a profiling event envelope
758
+ * and delete the profile from the transaction metadata.
759
+ */
760
+ const nodeProfilingIntegration = defineIntegration(_nodeProfilingIntegration);
761
+
762
+ export { nodeProfilingIntegration };
763
+ //# sourceMappingURL=index.js.map