@sentry/profiling-node 8.7.0 → 8.9.0

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.
Files changed (81) hide show
  1. package/bindings/cpu_profiler.cc +174 -73
  2. package/lib/cjs/index.js +479 -154
  3. package/lib/cjs/index.js.map +1 -1
  4. package/lib/esm/index.js +478 -154
  5. package/lib/esm/index.js.map +1 -1
  6. package/lib/sentry_cpu_profiler-darwin-arm64-108.node +0 -0
  7. package/lib/sentry_cpu_profiler-darwin-arm64-115.node +0 -0
  8. package/lib/sentry_cpu_profiler-darwin-arm64-127.node +0 -0
  9. package/lib/sentry_cpu_profiler-darwin-arm64-93.node +0 -0
  10. package/lib/sentry_cpu_profiler-darwin-x64-108.node +0 -0
  11. package/lib/sentry_cpu_profiler-darwin-x64-115.node +0 -0
  12. package/lib/sentry_cpu_profiler-darwin-x64-127.node +0 -0
  13. package/lib/sentry_cpu_profiler-darwin-x64-93.node +0 -0
  14. package/lib/sentry_cpu_profiler-linux-arm64-glibc-108.node +0 -0
  15. package/lib/sentry_cpu_profiler-linux-arm64-glibc-115.node +0 -0
  16. package/lib/sentry_cpu_profiler-linux-arm64-glibc-127.node +0 -0
  17. package/lib/sentry_cpu_profiler-linux-arm64-glibc-93.node +0 -0
  18. package/lib/sentry_cpu_profiler-linux-arm64-musl-108.node +0 -0
  19. package/lib/sentry_cpu_profiler-linux-arm64-musl-115.node +0 -0
  20. package/lib/sentry_cpu_profiler-linux-arm64-musl-127.node +0 -0
  21. package/lib/sentry_cpu_profiler-linux-arm64-musl-93.node +0 -0
  22. package/lib/sentry_cpu_profiler-linux-x64-glibc-108.node +0 -0
  23. package/lib/sentry_cpu_profiler-linux-x64-glibc-115.node +0 -0
  24. package/lib/sentry_cpu_profiler-linux-x64-glibc-127.node +0 -0
  25. package/lib/sentry_cpu_profiler-linux-x64-glibc-93.node +0 -0
  26. package/lib/sentry_cpu_profiler-linux-x64-musl-108.node +0 -0
  27. package/lib/sentry_cpu_profiler-linux-x64-musl-115.node +0 -0
  28. package/lib/sentry_cpu_profiler-linux-x64-musl-127.node +0 -0
  29. package/lib/sentry_cpu_profiler-linux-x64-musl-93.node +0 -0
  30. package/lib/sentry_cpu_profiler-win32-x64-108.node +0 -0
  31. package/lib/sentry_cpu_profiler-win32-x64-115.node +0 -0
  32. package/lib/sentry_cpu_profiler-win32-x64-127.node +0 -0
  33. package/lib/sentry_cpu_profiler-win32-x64-93.node +0 -0
  34. package/lib/types/profiling-node/src/cpu_profiler.d.ts +16 -0
  35. package/lib/types/profiling-node/src/cpu_profiler.d.ts.map +1 -0
  36. package/lib/types/profiling-node/src/debug-build.d.ts.map +1 -0
  37. package/lib/types/profiling-node/src/index.d.ts.map +1 -0
  38. package/lib/types/profiling-node/src/integration.d.ts +57 -0
  39. package/lib/types/profiling-node/src/integration.d.ts.map +1 -0
  40. package/lib/types/profiling-node/src/nodeVersion.d.ts.map +1 -0
  41. package/lib/types/{spanProfileUtils.d.ts → profiling-node/src/spanProfileUtils.d.ts} +2 -2
  42. package/lib/types/profiling-node/src/spanProfileUtils.d.ts.map +1 -0
  43. package/lib/types/profiling-node/src/types.d.ts +60 -0
  44. package/lib/types/profiling-node/src/types.d.ts.map +1 -0
  45. package/lib/types/{utils.d.ts → profiling-node/src/utils.d.ts} +30 -4
  46. package/lib/types/profiling-node/src/utils.d.ts.map +1 -0
  47. package/lib/types/types/src/debugMeta.d.ts +28 -0
  48. package/lib/types/types/src/debugMeta.d.ts.map +1 -0
  49. package/lib/types/types/src/measurement.d.ts +24 -0
  50. package/lib/types/types/src/measurement.d.ts.map +1 -0
  51. package/lib/types/types/src/profiling.d.ts +126 -0
  52. package/lib/types/types/src/profiling.d.ts.map +1 -0
  53. package/lib/types-ts3.8/profiling-node/src/cpu_profiler.d.ts +16 -0
  54. package/lib/types-ts3.8/profiling-node/src/integration.d.ts +57 -0
  55. package/lib/types-ts3.8/{spanProfileUtils.d.ts → profiling-node/src/spanProfileUtils.d.ts} +2 -2
  56. package/lib/types-ts3.8/profiling-node/src/types.d.ts +60 -0
  57. package/lib/types-ts3.8/{utils.d.ts → profiling-node/src/utils.d.ts} +30 -4
  58. package/lib/types-ts3.8/types/src/debugMeta.d.ts +28 -0
  59. package/lib/types-ts3.8/types/src/measurement.d.ts +24 -0
  60. package/lib/types-ts3.8/types/src/profiling.d.ts +126 -0
  61. package/package.json +5 -5
  62. package/lib/types/cpu_profiler.d.ts +0 -10
  63. package/lib/types/cpu_profiler.d.ts.map +0 -1
  64. package/lib/types/debug-build.d.ts.map +0 -1
  65. package/lib/types/index.d.ts.map +0 -1
  66. package/lib/types/integration.d.ts +0 -14
  67. package/lib/types/integration.d.ts.map +0 -1
  68. package/lib/types/nodeVersion.d.ts.map +0 -1
  69. package/lib/types/spanProfileUtils.d.ts.map +0 -1
  70. package/lib/types/types.d.ts +0 -100
  71. package/lib/types/types.d.ts.map +0 -1
  72. package/lib/types/utils.d.ts.map +0 -1
  73. package/lib/types-ts3.8/cpu_profiler.d.ts +0 -10
  74. package/lib/types-ts3.8/integration.d.ts +0 -14
  75. package/lib/types-ts3.8/types.d.ts +0 -100
  76. /package/lib/types/{debug-build.d.ts → profiling-node/src/debug-build.d.ts} +0 -0
  77. /package/lib/types/{index.d.ts → profiling-node/src/index.d.ts} +0 -0
  78. /package/lib/types/{nodeVersion.d.ts → profiling-node/src/nodeVersion.d.ts} +0 -0
  79. /package/lib/types-ts3.8/{debug-build.d.ts → profiling-node/src/debug-build.d.ts} +0 -0
  80. /package/lib/types-ts3.8/{index.d.ts → profiling-node/src/index.d.ts} +0 -0
  81. /package/lib/types-ts3.8/{nodeVersion.d.ts → profiling-node/src/nodeVersion.d.ts} +0 -0
