UNPKG

@sentry/profiling-node

Version:
1,186 lines (1,039 loc) 39.2 kB
import { _nullishCoalesce, _optionalChain, _optionalChainDelete } from '@sentry/utils'; import { spanIsSampled, spanToJSON, defineIntegration, getRootSpan, getCurrentScope, getIsolationScope, getGlobalScope } from '@sentry/core'; import { logger, GLOBAL_OBJ, parseSemver, forEachEnvelopeItem, createEnvelope, uuid4, dsnToString, LRUMap } from '@sentry/utils'; import { platform as platform$1, arch as arch$1 } from 'node:os'; import { resolve, join } from 'node:path'; import { versions, env } from 'node:process'; import { threadId } from 'node:worker_threads'; import { familySync } from 'detect-libc'; import { getAbi } from 'node-abi'; import * as os from 'os'; import { versions as versions$1, env as env$1 } from 'process'; import { threadId as threadId$1, isMainThread } from 'worker_threads'; // -- Shims -- import cjsUrl from 'node:url'; import cjsPath from 'node:path'; import cjsModule from 'node:module'; const __filename = cjsUrl.fileURLToPath(import.meta.url); const __dirname = cjsPath.dirname(__filename); const require = cjsModule.createRequire(import.meta.url); /** * This serves as a build time flag that will be true by default, but false in non-debug builds or if users replace `__SENTRY_DEBUG__` in their generated code. * * ATTENTION: This constant must never cross package boundaries (i.e. be exported) to guarantee that it can be used for tree shaking. */ const DEBUG_BUILD = (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__); const stdlib = familySync(); const platform = process.env['BUILD_PLATFORM'] || platform$1(); const arch = process.env['BUILD_ARCH'] || arch$1(); const abi = getAbi(versions.node, 'node'); const identifier = [platform, arch, stdlib, abi].filter(c => c !== undefined && c !== null).join('-'); const built_from_source_path = resolve(__dirname, '..', `./sentry_cpu_profiler-${identifier}`); /** * Imports cpp bindings based on the current platform and architecture. */ // eslint-disable-next-line complexity function importCppBindingsModule() { // If a binary path is specified, use that. if (env['SENTRY_PROFILER_BINARY_PATH']) { const envPath = env['SENTRY_PROFILER_BINARY_PATH']; return require(envPath); } // If a user specifies a different binary dir, they are in control of the binaries being moved there if (env['SENTRY_PROFILER_BINARY_DIR']) { const binaryPath = join(resolve(env['SENTRY_PROFILER_BINARY_DIR']), `sentry_cpu_profiler-${identifier}`); return require(`${binaryPath}.node`); } // We need the fallthrough so that in the end, we can fallback to the require dynamice require. // This is for cases where precompiled binaries were not provided, but may have been compiled from source. if (platform === 'darwin') { if (arch === 'x64') { if (abi === '93') { return require('../sentry_cpu_profiler-darwin-x64-93.node'); } if (abi === '108') { return require('../sentry_cpu_profiler-darwin-x64-108.node'); } if (abi === '115') { return require('../sentry_cpu_profiler-darwin-x64-115.node'); } if (abi === '127') { return require('../sentry_cpu_profiler-darwin-x64-127.node'); } } if (arch === 'arm64') { if (abi === '93') { return require('../sentry_cpu_profiler-darwin-arm64-93.node'); } if (abi === '108') { return require('../sentry_cpu_profiler-darwin-arm64-108.node'); } if (abi === '115') { return require('../sentry_cpu_profiler-darwin-arm64-115.node'); } if (abi === '127') { return require('../sentry_cpu_profiler-darwin-arm64-127.node'); } } } if (platform === 'win32') { if (arch === 'x64') { if (abi === '93') { return require('../sentry_cpu_profiler-win32-x64-93.node'); } if (abi === '108') { return require('../sentry_cpu_profiler-win32-x64-108.node'); } if (abi === '115') { return require('../sentry_cpu_profiler-win32-x64-115.node'); } if (abi === '127') { return require('../sentry_cpu_profiler-win32-x64-127.node'); } } } if (platform === 'linux') { if (arch === 'x64') { if (stdlib === 'musl') { if (abi === '93') { return require('../sentry_cpu_profiler-linux-x64-musl-93.node'); } if (abi === '108') { return require('../sentry_cpu_profiler-linux-x64-musl-108.node'); } if (abi === '115') { return require('../sentry_cpu_profiler-linux-x64-musl-115.node'); } if (abi === '127') { return require('../sentry_cpu_profiler-linux-x64-musl-127.node'); } } if (stdlib === 'glibc') { if (abi === '93') { return require('../sentry_cpu_profiler-linux-x64-glibc-93.node'); } if (abi === '108') { return require('../sentry_cpu_profiler-linux-x64-glibc-108.node'); } if (abi === '115') { return require('../sentry_cpu_profiler-linux-x64-glibc-115.node'); } if (abi === '127') { return require('../sentry_cpu_profiler-linux-x64-glibc-127.node'); } } } if (arch === 'arm64') { if (stdlib === 'musl') { if (abi === '93') { return require('../sentry_cpu_profiler-linux-arm64-musl-93.node'); } if (abi === '108') { return require('../sentry_cpu_profiler-linux-arm64-musl-108.node'); } if (abi === '115') { return require('../sentry_cpu_profiler-linux-arm64-musl-115.node'); } if (abi === '127') { return require('../sentry_cpu_profiler-linux-arm64-musl-127.node'); } } if (stdlib === 'glibc') { if (abi === '93') { return require('../sentry_cpu_profiler-linux-arm64-glibc-93.node'); } if (abi === '108') { return require('../sentry_cpu_profiler-linux-arm64-glibc-108.node'); } if (abi === '115') { return require('../sentry_cpu_profiler-linux-arm64-glibc-115.node'); } if (abi === '127') { return require('../sentry_cpu_profiler-linux-arm64-glibc-127.node'); } } } } return require(`${built_from_source_path}.node`); } const PrivateCpuProfilerBindings = importCppBindingsModule(); class Bindings { startProfiling(name) { if (!PrivateCpuProfilerBindings) { DEBUG_BUILD && logger.log('[Profiling] Bindings not loaded, ignoring call to startProfiling.'); return; } return PrivateCpuProfilerBindings.startProfiling(name); } stopProfiling( name, format, ) { if (!PrivateCpuProfilerBindings) { DEBUG_BUILD && logger.log('[Profiling] Bindings not loaded or profile was never started, ignoring call to stopProfiling.'); return null; } return PrivateCpuProfilerBindings.stopProfiling( name, format , threadId, !!GLOBAL_OBJ._sentryDebugIds, ); } } const CpuProfilerBindings = new Bindings(); const NODE_VERSION = parseSemver(process.versions.node) ; const NODE_MAJOR = NODE_VERSION.major; function _nullishCoalesce$1(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } }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 */ // We require the file because if we import it, it will be included in the bundle. // I guess tsc does not check file contents when it's imported. const PROFILER_THREAD_ID_STRING = String(threadId$1); const PROFILER_THREAD_NAME = isMainThread ? 'main' : 'worker'; const FORMAT_VERSION = '1'; const CONTINUOUS_FORMAT_VERSION = '2'; // Os machine was backported to 16.18, but this was not reflected in the types // @ts-expect-error ignore missing const machine = typeof os.machine === 'function' ? os.machine() : os.arch(); // Machine properties (eval only once) const PLATFORM = os.platform(); const RELEASE = os.release(); const VERSION = os.version(); const TYPE = os.type(); const MODEL = machine; const ARCH = os.arch(); /** * Checks if the profile is a raw profile or a profile enriched with thread information. * @param {ThreadCpuProfile | RawThreadCpuProfile} profile * @returns {boolean} */ function isRawThreadCpuProfile( 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. * * @param {ThreadCpuProfile | RawThreadCpuProfile} profile * @returns {ThreadCpuProfile} */ function enrichWithThreadInformation( profile, ) { if (!isRawThreadCpuProfile(profile)) { return profile; } return { samples: profile.samples, frames: profile.frames, stacks: profile.stacks, thread_metadata: { [PROFILER_THREAD_ID_STRING]: { name: PROFILER_THREAD_NAME, }, }, } ; } /** * Creates a profiling envelope item, if the profile does not pass validation, returns null. * @param {RawThreadCpuProfile} * @param {Event} * @returns {Profile | null} */ function createProfilingEvent(client, profile, event) { if (!isValidProfile(profile)) { return null; } return createProfilePayload(client, profile, { release: _nullishCoalesce$1(event.release, () => ( '')), environment: _nullishCoalesce$1(event.environment, () => ( '')), event_id: _nullishCoalesce$1(event.event_id, () => ( '')), transaction: _nullishCoalesce$1(event.transaction, () => ( '')), start_timestamp: event.start_timestamp ? event.start_timestamp * 1000 : Date.now(), trace_id: _nullishCoalesce$1(_optionalChain$1([event, 'access', _ => _.contexts, 'optionalAccess', _2 => _2['trace'], 'optionalAccess', _3 => _3['trace_id']]), () => ( '')), profile_id: profile.profile_id, }); } /** * Create a profile * @param {RawThreadCpuProfile} cpuProfile * @param {options} * @returns {Profile} */ function createProfilePayload( client, cpuProfile, { release, environment, event_id, transaction, start_timestamp, trace_id, profile_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 (trace_id && trace_id.length !== 32) { DEBUG_BUILD && logger.log(`[Profiling] Invalid traceId: ${trace_id} on profiled event`); } const enrichedThreadProfile = enrichWithThreadInformation(cpuProfile); const profile = { event_id: profile_id, timestamp: new Date(start_timestamp).toISOString(), platform: 'node', version: FORMAT_VERSION, release: release, environment: environment, measurements: cpuProfile.measurements, runtime: { name: 'node', version: versions$1.node || '', }, os: { name: PLATFORM, version: RELEASE, build_number: VERSION, }, device: { locale: env$1['LC_ALL'] || env$1['LC_MESSAGES'] || env$1['LANG'] || env$1['LANGUAGE'] || '', model: MODEL, manufacturer: TYPE, architecture: ARCH, is_emulator: false, }, debug_meta: { images: applyDebugMetadata(client, cpuProfile.resources), }, profile: enrichedThreadProfile , transaction: { name: transaction, id: event_id, trace_id: trace_id || '', active_thread_id: PROFILER_THREAD_ID_STRING, }, }; return profile; } /** * Create a profile chunk from raw thread profile * @param {RawThreadCpuProfile} cpuProfile * @param {options} * @returns {Profile} */ function createProfileChunkPayload( client, cpuProfile, { release, environment, trace_id, profiler_id, chunk_id, sdk, } , ) { // 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 (trace_id && trace_id.length !== 32) { DEBUG_BUILD && logger.log(`[Profiling] Invalid traceId: ${trace_id} on profiled event`); } const enrichedThreadProfile = enrichWithThreadInformation(cpuProfile); const profile = { chunk_id: chunk_id, client_sdk: { name: _nullishCoalesce$1(_optionalChain$1([sdk, 'optionalAccess', _4 => _4.name]), () => ( 'sentry.javascript.node')), version: _nullishCoalesce$1(_optionalChain$1([sdk, 'optionalAccess', _5 => _5.version]), () => ( '0.0.0')), }, profiler_id: profiler_id, platform: 'node', version: CONTINUOUS_FORMAT_VERSION, release: release, environment: environment, measurements: cpuProfile.measurements, debug_meta: { images: applyDebugMetadata(client, cpuProfile.resources), }, profile: enrichedThreadProfile , }; return profile; } /** * Creates a profiling chunk envelope item, if the profile does not pass validation, returns null. */ function createProfilingChunkEvent( client, options, profile, sdk, identifiers, ) { if (!isValidProfileChunk(profile)) { return null; } return createProfileChunkPayload(client, profile, { release: _nullishCoalesce$1(options.release, () => ( '')), environment: _nullishCoalesce$1(options.environment, () => ( '')), trace_id: _nullishCoalesce$1(identifiers.trace_id, () => ( '')), chunk_id: identifiers.chunk_id, profiler_id: identifiers.profiler_id, sdk, }); } /** * Checks the given sample rate to make sure it is valid type and value (a boolean, or a number between 0 and 1). * @param {unknown} rate * @returns {boolean} */ 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; } /** * Checks if the profile is valid and can be sent to Sentry. * @param {RawThreadCpuProfile} profile * @returns {boolean} */ function isValidProfile(profile) { if (profile.samples.length <= 1) { 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.profile_id) { return false; } return true; } /** * Checks if the profile chunk is valid and can be sent to Sentry. * @param profile * @returns */ function isValidProfileChunk(profile) { if (profile.samples.length <= 1) { 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 chunk because it contains less than 2 samples'); return false; } return true; } /** * Adds items to envelope if they are not already present - mutates the envelope. * @param {Envelope} envelope * @param {Profile[]} profiles * @returns {Envelope} */ function addProfilesToEnvelope(envelope, profiles) { if (!profiles.length) { return envelope; } for (const profile of profiles) { // @ts-expect-error untyped envelope envelope[1].push([{ type: 'profile' }, profile]); } return envelope; } /** * Finds transactions with profile_id context in the envelope * @param {Envelope} envelope * @returns {Event[]} */ function findProfiledTransactionsFromEnvelope(envelope) { const events = []; forEachEnvelopeItem(envelope, (item, type) => { if (type !== 'transaction') { return; } // First item is the type, so we can skip it, everything else is an event for (let j = 1; j < item.length; j++) { const event = item[j]; if (!event) { // Shouldnt happen, but lets be safe continue; } // @ts-expect-error profile_id is not part of the metadata type const profile_id = _optionalChain$1([(event.contexts ), 'optionalAccess', _6 => _6['profile'], 'optionalAccess', _7 => _7['profile_id']]); if (event && profile_id) { events.push(item[j] ); } } }); return events; } /** * Creates event envelope headers for a profile chunk. This is separate from createEventEnvelopeHeaders util * as the profile chunk does not conform to the sentry event type */ function createEventEnvelopeHeaders( sdkInfo, tunnel, dsn, ) { return { event_id: uuid4(), sent_at: new Date().toISOString(), ...(sdkInfo && { sdk: sdkInfo }), ...(!!tunnel && dsn && { dsn: dsnToString(dsn) }), }; } /** * Creates a standalone profile_chunk envelope. */ function makeProfileChunkEnvelope( chunk, sdkInfo, tunnel, dsn, ) { const profileChunkHeader = { type: 'profile_chunk', }; return createEnvelope(createEventEnvelopeHeaders(sdkInfo, tunnel, dsn), [ [profileChunkHeader, chunk], ]); } const debugIdStackParserCache = new WeakMap(); /** * Cross reference profile collected resources with debug_ids and return a list of debug images. * @param {string[]} resource_paths * @returns {DebugImage[]} */ function applyDebugMetadata(client, resource_paths) { const debugIdMap = GLOBAL_OBJ._sentryDebugIds; if (!debugIdMap) { return []; } const options = client.getOptions(); if (!options || !options.stackParser) { return []; } let debugIdStackFramesCache; const cachedDebugIdStackFrameCache = debugIdStackParserCache.get(options.stackParser); if (cachedDebugIdStackFrameCache) { debugIdStackFramesCache = cachedDebugIdStackFrameCache; } else { debugIdStackFramesCache = new Map(); debugIdStackParserCache.set(options.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 = options.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 resource of resource_paths) { if (resource && filenameDebugIdMap[resource]) { images.push({ type: 'sourcemap', code_file: resource, debug_id: filenameDebugIdMap[resource] , }); } } return images; } const MAX_PROFILE_DURATION_MS = 30 * 1000; /** * Takes a transaction and determines if it should be profiled or not. If it should be profiled, it returns the * profile_id, otherwise returns undefined. Takes care of setting profile context on transaction as well */ function maybeProfileSpan( client, span, customSamplingContext, ) { // profilesSampleRate is multiplied with tracesSampleRate to get the final sampling rate. We dont perform // the actual multiplication to get the final rate, but we discard the profile if the span was sampled, // so anything after this block from here is based on the span sampling. if (!spanIsSampled(span)) { return; } // Client and options are required for profiling if (!client) { DEBUG_BUILD && logger.log('[Profiling] Profiling disabled, no client found.'); return; } const options = client.getOptions(); if (!options) { DEBUG_BUILD && logger.log('[Profiling] Profiling disabled, no options found.'); return; } const profilesSampler = options.profilesSampler; let profilesSampleRate = options.profilesSampleRate; // Prefer sampler to sample rate if both are provided. if (typeof profilesSampler === 'function') { const { description: spanName = '<unknown>', data } = spanToJSON(span); // We bail out early if that is not the case const parentSampled = true; profilesSampleRate = profilesSampler({ name: spanName, attributes: data, transactionContext: { name: spanName, parentSampled, }, parentSampled, ...customSamplingContext, }); } // 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; } // 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 ${ typeof profilesSampler === 'function' ? 'profileSampler returned 0 or false' : 'a negative sampling decision was inherited or profileSampleRate is set to 0' }`, ); return; } // 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; } const profile_id = uuid4(); CpuProfilerBindings.startProfiling(profile_id); DEBUG_BUILD && logger.log(`[Profiling] started profiling transaction: ${spanToJSON(span).description}`); // set transaction context - do this regardless if profiling fails down the line // so that we can still see the profile_id in the transaction context return profile_id; } /** * Stops the profiler for profile_id and returns the profile * @param transaction * @param profile_id * @returns */ function stopSpanProfile(span, profile_id) { // Should not happen, but satisfy the type checker and be safe regardless. if (!profile_id) { return null; } const profile = CpuProfilerBindings.stopProfiling(profile_id, 0); DEBUG_BUILD && logger.log(`[Profiling] stopped profiling of transaction: ${spanToJSON(span).description}`); // In case of an overlapping span, stopProfiling may return null and silently ignore the overlapping profile. if (!profile) { DEBUG_BUILD && logger.log( `[Profiling] profiler returned null profile for: ${spanToJSON(span).description}`, 'this may indicate an overlapping span or a call to stopProfiling with a profile title that was never started', ); return null; } // Assign profile_id to the profile profile.profile_id = profile_id; return profile; } var ProfileFormat;(function (ProfileFormat) { const THREAD = 0; ProfileFormat[ProfileFormat["THREAD"] = THREAD] = "THREAD"; const CHUNK = 1; ProfileFormat[ProfileFormat["CHUNK"] = CHUNK] = "CHUNK"; })(ProfileFormat || (ProfileFormat = {})); /* eslint-disable max-lines */ const CHUNK_INTERVAL_MS = 5000; const PROFILE_MAP = new LRUMap(50); const PROFILE_TIMEOUTS = {}; function addToProfileQueue(profile_id, profile) { PROFILE_MAP.set(profile_id, profile); } function takeFromProfileQueue(profile_id) { const profile = PROFILE_MAP.get(profile_id); PROFILE_MAP.remove(profile_id); return profile; } /** * Instruments the client to automatically invoke the profiler on span start and stop events. * @param client */ function setupAutomatedSpanProfiling(client) { const spanToProfileIdMap = new WeakMap(); client.on('spanStart', span => { if (span !== getRootSpan(span)) { return; } const profile_id = maybeProfileSpan(client, span); if (profile_id) { const options = client.getOptions(); // Not intended for external use, hence missing types, but we want to profile a couple of things at Sentry that // currently exceed the default timeout set by the SDKs. const maxProfileDurationMs = (options._experiments && options._experiments['maxProfileDurationMs']) || MAX_PROFILE_DURATION_MS; if (PROFILE_TIMEOUTS[profile_id]) { global.clearTimeout(PROFILE_TIMEOUTS[profile_id]); // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete PROFILE_TIMEOUTS[profile_id]; } // Enqueue a timeout to prevent profiles from running over max duration. const timeout = global.setTimeout(() => { DEBUG_BUILD && logger.log('[Profiling] max profile duration elapsed, stopping profiling for:', spanToJSON(span).description); const profile = stopSpanProfile(span, profile_id); if (profile) { addToProfileQueue(profile_id, profile); } }, maxProfileDurationMs); // Unref timeout so it doesn't keep the process alive. timeout.unref(); getCurrentScope().setContext('profile', { profile_id }); spanToProfileIdMap.set(span, profile_id); } }); client.on('spanEnd', span => { const profile_id = spanToProfileIdMap.get(span); if (profile_id) { if (PROFILE_TIMEOUTS[profile_id]) { global.clearTimeout(PROFILE_TIMEOUTS[profile_id]); // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete PROFILE_TIMEOUTS[profile_id]; } const profile = stopSpanProfile(span, profile_id); if (profile) { addToProfileQueue(profile_id, profile); } } }); client.on('beforeEnvelope', (envelope) => { // if not profiles are in queue, there is nothing to add to the envelope. if (!PROFILE_MAP.size) { return; } const profiledTransactionEvents = findProfiledTransactionsFromEnvelope(envelope); if (!profiledTransactionEvents.length) { return; } const profilesToAddToEnvelope = []; for (const profiledTransaction of profiledTransactionEvents) { const profileContext = _optionalChain([profiledTransaction, 'access', _ => _.contexts, 'optionalAccess', _2 => _2['profile']]); const profile_id = _optionalChain([profileContext, 'optionalAccess', _3 => _3['profile_id']]); if (!profile_id) { throw new TypeError('[Profiling] cannot find profile for a transaction without a profile context'); } // Remove the profile from the transaction context before sending, relay will take care of the rest. if (profileContext) { _optionalChainDelete([profiledTransaction, 'access', _4 => _4.contexts, 'optionalAccess', _5 => delete _5['profile']]); } const cpuProfile = takeFromProfileQueue(profile_id); if (!cpuProfile) { DEBUG_BUILD && logger.log(`[Profiling] Could not retrieve profile for transaction: ${profile_id}`); continue; } const profile = createProfilingEvent(client, cpuProfile, profiledTransaction); if (!profile) return; profilesToAddToEnvelope.push(profile); // @ts-expect-error profile does not inherit from Event client.emit('preprocessEvent', profile, { event_id: profiledTransaction.event_id, }); } addProfilesToEnvelope(envelope, profilesToAddToEnvelope); }); } class ContinuousProfiler {constructor() { ContinuousProfiler.prototype.__init.call(this);ContinuousProfiler.prototype.__init2.call(this);ContinuousProfiler.prototype.__init3.call(this); } __init() {this._profilerId = uuid4();} __init2() {this._client = undefined;} __init3() {this._chunkData = undefined;} /** * Called when the profiler is attached to the client (continuous mode is enabled). If of the profiler * methods called before the profiler is initialized will result in a noop action with debug logs. * @param client */ initialize(client) { this._client = client; } /** * Recursively schedules chunk profiling to start and stop at a set interval. * Once the user calls stop(), the current chunk will be stopped and flushed to Sentry and no new chunks will * will be started. To restart continuous mode after calling stop(), the user must call start() again. * @returns void */ start() { if (!this._client) { // The client is not attached to the profiler if the user has not enabled continuous profiling. // In this case, calling start() and stop() is a noop action.The reason this exists is because // it makes the types easier to work with and avoids users having to do null checks. DEBUG_BUILD && logger.log('[Profiling] Profiler was never attached to the client.'); return; } if (this._chunkData) { DEBUG_BUILD && logger.log( `[Profiling] Chunk with chunk_id ${this._chunkData.id} is still running, current chunk will be stopped a new chunk will be started.`, ); this.stop(); } const traceId = getCurrentScope().getPropagationContext().traceId || getIsolationScope().getPropagationContext().traceId; this._initializeChunk(traceId); this._startChunkProfiling(this._chunkData); } /** * Stops the current chunk and flushes the profile to Sentry. * @returns void */ stop() { if (_optionalChain([this, 'access', _6 => _6._chunkData, 'optionalAccess', _7 => _7.timer])) { global.clearTimeout(this._chunkData.timer); this._chunkData.timer = undefined; DEBUG_BUILD && logger.log(`[Profiling] Stopping profiling chunk: ${this._chunkData.id}`); } if (!this._client) { DEBUG_BUILD && logger.log('[Profiling] Failed to collect profile, sentry client was never attached to the profiler.'); return; } if (!_optionalChain([this, 'access', _8 => _8._chunkData, 'optionalAccess', _9 => _9.id])) { DEBUG_BUILD && logger.log(`[Profiling] Failed to collect profile for: ${_optionalChain([this, 'access', _10 => _10._chunkData, 'optionalAccess', _11 => _11.id])}, the chunk_id is missing.`); return; } const profile = this._stopChunkProfiling(this._chunkData); if (!profile) { DEBUG_BUILD && logger.log(`[Profiling] _chunkiledStartTraceID to collect profile for: ${this._chunkData.id}`); return; } if (profile) { DEBUG_BUILD && logger.log(`[Profiling] Sending profile chunk ${this._chunkData.id}.`); } DEBUG_BUILD && logger.log(`[Profiling] Profile chunk ${this._chunkData.id} sent to Sentry.`); const chunk = createProfilingChunkEvent( this._client, this._client.getOptions(), profile, _optionalChain([this, 'access', _12 => _12._client, 'access', _13 => _13.getSdkMetadata, 'call', _14 => _14(), 'optionalAccess', _15 => _15.sdk]), { chunk_id: this._chunkData.id, trace_id: this._chunkData.startTraceID, profiler_id: this._profilerId, }, ); if (!chunk) { DEBUG_BUILD && logger.log(`[Profiling] Failed to create profile chunk for: ${this._chunkData.id}`); this._reset(); return; } this._flush(chunk); // Depending on the profile and stack sizes, stopping the profile and converting // the format may negatively impact the performance of the application. To avoid // blocking for too long, enqueue the next chunk start inside the next macrotask. // clear current chunk this._reset(); } /** * Flushes the profile chunk to Sentry. * @param chunk */ _flush(chunk) { if (!this._client) { DEBUG_BUILD && logger.log('[Profiling] Failed to collect profile, sentry client was never attached to the profiler.'); return; } const transport = this._client.getTransport(); if (!transport) { DEBUG_BUILD && logger.log('[Profiling] No transport available to send profile chunk.'); return; } const dsn = this._client.getDsn(); const metadata = this._client.getSdkMetadata(); const tunnel = this._client.getOptions().tunnel; const envelope = makeProfileChunkEnvelope(chunk, _optionalChain([metadata, 'optionalAccess', _16 => _16.sdk]), tunnel, dsn); transport.send(envelope).then(null, reason => { DEBUG_BUILD && logger.error('Error while sending profile chunk envelope:', reason); }); } /** * Stops the profile and clears chunk instrumentation from global scope * @returns void */ _stopChunkProfiling(chunk) { this._teardownSpanChunkInstrumentation(); return CpuProfilerBindings.stopProfiling(chunk.id, ProfileFormat.CHUNK); } /** * Starts the profiler and registers the flush timer for a given chunk. * @param chunk */ _startChunkProfiling(chunk) { this._setupSpanChunkInstrumentation(); CpuProfilerBindings.startProfiling(chunk.id); DEBUG_BUILD && logger.log(`[Profiling] starting profiling chunk: ${chunk.id}`); chunk.timer = global.setTimeout(() => { DEBUG_BUILD && logger.log(`[Profiling] Stopping profiling chunk: ${chunk.id}`); this.stop(); DEBUG_BUILD && logger.log('[Profiling] Starting new profiling chunk.'); setImmediate(this.start.bind(this)); }, CHUNK_INTERVAL_MS); // Unref timeout so it doesn't keep the process alive. chunk.timer.unref(); } /** * Attaches profiling information to spans that were started * during a profiling session. */ _setupSpanChunkInstrumentation() { if (!this._client) { DEBUG_BUILD && logger.log('[Profiling] Failed to collect profile, sentry client was never attached to the profiler.'); return; } getGlobalScope().setContext('profile', { profiler_id: this._profilerId, }); this._client.on('beforeSendEvent', e => this._assignThreadIdContext(e)); } /** * Clear profiling information from global context when a profile is not running. */ _teardownSpanChunkInstrumentation() { const globalScope = getGlobalScope(); globalScope.setContext('profile', {}); } /** * Initializes new profile chunk metadata */ _initializeChunk(traceId) { this._chunkData = { id: uuid4(), startTraceID: traceId, timer: undefined, }; } /** * Assigns thread_id and thread name context to a profiled event. */ _assignThreadIdContext(event) { if (!_optionalChain([event, 'optionalAccess', _17 => _17['contexts'], 'optionalAccess', _18 => _18['profile']])) { return; } if (!event.contexts) { return; } // @ts-expect-error the trace fallback value is wrong, though it should never happen // and in case it does, we dont want to override whatever was passed initially. event.contexts['trace'] = { ...(_nullishCoalesce(_optionalChain([event, 'access', _19 => _19.contexts, 'optionalAccess', _20 => _20['trace']]), () => ( {}))), data: { ...(_nullishCoalesce(_optionalChain([event, 'access', _21 => _21.contexts, 'optionalAccess', _22 => _22['trace'], 'optionalAccess', _23 => _23['data']]), () => ( {}))), ['thread.id']: PROFILER_THREAD_ID_STRING, ['thread.name']: PROFILER_THREAD_NAME, }, }; } /** * Resets the current chunk state. */ _reset() { this._chunkData = undefined; } } /** Exported only for tests. */ const _nodeProfilingIntegration = (() => { if (DEBUG_BUILD && ![16, 18, 20, 22].includes(NODE_MAJOR)) { logger.warn( `[Profiling] You are using a Node.js version that does not have prebuilt binaries (${NODE_VERSION}).`, 'The @sentry/profiling-node package only has prebuilt support for the following LTS versions of Node.js: 16, 18, 20, 22.', 'To use the @sentry/profiling-node package with this version of Node.js, you will need to compile the native addon from source.', 'See: https://github.com/getsentry/sentry-javascript/tree/develop/packages/profiling-node#building-the-package-from-source', ); } return { name: 'ProfilingIntegration', _profiler: new ContinuousProfiler(), setup(client) { DEBUG_BUILD && logger.log('[Profiling] Profiling integration setup.'); const options = client.getOptions(); const mode = (options.profilesSampleRate === undefined || options.profilesSampleRate === 0) && !options.profilesSampler ? 'continuous' : 'span'; switch (mode) { case 'continuous': { DEBUG_BUILD && logger.log('[Profiling] Continuous profiler mode enabled.'); this._profiler.initialize(client); break; } // Default to span profiling when no mode profiler mode is set case 'span': case undefined: { DEBUG_BUILD && logger.log('[Profiling] Span profiler mode enabled.'); setupAutomatedSpanProfiling(client); break; } default: { DEBUG_BUILD && logger.warn(`[Profiling] Unknown profiler mode: ${mode}, profiler was not initialized`); } } }, }; }) ; /** * We need this integration in order to send data to Sentry. We hook into the event processor * and inspect each event to see if it is a transaction event and if that transaction event * contains a profile on it's metadata. If that is the case, we create a profiling event envelope * and delete the profile from the transaction metadata. */ const nodeProfilingIntegration = defineIntegration(_nodeProfilingIntegration); export { nodeProfilingIntegration }; //# sourceMappingURL=index.js.map