@sentry/profiling-node 8.8.0 → 8.9.1

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