package/lib/cjs/index.js CHANGED
@@ -8,12 +8,15 @@ Object.defineProperty(exports, '__esModule', { value: true });
8
8
 
9
9
  const core = require('@sentry/core');
10
10
  const utils = require('@sentry/utils');
11
- const os = require('node:os');
11
+ const node_os = require('node:os');
12
12
  const node_path = require('node:path');
13
13
  const node_process = require('node:process');
14
14
  const node_worker_threads = require('node:worker_threads');
15
15
  const detectLibc = require('detect-libc');
16
16
  const nodeAbi = require('node-abi');
17
+ const os = require('os');
18
+ const process$1 = require('process');
19
+ const worker_threads = require('worker_threads');
17
20
 
18
21
  /**
19
22
  * 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,12 +25,9 @@ const nodeAbi = require('node-abi');
22
25
  */
23
26
  const DEBUG_BUILD = (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__);
24
27
 
25
- const NODE_VERSION = utils.parseSemver(process.versions.node) ;
26
- const NODE_MAJOR = NODE_VERSION.major;
27
-
28
28
  const stdlib = detectLibc.familySync();
29
- const platform = process.env['BUILD_PLATFORM'] || os.platform();
30
- const arch = process.env['BUILD_ARCH'] || os.arch();
29
+ const platform = process.env['BUILD_PLATFORM'] || node_os.platform();
30
+ const arch = process.env['BUILD_ARCH'] || node_os.arch();
31
31
  const abi = nodeAbi.getAbi(node_process.versions.node, 'node');
32
32
  const identifier = [platform, arch, stdlib, abi].filter(c => c !== undefined && c !== null).join('-');
33
33
 
@@ -167,31 +167,49 @@ function importCppBindingsModule() {
167
167
  }
168
168
 
169
169
  const PrivateCpuProfilerBindings = importCppBindingsModule();
170
- const CpuProfilerBindings = {
171
- startProfiling(name) {
170
+
171
+ class Bindings {
172
+ startProfiling(name) {
172
173
  if (!PrivateCpuProfilerBindings) {
173
174
  DEBUG_BUILD && utils.logger.log('[Profiling] Bindings not loaded, ignoring call to startProfiling.');
174
175
  return;
175
176
  }
176
177
 
177
178
  return PrivateCpuProfilerBindings.startProfiling(name);
178
- },
179
- stopProfiling(name) {
179
+ }
180
+
181
+ stopProfiling(
182
+ name,
183
+ format,
184
+ ) {
180
185
  if (!PrivateCpuProfilerBindings) {
181
186
  DEBUG_BUILD &&
182
187
  utils.logger.log('[Profiling] Bindings not loaded or profile was never started, ignoring call to stopProfiling.');
183
188
  return null;
184
189
  }
185
- return PrivateCpuProfilerBindings.stopProfiling(name, node_worker_threads.threadId, !!utils.GLOBAL_OBJ._sentryDebugIds);
186
- },
187
- };
188
190
 
189
- 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; }
191
+ return PrivateCpuProfilerBindings.stopProfiling(
192
+ name,
193
+ format ,
194
+ node_worker_threads.threadId,
195
+ !!utils.GLOBAL_OBJ._sentryDebugIds,
196
+ );
197
+ }
198
+ }
199
+
200
+ const CpuProfilerBindings = new Bindings();
201
+
202
+ const NODE_VERSION = utils.parseSemver(process.versions.node) ;
203
+ const NODE_MAJOR = NODE_VERSION.major;
204
+
205
+ 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; }/* eslint-disable max-lines */
206
+
190
207
  // We require the file because if we import it, it will be included in the bundle.
