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