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