191
208
  // I guess tsc does not check file contents when it's imported.
192
- const THREAD_ID_STRING = String(node_worker_threads.threadId);
193
- const THREAD_NAME = node_worker_threads.isMainThread ? 'main' : 'worker';
209
+ const THREAD_ID_STRING = String(worker_threads.threadId);
210
+ const THREAD_NAME = worker_threads.isMainThread ? 'main' : 'worker';
194
211
  const FORMAT_VERSION = '1';
212
+ const CONTINUOUS_FORMAT_VERSION = '2';
195
213
 
196
214
  // Os machine was backported to 16.18, but this was not reflected in the types
197
215
  // @ts-expect-error ignore missing
@@ -210,7 +228,9 @@ const ARCH = os.arch();
210
228
  * @param {ThreadCpuProfile | RawThreadCpuProfile} profile
211
229
  * @returns {boolean}
212
230
  */
213
- function isRawThreadCpuProfile(profile) {
231
+ function isRawThreadCpuProfile(
232
+ profile,
233
+ ) {
214
234
  return !('thread_metadata' in profile);
215
235
  }
216
236
 
@@ -221,7 +241,9 @@ function isRawThreadCpuProfile(profile) {
221
241
  * @param {ThreadCpuProfile | RawThreadCpuProfile} profile
222
242
  * @returns {ThreadCpuProfile}
223
243
  */
224
- function enrichWithThreadInformation(profile) {
244
+ function enrichWithThreadInformation(
245
+ profile,
246
+ ) {
225
247
  if (!isRawThreadCpuProfile(profile)) {
226
248
  return profile;
227
249
  }
@@ -235,7 +257,7 @@ function enrichWithThreadInformation(profile) {
235
257
  name: THREAD_NAME,
236
258
  },
237
259
  },
238
- };
260
+ } ;
239
261
  }
240
262
 
241
263
  /**
@@ -266,7 +288,6 @@ function createProfilingEvent(client, profile, event) {
266
288
  * @param {options}
267
289
  * @returns {Profile}
268
290
  */
