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