UNPKG

@sentry/browser

Version:
475 lines (472 loc) 16.7 kB
import { GLOBAL_OBJ, spanToJSON, debug, getClient, forEachEnvelopeItem, uuid4, timestampInSeconds, DEFAULT_ENVIRONMENT, browserPerformanceTimeOrigin, getDebugImagesForResources } from '@sentry/core/browser'; import { DEBUG_BUILD } from '../debug-build.js'; import { WINDOW } from '../helpers.js'; const MS_TO_NS = 1e6; const isMainThread = "window" in GLOBAL_OBJ && GLOBAL_OBJ.window === GLOBAL_OBJ && typeof importScripts === "undefined"; const PROFILER_THREAD_ID_STRING = String(0); const PROFILER_THREAD_NAME = isMainThread ? "main" : "worker"; const navigator = WINDOW.navigator; let OS_PLATFORM = ""; let OS_PLATFORM_VERSION = ""; let OS_ARCH = ""; let OS_BROWSER = navigator?.userAgent || ""; let OS_MODEL = ""; const OS_LOCALE = navigator?.language || navigator?.languages?.[0] || ""; function isUserAgentData(data) { return typeof data === "object" && data !== null && "getHighEntropyValues" in data; } const userAgentData = 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?.length) { const firstUa = ua.fullVersionList[ua.fullVersionList.length - 1]; OS_BROWSER = `${firstUa.brand} ${firstUa.version}`; } }).catch((e) => void 0); } function isProcessedJSSelfProfile(profile) { return !("thread_metadata" in profile); } function enrichWithThreadInformation(profile) { if (!isProcessedJSSelfProfile(profile)) { return profile; } return convertJSSelfProfileToSampledFormat(profile); } function getTraceId(event) { const traceId = event.contexts?.trace?.trace_id; if (typeof traceId === "string" && traceId.length !== 32) { if (DEBUG_BUILD) { debug.log(`[Profiling] Invalid traceId: ${traceId} on profiled event`); } } if (typeof traceId !== "string") { return ""; } return traceId; } function createProfilePayload(profile_id, start_timestamp, processed_profile, event) { if (event.type !== "transaction") { throw new TypeError("Profiling events may only be attached to transactions, this should never occur."); } if (processed_profile === void 0 || 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 * 1e3 : timestampInSeconds() * 1e3; const transactionEndMs = typeof event.timestamp === "number" ? event.timestamp * 1e3 : timestampInSeconds() * 1e3; 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: PROFILER_THREAD_ID_STRING, relative_start_ns: "0", relative_end_ns: ((transactionEndMs - transactionStartMs) * 1e6).toFixed(0) } ] }; return profile; } function createProfileChunkPayload(jsSelfProfile, client, profilerId) { if (jsSelfProfile == null) { throw new TypeError( `Cannot construct profiling event envelope without a valid profile. Got ${jsSelfProfile} instead.` ); } const continuousProfile = convertToContinuousProfile(jsSelfProfile); const options = client.getOptions(); const sdk = client.getSdkMetadata?.()?.sdk; return { chunk_id: uuid4(), client_sdk: { name: sdk?.name ?? "sentry.javascript.browser", version: sdk?.version ?? "0.0.0" }, profiler_id: profilerId || uuid4(), platform: "javascript", version: "2", release: options.release ?? "", environment: options.environment ?? "production", debug_meta: { // function name obfuscation images: applyDebugMetadata(jsSelfProfile.resources) }, profile: continuousProfile }; } function validateProfileChunk(chunk) { try { if (!chunk || typeof chunk !== "object") { return { reason: "chunk is not an object" }; } const isHex32 = (val) => typeof val === "string" && /^[a-f0-9]{32}$/.test(val); if (!isHex32(chunk.profiler_id)) { return { reason: "missing or invalid profiler_id" }; } if (!isHex32(chunk.chunk_id)) { return { reason: "missing or invalid chunk_id" }; } if (!chunk.client_sdk) { return { reason: "missing client_sdk metadata" }; } const profile = chunk.profile; if (!profile) { return { reason: "missing profile data" }; } if (!Array.isArray(profile.frames) || !profile.frames.length) { return { reason: "profile has no frames" }; } if (!Array.isArray(profile.stacks) || !profile.stacks.length) { return { reason: "profile has no stacks" }; } if (!Array.isArray(profile.samples) || !profile.samples.length) { return { reason: "profile has no samples" }; } return { valid: true }; } catch (e) { return { reason: `unknown validation error: ${e}` }; } } function convertToContinuousProfile(input) { const frames = []; for (let i = 0; i < input.frames.length; i++) { const frame = input.frames[i]; if (!frame) { continue; } frames[i] = { function: frame.name, abs_path: typeof frame.resourceId === "number" ? input.resources[frame.resourceId] : void 0, lineno: frame.line, colno: frame.column }; } const stacks = []; for (let i = 0; i < input.stacks.length; i++) { const stackHead = input.stacks[i]; if (!stackHead) { continue; } const list = []; let current = stackHead; while (current) { list.push(current.frameId); current = current.parentId === void 0 ? void 0 : input.stacks[current.parentId]; } stacks[i] = list; } const perfOrigin = browserPerformanceTimeOrigin(); const origin = typeof performance.timeOrigin === "number" ? performance.timeOrigin : perfOrigin || 0; const adjustForOriginChange = origin - (perfOrigin || origin); const samples = []; for (let i = 0; i < input.samples.length; i++) { const sample = input.samples[i]; if (!sample) { continue; } const timestampSeconds = (origin + (sample.timestamp - adjustForOriginChange)) / 1e3; samples[i] = { stack_id: sample.stackId ?? 0, thread_id: PROFILER_THREAD_ID_STRING, timestamp: timestampSeconds }; } return { frames, stacks, samples, thread_metadata: { [PROFILER_THREAD_ID_STRING]: { name: PROFILER_THREAD_NAME } } }; } function isAutomatedPageLoadSpan(span) { return spanToJSON(span).op === "pageload"; } function convertJSSelfProfileToSampledFormat(input) { let EMPTY_STACK_ID = void 0; let STACK_ID = 0; const profile = { samples: [], stacks: [], frames: [], thread_metadata: { [PROFILER_THREAD_ID_STRING]: { name: PROFILER_THREAD_NAME } } }; const firstSample = input.samples[0]; if (!firstSample) { return profile; } const start = firstSample.timestamp; const perfOrigin = browserPerformanceTimeOrigin(); const origin = typeof performance.timeOrigin === "number" ? performance.timeOrigin : perfOrigin || 0; const adjustForOriginChange = origin - (perfOrigin || origin); input.samples.forEach((jsSample, i) => { if (jsSample.stackId === void 0) { if (EMPTY_STACK_ID === void 0) { 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: PROFILER_THREAD_ID_STRING }; return; } let stackTop = input.stacks[jsSample.stackId]; const stack = []; while (stackTop) { stack.push(stackTop.frameId); const frame = input.frames[stackTop.frameId]; if (frame && profile.frames[stackTop.frameId] === void 0) { profile.frames[stackTop.frameId] = { function: frame.name, abs_path: typeof frame.resourceId === "number" ? input.resources[frame.resourceId] : void 0, lineno: frame.line, colno: frame.column }; } stackTop = stackTop.parentId === void 0 ? void 0 : 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: PROFILER_THREAD_ID_STRING }; profile["stacks"][STACK_ID] = stack; profile["samples"][i] = sample; STACK_ID++; }); return profile; } function addProfilesToEnvelope(envelope, profiles) { if (!profiles.length) { return envelope; } for (const profile of profiles) { envelope[1].push([{ type: "profile" }, profile]); } return envelope; } 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?.contexts?.profile?.profile_id) { events.push(item[j]); } } }); return events; } function applyDebugMetadata(resource_paths) { const client = getClient(); const options = client?.getOptions(); const stackParser = options?.stackParser; if (!stackParser) { return []; } return getDebugImagesForResources(stackParser, resource_paths); } function isValidSampleRate(rate) { if (typeof rate !== "number" && typeof rate !== "boolean" || typeof rate === "number" && isNaN(rate)) { DEBUG_BUILD && debug.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; } if (rate === true || rate === false) { return true; } if (rate < 0 || rate > 1) { DEBUG_BUILD && debug.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) { debug.log("[Profiling] Discarding profile because it contains less than 2 samples"); } return false; } if (!profile.frames.length) { if (DEBUG_BUILD) { debug.log("[Profiling] Discarding profile because it contains no frames"); } return false; } return true; } let PROFILING_CONSTRUCTOR_FAILED = false; const MAX_PROFILE_DURATION_MS = 3e4; function isJSProfilerSupported(maybeProfiler) { return typeof maybeProfiler === "function"; } function startJSSelfProfile() { const JSProfilerConstructor = WINDOW.Profiler; if (!isJSProfilerSupported(JSProfilerConstructor)) { if (DEBUG_BUILD) { debug.log("[Profiling] Profiling is not supported by this browser, Profiler interface missing on window object."); } return; } const samplingIntervalMS = 10; const maxSamples = Math.floor(MAX_PROFILE_DURATION_MS / samplingIntervalMS); try { return new JSProfilerConstructor({ sampleInterval: samplingIntervalMS, maxBufferSize: maxSamples }); } catch (_e) { if (DEBUG_BUILD) { debug.log( "[Profiling] Failed to initialize the Profiling constructor, this is likely due to a missing 'Document-Policy': 'js-profiling' header." ); debug.log("[Profiling] Disabling profiling for current user session."); } PROFILING_CONSTRUCTOR_FAILED = true; } return; } function shouldProfileSpanLegacy(span) { if (PROFILING_CONSTRUCTOR_FAILED) { if (DEBUG_BUILD) { debug.log("[Profiling] Profiling has been disabled for the duration of the current user session."); } return false; } if (!span.isRecording()) { DEBUG_BUILD && debug.log("[Profiling] Discarding profile because root span was not sampled."); return false; } const client = getClient(); const options = client?.getOptions(); if (!options) { DEBUG_BUILD && debug.log("[Profiling] Profiling disabled, no options found."); return false; } const profilesSampleRate = options.profilesSampleRate; if (!isValidSampleRate(profilesSampleRate)) { DEBUG_BUILD && debug.warn("[Profiling] Discarding profile because of invalid sample rate."); return false; } if (!profilesSampleRate) { DEBUG_BUILD && debug.log( "[Profiling] Discarding profile because a negative sampling decision was inherited or profileSampleRate is set to 0" ); return false; } const sampled = profilesSampleRate === true ? true : Math.random() < profilesSampleRate; if (!sampled) { DEBUG_BUILD && debug.log( `[Profiling] Discarding profile because it's not included in the random sample (sampling rate = ${Number( profilesSampleRate )})` ); return false; } return true; } function shouldProfileSession(options) { if (PROFILING_CONSTRUCTOR_FAILED) { if (DEBUG_BUILD) { debug.log( "[Profiling] Profiling has been disabled for the duration of the current user session as the JS Profiler could not be started." ); } return false; } if (options.profileLifecycle !== "trace" && options.profileLifecycle !== "manual") { DEBUG_BUILD && debug.warn("[Profiling] Session not sampled. Invalid `profileLifecycle` option."); return false; } const profileSessionSampleRate = options.profileSessionSampleRate; if (!isValidSampleRate(profileSessionSampleRate)) { DEBUG_BUILD && debug.warn("[Profiling] Discarding profile because of invalid profileSessionSampleRate."); return false; } if (!profileSessionSampleRate) { DEBUG_BUILD && debug.log("[Profiling] Discarding profile because profileSessionSampleRate is not defined or set to 0"); return false; } return Math.random() <= profileSessionSampleRate; } function hasLegacyProfiling(options) { return typeof options.profilesSampleRate !== "undefined"; } function createProfilingEvent(profile_id, start_timestamp, profile, event) { if (!isValidProfile(profile)) { return null; } return createProfilePayload(profile_id, start_timestamp, profile, event); } const PROFILE_MAP = /* @__PURE__ */ new Map(); function getActiveProfilesCount() { return PROFILE_MAP.size; } function takeProfileFromGlobalCache(profile_id) { const profile = PROFILE_MAP.get(profile_id); if (profile) { PROFILE_MAP.delete(profile_id); } return profile; } function addProfileToGlobalCache(profile_id, profile) { PROFILE_MAP.set(profile_id, profile); if (PROFILE_MAP.size > 30) { const last = PROFILE_MAP.keys().next().value; if (last !== void 0) { PROFILE_MAP.delete(last); } } } const PROFILED_ROOT_SPANS = /* @__PURE__ */ new WeakSet(); function setThreadAttributes(span) { span.setAttribute("thread.id", PROFILER_THREAD_ID_STRING); span.setAttribute("thread.name", PROFILER_THREAD_NAME); } export { MAX_PROFILE_DURATION_MS, PROFILED_ROOT_SPANS, PROFILER_THREAD_ID_STRING, PROFILER_THREAD_NAME, addProfileToGlobalCache, addProfilesToEnvelope, applyDebugMetadata, convertJSSelfProfileToSampledFormat, createProfileChunkPayload, createProfilePayload, createProfilingEvent, enrichWithThreadInformation, findProfiledTransactionsFromEnvelope, getActiveProfilesCount, hasLegacyProfiling, isAutomatedPageLoadSpan, isValidSampleRate, setThreadAttributes, shouldProfileSession, shouldProfileSpanLegacy, startJSSelfProfile, takeProfileFromGlobalCache, validateProfileChunk }; //# sourceMappingURL=utils.js.map