269
-
270
291
  function createProfilePayload(
271
292
  client,
272
293
  cpuProfile,
@@ -301,7 +322,7 @@ function createProfilePayload(
301
322
  measurements: cpuProfile.measurements,
302
323
  runtime: {
303
324
  name: 'node',
304
- version: node_process.versions.node || '',
325
+ version: process$1.versions.node || '',
305
326
  },
306
327
  os: {
307
328
  name: PLATFORM,
@@ -309,7 +330,7 @@ function createProfilePayload(
309
330
  build_number: VERSION,
310
331
  },
311
332
  device: {
312
- locale: node_process.env['LC_ALL'] || node_process.env['LC_MESSAGES'] || node_process.env['LANG'] || node_process.env['LANGUAGE'] || '',
333
+ locale: process$1.env['LC_ALL'] || process$1.env['LC_MESSAGES'] || process$1.env['LANG'] || process$1.env['LANGUAGE'] || '',
313
334
  model: MODEL,
314
335
  manufacturer: TYPE,
315
336
  architecture: ARCH,
@@ -318,7 +339,7 @@ function createProfilePayload(
318
339
  debug_meta: {
319
340
  images: applyDebugMetadata(client, cpuProfile.resources),
320
341
  },
321
- profile: enrichedThreadProfile,
342
+ profile: enrichedThreadProfile ,
322
343
  transaction: {
323
344
  name: transaction,
324
345
  id: event_id,
@@ -330,6 +351,77 @@ function createProfilePayload(
330
351
  return profile;
331
352
  }
332
353
 
354
+ /**
355
+ * Create a profile chunk from raw thread profile
356
+ * @param {RawThreadCpuProfile} cpuProfile
357
+ * @param {options}
358
+ * @returns {Profile}
359
+ */
360
+ function createProfileChunkPayload(
361
+ client,
362
+ cpuProfile,
363
+ {
364
+ release,
365
+ environment,
366
+ start_timestamp,
367
+ trace_id,
368
+ profiler_id,
369
+ chunk_id,
370
+ }
371
+
372
+ ,
373
+ ) {
374
+ // Log a warning if the profile has an invalid traceId (should be uuidv4).
375
+ // All profiles and transactions are rejected if this is the case and we want to
376
+ // warn users that this is happening if they enable debug flag
377
+ if (trace_id && trace_id.length !== 32) {
378
+ DEBUG_BUILD && utils.logger.log(`[Profiling] Invalid traceId: ${trace_id} on profiled event`);
379
+ }
380
+
381
+ const enrichedThreadProfile = enrichWithThreadInformation(cpuProfile);
382
+
383
+ const profile = {
384
+ chunk_id: chunk_id,
385
+ profiler_id: profiler_id,
386
+ timestamp: new Date(start_timestamp).toISOString(),
387
+ platform: 'node',
388
+ version: CONTINUOUS_FORMAT_VERSION,
389
+ release: release,
390
+ environment: environment,
391
+ measurements: cpuProfile.measurements,
392
+ debug_meta: {
393
+ images: applyDebugMetadata(client, cpuProfile.resources),
394
+ },
395
+ profile: enrichedThreadProfile ,
396
+ };
397
+
398
+ return profile;
399
+ }
400
+
401
+ /**
402
+ * Creates a profiling chunk envelope item, if the profile does not pass validation, returns null.
403
+ */
404
+ function createProfilingChunkEvent(
405
+ start_timestamp,
406
+ client,
407
+ options,
408
+ profile,
409
+ identifiers,
410
+ ) {
411
+ if (!isValidProfileChunk(profile)) {
412
+ return null;
413
+ }
414
+
415
+ return createProfileChunkPayload(client, profile, {
416
+ release: _nullishCoalesce(options.release, () => ( '')),
417
+ environment: _nullishCoalesce(options.environment, () => ( '')),
418
+ start_timestamp: start_timestamp,
419
+ trace_id: _nullishCoalesce(identifiers.trace_id, () => ( '')),
420
+ chunk_id: identifiers.chunk_id,
421
+ profiler_id: identifiers.profiler_id,
422
+ });
423
+ }
424
+
333
425
  /**
334
426
  * Checks the given sample rate to make sure it is valid type and value (a boolean, or a number between 0 and 1).
335
427
  * @param {unknown} rate
@@ -382,6 +474,24 @@ function isValidProfile(profile) {
382
474
  return true;
383
475
  }
384
476
 
477
+ /**
478
+ * Checks if the profile chunk is valid and can be sent to Sentry.
479
+ * @param profile
480
+ * @returns
481
+ */
482
+ function isValidProfileChunk(profile) {
483
+ if (profile.samples.length <= 1) {
484
+ DEBUG_BUILD &&
485
+ // Log a warning if the profile has less than 2 samples so users can know why
486
+ // they are not seeing any profiling data and we cant avoid the back and forth
487
+ // of asking them to provide us with a dump of the profile data.
488
+ utils.logger.log('[Profiling] Discarding profile chunk because it contains less than 2 samples');
489
+ return false;
490
+ }
491
+
492
+ return true;
493
+ }
494
+
385
495
  /**
386
496
  * Adds items to envelope if they are not already present - mutates the envelope.
387
497
  * @param {Envelope} envelope
@@ -434,6 +544,41 @@ function findProfiledTransactionsFromEnvelope(envelope) {
434
544
  return events;
435
545
  }
436
546
 
547
+ /**
548
+ * Creates event envelope headers for a profile chunk. This is separate from createEventEnvelopeHeaders util
549
+ * as the profile chunk does not conform to the sentry event type
550
+ */
551
+ function createEventEnvelopeHeaders(
552
+ sdkInfo,
553
+ tunnel,
554
+ dsn,
555
+ ) {
556
+ return {
557
+ event_id: utils.uuid4(),
558
+ sent_at: new Date().toISOString(),
559
+ ...(sdkInfo && { sdk: sdkInfo }),
560
+ ...(!!tunnel && dsn && { dsn: utils.dsnToString(dsn) }),
561
+ };
562
+ }
563
+
564
+ /**
565
+ * Creates a standalone profile_chunk envelope.
566
+ */
567
+ function makeProfileChunkEnvelope(
568
+ chunk,
569
+ sdkInfo,
570
+ tunnel,
571
+ dsn,
572
+ ) {
573
+ const profileChunkHeader = {
574
+ type: 'profile_chunk',
575
+ };
576
+
577
+ return utils.createEnvelope(createEventEnvelopeHeaders(sdkInfo, tunnel, dsn), [
578
+ [profileChunkHeader, chunk],
579
+ ]);
580
+ }
581
+
437
582
  const debugIdStackParserCache = new WeakMap();
438
583
 
439
584
  /**
@@ -601,16 +746,13 @@ function maybeProfileSpan(
601
746
  * @param profile_id
602
747
  * @returns
603
748
  */
604
- function stopSpanProfile(
605
- span,
606
- profile_id,
607
- ) {
749
+ function stopSpanProfile(span, profile_id) {
608
750
  // Should not happen, but satisfy the type checker and be safe regardless.
609
751
  if (!profile_id) {
610
752
  return null;
611
753
  }
612
754
 
613
- const profile = CpuProfilerBindings.stopProfiling(profile_id);
755
+ const profile = CpuProfilerBindings.stopProfiling(profile_id, 0);
614
756
 
615
757
  DEBUG_BUILD && utils.logger.log(`[Profiling] stopped profiling of transaction: ${core.spanToJSON(span).description}`);
616
758
 
@@ -629,16 +771,296 @@ function stopSpanProfile(
629
771
  return profile;
630
772
  }
631
773
 
632
- const MAX_PROFILE_QUEUE_LENGTH = 50;
633
- const PROFILE_QUEUE = [];
774
+ var ProfileFormat;(function (ProfileFormat) {
775
+ const THREAD = 0; ProfileFormat[ProfileFormat["THREAD"] = THREAD] = "THREAD";
776
+ const CHUNK = 1; ProfileFormat[ProfileFormat["CHUNK"] = CHUNK] = "CHUNK";
777
+ })(ProfileFormat || (ProfileFormat = {}));
778
+
779
+ const CHUNK_INTERVAL_MS = 5000;
780
+ const PROFILE_MAP = new utils.LRUMap(50);
634
781
  const PROFILE_TIMEOUTS = {};
635
782
 
636
- function addToProfileQueue(profile) {
637
- PROFILE_QUEUE.push(profile);
783
+ function addToProfileQueue(profile_id, profile) {
784
+ PROFILE_MAP.set(profile_id, profile);
785
+ }
786
+
787
+ function takeFromProfileQueue(profile_id) {
788
+ const profile = PROFILE_MAP.get(profile_id);
789
+ PROFILE_MAP.remove(profile_id);
790
+ return profile;
791
+ }
638
792
 
639
- // We only want to keep the last n profiles in the queue.
640
- if (PROFILE_QUEUE.length > MAX_PROFILE_QUEUE_LENGTH) {
641
- PROFILE_QUEUE.shift();
793
+ /**
794
+ * Instruments the client to automatically invoke the profiler on span start and stop events.
795
+ * @param client
796
+ */
797
+ function setupAutomatedSpanProfiling(client) {
798
+ const spanToProfileIdMap = new WeakMap();
799
+
800
+ client.on('spanStart', span => {
801
+ if (span !== core.getRootSpan(span)) {
802
+ return;
803
+ }
804
+
805
+ const profile_id = maybeProfileSpan(client, span);
806
+
807
+ if (profile_id) {
808
+ const options = client.getOptions();
809
+ // Not intended for external use, hence missing types, but we want to profile a couple of things at Sentry that
810
+ // currently exceed the default timeout set by the SDKs.
811
+ const maxProfileDurationMs =
812
+ (options._experiments && options._experiments['maxProfileDurationMs']) || MAX_PROFILE_DURATION_MS;
813
+
814
+ if (PROFILE_TIMEOUTS[profile_id]) {
815
+ global.clearTimeout(PROFILE_TIMEOUTS[profile_id]);
816
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
817
+ delete PROFILE_TIMEOUTS[profile_id];
818
+ }
819
+
820
+ // Enqueue a timeout to prevent profiles from running over max duration.
821
+ const timeout = global.setTimeout(() => {
822
+ DEBUG_BUILD &&
823
+ utils.logger.log('[Profiling] max profile duration elapsed, stopping profiling for:', core.spanToJSON(span).description);
824
+
825
+ const profile = stopSpanProfile(span, profile_id);
826
+ if (profile) {
827
+ addToProfileQueue(profile_id, profile);
828
+ }
829
+ }, maxProfileDurationMs);
830
+
831
+ // Unref timeout so it doesn't keep the process alive.
832
+ timeout.unref();
833
+
834
+ core.getCurrentScope().setContext('profile', { profile_id });
835
+ spanToProfileIdMap.set(span, profile_id);
836
+ }
837
+ });
838
+
839
+ client.on('spanEnd', span => {
840
+ const profile_id = spanToProfileIdMap.get(span);
841
+
842
+ if (profile_id) {
843
+ if (PROFILE_TIMEOUTS[profile_id]) {
844
+ global.clearTimeout(PROFILE_TIMEOUTS[profile_id]);
845
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
846
+ delete PROFILE_TIMEOUTS[profile_id];
847
+ }
848
+ const profile = stopSpanProfile(span, profile_id);
849
+
850
+ if (profile) {
851
+ addToProfileQueue(profile_id, profile);
852
+ }
853
+ }
854
+ });
855
+
856
+ client.on('beforeEnvelope', (envelope) => {
857
+ // if not profiles are in queue, there is nothing to add to the envelope.
858
+ if (!PROFILE_MAP.size) {
859
+ return;
860
+ }
861
+
862
+ const profiledTransactionEvents = findProfiledTransactionsFromEnvelope(envelope);
863
+ if (!profiledTransactionEvents.length) {
864
+ return;
865
+ }
866
+
867
+ const profilesToAddToEnvelope = [];
868
+
869
+ for (const profiledTransaction of profiledTransactionEvents) {
870
+ const profileContext = _optionalChain([profiledTransaction, 'access', _ => _.contexts, 'optionalAccess', _2 => _2['profile']]);
871
+ const profile_id = _optionalChain([profileContext, 'optionalAccess', _3 => _3['profile_id']]);
872
+
873
+ if (!profile_id) {
874
+ throw new TypeError('[Profiling] cannot find profile for a transaction without a profile context');
875
+ }
876
+
877
+ // Remove the profile from the transaction context before sending, relay will take care of the rest.
878
+ if (profileContext) {
879
+ _optionalChainDelete([profiledTransaction, 'access', _4 => _4.contexts, 'optionalAccess', _5 => delete _5['profile']]);
880
+ }
881
+
882
+ const cpuProfile = takeFromProfileQueue(profile_id);
883
+ if (!cpuProfile) {
884
+ DEBUG_BUILD && utils.logger.log(`[Profiling] Could not retrieve profile for transaction: ${profile_id}`);
885
+ continue;
886
+ }
887
+
888
+ const profile = createProfilingEvent(client, cpuProfile, profiledTransaction);
889
+ if (!profile) return;
890
+
891
+ profilesToAddToEnvelope.push(profile);
892
+
893
+ // @ts-expect-error profile does not inherit from Event
894
+ client.emit('preprocessEvent', profile, {
895
+ event_id: profiledTransaction.event_id,
896
+ });
897
+ }
898
+
899
+ addProfilesToEnvelope(envelope, profilesToAddToEnvelope);
900
+ });
901
+ }
902
+
903
+ class ContinuousProfiler {constructor() { ContinuousProfiler.prototype.__init.call(this);ContinuousProfiler.prototype.__init2.call(this);ContinuousProfiler.prototype.__init3.call(this); }
904
+ __init() {this._profilerId = utils.uuid4();}
905
+ __init2() {this._client = undefined;}
906
+ __init3() {this._chunkData = undefined;}
907
+
908
+ /**
909
+ * Called when the profiler is attached to the client (continuous mode is enabled). If of the profiler
910
+ * methods called before the profiler is initialized will result in a noop action with debug logs.
911
+ * @param client
912
+ */
913
+ initialize(client) {
914
+ this._client = client;
915
+ }
916
+
917
+ /**
918
+ * Recursively schedules chunk profiling to start and stop at a set interval.
919
+ * Once the user calls stop(), the current chunk will be stopped and flushed to Sentry and no new chunks will
920
+ * will be started. To restart continuous mode after calling stop(), the user must call start() again.
921
+ * @returns void
922
+ */
923
+ start() {
924
+ if (!this._client) {
925
+ // The client is not attached to the profiler if the user has not enabled continuous profiling.
926
+ // In this case, calling start() and stop() is a noop action.The reason this exists is because
927
+ // it makes the types easier to work with and avoids users having to do null checks.
928
+ DEBUG_BUILD && utils.logger.log('[Profiling] Profiler was never attached to the client.');
929
+ return;
930
+ }
931
+ if (this._chunkData) {
932
+ DEBUG_BUILD &&
933
+ utils.logger.log(
934
+ `[Profiling] Chunk with chunk_id ${this._chunkData.id} is still running, current chunk will be stopped a new chunk will be started.`,
935
+ );
936
+ this.stop();
937
+ }
938
+
939
+ const traceId =
940
+ core.getCurrentScope().getPropagationContext().traceId || core.getIsolationScope().getPropagationContext().traceId;
941
+ this._initializeChunk(traceId);
942
+ this._startChunkProfiling(this._chunkData);
943
+ }
944
+
945
+ /**
946
+ * Stops the current chunk and flushes the profile to Sentry.
947
+ * @returns void
948
+ */
949
+ stop() {
950
+ if (_optionalChain([this, 'access', _6 => _6._chunkData, 'optionalAccess', _7 => _7.timer])) {
951
+ global.clearTimeout(this._chunkData.timer);
952
+ this._chunkData.timer = undefined;
953
+ DEBUG_BUILD && utils.logger.log(`[Profiling] Stopping profiling chunk: ${this._chunkData.id}`);
954
+ }
955
+ if (!this._client) {
956
+ DEBUG_BUILD &&
957
+ utils.logger.log('[Profiling] Failed to collect profile, sentry client was never attached to the profiler.');
958
+ return;
959
+ }
960
+ if (!_optionalChain([this, 'access', _8 => _8._chunkData, 'optionalAccess', _9 => _9.id])) {
961
+ DEBUG_BUILD &&
962
+ utils.logger.log(`[Profiling] Failed to collect profile for: ${_optionalChain([this, 'access', _10 => _10._chunkData, 'optionalAccess', _11 => _11.id])}, the chunk_id is missing.`);
963
+ return;
964
+ }
965
+ const profile = CpuProfilerBindings.stopProfiling(this._chunkData.id, ProfileFormat.CHUNK);
966
+ if (!profile || !this._chunkData.startTimestampMS) {
967
+ DEBUG_BUILD && utils.logger.log(`[Profiling] _chunkiledStartTraceID to collect profile for: ${this._chunkData.id}`);
968
+ return;
969
+ }
970
+ if (profile) {
971
+ DEBUG_BUILD && utils.logger.log(`[Profiling] Sending profile chunk ${this._chunkData.id}.`);
972
+ }
973
+
974
+ DEBUG_BUILD && utils.logger.log(`[Profiling] Profile chunk ${this._chunkData.id} sent to Sentry.`);
975
+ const chunk = createProfilingChunkEvent(
976
+ this._chunkData.startTimestampMS,
977
+ this._client,
978
+ this._client.getOptions(),
979
+ profile,
980
+ {
981
+ chunk_id: this._chunkData.id,
982
+ trace_id: this._chunkData.startTraceID,
983
+ profiler_id: this._profilerId,
984
+ },
985
+ );
986
+
987
+ if (!chunk) {
988
+ DEBUG_BUILD && utils.logger.log(`[Profiling] Failed to create profile chunk for: ${this._chunkData.id}`);
989
+ this._reset();
990
+ return;
991
+ }
992
+
993
+ this._flush(chunk);
994
+ // Depending on the profile and stack sizes, stopping the profile and converting
995
+ // the format may negatively impact the performance of the application. To avoid
996
+ // blocking for too long, enqueue the next chunk start inside the next macrotask.
997
+ // clear current chunk
998
+ this._reset();
999
+ }
1000
+
1001
+ /**
1002
+ * Flushes the profile chunk to Sentry.
1003
+ * @param chunk
1004
+ */
1005
+ _flush(chunk) {
1006
+ if (!this._client) {
1007
+ DEBUG_BUILD &&
1008
+ utils.logger.log('[Profiling] Failed to collect profile, sentry client was never attached to the profiler.');
1009
+ return;
1010
+ }
1011
+
1012
+ const transport = this._client.getTransport();
1013
+ if (!transport) {
1014
+ DEBUG_BUILD && utils.logger.log('[Profiling] No transport available to send profile chunk.');
1015
+ return;
1016
+ }
1017
+
1018
+ const dsn = this._client.getDsn();
1019
+ const metadata = this._client.getSdkMetadata();
1020
+ const tunnel = this._client.getOptions().tunnel;
1021
+
1022
+ const envelope = makeProfileChunkEnvelope(chunk, _optionalChain([metadata, 'optionalAccess', _12 => _12.sdk]), tunnel, dsn);
1023
+ transport.send(envelope).then(null, reason => {
1024
+ DEBUG_BUILD && utils.logger.error('Error while sending profile chunk envelope:', reason);
1025
+ });
1026
+ }
1027
+
1028
+ /**
1029
+ * Starts the profiler and registers the flush timer for a given chunk.
1030
+ * @param chunk
1031
+ */
1032
+ _startChunkProfiling(chunk) {
1033
+ CpuProfilerBindings.startProfiling(chunk.id);
1034
+ DEBUG_BUILD && utils.logger.log(`[Profiling] starting profiling chunk: ${chunk.id}`);
1035
+
1036
+ chunk.timer = global.setTimeout(() => {
1037
+ DEBUG_BUILD && utils.logger.log(`[Profiling] Stopping profiling chunk: ${chunk.id}`);
1038
+ this.stop();
1039
+ DEBUG_BUILD && utils.logger.log('[Profiling] Starting new profiling chunk.');
1040
+ setImmediate(this.start.bind(this));
1041
+ }, CHUNK_INTERVAL_MS);
1042
+
1043
+ // Unref timeout so it doesn't keep the process alive.
1044
+ chunk.timer.unref();
1045
+ }
1046
+
1047
+ /**
1048
+ * Initializes new profile chunk metadata
1049
+ */
1050
+ _initializeChunk(traceId) {
1051
+ this._chunkData = {
1052
+ id: utils.uuid4(),
1053
+ startTraceID: traceId,
1054
+ startTimestampMS: utils.timestampInSeconds(),
1055
+ timer: undefined,
1056
+ };
1057
+ }
1058
+
1059
+ /**
1060
+ * Resets the current chunk state.
1061
+ */
1062
+ _reset() {
1063
+ this._chunkData = undefined;
642
1064
  }
643
1065
  }
644
1066
 
@@ -655,129 +1077,32 @@ const _nodeProfilingIntegration = (() => {
655
1077
 
656
1078
  return {
657
1079
  name: 'ProfilingIntegration',
1080
+ _profiler: new ContinuousProfiler(),
658
1081
  setup(client) {
659
- const spanToProfileIdMap = new WeakMap();
660
-
661
- client.on('spanStart', span => {
662
- if (span !== core.getRootSpan(span)) {
663
- return;
664
- }
665
-
666
- const profile_id = maybeProfileSpan(client, span, undefined);
667
-
668
- if (profile_id) {
669
- const options = client.getOptions();
670
- // Not intended for external use, hence missing types, but we want to profile a couple of things at Sentry that
671
- // currently exceed the default timeout set by the SDKs.
672
- const maxProfileDurationMs =
673
- (options._experiments && options._experiments['maxProfileDurationMs']) || MAX_PROFILE_DURATION_MS;
674
-
675
- if (PROFILE_TIMEOUTS[profile_id]) {
676
- global.clearTimeout(PROFILE_TIMEOUTS[profile_id]);
677
- // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
678
- delete PROFILE_TIMEOUTS[profile_id];
679
- }
680
-
681
- // Enqueue a timeout to prevent profiles from running over max duration.
682
- PROFILE_TIMEOUTS[profile_id] = global.setTimeout(() => {
683
- DEBUG_BUILD &&
684
- utils.logger.log(
685
- '[Profiling] max profile duration elapsed, stopping profiling for:',
686
- core.spanToJSON(span).description,
687
- );
688
-
689
- const profile = stopSpanProfile(span, profile_id);
690
- if (profile) {
691
- addToProfileQueue(profile);
692
- }
693
- }, maxProfileDurationMs);
694
-
695
- core.getCurrentScope().setContext('profile', { profile_id });
696
-
697
- spanToProfileIdMap.set(span, profile_id);
1082
+ DEBUG_BUILD && utils.logger.log('[Profiling] Profiling integration setup.');
1083
+ const options = client.getOptions();
1084
+
1085
+ const mode =
1086
+ (options.profilesSampleRate === undefined || options.profilesSampleRate === 0) && !options.profilesSampler
1087
+ ? 'continuous'
1088
+ : 'span';
1089
+ switch (mode) {
1090
+ case 'continuous': {
1091
+ DEBUG_BUILD && utils.logger.log('[Profiling] Continuous profiler mode enabled.');
1092
+ this._profiler.initialize(client);
1093
+ break;
698
1094
  }
699
- });
700
-
701
- client.on('spanEnd', span => {
702
- const profile_id = spanToProfileIdMap.get(span);
703
-
704
- if (profile_id) {
705
- if (PROFILE_TIMEOUTS[profile_id]) {
706
- global.clearTimeout(PROFILE_TIMEOUTS[profile_id]);
707
- // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
708
- delete PROFILE_TIMEOUTS[profile_id];
709
- }
710
- const profile = stopSpanProfile(span, profile_id);
711
-
712
- if (profile) {
713
- addToProfileQueue(profile);
714
- }
1095
+ // Default to span profiling when no mode profiler mode is set
1096
+ case 'span':
1097
+ case undefined: {
1098
+ DEBUG_BUILD && utils.logger.log('[Profiling] Span profiler mode enabled.');
1099
+ setupAutomatedSpanProfiling(client);
1100
+ break;
715
1101
  }
716
- });
717
-
718
- client.on('beforeEnvelope', (envelope) => {
719
- // if not profiles are in queue, there is nothing to add to the envelope.
720
- if (!PROFILE_QUEUE.length) {
721
- return;
1102
+ default: {
1103
+ DEBUG_BUILD && utils.logger.warn(`[Profiling] Unknown profiler mode: ${mode}, profiler was not initialized`);
722
1104
  }
723
-
724
- const profiledTransactionEvents = findProfiledTransactionsFromEnvelope(envelope);
725
- if (!profiledTransactionEvents.length) {
726
- return;
727
- }
728
-
729
- const profilesToAddToEnvelope = [];
730
-
731
- for (const profiledTransaction of profiledTransactionEvents) {
732
- const profileContext = _optionalChain([profiledTransaction, 'access', _ => _.contexts, 'optionalAccess', _2 => _2['profile']]);
733
- const profile_id = _optionalChain([profileContext, 'optionalAccess', _3 => _3['profile_id']]);
734
-
735
- if (!profile_id) {
736
- throw new TypeError('[Profiling] cannot find profile for a transaction without a profile context');
737
- }
738
-
739
- // Remove the profile from the transaction context before sending, relay will take care of the rest.
740
- if (profileContext) {
741
- _optionalChainDelete([profiledTransaction, 'access', _4 => _4.contexts, 'optionalAccess', _5 => delete _5['profile']]);
742
- }
743
-
744
- // We need to find both a profile and a transaction event for the same profile_id.
745
- const profileIndex = PROFILE_QUEUE.findIndex(p => p.profile_id === profile_id);
746
- if (profileIndex === -1) {
747
- DEBUG_BUILD && utils.logger.log(`[Profiling] Could not retrieve profile for transaction: ${profile_id}`);
748
- continue;
749
- }
750
-
751
- const cpuProfile = PROFILE_QUEUE[profileIndex];
752
- if (!cpuProfile) {
753
- DEBUG_BUILD && utils.logger.log(`[Profiling] Could not retrieve profile for transaction: ${profile_id}`);
754
- continue;
755
- }
756
-
757
- // Remove the profile from the queue.
758
- PROFILE_QUEUE.splice(profileIndex, 1);
759
- const profile = createProfilingEvent(client, cpuProfile, profiledTransaction);
760
-
761
- if (client.emit && profile) {
762
- const integrations =
763
- client['_integrations'] && client['_integrations'] !== null && !Array.isArray(client['_integrations'])
764
- ? Object.keys(client['_integrations'])
765
- : undefined;
766
-
767
- // @ts-expect-error bad overload due to unknown event
768
- client.emit('preprocessEvent', profile, {
769
- event_id: profiledTransaction.event_id,
770
- integrations,
771
- });
772
- }
773
-
774
- if (profile) {
775
- profilesToAddToEnvelope.push(profile);
776
- }
777
- }
778
-
779
- addProfilesToEnvelope(envelope, profilesToAddToEnvelope);
780
- });
1105
+ }
781
1106
  },
782
1107
  };
783
1108
  }) ;