123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586 |
- import { getClient, DEFAULT_ENVIRONMENT } from '@sentry/core';
- import { forEachEnvelopeItem, logger, uuid4, GLOBAL_OBJ, browserPerformanceTimeOrigin } from '@sentry/utils';
- import { DEBUG_BUILD } from '../debug-build.js';
- 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-expect-error 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 (DEBUG_BUILD) {
- 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(
- profile_id,
- start_timestamp,
- processed_profile,
- event,
- ) {
- 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 (processed_profile === undefined || processed_profile === null) {
- throw new TypeError(
- `Cannot construct profiling event envelope without a valid profile. Got ${processed_profile} instead.`,
- );
- }
- const traceId = getTraceId(event);
- const enrichedThreadProfile = enrichWithThreadInformation(processed_profile);
- const transactionStartMs = start_timestamp
- ? start_timestamp
- : 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(processed_profile.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;
- }
- /*
- See packages/tracing-internal/src/browser/router.ts
- */
- /**
- *
- */
- function isAutomatedPageLoadTransaction(transaction) {
- return transaction.op === 'pageload';
- }
- /**
- * 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;
- // The JS SDK might change it's time origin based on some heuristic (see See packages/utils/src/time.ts)
- // when that happens, we need to ensure we are correcting the profile timings so the two timelines stay in sync.
- // Since JS self profiling time origin is always initialized to performance.timeOrigin, we need to adjust for
- // the drift between the SDK selected value and our profile time origin.
- const origin =
- typeof performance.timeOrigin === 'number' ? performance.timeOrigin : browserPerformanceTimeOrigin || 0;
- const adjustForOriginChange = origin - (browserPerformanceTimeOrigin || origin);
- 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 + adjustForOriginChange - 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,
- abs_path: typeof frame.resourceId === 'number' ? input.resources[frame.resourceId] : undefined,
- lineno: frame.line,
- colno: frame.column,
- };
- }
- stackTop = stackTop.parentId === undefined ? undefined : input.stacks[stackTop.parentId];
- }
- const sample = {
- // convert ms timestamp to ns
- elapsed_since_start_ns: ((jsSample.timestamp + adjustForOriginChange - 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) {
- 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 client = getClient();
- const options = client && client.getOptions();
- const stackParser = options && 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))) {
- DEBUG_BUILD &&
- 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) {
- DEBUG_BUILD && 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 (DEBUG_BUILD) {
- // 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 (DEBUG_BUILD) {
- logger.log('[Profiling] Discarding profile because it contains no frames');
- }
- return false;
- }
- return true;
- }
- // Keep a flag value to avoid re-initializing the profiler constructor. If it fails
- // once, it will always fail and this allows us to early return.
- let PROFILING_CONSTRUCTOR_FAILED = false;
- const MAX_PROFILE_DURATION_MS = 30000;
- /**
- * Check if profiler constructor is available.
- * @param maybeProfiler
- */
- function isJSProfilerSupported(maybeProfiler) {
- return typeof maybeProfiler === 'function';
- }
- /**
- * Starts the profiler and returns the profiler instance.
- */
- function startJSSelfProfile() {
- // Feature support check first
- const JSProfilerConstructor = WINDOW.Profiler;
- if (!isJSProfilerSupported(JSProfilerConstructor)) {
- if (DEBUG_BUILD) {
- logger.log(
- '[Profiling] Profiling is not supported by this browser, Profiler interface missing on window object.',
- );
- }
- return;
- }
- // From initial testing, it seems that the minimum value for sampleInterval is 10ms.
- const samplingIntervalMS = 10;
- // Start the profiler
- const maxSamples = Math.floor(MAX_PROFILE_DURATION_MS / samplingIntervalMS);
- // Attempt to initialize the profiler constructor, if it fails, we disable profiling for the current user session.
- // This is likely due to a missing 'Document-Policy': 'js-profiling' header. We do not want to throw an error if this happens
- // as we risk breaking the user's application, so just disable profiling and log an error.
- try {
- return new JSProfilerConstructor({ sampleInterval: samplingIntervalMS, maxBufferSize: maxSamples });
- } catch (e) {
- if (DEBUG_BUILD) {
- logger.log(
- "[Profiling] Failed to initialize the Profiling constructor, this is likely due to a missing 'Document-Policy': 'js-profiling' header.",
- );
- logger.log('[Profiling] Disabling profiling for current user session.');
- }
- PROFILING_CONSTRUCTOR_FAILED = true;
- }
- return;
- }
- /**
- * Determine if a profile should be profiled.
- */
- function shouldProfileTransaction(transaction) {
- // If constructor failed once, it will always fail, so we can early return.
- if (PROFILING_CONSTRUCTOR_FAILED) {
- if (DEBUG_BUILD) {
- logger.log('[Profiling] Profiling has been disabled for the duration of the current user session.');
- }
- return false;
- }
- if (!transaction.isRecording()) {
- if (DEBUG_BUILD) {
- logger.log('[Profiling] Discarding profile because transaction was not sampled.');
- }
- return false;
- }
- const client = getClient();
- const options = client && client.getOptions();
- if (!options) {
- DEBUG_BUILD && logger.log('[Profiling] Profiling disabled, no options found.');
- return false;
- }
- // @ts-expect-error profilesSampleRate is not part of the browser options yet
- const profilesSampleRate = options.profilesSampleRate;
- // Since this is coming from the user (or from a function provided by the user), who knows what we might get. (The
- // only valid values are booleans or numbers between 0 and 1.)
- if (!isValidSampleRate(profilesSampleRate)) {
- DEBUG_BUILD && logger.warn('[Profiling] Discarding profile because of invalid sample rate.');
- return false;
- }
- // if the function returned 0 (or false), or if `profileSampleRate` is 0, it's a sign the profile should be dropped
- if (!profilesSampleRate) {
- DEBUG_BUILD &&
- logger.log(
- '[Profiling] Discarding profile because a negative sampling decision was inherited or profileSampleRate is set to 0',
- );
- return false;
- }
- // Now we roll the dice. Math.random is inclusive of 0, but not of 1, so strict < is safe here. In case sampleRate is
- // a boolean, the < comparison will cause it to be automatically cast to 1 if it's true and 0 if it's false.
- const sampled = profilesSampleRate === true ? true : Math.random() < profilesSampleRate;
- // Check if we should sample this profile
- if (!sampled) {
- DEBUG_BUILD &&
- logger.log(
- `[Profiling] Discarding profile because it's not included in the random sample (sampling rate = ${Number(
- profilesSampleRate,
- )})`,
- );
- 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,
- start_timestamp,
- profile,
- event,
- ) {
- if (!isValidProfile(profile)) {
- return null;
- }
- return createProfilePayload(profile_id, start_timestamp, profile, event);
- }
- const PROFILE_MAP = new Map();
- /**
- *
- */
- function getActiveProfilesCount() {
- return PROFILE_MAP.size;
- }
- /**
- * Retrieves profile from global cache and removes it.
- */
- function takeProfileFromGlobalCache(profile_id) {
- const profile = PROFILE_MAP.get(profile_id);
- if (profile) {
- PROFILE_MAP.delete(profile_id);
- }
- return profile;
- }
- /**
- * Adds profile to global cache and evicts the oldest profile if the cache is full.
- */
- function addProfileToGlobalCache(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 { MAX_PROFILE_DURATION_MS, addProfileToGlobalCache, addProfilesToEnvelope, applyDebugMetadata, convertJSSelfProfileToSampledFormat, createProfilePayload, createProfilingEvent, enrichWithThreadInformation, findProfiledTransactionsFromEnvelope, getActiveProfilesCount, isAutomatedPageLoadTransaction, isValidSampleRate, shouldProfileTransaction, startJSSelfProfile, takeProfileFromGlobalCache };
- //# sourceMappingURL=utils.js.map
|