this repo has no description
1import { DEFAULT_ENVIRONMENT, getCurrentHub } from '@sentry/core';
2import { forEachEnvelopeItem, logger, uuid4, GLOBAL_OBJ } from '@sentry/utils';
3import { WINDOW } from '../helpers.js';
4
5/* eslint-disable max-lines */
6
7const MS_TO_NS = 1e6;
8// Use 0 as main thread id which is identical to threadId in node:worker_threads
9// where main logs 0 and workers seem to log in increments of 1
10const THREAD_ID_STRING = String(0);
11const THREAD_NAME = 'main';
12
13// Machine properties (eval only once)
14let OS_PLATFORM = '';
15let OS_PLATFORM_VERSION = '';
16let OS_ARCH = '';
17let OS_BROWSER = (WINDOW.navigator && WINDOW.navigator.userAgent) || '';
18let OS_MODEL = '';
19const OS_LOCALE =
20 (WINDOW.navigator && WINDOW.navigator.language) ||
21 (WINDOW.navigator && WINDOW.navigator.languages && WINDOW.navigator.languages[0]) ||
22 '';
23
24function isUserAgentData(data) {
25 return typeof data === 'object' && data !== null && 'getHighEntropyValues' in data;
26}
27
28// @ts-ignore userAgentData is not part of the navigator interface yet
29const userAgentData = WINDOW.navigator && WINDOW.navigator.userAgentData;
30
31if (isUserAgentData(userAgentData)) {
32 userAgentData
33 .getHighEntropyValues(['architecture', 'model', 'platform', 'platformVersion', 'fullVersionList'])
34 .then((ua) => {
35 OS_PLATFORM = ua.platform || '';
36 OS_ARCH = ua.architecture || '';
37 OS_MODEL = ua.model || '';
38 OS_PLATFORM_VERSION = ua.platformVersion || '';
39
40 if (ua.fullVersionList && ua.fullVersionList.length > 0) {
41 const firstUa = ua.fullVersionList[ua.fullVersionList.length - 1];
42 OS_BROWSER = `${firstUa.brand} ${firstUa.version}`;
43 }
44 })
45 .catch(e => void e);
46}
47
48function isProcessedJSSelfProfile(profile) {
49 return !('thread_metadata' in profile);
50}
51
52// Enriches the profile with threadId of the current thread.
53// This is done in node as we seem to not be able to get the info from C native code.
54/**
55 *
56 */
57function enrichWithThreadInformation(profile) {
58 if (!isProcessedJSSelfProfile(profile)) {
59 return profile;
60 }
61
62 return convertJSSelfProfileToSampledFormat(profile);
63}
64
65// Profile is marked as optional because it is deleted from the metadata
66// by the integration before the event is processed by other integrations.
67
68function getTraceId(event) {
69 const traceId = event && event.contexts && event.contexts['trace'] && event.contexts['trace']['trace_id'];
70 // Log a warning if the profile has an invalid traceId (should be uuidv4).
71 // All profiles and transactions are rejected if this is the case and we want to
72 // warn users that this is happening if they enable debug flag
73 if (typeof traceId === 'string' && traceId.length !== 32) {
74 if ((typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__)) {
75 logger.log(`[Profiling] Invalid traceId: ${traceId} on profiled event`);
76 }
77 }
78 if (typeof traceId !== 'string') {
79 return '';
80 }
81
82 return traceId;
83}
84/**
85 * Creates a profiling event envelope from a Sentry event. If profile does not pass
86 * validation, returns null.
87 * @param event
88 * @param dsn
89 * @param metadata
90 * @param tunnel
91 * @returns {EventEnvelope | null}
92 */
93
94/**
95 * Creates a profiling event envelope from a Sentry event.
96 */
97function createProfilePayload(
98 event,
99 processedProfile,
100 profile_id,
101) {
102 if (event.type !== 'transaction') {
103 // createProfilingEventEnvelope should only be called for transactions,
104 // we type guard this behavior with isProfiledTransactionEvent.
105 throw new TypeError('Profiling events may only be attached to transactions, this should never occur.');
106 }
107
108 if (processedProfile === undefined || processedProfile === null) {
109 throw new TypeError(
110 `Cannot construct profiling event envelope without a valid profile. Got ${processedProfile} instead.`,
111 );
112 }
113
114 const traceId = getTraceId(event);
115 const enrichedThreadProfile = enrichWithThreadInformation(processedProfile);
116 const transactionStartMs = typeof event.start_timestamp === 'number' ? event.start_timestamp * 1000 : Date.now();
117 const transactionEndMs = typeof event.timestamp === 'number' ? event.timestamp * 1000 : Date.now();
118
119 const profile = {
120 event_id: profile_id,
121 timestamp: new Date(transactionStartMs).toISOString(),
122 platform: 'javascript',
123 version: '1',
124 release: event.release || '',
125 environment: event.environment || DEFAULT_ENVIRONMENT,
126 runtime: {
127 name: 'javascript',
128 version: WINDOW.navigator.userAgent,
129 },
130 os: {
131 name: OS_PLATFORM,
132 version: OS_PLATFORM_VERSION,
133 build_number: OS_BROWSER,
134 },
135 device: {
136 locale: OS_LOCALE,
137 model: OS_MODEL,
138 manufacturer: OS_BROWSER,
139 architecture: OS_ARCH,
140 is_emulator: false,
141 },
142 debug_meta: {
143 images: applyDebugMetadata(processedProfile.resources),
144 },
145 profile: enrichedThreadProfile,
146 transactions: [
147 {
148 name: event.transaction || '',
149 id: event.event_id || uuid4(),
150 trace_id: traceId,
151 active_thread_id: THREAD_ID_STRING,
152 relative_start_ns: '0',
153 relative_end_ns: ((transactionEndMs - transactionStartMs) * 1e6).toFixed(0),
154 },
155 ],
156 };
157
158 return profile;
159}
160
161/**
162 * Converts a JSSelfProfile to a our sampled format.
163 * Does not currently perform stack indexing.
164 */
165function convertJSSelfProfileToSampledFormat(input) {
166 let EMPTY_STACK_ID = undefined;
167 let STACK_ID = 0;
168
169 // Initialize the profile that we will fill with data
170 const profile = {
171 samples: [],
172 stacks: [],
173 frames: [],
174 thread_metadata: {
175 [THREAD_ID_STRING]: { name: THREAD_NAME },
176 },
177 };
178
179 if (!input.samples.length) {
180 return profile;
181 }
182
183 // We assert samples.length > 0 above and timestamp should always be present
184 const start = input.samples[0].timestamp;
185
186 for (let i = 0; i < input.samples.length; i++) {
187 const jsSample = input.samples[i];
188
189 // If sample has no stack, add an empty sample
190 if (jsSample.stackId === undefined) {
191 if (EMPTY_STACK_ID === undefined) {
192 EMPTY_STACK_ID = STACK_ID;
193 profile.stacks[EMPTY_STACK_ID] = [];
194 STACK_ID++;
195 }
196
197 profile['samples'][i] = {
198 // convert ms timestamp to ns
199 elapsed_since_start_ns: ((jsSample.timestamp - start) * MS_TO_NS).toFixed(0),
200 stack_id: EMPTY_STACK_ID,
201 thread_id: THREAD_ID_STRING,
202 };
203 continue;
204 }
205
206 let stackTop = input.stacks[jsSample.stackId];
207
208 // Functions in top->down order (root is last)
209 // We follow the stackTop.parentId trail and collect each visited frameId
210 const stack = [];
211
212 while (stackTop) {
213 stack.push(stackTop.frameId);
214
215 const frame = input.frames[stackTop.frameId];
216
217 // If our frame has not been indexed yet, index it
218 if (profile.frames[stackTop.frameId] === undefined) {
219 profile.frames[stackTop.frameId] = {
220 function: frame.name,
221 file: frame.resourceId ? input.resources[frame.resourceId] : undefined,
222 line: frame.line,
223 column: frame.column,
224 };
225 }
226
227 stackTop = stackTop.parentId === undefined ? undefined : input.stacks[stackTop.parentId];
228 }
229
230 const sample = {
231 // convert ms timestamp to ns
232 elapsed_since_start_ns: ((jsSample.timestamp - start) * MS_TO_NS).toFixed(0),
233 stack_id: STACK_ID,
234 thread_id: THREAD_ID_STRING,
235 };
236
237 profile['stacks'][STACK_ID] = stack;
238 profile['samples'][i] = sample;
239 STACK_ID++;
240 }
241
242 return profile;
243}
244
245/**
246 * Adds items to envelope if they are not already present - mutates the envelope.
247 * @param envelope
248 */
249function addProfilesToEnvelope(envelope, profiles) {
250 if (!profiles.length) {
251 return envelope;
252 }
253
254 for (const profile of profiles) {
255 // @ts-ignore untyped envelope
256 envelope[1].push([{ type: 'profile' }, profile]);
257 }
258 return envelope;
259}
260
261/**
262 * Finds transactions with profile_id context in the envelope
263 * @param envelope
264 * @returns
265 */
266function findProfiledTransactionsFromEnvelope(envelope) {
267 const events = [];
268
269 forEachEnvelopeItem(envelope, (item, type) => {
270 if (type !== 'transaction') {
271 return;
272 }
273
274 for (let j = 1; j < item.length; j++) {
275 const event = item[j] ;
276
277 if (event && event.contexts && event.contexts['profile'] && event.contexts['profile']['profile_id']) {
278 events.push(item[j] );
279 }
280 }
281 });
282
283 return events;
284}
285
286const debugIdStackParserCache = new WeakMap();
287/**
288 * Applies debug meta data to an event from a list of paths to resources (sourcemaps)
289 */
290function applyDebugMetadata(resource_paths) {
291 const debugIdMap = GLOBAL_OBJ._sentryDebugIds;
292
293 if (!debugIdMap) {
294 return [];
295 }
296
297 const hub = getCurrentHub();
298 if (!hub) {
299 return [];
300 }
301 const client = hub.getClient();
302 if (!client) {
303 return [];
304 }
305 const options = client.getOptions();
306 if (!options) {
307 return [];
308 }
309 const stackParser = options.stackParser;
310 if (!stackParser) {
311 return [];
312 }
313
314 let debugIdStackFramesCache;
315 const cachedDebugIdStackFrameCache = debugIdStackParserCache.get(stackParser);
316 if (cachedDebugIdStackFrameCache) {
317 debugIdStackFramesCache = cachedDebugIdStackFrameCache;
318 } else {
319 debugIdStackFramesCache = new Map();
320 debugIdStackParserCache.set(stackParser, debugIdStackFramesCache);
321 }
322
323 // Build a map of filename -> debug_id
324 const filenameDebugIdMap = Object.keys(debugIdMap).reduce((acc, debugIdStackTrace) => {
325 let parsedStack;
326
327 const cachedParsedStack = debugIdStackFramesCache.get(debugIdStackTrace);
328 if (cachedParsedStack) {
329 parsedStack = cachedParsedStack;
330 } else {
331 parsedStack = stackParser(debugIdStackTrace);
332 debugIdStackFramesCache.set(debugIdStackTrace, parsedStack);
333 }
334
335 for (let i = parsedStack.length - 1; i >= 0; i--) {
336 const stackFrame = parsedStack[i];
337 const file = stackFrame && stackFrame.filename;
338
339 if (stackFrame && file) {
340 acc[file] = debugIdMap[debugIdStackTrace] ;
341 break;
342 }
343 }
344 return acc;
345 }, {});
346
347 const images = [];
348 for (const path of resource_paths) {
349 if (path && filenameDebugIdMap[path]) {
350 images.push({
351 type: 'sourcemap',
352 code_file: path,
353 debug_id: filenameDebugIdMap[path] ,
354 });
355 }
356 }
357
358 return images;
359}
360
361/**
362 * Checks the given sample rate to make sure it is valid type and value (a boolean, or a number between 0 and 1).
363 */
364function isValidSampleRate(rate) {
365 // we need to check NaN explicitly because it's of type 'number' and therefore wouldn't get caught by this typecheck
366 if ((typeof rate !== 'number' && typeof rate !== 'boolean') || (typeof rate === 'number' && isNaN(rate))) {
367 (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) &&
368 logger.warn(
369 `[Profiling] Invalid sample rate. Sample rate must be a boolean or a number between 0 and 1. Got ${JSON.stringify(
370 rate,
371 )} of type ${JSON.stringify(typeof rate)}.`,
372 );
373 return false;
374 }
375
376 // Boolean sample rates are always valid
377 if (rate === true || rate === false) {
378 return true;
379 }
380
381 // in case sampleRate is a boolean, it will get automatically cast to 1 if it's true and 0 if it's false
382 if (rate < 0 || rate > 1) {
383 (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) &&
384 logger.warn(`[Profiling] Invalid sample rate. Sample rate must be between 0 and 1. Got ${rate}.`);
385 return false;
386 }
387 return true;
388}
389
390function isValidProfile(profile) {
391 if (profile.samples.length < 2) {
392 if ((typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__)) {
393 // Log a warning if the profile has less than 2 samples so users can know why
394 // they are not seeing any profiling data and we cant avoid the back and forth
395 // of asking them to provide us with a dump of the profile data.
396 logger.log('[Profiling] Discarding profile because it contains less than 2 samples');
397 }
398 return false;
399 }
400
401 if (!profile.frames.length) {
402 if ((typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__)) {
403 logger.log('[Profiling] Discarding profile because it contains no frames');
404 }
405 return false;
406 }
407
408 return true;
409}
410
411/**
412 * Creates a profiling envelope item, if the profile does not pass validation, returns null.
413 * @param event
414 * @returns {Profile | null}
415 */
416function createProfilingEvent(profile_id, profile, event) {
417 if (!isValidProfile(profile)) {
418 return null;
419 }
420
421 return createProfilePayload(event, profile, profile_id);
422}
423
424const PROFILE_MAP = new Map();
425/**
426 *
427 */
428function addProfileToMap(profile_id, profile) {
429 PROFILE_MAP.set(profile_id, profile);
430
431 if (PROFILE_MAP.size > 30) {
432 const last = PROFILE_MAP.keys().next().value;
433 PROFILE_MAP.delete(last);
434 }
435}
436
437export { PROFILE_MAP, addProfileToMap, addProfilesToEnvelope, applyDebugMetadata, convertJSSelfProfileToSampledFormat, createProfilePayload, createProfilingEvent, enrichWithThreadInformation, findProfiledTransactionsFromEnvelope, isValidSampleRate };
438//# sourceMappingURL=utils.js.map