@sentry/profiling-node 10.53.1 → 10.54.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.
- package/build/cjs/index.js +246 -626
- package/build/cjs/index.js.map +1 -1
- package/build/esm/index.js +246 -626
- package/build/esm/index.js.map +1 -1
- package/build/esm/package.json +1 -1
- package/package.json +3 -3
package/build/esm/index.js
CHANGED
|
@@ -4,622 +4,385 @@ import { isMainThread, threadId } from 'worker_threads';
|
|
|
4
4
|
import * as os from 'os';
|
|
5
5
|
import { env, versions } from 'process';
|
|
6
6
|
|
|
7
|
-
/**
|
|
8
|
-
* 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.
|
|
9
|
-
*
|
|
10
|
-
* ATTENTION: This constant must never cross package boundaries (i.e. be exported) to guarantee that it can be used for tree shaking.
|
|
11
|
-
*/
|
|
12
7
|
const DEBUG_BUILD = (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__);
|
|
13
8
|
|
|
14
|
-
const NODE_VERSION = parseSemver(process.versions.node)
|
|
9
|
+
const NODE_VERSION = parseSemver(process.versions.node);
|
|
15
10
|
const NODE_MAJOR = NODE_VERSION.major;
|
|
16
11
|
|
|
17
|
-
// We require the file because if we import it, it will be included in the bundle.
|
|
18
|
-
// I guess tsc does not check file contents when it's imported.
|
|
19
12
|
const PROFILER_THREAD_ID_STRING = String(threadId);
|
|
20
|
-
const PROFILER_THREAD_NAME = isMainThread ?
|
|
21
|
-
const FORMAT_VERSION =
|
|
22
|
-
const CONTINUOUS_FORMAT_VERSION =
|
|
23
|
-
|
|
24
|
-
// Machine properties (eval only once)
|
|
13
|
+
const PROFILER_THREAD_NAME = isMainThread ? "main" : "worker";
|
|
14
|
+
const FORMAT_VERSION = "1";
|
|
15
|
+
const CONTINUOUS_FORMAT_VERSION = "2";
|
|
25
16
|
const PLATFORM = os.platform();
|
|
26
17
|
const RELEASE = os.release();
|
|
27
18
|
const VERSION = os.version();
|
|
28
19
|
const TYPE = os.type();
|
|
29
20
|
const MODEL = os.machine();
|
|
30
21
|
const ARCH = os.arch();
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
* Checks if the profile is a raw profile or a profile enriched with thread information.
|
|
34
|
-
* @param {ThreadCpuProfile | RawThreadCpuProfile} profile
|
|
35
|
-
* @returns {boolean}
|
|
36
|
-
*/
|
|
37
|
-
function isRawThreadCpuProfile(
|
|
38
|
-
profile,
|
|
39
|
-
) {
|
|
40
|
-
return !('thread_metadata' in profile);
|
|
22
|
+
function isRawThreadCpuProfile(profile) {
|
|
23
|
+
return !("thread_metadata" in profile);
|
|
41
24
|
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Enriches the profile with threadId of the current thread.
|
|
45
|
-
* This is done in node as we seem to not be able to get the info from C native code.
|
|
46
|
-
*
|
|
47
|
-
* @param {ThreadCpuProfile | RawThreadCpuProfile} profile
|
|
48
|
-
* @returns {ThreadCpuProfile}
|
|
49
|
-
*/
|
|
50
|
-
function enrichWithThreadInformation(
|
|
51
|
-
profile,
|
|
52
|
-
) {
|
|
25
|
+
function enrichWithThreadInformation(profile) {
|
|
53
26
|
if (!isRawThreadCpuProfile(profile)) {
|
|
54
27
|
return profile;
|
|
55
28
|
}
|
|
56
|
-
|
|
57
29
|
return {
|
|
58
30
|
samples: profile.samples,
|
|
59
31
|
frames: profile.frames,
|
|
60
32
|
stacks: profile.stacks,
|
|
61
33
|
thread_metadata: {
|
|
62
34
|
[PROFILER_THREAD_ID_STRING]: {
|
|
63
|
-
name: PROFILER_THREAD_NAME
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
35
|
+
name: PROFILER_THREAD_NAME
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
};
|
|
67
39
|
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Creates a profiling envelope item, if the profile does not pass validation, returns null.
|
|
71
|
-
* @param {RawThreadCpuProfile}
|
|
72
|
-
* @param {Event}
|
|
73
|
-
* @returns {Profile | null}
|
|
74
|
-
*/
|
|
75
40
|
function createProfilingEvent(client, profile, event) {
|
|
76
41
|
if (!isValidProfile(profile)) {
|
|
77
42
|
return null;
|
|
78
43
|
}
|
|
79
|
-
|
|
80
44
|
return createProfilePayload(client, profile, {
|
|
81
|
-
release: event.release ??
|
|
82
|
-
environment: event.environment ??
|
|
83
|
-
event_id: event.event_id ??
|
|
84
|
-
transaction: event.transaction ??
|
|
85
|
-
start_timestamp: event.start_timestamp ? event.start_timestamp *
|
|
86
|
-
trace_id: event.contexts?.trace?.trace_id ??
|
|
87
|
-
profile_id: profile.profile_id
|
|
45
|
+
release: event.release ?? "",
|
|
46
|
+
environment: event.environment ?? "",
|
|
47
|
+
event_id: event.event_id ?? "",
|
|
48
|
+
transaction: event.transaction ?? "",
|
|
49
|
+
start_timestamp: event.start_timestamp ? event.start_timestamp * 1e3 : Date.now(),
|
|
50
|
+
trace_id: event.contexts?.trace?.trace_id ?? "",
|
|
51
|
+
profile_id: profile.profile_id
|
|
88
52
|
});
|
|
89
53
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
cpuProfile,
|
|
100
|
-
{
|
|
101
|
-
release,
|
|
102
|
-
environment,
|
|
103
|
-
event_id,
|
|
104
|
-
transaction,
|
|
105
|
-
start_timestamp,
|
|
106
|
-
trace_id,
|
|
107
|
-
profile_id,
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
,
|
|
111
|
-
) {
|
|
112
|
-
// Log a warning if the profile has an invalid traceId (should be uuidv4).
|
|
113
|
-
// All profiles and transactions are rejected if this is the case and we want to
|
|
114
|
-
// warn users that this is happening if they enable debug flag
|
|
54
|
+
function createProfilePayload(client, cpuProfile, {
|
|
55
|
+
release,
|
|
56
|
+
environment,
|
|
57
|
+
event_id,
|
|
58
|
+
transaction,
|
|
59
|
+
start_timestamp,
|
|
60
|
+
trace_id,
|
|
61
|
+
profile_id
|
|
62
|
+
}) {
|
|
115
63
|
if (trace_id?.length !== 32) {
|
|
116
64
|
DEBUG_BUILD && debug.log(`[Profiling] Invalid traceId: ${trace_id} on profiled event`);
|
|
117
65
|
}
|
|
118
|
-
|
|
119
66
|
const enrichedThreadProfile = enrichWithThreadInformation(cpuProfile);
|
|
120
|
-
|
|
121
67
|
const profile = {
|
|
122
68
|
event_id: profile_id,
|
|
123
69
|
timestamp: new Date(start_timestamp).toISOString(),
|
|
124
|
-
platform:
|
|
70
|
+
platform: "node",
|
|
125
71
|
version: FORMAT_VERSION,
|
|
126
|
-
release
|
|
127
|
-
environment
|
|
72
|
+
release,
|
|
73
|
+
environment,
|
|
128
74
|
measurements: cpuProfile.measurements,
|
|
129
75
|
runtime: {
|
|
130
|
-
name:
|
|
131
|
-
version: versions.node ||
|
|
76
|
+
name: "node",
|
|
77
|
+
version: versions.node || ""
|
|
132
78
|
},
|
|
133
79
|
os: {
|
|
134
80
|
name: PLATFORM,
|
|
135
81
|
version: RELEASE,
|
|
136
|
-
build_number: VERSION
|
|
82
|
+
build_number: VERSION
|
|
137
83
|
},
|
|
138
84
|
device: {
|
|
139
|
-
locale: env[
|
|
85
|
+
locale: env["LC_ALL"] || env["LC_MESSAGES"] || env["LANG"] || env["LANGUAGE"] || "",
|
|
140
86
|
model: MODEL,
|
|
141
87
|
manufacturer: TYPE,
|
|
142
88
|
architecture: ARCH,
|
|
143
|
-
is_emulator: false
|
|
89
|
+
is_emulator: false
|
|
144
90
|
},
|
|
145
91
|
debug_meta: {
|
|
146
|
-
images: applyDebugMetadata(client, cpuProfile.resources)
|
|
92
|
+
images: applyDebugMetadata(client, cpuProfile.resources)
|
|
147
93
|
},
|
|
148
|
-
profile: enrichedThreadProfile
|
|
94
|
+
profile: enrichedThreadProfile,
|
|
149
95
|
transaction: {
|
|
150
96
|
name: transaction,
|
|
151
97
|
id: event_id,
|
|
152
|
-
trace_id: trace_id ||
|
|
153
|
-
active_thread_id: PROFILER_THREAD_ID_STRING
|
|
154
|
-
}
|
|
98
|
+
trace_id: trace_id || "",
|
|
99
|
+
active_thread_id: PROFILER_THREAD_ID_STRING
|
|
100
|
+
}
|
|
155
101
|
};
|
|
156
|
-
|
|
157
102
|
return profile;
|
|
158
103
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
client,
|
|
168
|
-
cpuProfile,
|
|
169
|
-
{
|
|
170
|
-
release,
|
|
171
|
-
environment,
|
|
172
|
-
trace_id,
|
|
173
|
-
profiler_id,
|
|
174
|
-
chunk_id,
|
|
175
|
-
sdk,
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
,
|
|
179
|
-
) {
|
|
180
|
-
// Log a warning if the profile has an invalid traceId (should be uuidv4).
|
|
181
|
-
// All profiles and transactions are rejected if this is the case and we want to
|
|
182
|
-
// warn users that this is happening if they enable debug flag
|
|
104
|
+
function createProfileChunkPayload(client, cpuProfile, {
|
|
105
|
+
release,
|
|
106
|
+
environment,
|
|
107
|
+
trace_id,
|
|
108
|
+
profiler_id,
|
|
109
|
+
chunk_id,
|
|
110
|
+
sdk
|
|
111
|
+
}) {
|
|
183
112
|
if (trace_id?.length !== 32) {
|
|
184
113
|
DEBUG_BUILD && debug.log(`[Profiling] Invalid traceId: ${trace_id} on profiled event`);
|
|
185
114
|
}
|
|
186
|
-
|
|
187
115
|
const enrichedThreadProfile = enrichWithThreadInformation(cpuProfile);
|
|
188
|
-
|
|
189
116
|
const profile = {
|
|
190
|
-
chunk_id
|
|
117
|
+
chunk_id,
|
|
191
118
|
client_sdk: {
|
|
192
|
-
name: sdk?.name ??
|
|
193
|
-
version: sdk?.version ??
|
|
119
|
+
name: sdk?.name ?? "sentry.javascript.node",
|
|
120
|
+
version: sdk?.version ?? "0.0.0"
|
|
194
121
|
},
|
|
195
|
-
profiler_id
|
|
196
|
-
platform:
|
|
122
|
+
profiler_id,
|
|
123
|
+
platform: "node",
|
|
197
124
|
version: CONTINUOUS_FORMAT_VERSION,
|
|
198
|
-
release
|
|
199
|
-
environment
|
|
125
|
+
release,
|
|
126
|
+
environment,
|
|
200
127
|
measurements: cpuProfile.measurements,
|
|
201
128
|
debug_meta: {
|
|
202
|
-
images: applyDebugMetadata(client, cpuProfile.resources)
|
|
129
|
+
images: applyDebugMetadata(client, cpuProfile.resources)
|
|
203
130
|
},
|
|
204
|
-
profile: enrichedThreadProfile
|
|
131
|
+
profile: enrichedThreadProfile
|
|
205
132
|
};
|
|
206
|
-
|
|
207
133
|
return profile;
|
|
208
134
|
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Creates a profiling chunk envelope item, if the profile does not pass validation, returns null.
|
|
212
|
-
*/
|
|
213
|
-
function createProfilingChunkEvent(
|
|
214
|
-
client,
|
|
215
|
-
options,
|
|
216
|
-
profile,
|
|
217
|
-
sdk,
|
|
218
|
-
identifiers,
|
|
219
|
-
) {
|
|
135
|
+
function createProfilingChunkEvent(client, options, profile, sdk, identifiers) {
|
|
220
136
|
if (!isValidProfileChunk(profile)) {
|
|
221
137
|
return null;
|
|
222
138
|
}
|
|
223
|
-
|
|
224
139
|
return createProfileChunkPayload(client, profile, {
|
|
225
|
-
release: options.release ??
|
|
226
|
-
environment: options.environment ??
|
|
227
|
-
trace_id: identifiers.trace_id ??
|
|
140
|
+
release: options.release ?? "",
|
|
141
|
+
environment: options.environment ?? "",
|
|
142
|
+
trace_id: identifiers.trace_id ?? "",
|
|
228
143
|
chunk_id: identifiers.chunk_id,
|
|
229
144
|
profiler_id: identifiers.profiler_id,
|
|
230
|
-
sdk
|
|
145
|
+
sdk
|
|
231
146
|
});
|
|
232
147
|
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Checks the given sample rate to make sure it is valid type and value (a boolean, or a number between 0 and 1).
|
|
236
|
-
* @param {unknown} rate
|
|
237
|
-
* @returns {boolean}
|
|
238
|
-
*/
|
|
239
148
|
function isValidSampleRate(rate) {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
)} of type ${JSON.stringify(typeof rate)}.`,
|
|
247
|
-
);
|
|
149
|
+
if (typeof rate !== "number" && typeof rate !== "boolean" || typeof rate === "number" && isNaN(rate)) {
|
|
150
|
+
DEBUG_BUILD && debug.warn(
|
|
151
|
+
`[Profiling] Invalid sample rate. Sample rate must be a boolean or a number between 0 and 1. Got ${JSON.stringify(
|
|
152
|
+
rate
|
|
153
|
+
)} of type ${JSON.stringify(typeof rate)}.`
|
|
154
|
+
);
|
|
248
155
|
return false;
|
|
249
156
|
}
|
|
250
|
-
|
|
251
|
-
// Boolean sample rates are always valid
|
|
252
157
|
if (rate === true || rate === false) {
|
|
253
158
|
return true;
|
|
254
159
|
}
|
|
255
|
-
|
|
256
|
-
// in case sampleRate is a boolean, it will get automatically cast to 1 if it's true and 0 if it's false
|
|
257
160
|
if (rate < 0 || rate > 1) {
|
|
258
161
|
DEBUG_BUILD && debug.warn(`[Profiling] Invalid sample rate. Sample rate must be between 0 and 1. Got ${rate}.`);
|
|
259
162
|
return false;
|
|
260
163
|
}
|
|
261
164
|
return true;
|
|
262
165
|
}
|
|
263
|
-
|
|
264
|
-
/**
|
|
265
|
-
* Checks if the profile is valid and can be sent to Sentry.
|
|
266
|
-
* @param {RawThreadCpuProfile} profile
|
|
267
|
-
* @returns {boolean}
|
|
268
|
-
*/
|
|
269
166
|
function isValidProfile(profile) {
|
|
270
167
|
if (profile.samples.length <= 1) {
|
|
271
|
-
DEBUG_BUILD &&
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
debug.log('[Profiling] Discarding profile because it contains less than 2 samples');
|
|
168
|
+
DEBUG_BUILD && // Log a warning if the profile has less than 2 samples so users can know why
|
|
169
|
+
// they are not seeing any profiling data and we cant avoid the back and forth
|
|
170
|
+
// of asking them to provide us with a dump of the profile data.
|
|
171
|
+
debug.log("[Profiling] Discarding profile because it contains less than 2 samples");
|
|
276
172
|
return false;
|
|
277
173
|
}
|
|
278
|
-
|
|
279
174
|
if (!profile.profile_id) {
|
|
280
175
|
return false;
|
|
281
176
|
}
|
|
282
|
-
|
|
283
177
|
return true;
|
|
284
178
|
}
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* Checks if the profile chunk is valid and can be sent to Sentry.
|
|
288
|
-
* @param profile
|
|
289
|
-
* @returns
|
|
290
|
-
*/
|
|
291
179
|
function isValidProfileChunk(profile) {
|
|
292
180
|
if (profile.samples.length <= 1) {
|
|
293
|
-
DEBUG_BUILD &&
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
debug.log('[Profiling] Discarding profile chunk because it contains less than 2 samples');
|
|
181
|
+
DEBUG_BUILD && // Log a warning if the profile has less than 2 samples so users can know why
|
|
182
|
+
// they are not seeing any profiling data and we cant avoid the back and forth
|
|
183
|
+
// of asking them to provide us with a dump of the profile data.
|
|
184
|
+
debug.log("[Profiling] Discarding profile chunk because it contains less than 2 samples");
|
|
298
185
|
return false;
|
|
299
186
|
}
|
|
300
|
-
|
|
301
187
|
return true;
|
|
302
188
|
}
|
|
303
|
-
|
|
304
|
-
/**
|
|
305
|
-
* Adds items to envelope if they are not already present - mutates the envelope.
|
|
306
|
-
* @param {Envelope} envelope
|
|
307
|
-
* @param {Profile[]} profiles
|
|
308
|
-
* @returns {Envelope}
|
|
309
|
-
*/
|
|
310
189
|
function addProfilesToEnvelope(envelope, profiles) {
|
|
311
190
|
if (!profiles.length) {
|
|
312
191
|
return envelope;
|
|
313
192
|
}
|
|
314
|
-
|
|
315
193
|
for (const profile of profiles) {
|
|
316
|
-
|
|
317
|
-
envelope[1].push([{ type: 'profile' }, profile]);
|
|
194
|
+
envelope[1].push([{ type: "profile" }, profile]);
|
|
318
195
|
}
|
|
319
196
|
return envelope;
|
|
320
197
|
}
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* Finds transactions with profile_id context in the envelope
|
|
324
|
-
* @param {Envelope} envelope
|
|
325
|
-
* @returns {Event[]}
|
|
326
|
-
*/
|
|
327
198
|
function findProfiledTransactionsFromEnvelope(envelope) {
|
|
328
199
|
const events = [];
|
|
329
|
-
|
|
330
200
|
forEachEnvelopeItem(envelope, (item, type) => {
|
|
331
|
-
if (type !==
|
|
201
|
+
if (type !== "transaction") {
|
|
332
202
|
return;
|
|
333
203
|
}
|
|
334
|
-
|
|
335
|
-
// First item is the type, so we can skip it, everything else is an event
|
|
336
204
|
for (let j = 1; j < item.length; j++) {
|
|
337
|
-
const event = item[j]
|
|
338
|
-
|
|
205
|
+
const event = item[j];
|
|
339
206
|
if (!event) {
|
|
340
|
-
// Shouldn't happen, but lets be safe
|
|
341
207
|
continue;
|
|
342
208
|
}
|
|
343
|
-
|
|
344
209
|
const profile_id = event.contexts?.profile?.profile_id;
|
|
345
|
-
|
|
346
210
|
if (event && profile_id) {
|
|
347
211
|
events.push(event);
|
|
348
212
|
}
|
|
349
213
|
}
|
|
350
214
|
});
|
|
351
|
-
|
|
352
215
|
return events;
|
|
353
216
|
}
|
|
354
|
-
|
|
355
|
-
/**
|
|
356
|
-
* Creates event envelope headers for a profile chunk. This is separate from createEventEnvelopeHeaders util
|
|
357
|
-
* as the profile chunk does not conform to the sentry event type
|
|
358
|
-
*/
|
|
359
|
-
function createEventEnvelopeHeaders(
|
|
360
|
-
sdkInfo,
|
|
361
|
-
tunnel,
|
|
362
|
-
dsn,
|
|
363
|
-
) {
|
|
217
|
+
function createEventEnvelopeHeaders(sdkInfo, tunnel, dsn) {
|
|
364
218
|
return {
|
|
365
219
|
event_id: uuid4(),
|
|
366
|
-
sent_at: new Date().toISOString(),
|
|
367
|
-
...
|
|
368
|
-
|
|
220
|
+
sent_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
221
|
+
...sdkInfo && { sdk: sdkInfo },
|
|
222
|
+
...!!tunnel && dsn && { dsn: dsnToString(dsn) }
|
|
369
223
|
};
|
|
370
224
|
}
|
|
371
|
-
|
|
372
|
-
/**
|
|
373
|
-
* Creates a standalone profile_chunk envelope.
|
|
374
|
-
*/
|
|
375
|
-
function makeProfileChunkEnvelope(
|
|
376
|
-
platform,
|
|
377
|
-
chunk,
|
|
378
|
-
sdkInfo,
|
|
379
|
-
tunnel,
|
|
380
|
-
dsn,
|
|
381
|
-
) {
|
|
225
|
+
function makeProfileChunkEnvelope(platform, chunk, sdkInfo, tunnel, dsn) {
|
|
382
226
|
const profileChunkHeader = {
|
|
383
|
-
type:
|
|
384
|
-
platform
|
|
227
|
+
type: "profile_chunk",
|
|
228
|
+
platform
|
|
385
229
|
};
|
|
386
|
-
|
|
387
230
|
return createEnvelope(createEventEnvelopeHeaders(sdkInfo, tunnel, dsn), [
|
|
388
|
-
[profileChunkHeader, chunk]
|
|
231
|
+
[profileChunkHeader, chunk]
|
|
389
232
|
]);
|
|
390
233
|
}
|
|
391
|
-
|
|
392
|
-
/**
|
|
393
|
-
* Cross reference profile collected resources with debug_ids and return a list of debug images.
|
|
394
|
-
* @param {string[]} resource_paths
|
|
395
|
-
* @returns {DebugImage[]}
|
|
396
|
-
*/
|
|
397
234
|
function applyDebugMetadata(client, resource_paths) {
|
|
398
235
|
const options = client.getOptions();
|
|
399
|
-
|
|
400
236
|
if (!options?.stackParser) {
|
|
401
237
|
return [];
|
|
402
238
|
}
|
|
403
|
-
|
|
404
239
|
return getDebugImagesForResources(options.stackParser, resource_paths);
|
|
405
240
|
}
|
|
406
241
|
|
|
407
|
-
const MAX_PROFILE_DURATION_MS = 30 *
|
|
408
|
-
|
|
409
|
-
/**
|
|
410
|
-
* Takes a transaction and determines if it should be profiled or not. If it should be profiled, it returns the
|
|
411
|
-
* profile_id, otherwise returns undefined. Takes care of setting profile context on transaction as well
|
|
412
|
-
*/
|
|
413
|
-
function maybeProfileSpan(
|
|
414
|
-
client,
|
|
415
|
-
span,
|
|
416
|
-
customSamplingContext,
|
|
417
|
-
) {
|
|
418
|
-
// Profiling is not supported in worker threads as the native CPU profiler's
|
|
419
|
-
// sampling thread can race with V8's GC across isolates, causing heap corruption.
|
|
242
|
+
const MAX_PROFILE_DURATION_MS = 30 * 1e3;
|
|
243
|
+
function maybeProfileSpan(client, span, customSamplingContext) {
|
|
420
244
|
if (!isMainThread) {
|
|
421
|
-
DEBUG_BUILD && debug.log(
|
|
245
|
+
DEBUG_BUILD && debug.log("[Profiling] Skipping span profiling in worker thread.");
|
|
422
246
|
return;
|
|
423
247
|
}
|
|
424
|
-
|
|
425
|
-
// profilesSampleRate is multiplied with tracesSampleRate to get the final sampling rate. We dont perform
|
|
426
|
-
// the actual multiplication to get the final rate, but we discard the profile if the span was sampled,
|
|
427
|
-
// so anything after this block from here is based on the span sampling.
|
|
428
248
|
if (!spanIsSampled(span)) {
|
|
429
249
|
return;
|
|
430
250
|
}
|
|
431
|
-
|
|
432
|
-
// Client and options are required for profiling
|
|
433
251
|
if (!client) {
|
|
434
|
-
DEBUG_BUILD && debug.log(
|
|
252
|
+
DEBUG_BUILD && debug.log("[Profiling] Profiling disabled, no client found.");
|
|
435
253
|
return;
|
|
436
254
|
}
|
|
437
|
-
|
|
438
255
|
const options = client.getOptions();
|
|
439
256
|
if (!options) {
|
|
440
|
-
DEBUG_BUILD && debug.log(
|
|
257
|
+
DEBUG_BUILD && debug.log("[Profiling] Profiling disabled, no options found.");
|
|
441
258
|
return;
|
|
442
259
|
}
|
|
443
|
-
|
|
444
260
|
const profilesSampler = options.profilesSampler;
|
|
445
261
|
let profilesSampleRate = options.profilesSampleRate;
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
if (typeof profilesSampler === 'function') {
|
|
449
|
-
const { description: spanName = '<unknown>', data } = spanToJSON(span);
|
|
450
|
-
// We bail out early if that is not the case
|
|
262
|
+
if (typeof profilesSampler === "function") {
|
|
263
|
+
const { description: spanName = "<unknown>", data } = spanToJSON(span);
|
|
451
264
|
const parentSampled = true;
|
|
452
|
-
|
|
453
265
|
profilesSampleRate = profilesSampler({
|
|
454
266
|
name: spanName,
|
|
455
267
|
attributes: data,
|
|
456
268
|
parentSampled,
|
|
457
|
-
...customSamplingContext
|
|
269
|
+
...customSamplingContext
|
|
458
270
|
});
|
|
459
271
|
}
|
|
460
|
-
|
|
461
|
-
// Since this is coming from the user (or from a function provided by the user), who knows what we might get. (The
|
|
462
|
-
// only valid values are booleans or numbers between 0 and 1.)
|
|
463
272
|
if (!isValidSampleRate(profilesSampleRate)) {
|
|
464
|
-
DEBUG_BUILD && debug.warn(
|
|
273
|
+
DEBUG_BUILD && debug.warn("[Profiling] Discarding profile because of invalid sample rate.");
|
|
465
274
|
return;
|
|
466
275
|
}
|
|
467
|
-
|
|
468
|
-
// if the function returned 0 (or false), or if `profileSampleRate` is 0, it's a sign the profile should be dropped
|
|
469
276
|
if (!profilesSampleRate) {
|
|
470
|
-
DEBUG_BUILD &&
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
typeof profilesSampler === 'function'
|
|
474
|
-
? 'profileSampler returned 0 or false'
|
|
475
|
-
: 'a negative sampling decision was inherited or profileSampleRate is set to 0'
|
|
476
|
-
}`,
|
|
477
|
-
);
|
|
277
|
+
DEBUG_BUILD && debug.log(
|
|
278
|
+
`[Profiling] Discarding profile because ${typeof profilesSampler === "function" ? "profileSampler returned 0 or false" : "a negative sampling decision was inherited or profileSampleRate is set to 0"}`
|
|
279
|
+
);
|
|
478
280
|
return;
|
|
479
281
|
}
|
|
480
|
-
|
|
481
|
-
// Now we roll the dice. Math.random is inclusive of 0, but not of 1, so strict < is safe here. In case sampleRate is
|
|
482
|
-
// a boolean, the < comparison will cause it to be automatically cast to 1 if it's true and 0 if it's false.
|
|
483
282
|
const sampled = profilesSampleRate === true ? true : Math.random() < profilesSampleRate;
|
|
484
|
-
// Check if we should sample this profile
|
|
485
283
|
if (!sampled) {
|
|
486
|
-
DEBUG_BUILD &&
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
);
|
|
284
|
+
DEBUG_BUILD && debug.log(
|
|
285
|
+
`[Profiling] Discarding profile because it's not included in the random sample (sampling rate = ${Number(
|
|
286
|
+
profilesSampleRate
|
|
287
|
+
)})`
|
|
288
|
+
);
|
|
492
289
|
return;
|
|
493
290
|
}
|
|
494
|
-
|
|
495
291
|
const profile_id = uuid4();
|
|
496
292
|
CpuProfilerBindings.startProfiling(profile_id);
|
|
497
293
|
DEBUG_BUILD && debug.log(`[Profiling] started profiling transaction: ${spanToJSON(span).description}`);
|
|
498
|
-
|
|
499
|
-
// set transaction context - do this regardless if profiling fails down the line
|
|
500
|
-
// so that we can still see the profile_id in the transaction context
|
|
501
294
|
return profile_id;
|
|
502
295
|
}
|
|
503
|
-
|
|
504
|
-
/**
|
|
505
|
-
* Stops the profiler for profile_id and returns the profile
|
|
506
|
-
* @param transaction
|
|
507
|
-
* @param profile_id
|
|
508
|
-
* @returns
|
|
509
|
-
*/
|
|
510
296
|
function stopSpanProfile(span, profile_id) {
|
|
511
|
-
// Should not happen, but satisfy the type checker and be safe regardless.
|
|
512
297
|
if (!profile_id) {
|
|
513
298
|
return null;
|
|
514
299
|
}
|
|
515
|
-
|
|
516
300
|
const profile = CpuProfilerBindings.stopProfiling(profile_id, 0);
|
|
517
301
|
DEBUG_BUILD && debug.log(`[Profiling] stopped profiling of transaction: ${spanToJSON(span).description}`);
|
|
518
|
-
|
|
519
|
-
// In case of an overlapping span, stopProfiling may return null and silently ignore the overlapping profile.
|
|
520
302
|
if (!profile) {
|
|
521
|
-
DEBUG_BUILD &&
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
);
|
|
303
|
+
DEBUG_BUILD && debug.log(
|
|
304
|
+
`[Profiling] profiler returned null profile for: ${spanToJSON(span).description}`,
|
|
305
|
+
"this may indicate an overlapping span or a call to stopProfiling with a profile title that was never started"
|
|
306
|
+
);
|
|
526
307
|
return null;
|
|
527
308
|
}
|
|
528
|
-
|
|
529
|
-
// Assign profile_id to the profile
|
|
530
309
|
profile.profile_id = profile_id;
|
|
531
310
|
return profile;
|
|
532
311
|
}
|
|
533
312
|
|
|
534
|
-
const CHUNK_INTERVAL_MS =
|
|
313
|
+
const CHUNK_INTERVAL_MS = 1e3 * 60;
|
|
535
314
|
const PROFILE_MAP = new LRUMap(50);
|
|
536
315
|
const PROFILE_TIMEOUTS = {};
|
|
537
|
-
|
|
538
316
|
function addToProfileQueue(profile_id, profile) {
|
|
539
317
|
PROFILE_MAP.set(profile_id, profile);
|
|
540
318
|
}
|
|
541
|
-
|
|
542
319
|
function takeFromProfileQueue(profile_id) {
|
|
543
320
|
const profile = PROFILE_MAP.get(profile_id);
|
|
544
321
|
PROFILE_MAP.remove(profile_id);
|
|
545
322
|
return profile;
|
|
546
323
|
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
324
|
+
class ContinuousProfiler {
|
|
325
|
+
constructor() {
|
|
326
|
+
this._client = void 0;
|
|
327
|
+
this._chunkData = void 0;
|
|
328
|
+
this._mode = void 0;
|
|
329
|
+
this._legacyProfilerMode = void 0;
|
|
330
|
+
this._profileLifecycle = void 0;
|
|
331
|
+
this._sampled = void 0;
|
|
332
|
+
this._sessionSamplingRate = void 0;
|
|
333
|
+
}
|
|
557
334
|
/**
|
|
558
335
|
* Called when the profiler is attached to the client (continuous mode is enabled). If of the profiler
|
|
559
336
|
* methods called before the profiler is initialized will result in a noop action with debug logs.
|
|
560
337
|
* @param client
|
|
561
338
|
*/
|
|
562
|
-
|
|
339
|
+
initialize(client) {
|
|
563
340
|
if (!isMainThread) {
|
|
564
|
-
DEBUG_BUILD &&
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
);
|
|
341
|
+
DEBUG_BUILD && debug.warn(
|
|
342
|
+
"[Profiling] nodeProfilingIntegration() does not support worker threads \u2014 profiling will be disabled for this thread."
|
|
343
|
+
);
|
|
568
344
|
return;
|
|
569
345
|
}
|
|
570
|
-
|
|
571
346
|
this._client = client;
|
|
572
347
|
const options = client.getOptions();
|
|
573
|
-
|
|
574
348
|
this._mode = getProfilingMode(options);
|
|
575
349
|
this._sessionSamplingRate = Math.random();
|
|
576
350
|
this._sampled = this._sessionSamplingRate < (options.profileSessionSampleRate ?? 0);
|
|
577
|
-
this._profileLifecycle = options.profileLifecycle ??
|
|
578
|
-
|
|
351
|
+
this._profileLifecycle = options.profileLifecycle ?? "manual";
|
|
579
352
|
switch (this._mode) {
|
|
580
|
-
case
|
|
581
|
-
this._legacyProfilerMode =
|
|
582
|
-
'profilesSampleRate' in options || 'profilesSampler' in options ? 'span' : 'continuous';
|
|
583
|
-
|
|
353
|
+
case "legacy": {
|
|
354
|
+
this._legacyProfilerMode = "profilesSampleRate" in options || "profilesSampler" in options ? "span" : "continuous";
|
|
584
355
|
DEBUG_BUILD && debug.log(`[Profiling] Profiling mode is ${this._legacyProfilerMode}.`);
|
|
585
|
-
|
|
586
356
|
switch (this._legacyProfilerMode) {
|
|
587
|
-
case
|
|
357
|
+
case "span": {
|
|
588
358
|
this._setupAutomaticSpanProfiling();
|
|
589
359
|
break;
|
|
590
360
|
}
|
|
591
|
-
case
|
|
592
|
-
// Continous mode requires manual calls to profiler.start() and profiler.stop()
|
|
361
|
+
case "continuous": {
|
|
593
362
|
break;
|
|
594
363
|
}
|
|
595
364
|
default: {
|
|
596
|
-
DEBUG_BUILD &&
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
);
|
|
365
|
+
DEBUG_BUILD && debug.warn(
|
|
366
|
+
`[Profiling] Unknown profiler mode: ${this._legacyProfilerMode}, profiler was not initialized`
|
|
367
|
+
);
|
|
600
368
|
break;
|
|
601
369
|
}
|
|
602
370
|
}
|
|
603
371
|
break;
|
|
604
372
|
}
|
|
605
|
-
|
|
606
|
-
case 'current': {
|
|
373
|
+
case "current": {
|
|
607
374
|
this._setupSpanChunkInstrumentation();
|
|
608
|
-
|
|
609
375
|
DEBUG_BUILD && debug.log(`[Profiling] Profiling mode is ${this._profileLifecycle}.`);
|
|
610
|
-
|
|
611
376
|
switch (this._profileLifecycle) {
|
|
612
|
-
case
|
|
377
|
+
case "trace": {
|
|
613
378
|
this._startTraceLifecycleProfiling();
|
|
614
379
|
break;
|
|
615
380
|
}
|
|
616
|
-
case
|
|
617
|
-
// Manual mode requires manual calls to profiler.startProfiler() and profiler.stopProfiler()
|
|
381
|
+
case "manual": {
|
|
618
382
|
break;
|
|
619
383
|
}
|
|
620
384
|
default: {
|
|
621
|
-
DEBUG_BUILD &&
|
|
622
|
-
debug.warn(`[Profiling] Unknown profiler mode: ${this._profileLifecycle}, profiler was not initialized`);
|
|
385
|
+
DEBUG_BUILD && debug.warn(`[Profiling] Unknown profiler mode: ${this._profileLifecycle}, profiler was not initialized`);
|
|
623
386
|
break;
|
|
624
387
|
}
|
|
625
388
|
}
|
|
@@ -630,350 +393,258 @@ class ContinuousProfiler {constructor() { ContinuousProfiler.prototype.__init.ca
|
|
|
630
393
|
break;
|
|
631
394
|
}
|
|
632
395
|
}
|
|
633
|
-
|
|
634
|
-
// Attaches a listener to beforeSend which will add the threadId data to the event being sent.
|
|
635
|
-
// This adds a constant overhead to all events being sent which could be improved to only attach
|
|
636
|
-
// and detach the listener during a profiler session
|
|
637
|
-
this._client.on('beforeSendEvent', this._onBeforeSendThreadContextAssignment.bind(this));
|
|
396
|
+
this._client.on("beforeSendEvent", this._onBeforeSendThreadContextAssignment.bind(this));
|
|
638
397
|
}
|
|
639
|
-
|
|
640
398
|
/**
|
|
641
399
|
* Initializes a new profilerId session and schedules chunk profiling.
|
|
642
400
|
* @returns void
|
|
643
401
|
*/
|
|
644
|
-
|
|
645
|
-
if (this._mode ===
|
|
402
|
+
start() {
|
|
403
|
+
if (this._mode === "current") {
|
|
646
404
|
this._startProfiler();
|
|
647
405
|
return;
|
|
648
406
|
}
|
|
649
|
-
|
|
650
407
|
if (!this._client) {
|
|
651
|
-
DEBUG_BUILD && debug.log(
|
|
408
|
+
DEBUG_BUILD && debug.log("[Profiling] Failed to start, sentry client was never attached to the profiler.");
|
|
652
409
|
return;
|
|
653
410
|
}
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
DEBUG_BUILD && debug.log('[Profiling] Continuous profiling is not supported in the current mode.');
|
|
411
|
+
if (this._mode !== "legacy") {
|
|
412
|
+
DEBUG_BUILD && debug.log("[Profiling] Continuous profiling is not supported in the current mode.");
|
|
657
413
|
return;
|
|
658
414
|
}
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
DEBUG_BUILD && debug.log('[Profiling] Calls to profiler.start() are not supported in span profiling mode.');
|
|
415
|
+
if (this._legacyProfilerMode === "span") {
|
|
416
|
+
DEBUG_BUILD && debug.log("[Profiling] Calls to profiler.start() are not supported in span profiling mode.");
|
|
662
417
|
return;
|
|
663
418
|
}
|
|
664
|
-
|
|
665
|
-
// Flush any existing chunks before starting a new one.
|
|
666
|
-
|
|
667
419
|
this._stopChunkProfiling();
|
|
668
|
-
|
|
669
|
-
// Restart the profiler session
|
|
670
420
|
this._setupSpanChunkInstrumentation();
|
|
671
421
|
this._restartChunkProfiling();
|
|
672
422
|
}
|
|
673
|
-
|
|
674
423
|
/**
|
|
675
424
|
* Stops the current chunk and flushes the profile to Sentry.
|
|
676
425
|
* @returns void
|
|
677
426
|
*/
|
|
678
|
-
|
|
679
|
-
if (this._mode ===
|
|
427
|
+
stop() {
|
|
428
|
+
if (this._mode === "current") {
|
|
680
429
|
this._stopProfiler();
|
|
681
430
|
return;
|
|
682
431
|
}
|
|
683
|
-
|
|
684
432
|
if (!this._client) {
|
|
685
|
-
DEBUG_BUILD && debug.log(
|
|
433
|
+
DEBUG_BUILD && debug.log("[Profiling] Failed to stop, sentry client was never attached to the profiler.");
|
|
686
434
|
return;
|
|
687
435
|
}
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
DEBUG_BUILD && debug.log('[Profiling] Continuous profiling is not supported in the current mode.');
|
|
436
|
+
if (this._mode !== "legacy") {
|
|
437
|
+
DEBUG_BUILD && debug.log("[Profiling] Continuous profiling is not supported in the current mode.");
|
|
691
438
|
return;
|
|
692
439
|
}
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
DEBUG_BUILD && debug.log('[Profiling] Calls to profiler.stop() are not supported in span profiling mode.');
|
|
440
|
+
if (this._legacyProfilerMode === "span") {
|
|
441
|
+
DEBUG_BUILD && debug.log("[Profiling] Calls to profiler.stop() are not supported in span profiling mode.");
|
|
696
442
|
return;
|
|
697
443
|
}
|
|
698
|
-
|
|
699
444
|
this._stopChunkProfiling();
|
|
700
445
|
this._teardownSpanChunkInstrumentation();
|
|
701
446
|
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
DEBUG_BUILD && debug.log('[Profiling] Continuous profiling is not supported in the current mode.');
|
|
447
|
+
_startProfiler() {
|
|
448
|
+
if (this._mode !== "current") {
|
|
449
|
+
DEBUG_BUILD && debug.log("[Profiling] Continuous profiling is not supported in the current mode.");
|
|
706
450
|
return;
|
|
707
451
|
}
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
DEBUG_BUILD && debug.log('[Profiling] Profile session already running, no-op.');
|
|
452
|
+
if (this._chunkData !== void 0) {
|
|
453
|
+
DEBUG_BUILD && debug.log("[Profiling] Profile session already running, no-op.");
|
|
711
454
|
return;
|
|
712
455
|
}
|
|
713
|
-
|
|
714
|
-
if (this._mode === 'current') {
|
|
456
|
+
if (this._mode === "current") {
|
|
715
457
|
if (!this._sampled) {
|
|
716
|
-
DEBUG_BUILD && debug.log(
|
|
458
|
+
DEBUG_BUILD && debug.log("[Profiling] Profile session not sampled, no-op.");
|
|
717
459
|
return;
|
|
718
460
|
}
|
|
719
461
|
}
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
'[Profiling] You are using the trace profile lifecycle, manual calls to profiler.startProfiler() and profiler.stopProfiler() will be ignored.',
|
|
725
|
-
);
|
|
462
|
+
if (this._profileLifecycle === "trace") {
|
|
463
|
+
DEBUG_BUILD && debug.log(
|
|
464
|
+
"[Profiling] You are using the trace profile lifecycle, manual calls to profiler.startProfiler() and profiler.stopProfiler() will be ignored."
|
|
465
|
+
);
|
|
726
466
|
return;
|
|
727
467
|
}
|
|
728
|
-
|
|
729
468
|
this._startChunkProfiling();
|
|
730
469
|
}
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
DEBUG_BUILD && debug.log('[Profiling] Continuous profiling is not supported in the current mode.');
|
|
470
|
+
_stopProfiler() {
|
|
471
|
+
if (this._mode !== "current") {
|
|
472
|
+
DEBUG_BUILD && debug.log("[Profiling] Continuous profiling is not supported in the current mode.");
|
|
735
473
|
return;
|
|
736
474
|
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
'[Profiling] You are using the trace profile lifecycle, manual calls to profiler.startProfiler() and profiler.stopProfiler() will be ignored.',
|
|
742
|
-
);
|
|
475
|
+
if (this._profileLifecycle === "trace") {
|
|
476
|
+
DEBUG_BUILD && debug.log(
|
|
477
|
+
"[Profiling] You are using the trace profile lifecycle, manual calls to profiler.startProfiler() and profiler.stopProfiler() will be ignored."
|
|
478
|
+
);
|
|
743
479
|
return;
|
|
744
480
|
}
|
|
745
|
-
|
|
746
481
|
if (!this._chunkData) {
|
|
747
|
-
DEBUG_BUILD && debug.log(
|
|
482
|
+
DEBUG_BUILD && debug.log("[Profiling] No profile session running, no-op.");
|
|
748
483
|
return;
|
|
749
484
|
}
|
|
750
|
-
|
|
751
485
|
this._stopChunkProfiling();
|
|
752
486
|
}
|
|
753
|
-
|
|
754
487
|
/**
|
|
755
488
|
* Starts trace lifecycle profiling. Profiling will remain active as long as there is an active span.
|
|
756
489
|
*/
|
|
757
|
-
|
|
490
|
+
_startTraceLifecycleProfiling() {
|
|
758
491
|
if (!this._client) {
|
|
759
|
-
DEBUG_BUILD &&
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
);
|
|
492
|
+
DEBUG_BUILD && debug.log(
|
|
493
|
+
"[Profiling] Failed to start trace lifecycle profiling, sentry client was never attached to the profiler."
|
|
494
|
+
);
|
|
763
495
|
return;
|
|
764
496
|
}
|
|
765
|
-
|
|
766
497
|
let activeSpanCounter = 0;
|
|
767
|
-
this._client.on(
|
|
498
|
+
this._client.on("spanStart", (_span) => {
|
|
768
499
|
if (activeSpanCounter === 0) {
|
|
769
500
|
this._startChunkProfiling();
|
|
770
501
|
}
|
|
771
502
|
activeSpanCounter++;
|
|
772
503
|
});
|
|
773
|
-
|
|
774
|
-
this._client.on('spanEnd', _span => {
|
|
504
|
+
this._client.on("spanEnd", (_span) => {
|
|
775
505
|
if (activeSpanCounter === 1) {
|
|
776
506
|
this._stopChunkProfiling();
|
|
777
507
|
}
|
|
778
508
|
activeSpanCounter--;
|
|
779
509
|
});
|
|
780
510
|
}
|
|
781
|
-
|
|
782
|
-
_setupAutomaticSpanProfiling() {
|
|
511
|
+
_setupAutomaticSpanProfiling() {
|
|
783
512
|
if (!this._client) {
|
|
784
|
-
DEBUG_BUILD &&
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
);
|
|
513
|
+
DEBUG_BUILD && debug.log(
|
|
514
|
+
"[Profiling] Failed to setup automatic span profiling, sentry client was never attached to the profiler."
|
|
515
|
+
);
|
|
788
516
|
return;
|
|
789
517
|
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
this._client.on('spanStart', span => {
|
|
518
|
+
const spanToProfileIdMap = /* @__PURE__ */ new WeakMap();
|
|
519
|
+
this._client.on("spanStart", (span) => {
|
|
794
520
|
if (span !== getRootSpan(span)) {
|
|
795
521
|
return;
|
|
796
522
|
}
|
|
797
|
-
|
|
798
523
|
const profile_id = maybeProfileSpan(this._client, span);
|
|
799
|
-
|
|
800
524
|
if (profile_id) {
|
|
801
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
802
525
|
const options = this._client.getOptions();
|
|
803
|
-
// Not intended for external use, hence missing types, but we want to profile a couple of things at Sentry that
|
|
804
|
-
// currently exceed the default timeout set by the SDKs.
|
|
805
526
|
const maxProfileDurationMs = options._experiments?.maxProfileDurationMs || MAX_PROFILE_DURATION_MS;
|
|
806
|
-
|
|
807
527
|
if (PROFILE_TIMEOUTS[profile_id]) {
|
|
808
528
|
global.clearTimeout(PROFILE_TIMEOUTS[profile_id]);
|
|
809
|
-
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
810
529
|
delete PROFILE_TIMEOUTS[profile_id];
|
|
811
530
|
}
|
|
812
|
-
|
|
813
|
-
// Enqueue a timeout to prevent profiles from running over max duration.
|
|
814
531
|
const timeout = global.setTimeout(() => {
|
|
815
|
-
DEBUG_BUILD &&
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
);
|
|
820
|
-
|
|
532
|
+
DEBUG_BUILD && debug.log(
|
|
533
|
+
"[Profiling] max profile duration elapsed, stopping profiling for:",
|
|
534
|
+
spanToJSON(span).description
|
|
535
|
+
);
|
|
821
536
|
const profile = stopSpanProfile(span, profile_id);
|
|
822
537
|
if (profile) {
|
|
823
538
|
addToProfileQueue(profile_id, profile);
|
|
824
539
|
}
|
|
825
540
|
}, maxProfileDurationMs);
|
|
826
|
-
|
|
827
|
-
// Unref timeout so it doesn't keep the process alive.
|
|
828
541
|
timeout.unref();
|
|
829
|
-
|
|
830
|
-
getIsolationScope().setContext('profile', { profile_id });
|
|
542
|
+
getIsolationScope().setContext("profile", { profile_id });
|
|
831
543
|
spanToProfileIdMap.set(span, profile_id);
|
|
832
544
|
}
|
|
833
545
|
});
|
|
834
|
-
|
|
835
|
-
this._client.on('spanEnd', span => {
|
|
546
|
+
this._client.on("spanEnd", (span) => {
|
|
836
547
|
const profile_id = spanToProfileIdMap.get(span);
|
|
837
|
-
|
|
838
548
|
if (profile_id) {
|
|
839
549
|
if (PROFILE_TIMEOUTS[profile_id]) {
|
|
840
550
|
global.clearTimeout(PROFILE_TIMEOUTS[profile_id]);
|
|
841
|
-
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
842
551
|
delete PROFILE_TIMEOUTS[profile_id];
|
|
843
552
|
}
|
|
844
553
|
const profile = stopSpanProfile(span, profile_id);
|
|
845
|
-
|
|
846
554
|
if (profile) {
|
|
847
555
|
addToProfileQueue(profile_id, profile);
|
|
848
556
|
}
|
|
849
557
|
}
|
|
850
558
|
});
|
|
851
|
-
|
|
852
|
-
this._client.on('beforeEnvelope', (envelope) => {
|
|
853
|
-
// if not profiles are in queue, there is nothing to add to the envelope.
|
|
559
|
+
this._client.on("beforeEnvelope", (envelope) => {
|
|
854
560
|
if (!PROFILE_MAP.size) {
|
|
855
561
|
return;
|
|
856
562
|
}
|
|
857
|
-
|
|
858
563
|
const profiledTransactionEvents = findProfiledTransactionsFromEnvelope(envelope);
|
|
859
564
|
if (!profiledTransactionEvents.length) {
|
|
860
565
|
return;
|
|
861
566
|
}
|
|
862
|
-
|
|
863
567
|
const profilesToAddToEnvelope = [];
|
|
864
|
-
|
|
865
568
|
for (const profiledTransaction of profiledTransactionEvents) {
|
|
866
569
|
const profileContext = profiledTransaction.contexts?.profile;
|
|
867
570
|
const profile_id = profileContext?.profile_id;
|
|
868
|
-
|
|
869
571
|
if (!profile_id) {
|
|
870
|
-
throw new TypeError(
|
|
572
|
+
throw new TypeError("[Profiling] cannot find profile for a transaction without a profile context");
|
|
871
573
|
}
|
|
872
|
-
|
|
873
|
-
// Remove the profile from the transaction context before sending, relay will take care of the rest.
|
|
874
574
|
if (profileContext) {
|
|
875
575
|
delete profiledTransaction.contexts?.profile;
|
|
876
576
|
}
|
|
877
|
-
|
|
878
577
|
const cpuProfile = takeFromProfileQueue(profile_id);
|
|
879
578
|
if (!cpuProfile) {
|
|
880
579
|
DEBUG_BUILD && debug.log(`[Profiling] Could not retrieve profile for transaction: ${profile_id}`);
|
|
881
580
|
continue;
|
|
882
581
|
}
|
|
883
|
-
|
|
884
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
885
582
|
const profile = createProfilingEvent(this._client, cpuProfile, profiledTransaction);
|
|
886
583
|
if (!profile) return;
|
|
887
|
-
|
|
888
584
|
profilesToAddToEnvelope.push(profile);
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
892
|
-
this._client.emit('preprocessEvent', profile, {
|
|
893
|
-
event_id: profiledTransaction.event_id,
|
|
585
|
+
this._client.emit("preprocessEvent", profile, {
|
|
586
|
+
event_id: profiledTransaction.event_id
|
|
894
587
|
});
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
898
|
-
this._client.emit('postprocessEvent', profile, {
|
|
899
|
-
event_id: profiledTransaction.event_id,
|
|
588
|
+
this._client.emit("postprocessEvent", profile, {
|
|
589
|
+
event_id: profiledTransaction.event_id
|
|
900
590
|
});
|
|
901
591
|
}
|
|
902
|
-
|
|
903
592
|
addProfilesToEnvelope(envelope, profilesToAddToEnvelope);
|
|
904
593
|
});
|
|
905
594
|
}
|
|
906
|
-
|
|
907
595
|
/**
|
|
908
596
|
* Stop profiler and initializes profiling of the next chunk
|
|
909
597
|
*/
|
|
910
|
-
|
|
598
|
+
_restartChunkProfiling() {
|
|
911
599
|
if (!this._client) {
|
|
912
|
-
|
|
913
|
-
// In this case, calling start() and stop() is a noop action.The reason this exists is because
|
|
914
|
-
// it makes the types easier to work with and avoids users having to do null checks.
|
|
915
|
-
DEBUG_BUILD && debug.log('[Profiling] Profiler was never attached to the client.');
|
|
600
|
+
DEBUG_BUILD && debug.log("[Profiling] Profiler was never attached to the client.");
|
|
916
601
|
return;
|
|
917
602
|
}
|
|
918
|
-
|
|
919
603
|
if (this._chunkData) {
|
|
920
|
-
DEBUG_BUILD &&
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
);
|
|
604
|
+
DEBUG_BUILD && debug.log(
|
|
605
|
+
`[Profiling] Chunk with chunk_id ${this._chunkData.id} is still running, current chunk will be stopped a new chunk will be started.`
|
|
606
|
+
);
|
|
924
607
|
this._stopChunkProfiling();
|
|
925
608
|
}
|
|
926
|
-
|
|
927
609
|
this._startChunkProfiling();
|
|
928
610
|
}
|
|
929
|
-
|
|
930
611
|
/**
|
|
931
612
|
* Stops profiling of the current chunks and flushes the profile to Sentry
|
|
932
613
|
*/
|
|
933
|
-
|
|
614
|
+
_stopChunkProfiling() {
|
|
934
615
|
if (!this._chunkData) {
|
|
935
|
-
DEBUG_BUILD && debug.log(
|
|
616
|
+
DEBUG_BUILD && debug.log("[Profiling] No chunk data found, no-op.");
|
|
936
617
|
return;
|
|
937
618
|
}
|
|
938
|
-
|
|
939
619
|
if (this._chunkData?.timer) {
|
|
940
620
|
global.clearTimeout(this._chunkData.timer);
|
|
941
|
-
this._chunkData.timer =
|
|
621
|
+
this._chunkData.timer = void 0;
|
|
942
622
|
DEBUG_BUILD && debug.log(`[Profiling] Stopping profiling chunk: ${this._chunkData.id}`);
|
|
943
623
|
}
|
|
944
|
-
|
|
945
624
|
if (!this._client) {
|
|
946
|
-
DEBUG_BUILD &&
|
|
947
|
-
debug.log('[Profiling] Failed to collect profile, sentry client was never attached to the profiler.');
|
|
625
|
+
DEBUG_BUILD && debug.log("[Profiling] Failed to collect profile, sentry client was never attached to the profiler.");
|
|
948
626
|
this._resetChunkData();
|
|
949
627
|
return;
|
|
950
628
|
}
|
|
951
|
-
|
|
952
629
|
if (!this._chunkData?.id) {
|
|
953
|
-
DEBUG_BUILD &&
|
|
954
|
-
debug.log(`[Profiling] Failed to collect profile for: ${this._chunkData?.id}, the chunk_id is missing.`);
|
|
630
|
+
DEBUG_BUILD && debug.log(`[Profiling] Failed to collect profile for: ${this._chunkData?.id}, the chunk_id is missing.`);
|
|
955
631
|
this._resetChunkData();
|
|
956
632
|
return;
|
|
957
633
|
}
|
|
958
|
-
|
|
959
634
|
const profile = CpuProfilerBindings.stopProfiling(this._chunkData.id, ProfileFormat.CHUNK);
|
|
960
|
-
|
|
961
635
|
if (!profile) {
|
|
962
636
|
DEBUG_BUILD && debug.log(`[Profiling] Failed to collect profile for: ${this._chunkData.id}`);
|
|
963
637
|
this._resetChunkData();
|
|
964
638
|
return;
|
|
965
639
|
}
|
|
966
|
-
|
|
967
640
|
if (!this._profilerId) {
|
|
968
|
-
DEBUG_BUILD &&
|
|
969
|
-
debug.log('[Profiling] Profile chunk does not contain a valid profiler_id, this is a bug in the SDK');
|
|
641
|
+
DEBUG_BUILD && debug.log("[Profiling] Profile chunk does not contain a valid profiler_id, this is a bug in the SDK");
|
|
970
642
|
this._resetChunkData();
|
|
971
643
|
return;
|
|
972
644
|
}
|
|
973
645
|
if (profile) {
|
|
974
646
|
DEBUG_BUILD && debug.log(`[Profiling] Sending profile chunk ${this._chunkData.id}.`);
|
|
975
647
|
}
|
|
976
|
-
|
|
977
648
|
DEBUG_BUILD && debug.log(`[Profiling] Profile chunk ${this._chunkData.id} sent to Sentry.`);
|
|
978
649
|
const chunk = createProfilingChunkEvent(
|
|
979
650
|
this._client,
|
|
@@ -983,207 +654,156 @@ class ContinuousProfiler {constructor() { ContinuousProfiler.prototype.__init.ca
|
|
|
983
654
|
{
|
|
984
655
|
chunk_id: this._chunkData.id,
|
|
985
656
|
trace_id: this._chunkData.startTraceID,
|
|
986
|
-
profiler_id: this._profilerId
|
|
987
|
-
}
|
|
657
|
+
profiler_id: this._profilerId
|
|
658
|
+
}
|
|
988
659
|
);
|
|
989
|
-
|
|
990
660
|
if (!chunk) {
|
|
991
661
|
DEBUG_BUILD && debug.log(`[Profiling] Failed to create profile chunk for: ${this._chunkData.id}`);
|
|
992
662
|
this._resetChunkData();
|
|
993
663
|
return;
|
|
994
664
|
}
|
|
995
|
-
|
|
996
665
|
this._flush(chunk);
|
|
997
|
-
// Depending on the profile and stack sizes, stopping the profile and converting
|
|
998
|
-
// the format may negatively impact the performance of the application. To avoid
|
|
999
|
-
// blocking for too long, enqueue the next chunk start inside the next macrotask.
|
|
1000
|
-
// clear current chunk
|
|
1001
666
|
this._resetChunkData();
|
|
1002
667
|
}
|
|
1003
|
-
|
|
1004
668
|
/**
|
|
1005
669
|
* Flushes the profile chunk to Sentry.
|
|
1006
670
|
* @param chunk
|
|
1007
671
|
*/
|
|
1008
|
-
|
|
672
|
+
_flush(chunk) {
|
|
1009
673
|
if (!this._client) {
|
|
1010
|
-
DEBUG_BUILD &&
|
|
1011
|
-
debug.log('[Profiling] Failed to collect profile, sentry client was never attached to the profiler.');
|
|
674
|
+
DEBUG_BUILD && debug.log("[Profiling] Failed to collect profile, sentry client was never attached to the profiler.");
|
|
1012
675
|
return;
|
|
1013
676
|
}
|
|
1014
|
-
|
|
1015
677
|
const transport = this._client.getTransport();
|
|
1016
678
|
if (!transport) {
|
|
1017
|
-
DEBUG_BUILD && debug.log(
|
|
679
|
+
DEBUG_BUILD && debug.log("[Profiling] No transport available to send profile chunk.");
|
|
1018
680
|
return;
|
|
1019
681
|
}
|
|
1020
|
-
|
|
1021
682
|
const dsn = this._client.getDsn();
|
|
1022
683
|
const metadata = this._client.getSdkMetadata();
|
|
1023
684
|
const tunnel = this._client.getOptions().tunnel;
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
DEBUG_BUILD && debug.error('Error while sending profile chunk envelope:', reason);
|
|
685
|
+
const envelope = makeProfileChunkEnvelope("node", chunk, metadata?.sdk, tunnel, dsn);
|
|
686
|
+
transport.send(envelope).then(null, (reason) => {
|
|
687
|
+
DEBUG_BUILD && debug.error("Error while sending profile chunk envelope:", reason);
|
|
1028
688
|
});
|
|
1029
689
|
}
|
|
1030
|
-
|
|
1031
690
|
/**
|
|
1032
691
|
* Starts the profiler and registers the flush timer for a given chunk.
|
|
1033
692
|
* @param chunk
|
|
1034
693
|
*/
|
|
1035
|
-
|
|
694
|
+
_startChunkProfiling() {
|
|
1036
695
|
if (this._chunkData) {
|
|
1037
|
-
DEBUG_BUILD && debug.log(
|
|
696
|
+
DEBUG_BUILD && debug.log("[Profiling] Chunk is already running, no-op.");
|
|
1038
697
|
return;
|
|
1039
698
|
}
|
|
1040
|
-
|
|
1041
|
-
const traceId =
|
|
1042
|
-
getCurrentScope().getPropagationContext().traceId || getIsolationScope().getPropagationContext().traceId;
|
|
699
|
+
const traceId = getCurrentScope().getPropagationContext().traceId || getIsolationScope().getPropagationContext().traceId;
|
|
1043
700
|
const chunk = this._initializeChunk(traceId);
|
|
1044
|
-
|
|
1045
701
|
CpuProfilerBindings.startProfiling(chunk.id);
|
|
1046
702
|
DEBUG_BUILD && debug.log(`[Profiling] starting profiling chunk: ${chunk.id}`);
|
|
1047
|
-
|
|
1048
703
|
chunk.timer = global.setTimeout(() => {
|
|
1049
704
|
DEBUG_BUILD && debug.log(`[Profiling] Stopping profiling chunk: ${chunk.id}`);
|
|
1050
705
|
this._stopChunkProfiling();
|
|
1051
|
-
DEBUG_BUILD && debug.log(
|
|
706
|
+
DEBUG_BUILD && debug.log("[Profiling] Starting new profiling chunk.");
|
|
1052
707
|
setImmediate(this._restartChunkProfiling.bind(this));
|
|
1053
708
|
}, CHUNK_INTERVAL_MS);
|
|
1054
|
-
|
|
1055
|
-
// Unref timeout so it doesn't keep the process alive.
|
|
1056
709
|
chunk.timer.unref();
|
|
1057
710
|
}
|
|
1058
|
-
|
|
1059
711
|
/**
|
|
1060
712
|
* Attaches profiling information to spans that were started
|
|
1061
713
|
* during a profiling session.
|
|
1062
714
|
*/
|
|
1063
|
-
|
|
715
|
+
_setupSpanChunkInstrumentation() {
|
|
1064
716
|
if (!this._client) {
|
|
1065
|
-
DEBUG_BUILD &&
|
|
1066
|
-
debug.log('[Profiling] Failed to initialize span profiling, sentry client was never attached to the profiler.');
|
|
717
|
+
DEBUG_BUILD && debug.log("[Profiling] Failed to initialize span profiling, sentry client was never attached to the profiler.");
|
|
1067
718
|
return;
|
|
1068
719
|
}
|
|
1069
|
-
|
|
1070
720
|
this._profilerId = uuid4();
|
|
1071
|
-
getGlobalScope().setContext(
|
|
1072
|
-
profiler_id: this._profilerId
|
|
721
|
+
getGlobalScope().setContext("profile", {
|
|
722
|
+
profiler_id: this._profilerId
|
|
1073
723
|
});
|
|
1074
724
|
}
|
|
1075
|
-
|
|
1076
725
|
/**
|
|
1077
726
|
* Assigns thread_id and thread name context to a profiled event if there is an active profiler session
|
|
1078
727
|
*/
|
|
1079
|
-
|
|
728
|
+
_onBeforeSendThreadContextAssignment(event) {
|
|
1080
729
|
if (!this._client || !this._profilerId) return;
|
|
1081
730
|
this._assignThreadIdContext(event);
|
|
1082
731
|
}
|
|
1083
|
-
|
|
1084
732
|
/**
|
|
1085
733
|
* Clear profiling information from global context when a profile is not running.
|
|
1086
734
|
*/
|
|
1087
|
-
|
|
1088
|
-
this._profilerId =
|
|
735
|
+
_teardownSpanChunkInstrumentation() {
|
|
736
|
+
this._profilerId = void 0;
|
|
1089
737
|
const globalScope = getGlobalScope();
|
|
1090
|
-
globalScope.setContext(
|
|
738
|
+
globalScope.setContext("profile", {});
|
|
1091
739
|
}
|
|
1092
|
-
|
|
1093
740
|
/**
|
|
1094
741
|
* Initializes new profile chunk metadata
|
|
1095
742
|
*/
|
|
1096
|
-
|
|
743
|
+
_initializeChunk(traceId) {
|
|
1097
744
|
this._chunkData = {
|
|
1098
745
|
id: uuid4(),
|
|
1099
746
|
startTraceID: traceId,
|
|
1100
|
-
timer:
|
|
747
|
+
timer: void 0
|
|
1101
748
|
};
|
|
1102
749
|
return this._chunkData;
|
|
1103
750
|
}
|
|
1104
|
-
|
|
1105
751
|
/**
|
|
1106
752
|
* Assigns thread_id and thread name context to a profiled event.
|
|
1107
753
|
*/
|
|
1108
|
-
|
|
754
|
+
_assignThreadIdContext(event) {
|
|
1109
755
|
if (!event?.contexts?.profile) {
|
|
1110
756
|
return;
|
|
1111
757
|
}
|
|
1112
|
-
|
|
1113
758
|
if (!event.contexts) {
|
|
1114
759
|
return;
|
|
1115
760
|
}
|
|
1116
|
-
|
|
1117
|
-
// @ts-expect-error the trace fallback value is wrong, though it should never happen
|
|
1118
|
-
// and in case it does, we dont want to override whatever was passed initially.
|
|
1119
761
|
event.contexts.trace = {
|
|
1120
|
-
...
|
|
762
|
+
...event.contexts?.trace ?? {},
|
|
1121
763
|
data: {
|
|
1122
|
-
...
|
|
1123
|
-
[
|
|
1124
|
-
[
|
|
1125
|
-
}
|
|
764
|
+
...event.contexts?.trace?.data ?? {},
|
|
765
|
+
["thread.id"]: PROFILER_THREAD_ID_STRING,
|
|
766
|
+
["thread.name"]: PROFILER_THREAD_NAME
|
|
767
|
+
}
|
|
1126
768
|
};
|
|
1127
769
|
}
|
|
1128
|
-
|
|
1129
770
|
/**
|
|
1130
771
|
* Resets the current chunk state.
|
|
1131
772
|
*/
|
|
1132
|
-
|
|
1133
|
-
this._chunkData =
|
|
773
|
+
_resetChunkData() {
|
|
774
|
+
this._chunkData = void 0;
|
|
1134
775
|
}
|
|
1135
776
|
}
|
|
1136
|
-
|
|
1137
|
-
/** Exported only for tests. */
|
|
1138
777
|
const _nodeProfilingIntegration = (() => {
|
|
1139
778
|
if (![16, 18, 20, 22, 24, 26].includes(NODE_MAJOR)) {
|
|
1140
779
|
consoleSandbox(() => {
|
|
1141
|
-
// eslint-disable-next-line no-console
|
|
1142
780
|
console.warn(
|
|
1143
781
|
`[Sentry Profiling] You are using a Node.js version that does not have prebuilt binaries (${NODE_MAJOR}).`,
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
782
|
+
"The @sentry/profiling-node package only has prebuilt support for the following LTS versions of Node.js: 16, 18, 20, 22, 24.",
|
|
783
|
+
"To use the @sentry/profiling-node package with this version of Node.js, you will need to compile the native addon from source.",
|
|
784
|
+
"See: https://github.com/getsentry/sentry-javascript/tree/develop/packages/profiling-node#building-the-package-from-source"
|
|
1147
785
|
);
|
|
1148
786
|
});
|
|
1149
787
|
}
|
|
1150
|
-
|
|
1151
788
|
return {
|
|
1152
|
-
name:
|
|
789
|
+
name: "ProfilingIntegration",
|
|
1153
790
|
_profiler: new ContinuousProfiler(),
|
|
1154
791
|
setup(client) {
|
|
1155
|
-
DEBUG_BUILD && debug.log(
|
|
792
|
+
DEBUG_BUILD && debug.log("[Profiling] Profiling integration setup.");
|
|
1156
793
|
this._profiler.initialize(client);
|
|
1157
794
|
return;
|
|
1158
|
-
}
|
|
795
|
+
}
|
|
1159
796
|
};
|
|
1160
|
-
})
|
|
1161
|
-
|
|
1162
|
-
/**
|
|
1163
|
-
* Determines the profiling mode based on the options.
|
|
1164
|
-
* @param options
|
|
1165
|
-
* @returns 'legacy' if the options are using the legacy profiling API, 'current' if the options are using the current profiling API
|
|
1166
|
-
*/
|
|
797
|
+
});
|
|
1167
798
|
function getProfilingMode(options) {
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
return 'legacy';
|
|
799
|
+
if ("profilesSampleRate" in options || "profilesSampler" in options) {
|
|
800
|
+
return "legacy";
|
|
1171
801
|
}
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
return 'current';
|
|
802
|
+
if ("profileSessionSampleRate" in options || "profileLifecycle" in options) {
|
|
803
|
+
return "current";
|
|
1175
804
|
}
|
|
1176
|
-
|
|
1177
|
-
// If neither are set, we are in the legacy continuous profiling mode
|
|
1178
|
-
return 'legacy';
|
|
805
|
+
return "legacy";
|
|
1179
806
|
}
|
|
1180
|
-
|
|
1181
|
-
/**
|
|
1182
|
-
* We need this integration in order to send data to Sentry. We hook into the event processor
|
|
1183
|
-
* and inspect each event to see if it is a transaction event and if that transaction event
|
|
1184
|
-
* contains a profile on it's metadata. If that is the case, we create a profiling event envelope
|
|
1185
|
-
* and delete the profile from the transaction metadata.
|
|
1186
|
-
*/
|
|
1187
807
|
const nodeProfilingIntegration = defineIntegration(_nodeProfilingIntegration);
|
|
1188
808
|
|
|
1189
809
|
export { nodeProfilingIntegration };
|