summaryrefslogtreecommitdiff
path: root/shared/logger/node_modules/@sentry/browser/esm/profiling/utils.js
diff options
context:
space:
mode:
Diffstat (limited to 'shared/logger/node_modules/@sentry/browser/esm/profiling/utils.js')
-rw-r--r--shared/logger/node_modules/@sentry/browser/esm/profiling/utils.js438
1 files changed, 438 insertions, 0 deletions
diff --git a/shared/logger/node_modules/@sentry/browser/esm/profiling/utils.js b/shared/logger/node_modules/@sentry/browser/esm/profiling/utils.js
new file mode 100644
index 0000000..e8beae2
--- /dev/null
+++ b/shared/logger/node_modules/@sentry/browser/esm/profiling/utils.js
@@ -0,0 +1,438 @@
+import { DEFAULT_ENVIRONMENT, getCurrentHub } from '@sentry/core';
+import { forEachEnvelopeItem, logger, uuid4, GLOBAL_OBJ } from '@sentry/utils';
+import { WINDOW } from '../helpers.js';
+
+/* eslint-disable max-lines */
+
+const MS_TO_NS = 1e6;
+// Use 0 as main thread id which is identical to threadId in node:worker_threads
+// where main logs 0 and workers seem to log in increments of 1
+const THREAD_ID_STRING = String(0);
+const THREAD_NAME = 'main';
+
+// Machine properties (eval only once)
+let OS_PLATFORM = '';
+let OS_PLATFORM_VERSION = '';
+let OS_ARCH = '';
+let OS_BROWSER = (WINDOW.navigator && WINDOW.navigator.userAgent) || '';
+let OS_MODEL = '';
+const OS_LOCALE =
+ (WINDOW.navigator && WINDOW.navigator.language) ||
+ (WINDOW.navigator && WINDOW.navigator.languages && WINDOW.navigator.languages[0]) ||
+ '';
+
+function isUserAgentData(data) {
+ return typeof data === 'object' && data !== null && 'getHighEntropyValues' in data;
+}
+
+// @ts-ignore userAgentData is not part of the navigator interface yet
+const userAgentData = WINDOW.navigator && WINDOW.navigator.userAgentData;
+
+if (isUserAgentData(userAgentData)) {
+ userAgentData
+ .getHighEntropyValues(['architecture', 'model', 'platform', 'platformVersion', 'fullVersionList'])
+ .then((ua) => {
+ OS_PLATFORM = ua.platform || '';
+ OS_ARCH = ua.architecture || '';
+ OS_MODEL = ua.model || '';
+ OS_PLATFORM_VERSION = ua.platformVersion || '';
+
+ if (ua.fullVersionList && ua.fullVersionList.length > 0) {
+ const firstUa = ua.fullVersionList[ua.fullVersionList.length - 1];
+ OS_BROWSER = `${firstUa.brand} ${firstUa.version}`;
+ }
+ })
+ .catch(e => void e);
+}
+
+function isProcessedJSSelfProfile(profile) {
+ return !('thread_metadata' in profile);
+}
+
+// Enriches the profile with threadId of the current thread.
+// This is done in node as we seem to not be able to get the info from C native code.
+/**
+ *
+ */
+function enrichWithThreadInformation(profile) {
+ if (!isProcessedJSSelfProfile(profile)) {
+ return profile;
+ }
+
+ return convertJSSelfProfileToSampledFormat(profile);
+}
+
+// Profile is marked as optional because it is deleted from the metadata
+// by the integration before the event is processed by other integrations.
+
+function getTraceId(event) {
+ const traceId = event && event.contexts && event.contexts['trace'] && event.contexts['trace']['trace_id'];
+ // Log a warning if the profile has an invalid traceId (should be uuidv4).
+ // All profiles and transactions are rejected if this is the case and we want to
+ // warn users that this is happening if they enable debug flag
+ if (typeof traceId === 'string' && traceId.length !== 32) {
+ if ((typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__)) {
+ logger.log(`[Profiling] Invalid traceId: ${traceId} on profiled event`);
+ }
+ }
+ if (typeof traceId !== 'string') {
+ return '';
+ }
+
+ return traceId;
+}
+/**
+ * Creates a profiling event envelope from a Sentry event. If profile does not pass
+ * validation, returns null.
+ * @param event
+ * @param dsn
+ * @param metadata
+ * @param tunnel
+ * @returns {EventEnvelope | null}
+ */
+
+/**
+ * Creates a profiling event envelope from a Sentry event.
+ */
+function createProfilePayload(
+ event,
+ processedProfile,
+ profile_id,
+) {
+ if (event.type !== 'transaction') {
+ // createProfilingEventEnvelope should only be called for transactions,
+ // we type guard this behavior with isProfiledTransactionEvent.
+ throw new TypeError('Profiling events may only be attached to transactions, this should never occur.');
+ }
+
+ if (processedProfile === undefined || processedProfile === null) {
+ throw new TypeError(
+ `Cannot construct profiling event envelope without a valid profile. Got ${processedProfile} instead.`,
+ );
+ }
+
+ const traceId = getTraceId(event);
+ const enrichedThreadProfile = enrichWithThreadInformation(processedProfile);
+ const transactionStartMs = typeof event.start_timestamp === 'number' ? event.start_timestamp * 1000 : Date.now();
+ const transactionEndMs = typeof event.timestamp === 'number' ? event.timestamp * 1000 : Date.now();
+
+ const profile = {
+ event_id: profile_id,
+ timestamp: new Date(transactionStartMs).toISOString(),
+ platform: 'javascript',
+ version: '1',
+ release: event.release || '',
+ environment: event.environment || DEFAULT_ENVIRONMENT,
+ runtime: {
+ name: 'javascript',
+ version: WINDOW.navigator.userAgent,
+ },
+ os: {
+ name: OS_PLATFORM,
+ version: OS_PLATFORM_VERSION,
+ build_number: OS_BROWSER,
+ },
+ device: {
+ locale: OS_LOCALE,
+ model: OS_MODEL,
+ manufacturer: OS_BROWSER,
+ architecture: OS_ARCH,
+ is_emulator: false,
+ },
+ debug_meta: {
+ images: applyDebugMetadata(processedProfile.resources),
+ },
+ profile: enrichedThreadProfile,
+ transactions: [
+ {
+ name: event.transaction || '',
+ id: event.event_id || uuid4(),
+ trace_id: traceId,
+ active_thread_id: THREAD_ID_STRING,
+ relative_start_ns: '0',
+ relative_end_ns: ((transactionEndMs - transactionStartMs) * 1e6).toFixed(0),
+ },
+ ],
+ };
+
+ return profile;
+}
+
+/**
+ * Converts a JSSelfProfile to a our sampled format.
+ * Does not currently perform stack indexing.
+ */
+function convertJSSelfProfileToSampledFormat(input) {
+ let EMPTY_STACK_ID = undefined;
+ let STACK_ID = 0;
+
+ // Initialize the profile that we will fill with data
+ const profile = {
+ samples: [],
+ stacks: [],
+ frames: [],
+ thread_metadata: {
+ [THREAD_ID_STRING]: { name: THREAD_NAME },
+ },
+ };
+
+ if (!input.samples.length) {
+ return profile;
+ }
+
+ // We assert samples.length > 0 above and timestamp should always be present
+ const start = input.samples[0].timestamp;
+
+ for (let i = 0; i < input.samples.length; i++) {
+ const jsSample = input.samples[i];
+
+ // If sample has no stack, add an empty sample
+ if (jsSample.stackId === undefined) {
+ if (EMPTY_STACK_ID === undefined) {
+ EMPTY_STACK_ID = STACK_ID;
+ profile.stacks[EMPTY_STACK_ID] = [];
+ STACK_ID++;
+ }
+
+ profile['samples'][i] = {
+ // convert ms timestamp to ns
+ elapsed_since_start_ns: ((jsSample.timestamp - start) * MS_TO_NS).toFixed(0),
+ stack_id: EMPTY_STACK_ID,
+ thread_id: THREAD_ID_STRING,
+ };
+ continue;
+ }
+
+ let stackTop = input.stacks[jsSample.stackId];
+
+ // Functions in top->down order (root is last)
+ // We follow the stackTop.parentId trail and collect each visited frameId
+ const stack = [];
+
+ while (stackTop) {
+ stack.push(stackTop.frameId);
+
+ const frame = input.frames[stackTop.frameId];
+
+ // If our frame has not been indexed yet, index it
+ if (profile.frames[stackTop.frameId] === undefined) {
+ profile.frames[stackTop.frameId] = {
+ function: frame.name,
+ file: frame.resourceId ? input.resources[frame.resourceId] : undefined,
+ line: frame.line,
+ column: frame.column,
+ };
+ }
+
+ stackTop = stackTop.parentId === undefined ? undefined : input.stacks[stackTop.parentId];
+ }
+
+ const sample = {
+ // convert ms timestamp to ns
+ elapsed_since_start_ns: ((jsSample.timestamp - start) * MS_TO_NS).toFixed(0),
+ stack_id: STACK_ID,
+ thread_id: THREAD_ID_STRING,
+ };
+
+ profile['stacks'][STACK_ID] = stack;
+ profile['samples'][i] = sample;
+ STACK_ID++;
+ }
+
+ return profile;
+}
+
+/**
+ * Adds items to envelope if they are not already present - mutates the envelope.
+ * @param envelope
+ */
+function addProfilesToEnvelope(envelope, profiles) {
+ if (!profiles.length) {
+ return envelope;
+ }
+
+ for (const profile of profiles) {
+ // @ts-ignore untyped envelope
+ envelope[1].push([{ type: 'profile' }, profile]);
+ }
+ return envelope;
+}
+
+/**
+ * Finds transactions with profile_id context in the envelope
+ * @param envelope
+ * @returns
+ */
+function findProfiledTransactionsFromEnvelope(envelope) {
+ const events = [];
+
+ forEachEnvelopeItem(envelope, (item, type) => {
+ if (type !== 'transaction') {
+ return;
+ }
+
+ for (let j = 1; j < item.length; j++) {
+ const event = item[j] ;
+
+ if (event && event.contexts && event.contexts['profile'] && event.contexts['profile']['profile_id']) {
+ events.push(item[j] );
+ }
+ }
+ });
+
+ return events;
+}
+
+const debugIdStackParserCache = new WeakMap();
+/**
+ * Applies debug meta data to an event from a list of paths to resources (sourcemaps)
+ */
+function applyDebugMetadata(resource_paths) {
+ const debugIdMap = GLOBAL_OBJ._sentryDebugIds;
+
+ if (!debugIdMap) {
+ return [];
+ }
+
+ const hub = getCurrentHub();
+ if (!hub) {
+ return [];
+ }
+ const client = hub.getClient();
+ if (!client) {
+ return [];
+ }
+ const options = client.getOptions();
+ if (!options) {
+ return [];
+ }
+ const stackParser = options.stackParser;
+ if (!stackParser) {
+ return [];
+ }
+
+ let debugIdStackFramesCache;
+ const cachedDebugIdStackFrameCache = debugIdStackParserCache.get(stackParser);
+ if (cachedDebugIdStackFrameCache) {
+ debugIdStackFramesCache = cachedDebugIdStackFrameCache;
+ } else {
+ debugIdStackFramesCache = new Map();
+ debugIdStackParserCache.set(stackParser, debugIdStackFramesCache);
+ }
+
+ // Build a map of filename -> debug_id
+ const filenameDebugIdMap = Object.keys(debugIdMap).reduce((acc, debugIdStackTrace) => {
+ let parsedStack;
+
+ const cachedParsedStack = debugIdStackFramesCache.get(debugIdStackTrace);
+ if (cachedParsedStack) {
+ parsedStack = cachedParsedStack;
+ } else {
+ parsedStack = stackParser(debugIdStackTrace);
+ debugIdStackFramesCache.set(debugIdStackTrace, parsedStack);
+ }
+
+ for (let i = parsedStack.length - 1; i >= 0; i--) {
+ const stackFrame = parsedStack[i];
+ const file = stackFrame && stackFrame.filename;
+
+ if (stackFrame && file) {
+ acc[file] = debugIdMap[debugIdStackTrace] ;
+ break;
+ }
+ }
+ return acc;
+ }, {});
+
+ const images = [];
+ for (const path of resource_paths) {
+ if (path && filenameDebugIdMap[path]) {
+ images.push({
+ type: 'sourcemap',
+ code_file: path,
+ debug_id: filenameDebugIdMap[path] ,
+ });
+ }
+ }
+
+ return images;
+}
+
+/**
+ * Checks the given sample rate to make sure it is valid type and value (a boolean, or a number between 0 and 1).
+ */
+function isValidSampleRate(rate) {
+ // we need to check NaN explicitly because it's of type 'number' and therefore wouldn't get caught by this typecheck
+ if ((typeof rate !== 'number' && typeof rate !== 'boolean') || (typeof rate === 'number' && isNaN(rate))) {
+ (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) &&
+ logger.warn(
+ `[Profiling] Invalid sample rate. Sample rate must be a boolean or a number between 0 and 1. Got ${JSON.stringify(
+ rate,
+ )} of type ${JSON.stringify(typeof rate)}.`,
+ );
+ return false;
+ }
+
+ // Boolean sample rates are always valid
+ if (rate === true || rate === false) {
+ return true;
+ }
+
+ // in case sampleRate is a boolean, it will get automatically cast to 1 if it's true and 0 if it's false
+ if (rate < 0 || rate > 1) {
+ (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) &&
+ logger.warn(`[Profiling] Invalid sample rate. Sample rate must be between 0 and 1. Got ${rate}.`);
+ return false;
+ }
+ return true;
+}
+
+function isValidProfile(profile) {
+ if (profile.samples.length < 2) {
+ if ((typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__)) {
+ // Log a warning if the profile has less than 2 samples so users can know why
+ // they are not seeing any profiling data and we cant avoid the back and forth
+ // of asking them to provide us with a dump of the profile data.
+ logger.log('[Profiling] Discarding profile because it contains less than 2 samples');
+ }
+ return false;
+ }
+
+ if (!profile.frames.length) {
+ if ((typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__)) {
+ logger.log('[Profiling] Discarding profile because it contains no frames');
+ }
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * Creates a profiling envelope item, if the profile does not pass validation, returns null.
+ * @param event
+ * @returns {Profile | null}
+ */
+function createProfilingEvent(profile_id, profile, event) {
+ if (!isValidProfile(profile)) {
+ return null;
+ }
+
+ return createProfilePayload(event, profile, profile_id);
+}
+
+const PROFILE_MAP = new Map();
+/**
+ *
+ */
+function addProfileToMap(profile_id, profile) {
+ PROFILE_MAP.set(profile_id, profile);
+
+ if (PROFILE_MAP.size > 30) {
+ const last = PROFILE_MAP.keys().next().value;
+ PROFILE_MAP.delete(last);
+ }
+}
+
+export { PROFILE_MAP, addProfileToMap, addProfilesToEnvelope, applyDebugMetadata, convertJSSelfProfileToSampledFormat, createProfilePayload, createProfilingEvent, enrichWithThreadInformation, findProfiledTransactionsFromEnvelope, isValidSampleRate };
+//# sourceMappingURL=utils.js.map