@sentry/profiling-node 11.0.0-alpha.0 → 11.0.0-alpha.2

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.
package/README.md CHANGED
@@ -32,13 +32,13 @@ Sentry.init({
32
32
  dsn: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302',
33
33
  debug: true,
34
34
  tracesSampleRate: 1,
35
- profilesSampleRate: 1, // Set profiling sampling rate.
35
+ profileSessionSampleRate: 1,
36
+ profileLifecycle: 'trace',
36
37
  integrations: [nodeProfilingIntegration()],
37
38
  });
38
39
  ```
39
40
 
40
- Sentry SDK will now automatically profile all root spans, even the ones which may be started as a result of using an
41
- automatic instrumentation integration.
41
+ The Sentry SDK will now collect profile chunks while spans are active, including spans started by automatic instrumentation.
42
42
 
43
43
  ```javascript
44
44
  Sentry.startSpan({ name: 'some workflow' }, () => {
@@ -46,6 +46,8 @@ Sentry.startSpan({ name: 'some workflow' }, () => {
46
46
  });
47
47
  ```
48
48
 
49
+ With `profileLifecycle: 'manual'` (the default), you can start and stop the profiler by calling `Sentry.profiler.startProfiler()` and `Sentry.profiler.stopProfiler()`.
50
+
49
51
  ### Building the package from source
50
52
 
51
53
  Profiling uses native modules to interop with the v8 javascript engine which means that you may be required to build it
@@ -3,8 +3,6 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
3
3
  const core = require('@sentry/core');
4
4
  const nodeCpuProfiler = require('@sentry/node-cpu-profiler');
5
5
  const worker_threads = require('worker_threads');
6
- const os = require('os');
7
- const process$1 = require('process');
8
6
 
9
7
  const DEBUG_BUILD = (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__);
10
8
 
@@ -13,14 +11,7 @@ const NODE_MAJOR = NODE_VERSION.major;
13
11
 
14
12
  const PROFILER_THREAD_ID_STRING = String(worker_threads.threadId);
15
13
  const PROFILER_THREAD_NAME = worker_threads.isMainThread ? "main" : "worker";
16
- const FORMAT_VERSION = "1";
17
14
  const CONTINUOUS_FORMAT_VERSION = "2";
18
- const PLATFORM = os.platform();
19
- const RELEASE = os.release();
20
- const VERSION = os.version();
21
- const TYPE = os.type();
22
- const MODEL = os.machine();
23
- const ARCH = os.arch();
24
15
  function isRawThreadCpuProfile(profile) {
25
16
  return !("thread_metadata" in profile);
26
17
  }
@@ -39,70 +30,6 @@ function enrichWithThreadInformation(profile) {
39
30
  }
40
31
  };
41
32
  }
42
- function createProfilingEvent(client, profile, event) {
43
- if (!isValidProfile(profile)) {
44
- return null;
45
- }
46
- return createProfilePayload(client, profile, {
47
- release: event.release ?? "",
48
- environment: event.environment ?? "",
49
- event_id: event.event_id ?? "",
50
- transaction: event.transaction ?? "",
51
- start_timestamp: event.start_timestamp ? event.start_timestamp * 1e3 : Date.now(),
52
- trace_id: event.contexts?.trace?.trace_id ?? "",
53
- profile_id: profile.profile_id
54
- });
55
- }
56
- function createProfilePayload(client, cpuProfile, {
57
- release,
58
- environment,
59
- event_id,
60
- transaction,
61
- start_timestamp,
62
- trace_id,
63
- profile_id
64
- }) {
65
- if (trace_id?.length !== 32) {
66
- DEBUG_BUILD && core.debug.log(`[Profiling] Invalid traceId: ${trace_id} on profiled event`);
67
- }
68
- const enrichedThreadProfile = enrichWithThreadInformation(cpuProfile);
69
- const profile = {
70
- event_id: profile_id,
71
- timestamp: new Date(start_timestamp).toISOString(),
72
- platform: "node",
73
- version: FORMAT_VERSION,
74
- release,
75
- environment,
76
- measurements: cpuProfile.measurements,
77
- runtime: {
78
- name: "node",
79
- version: process$1.versions.node || ""
80
- },
81
- os: {
82
- name: PLATFORM,
83
- version: RELEASE,
84
- build_number: VERSION
85
- },
86
- device: {
87
- locale: process$1.env["LC_ALL"] || process$1.env["LC_MESSAGES"] || process$1.env["LANG"] || process$1.env["LANGUAGE"] || "",
88
- model: MODEL,
89
- manufacturer: TYPE,
90
- architecture: ARCH,
91
- is_emulator: false
92
- },
93
- debug_meta: {
94
- images: applyDebugMetadata(client, cpuProfile.resources)
95
- },
96
- profile: enrichedThreadProfile,
97
- transaction: {
98
- name: transaction,
99
- id: event_id,
100
- trace_id: trace_id || "",
101
- active_thread_id: PROFILER_THREAD_ID_STRING
102
- }
103
- };
104
- return profile;
105
- }
106
33
  function createProfileChunkPayload(client, cpuProfile, {
107
34
  release,
108
35
  environment,
@@ -147,37 +74,6 @@ function createProfilingChunkEvent(client, options, profile, sdk, identifiers) {
147
74
  sdk
148
75
  });
149
76
  }
150
- function isValidSampleRate(rate) {
151
- if (typeof rate !== "number" && typeof rate !== "boolean" || typeof rate === "number" && isNaN(rate)) {
152
- DEBUG_BUILD && core.debug.warn(
153
- `[Profiling] Invalid sample rate. Sample rate must be a boolean or a number between 0 and 1. Got ${JSON.stringify(
154
- rate
155
- )} of type ${JSON.stringify(typeof rate)}.`
156
- );
157
- return false;
158
- }
159
- if (rate === true || rate === false) {
160
- return true;
161
- }
162
- if (rate < 0 || rate > 1) {
163
- DEBUG_BUILD && core.debug.warn(`[Profiling] Invalid sample rate. Sample rate must be between 0 and 1. Got ${rate}.`);
164
- return false;
165
- }
166
- return true;
167
- }
168
- function isValidProfile(profile) {
169
- if (profile.samples.length <= 1) {
170
- DEBUG_BUILD && // Log a warning if the profile has less than 2 samples so users can know why
171
- // they are not seeing any profiling data and we cant avoid the back and forth
172
- // of asking them to provide us with a dump of the profile data.
173
- core.debug.log("[Profiling] Discarding profile because it contains less than 2 samples");
174
- return false;
175
- }
176
- if (!profile.profile_id) {
177
- return false;
178
- }
179
- return true;
180
- }
181
77
  function isValidProfileChunk(profile) {
182
78
  if (profile.samples.length <= 1) {
183
79
  DEBUG_BUILD && // Log a warning if the profile has less than 2 samples so users can know why
@@ -188,34 +84,6 @@ function isValidProfileChunk(profile) {
188
84
  }
189
85
  return true;
190
86
  }
191
- function addProfilesToEnvelope(envelope, profiles) {
192
- if (!profiles.length) {
193
- return envelope;
194
- }
195
- for (const profile of profiles) {
196
- envelope[1].push([{ type: "profile" }, profile]);
197
- }
198
- return envelope;
199
- }
200
- function findProfiledTransactionsFromEnvelope(envelope) {
201
- const events = [];
202
- core.forEachEnvelopeItem(envelope, (item, type) => {
203
- if (type !== "transaction") {
204
- return;
205
- }
206
- for (let j = 1; j < item.length; j++) {
207
- const event = item[j];
208
- if (!event) {
209
- continue;
210
- }
211
- const profile_id = event.contexts?.profile?.profile_id;
212
- if (event && profile_id) {
213
- events.push(event);
214
- }
215
- }
216
- });
217
- return events;
218
- }
219
87
  function createEventEnvelopeHeaders(sdkInfo, tunnel, dsn) {
220
88
  return {
221
89
  event_id: core.uuid4(),
@@ -241,94 +109,11 @@ function applyDebugMetadata(client, resource_paths) {
241
109
  return core.getDebugImagesForResources(options.stackParser, resource_paths);
242
110
  }
243
111
 
244
- const MAX_PROFILE_DURATION_MS = 30 * 1e3;
245
- function maybeProfileSpan(client, span, customSamplingContext) {
246
- if (!worker_threads.isMainThread) {
247
- DEBUG_BUILD && core.debug.log("[Profiling] Skipping span profiling in worker thread.");
248
- return;
249
- }
250
- if (!core.spanIsSampled(span)) {
251
- return;
252
- }
253
- if (!client) {
254
- DEBUG_BUILD && core.debug.log("[Profiling] Profiling disabled, no client found.");
255
- return;
256
- }
257
- const options = client.getOptions();
258
- if (!options) {
259
- DEBUG_BUILD && core.debug.log("[Profiling] Profiling disabled, no options found.");
260
- return;
261
- }
262
- const profilesSampler = options.profilesSampler;
263
- let profilesSampleRate = options.profilesSampleRate;
264
- if (typeof profilesSampler === "function") {
265
- const { description: spanName = "<unknown>", data } = core.spanToJSON(span);
266
- const parentSampled = true;
267
- profilesSampleRate = profilesSampler({
268
- name: spanName,
269
- attributes: data,
270
- parentSampled,
271
- ...customSamplingContext
272
- });
273
- }
274
- if (!isValidSampleRate(profilesSampleRate)) {
275
- DEBUG_BUILD && core.debug.warn("[Profiling] Discarding profile because of invalid sample rate.");
276
- return;
277
- }
278
- if (!profilesSampleRate) {
279
- DEBUG_BUILD && core.debug.log(
280
- `[Profiling] Discarding profile because ${typeof profilesSampler === "function" ? "profileSampler returned 0 or false" : "a negative sampling decision was inherited or profileSampleRate is set to 0"}`
281
- );
282
- return;
283
- }
284
- const sampled = profilesSampleRate === true ? true : Math.random() < profilesSampleRate;
285
- if (!sampled) {
286
- DEBUG_BUILD && core.debug.log(
287
- `[Profiling] Discarding profile because it's not included in the random sample (sampling rate = ${Number(
288
- profilesSampleRate
289
- )})`
290
- );
291
- return;
292
- }
293
- const profile_id = core.uuid4();
294
- nodeCpuProfiler.CpuProfilerBindings.startProfiling(profile_id);
295
- DEBUG_BUILD && core.debug.log(`[Profiling] started profiling transaction: ${core.spanToJSON(span).description}`);
296
- return profile_id;
297
- }
298
- function stopSpanProfile(span, profile_id) {
299
- if (!profile_id) {
300
- return null;
301
- }
302
- const profile = nodeCpuProfiler.CpuProfilerBindings.stopProfiling(profile_id, 0);
303
- DEBUG_BUILD && core.debug.log(`[Profiling] stopped profiling of transaction: ${core.spanToJSON(span).description}`);
304
- if (!profile) {
305
- DEBUG_BUILD && core.debug.log(
306
- `[Profiling] profiler returned null profile for: ${core.spanToJSON(span).description}`,
307
- "this may indicate an overlapping span or a call to stopProfiling with a profile title that was never started"
308
- );
309
- return null;
310
- }
311
- profile.profile_id = profile_id;
312
- return profile;
313
- }
314
-
315
112
  const CHUNK_INTERVAL_MS = 1e3 * 60;
316
- const PROFILE_MAP = new core.LRUMap(50);
317
- const PROFILE_TIMEOUTS = {};
318
- function addToProfileQueue(profile_id, profile) {
319
- PROFILE_MAP.set(profile_id, profile);
320
- }
321
- function takeFromProfileQueue(profile_id) {
322
- const profile = PROFILE_MAP.get(profile_id);
323
- PROFILE_MAP.remove(profile_id);
324
- return profile;
325
- }
326
113
  class ContinuousProfiler {
327
114
  constructor() {
328
115
  this._client = void 0;
329
116
  this._chunkData = void 0;
330
- this._mode = void 0;
331
- this._legacyProfilerMode = void 0;
332
117
  this._profileLifecycle = void 0;
333
118
  this._sampled = void 0;
334
119
  this._sessionSamplingRate = void 0;
@@ -347,51 +132,21 @@ class ContinuousProfiler {
347
132
  }
348
133
  this._client = client;
349
134
  const options = client.getOptions();
350
- this._mode = getProfilingMode(options);
351
135
  this._sessionSamplingRate = Math.random();
352
136
  this._sampled = this._sessionSamplingRate < (options.profileSessionSampleRate ?? 0);
353
137
  this._profileLifecycle = options.profileLifecycle ?? "manual";
354
- switch (this._mode) {
355
- case "legacy": {
356
- this._legacyProfilerMode = "profilesSampleRate" in options || "profilesSampler" in options ? "span" : "continuous";
357
- DEBUG_BUILD && core.debug.log(`[Profiling] Profiling mode is ${this._legacyProfilerMode}.`);
358
- switch (this._legacyProfilerMode) {
359
- case "span": {
360
- this._setupAutomaticSpanProfiling();
361
- break;
362
- }
363
- case "continuous": {
364
- break;
365
- }
366
- default: {
367
- DEBUG_BUILD && core.debug.warn(
368
- `[Profiling] Unknown profiler mode: ${this._legacyProfilerMode}, profiler was not initialized`
369
- );
370
- break;
371
- }
372
- }
138
+ this._setupSpanChunkInstrumentation();
139
+ DEBUG_BUILD && core.debug.log(`[Profiling] Profiling mode is ${this._profileLifecycle}.`);
140
+ switch (this._profileLifecycle) {
141
+ case "trace": {
142
+ this._startTraceLifecycleProfiling();
373
143
  break;
374
144
  }
375
- case "current": {
376
- this._setupSpanChunkInstrumentation();
377
- DEBUG_BUILD && core.debug.log(`[Profiling] Profiling mode is ${this._profileLifecycle}.`);
378
- switch (this._profileLifecycle) {
379
- case "trace": {
380
- this._startTraceLifecycleProfiling();
381
- break;
382
- }
383
- case "manual": {
384
- break;
385
- }
386
- default: {
387
- DEBUG_BUILD && core.debug.warn(`[Profiling] Unknown profiler mode: ${this._profileLifecycle}, profiler was not initialized`);
388
- break;
389
- }
390
- }
145
+ case "manual": {
391
146
  break;
392
147
  }
393
148
  default: {
394
- DEBUG_BUILD && core.debug.warn(`[Profiling] Unknown profiler mode: ${this._mode}, profiler was not initialized`);
149
+ DEBUG_BUILD && core.debug.warn(`[Profiling] Unknown profiler mode: ${this._profileLifecycle}, profiler was not initialized`);
395
150
  break;
396
151
  }
397
152
  }
@@ -402,64 +157,26 @@ class ContinuousProfiler {
402
157
  * @returns void
403
158
  */
404
159
  start() {
405
- if (this._mode === "current") {
406
- this._startProfiler();
407
- return;
408
- }
409
160
  if (!this._client) {
410
- DEBUG_BUILD && core.debug.log("[Profiling] Failed to start, sentry client was never attached to the profiler.");
411
- return;
412
- }
413
- if (this._mode !== "legacy") {
414
- DEBUG_BUILD && core.debug.log("[Profiling] Continuous profiling is not supported in the current mode.");
415
- return;
416
- }
417
- if (this._legacyProfilerMode === "span") {
418
- DEBUG_BUILD && core.debug.log("[Profiling] Calls to profiler.start() are not supported in span profiling mode.");
161
+ DEBUG_BUILD && core.debug.log("[Profiling] Failed to start, Sentry client was never attached to the profiler.");
419
162
  return;
420
163
  }
421
- this._stopChunkProfiling();
422
- this._setupSpanChunkInstrumentation();
423
- this._restartChunkProfiling();
164
+ this._startProfiler();
424
165
  }
425
166
  /**
426
167
  * Stops the current chunk and flushes the profile to Sentry.
427
- * @returns void
428
168
  */
429
169
  stop() {
430
- if (this._mode === "current") {
431
- this._stopProfiler();
432
- return;
433
- }
434
- if (!this._client) {
435
- DEBUG_BUILD && core.debug.log("[Profiling] Failed to stop, sentry client was never attached to the profiler.");
436
- return;
437
- }
438
- if (this._mode !== "legacy") {
439
- DEBUG_BUILD && core.debug.log("[Profiling] Continuous profiling is not supported in the current mode.");
440
- return;
441
- }
442
- if (this._legacyProfilerMode === "span") {
443
- DEBUG_BUILD && core.debug.log("[Profiling] Calls to profiler.stop() are not supported in span profiling mode.");
444
- return;
445
- }
446
- this._stopChunkProfiling();
447
- this._teardownSpanChunkInstrumentation();
170
+ this._stopProfiler();
448
171
  }
449
172
  _startProfiler() {
450
- if (this._mode !== "current") {
451
- DEBUG_BUILD && core.debug.log("[Profiling] Continuous profiling is not supported in the current mode.");
452
- return;
453
- }
454
173
  if (this._chunkData !== void 0) {
455
174
  DEBUG_BUILD && core.debug.log("[Profiling] Profile session already running, no-op.");
456
175
  return;
457
176
  }
458
- if (this._mode === "current") {
459
- if (!this._sampled) {
460
- DEBUG_BUILD && core.debug.log("[Profiling] Profile session not sampled, no-op.");
461
- return;
462
- }
177
+ if (!this._sampled) {
178
+ DEBUG_BUILD && core.debug.log("[Profiling] Profile session not sampled, no-op.");
179
+ return;
463
180
  }
464
181
  if (this._profileLifecycle === "trace") {
465
182
  DEBUG_BUILD && core.debug.log(
@@ -470,10 +187,6 @@ class ContinuousProfiler {
470
187
  this._startChunkProfiling();
471
188
  }
472
189
  _stopProfiler() {
473
- if (this._mode !== "current") {
474
- DEBUG_BUILD && core.debug.log("[Profiling] Continuous profiling is not supported in the current mode.");
475
- return;
476
- }
477
190
  if (this._profileLifecycle === "trace") {
478
191
  DEBUG_BUILD && core.debug.log(
479
192
  "[Profiling] You are using the trace profile lifecycle, manual calls to profiler.startProfiler() and profiler.stopProfiler() will be ignored."
@@ -514,90 +227,6 @@ class ContinuousProfiler {
514
227
  activeSpanCounter--;
515
228
  });
516
229
  }
517
- _setupAutomaticSpanProfiling() {
518
- if (!this._client) {
519
- DEBUG_BUILD && core.debug.log(
520
- "[Profiling] Failed to setup automatic span profiling, sentry client was never attached to the profiler."
521
- );
522
- return;
523
- }
524
- const spanToProfileIdMap = /* @__PURE__ */ new WeakMap();
525
- this._client.on("spanStart", (span) => {
526
- if (span !== core.getRootSpan(span)) {
527
- return;
528
- }
529
- const profile_id = maybeProfileSpan(this._client, span);
530
- if (profile_id) {
531
- const options = this._client.getOptions();
532
- const maxProfileDurationMs = options._experiments?.maxProfileDurationMs || MAX_PROFILE_DURATION_MS;
533
- if (PROFILE_TIMEOUTS[profile_id]) {
534
- global.clearTimeout(PROFILE_TIMEOUTS[profile_id]);
535
- delete PROFILE_TIMEOUTS[profile_id];
536
- }
537
- const timeout = global.setTimeout(() => {
538
- DEBUG_BUILD && core.debug.log(
539
- "[Profiling] max profile duration elapsed, stopping profiling for:",
540
- core.spanToJSON(span).description
541
- );
542
- const profile = stopSpanProfile(span, profile_id);
543
- if (profile) {
544
- addToProfileQueue(profile_id, profile);
545
- }
546
- }, maxProfileDurationMs);
547
- timeout.unref();
548
- core.getIsolationScope().setContext("profile", { profile_id });
549
- spanToProfileIdMap.set(span, profile_id);
550
- }
551
- });
552
- this._client.on("spanEnd", (span) => {
553
- const profile_id = spanToProfileIdMap.get(span);
554
- if (profile_id) {
555
- if (PROFILE_TIMEOUTS[profile_id]) {
556
- global.clearTimeout(PROFILE_TIMEOUTS[profile_id]);
557
- delete PROFILE_TIMEOUTS[profile_id];
558
- }
559
- const profile = stopSpanProfile(span, profile_id);
560
- if (profile) {
561
- addToProfileQueue(profile_id, profile);
562
- }
563
- }
564
- });
565
- this._client.on("beforeEnvelope", (envelope) => {
566
- if (!PROFILE_MAP.size) {
567
- return;
568
- }
569
- const profiledTransactionEvents = findProfiledTransactionsFromEnvelope(envelope);
570
- if (!profiledTransactionEvents.length) {
571
- return;
572
- }
573
- const profilesToAddToEnvelope = [];
574
- for (const profiledTransaction of profiledTransactionEvents) {
575
- const profileContext = profiledTransaction.contexts?.profile;
576
- const profile_id = profileContext?.profile_id;
577
- if (!profile_id) {
578
- throw new TypeError("[Profiling] cannot find profile for a transaction without a profile context");
579
- }
580
- if (profileContext) {
581
- delete profiledTransaction.contexts?.profile;
582
- }
583
- const cpuProfile = takeFromProfileQueue(profile_id);
584
- if (!cpuProfile) {
585
- DEBUG_BUILD && core.debug.log(`[Profiling] Could not retrieve profile for transaction: ${profile_id}`);
586
- continue;
587
- }
588
- const profile = createProfilingEvent(this._client, cpuProfile, profiledTransaction);
589
- if (!profile) return;
590
- profilesToAddToEnvelope.push(profile);
591
- this._client.emit("preprocessEvent", profile, {
592
- event_id: profiledTransaction.event_id
593
- });
594
- this._client.emit("postprocessEvent", profile, {
595
- event_id: profiledTransaction.event_id
596
- });
597
- }
598
- addProfilesToEnvelope(envelope, profilesToAddToEnvelope);
599
- });
600
- }
601
230
  /**
602
231
  * Stop profiler and initializes profiling of the next chunk
603
232
  */
@@ -801,15 +430,6 @@ const _nodeProfilingIntegration = (() => {
801
430
  }
802
431
  };
803
432
  });
804
- function getProfilingMode(options) {
805
- if ("profilesSampleRate" in options || "profilesSampler" in options) {
806
- return "legacy";
807
- }
808
- if ("profileSessionSampleRate" in options || "profileLifecycle" in options) {
809
- return "current";
810
- }
811
- return "legacy";
812
- }
813
433
  const nodeProfilingIntegration = core.defineIntegration(_nodeProfilingIntegration);
814
434
 
815
435
  exports.nodeProfilingIntegration = nodeProfilingIntegration;