UNPKG

@sentry/profiling-node

Version:
1,176 lines (1,015 loc) 36.5 kB
import { parseSemver, forEachEnvelopeItem, createEnvelope, debug, dsnToString, uuid4, getDebugImagesForResources, spanIsSampled, spanToJSON, LRUMap, defineIntegration, consoleSandbox, getRootSpan, getIsolationScope, getCurrentScope, getGlobalScope } from '@sentry/core'; import { CpuProfilerBindings, ProfileFormat } from '@sentry-internal/node-cpu-profiler'; import * as os from 'os'; import { env, versions } from 'process'; import { isMainThread, threadId } from 'worker_threads'; /** * 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 NODE_VERSION = parseSemver(process.versions.node) ; const NODE_MAJOR = NODE_VERSION.major; // 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); const PROFILER_THREAD_NAME = isMainThread ? 'main' : 'worker'; const FORMAT_VERSION = '1'; const CONTINUOUS_FORMAT_VERSION = '2'; // Machine properties (eval only once) const PLATFORM = os.platform(); const RELEASE = os.release(); const VERSION = os.version(); const TYPE = os.type(); const MODEL = os.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: event.release ?? '', environment: event.environment ?? '', event_id: event.event_id ?? '', transaction: event.transaction ?? '', start_timestamp: event.start_timestamp ? event.start_timestamp * 1000 : Date.now(), trace_id: event.contexts?.trace?.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?.length !== 32) { DEBUG_BUILD && debug.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.node || '', }, os: { name: PLATFORM, version: RELEASE, build_number: VERSION, }, device: { locale: env['LC_ALL'] || env['LC_MESSAGES'] || env['LANG'] || env['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?.length !== 32) { DEBUG_BUILD && debug.log(`[Profiling] Invalid traceId: ${trace_id} on profiled event`); } const enrichedThreadProfile = enrichWithThreadInformation(cpuProfile); const profile = { chunk_id: chunk_id, client_sdk: { name: sdk?.name ?? 'sentry.javascript.node', version: sdk?.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: options.release ?? '', environment: options.environment ?? '', trace_id: 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 && 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; } // 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 && debug.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. debug.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. debug.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) { // Shouldn't happen, but lets be safe continue; } const profile_id = event.contexts?.profile?.profile_id; if (event && profile_id) { events.push(event); } } }); 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( platform, chunk, sdkInfo, tunnel, dsn, ) { const profileChunkHeader = { type: 'profile_chunk', platform, }; return createEnvelope(createEventEnvelopeHeaders(sdkInfo, tunnel, dsn), [ [profileChunkHeader, chunk], ]); } /** * 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 options = client.getOptions(); if (!options?.stackParser) { return []; } return getDebugImagesForResources(options.stackParser, resource_paths); } 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 && debug.log('[Profiling] Profiling disabled, no client found.'); return; } const options = client.getOptions(); if (!options) { DEBUG_BUILD && debug.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, 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 && debug.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 && debug.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 && debug.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 && debug.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 && debug.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 && debug.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; } const CHUNK_INTERVAL_MS = 1000 * 60; 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; } class ContinuousProfiler {constructor() { ContinuousProfiler.prototype.__init.call(this);ContinuousProfiler.prototype.__init2.call(this);ContinuousProfiler.prototype.__init3.call(this);ContinuousProfiler.prototype.__init4.call(this);ContinuousProfiler.prototype.__init5.call(this);ContinuousProfiler.prototype.__init6.call(this);ContinuousProfiler.prototype.__init7.call(this); } __init() {this._client = undefined;} __init2() {this._chunkData = undefined;} __init3() {this._mode = undefined;} __init4() {this._legacyProfilerMode = undefined;} __init5() {this._profileLifecycle = undefined;} __init6() {this._sampled = undefined;} __init7() {this._sessionSamplingRate = 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; const options = client.getOptions(); this._mode = getProfilingMode(options); this._sessionSamplingRate = Math.random(); this._sampled = this._sessionSamplingRate < (options.profileSessionSampleRate ?? 0); this._profileLifecycle = options.profileLifecycle ?? 'manual'; switch (this._mode) { case 'legacy': { this._legacyProfilerMode = 'profilesSampleRate' in options || 'profilesSampler' in options ? 'span' : 'continuous'; DEBUG_BUILD && debug.log(`[Profiling] Profiling mode is ${this._legacyProfilerMode}.`); switch (this._legacyProfilerMode) { case 'span': { this._setupAutomaticSpanProfiling(); break; } case 'continuous': { // Continous mode requires manual calls to profiler.start() and profiler.stop() break; } default: { DEBUG_BUILD && debug.warn( `[Profiling] Unknown profiler mode: ${this._legacyProfilerMode}, profiler was not initialized`, ); break; } } break; } case 'current': { this._setupSpanChunkInstrumentation(); DEBUG_BUILD && debug.log(`[Profiling] Profiling mode is ${this._profileLifecycle}.`); switch (this._profileLifecycle) { case 'trace': { this._startTraceLifecycleProfiling(); break; } case 'manual': { // Manual mode requires manual calls to profiler.startProfiler() and profiler.stopProfiler() break; } default: { DEBUG_BUILD && debug.warn(`[Profiling] Unknown profiler mode: ${this._profileLifecycle}, profiler was not initialized`); break; } } break; } default: { DEBUG_BUILD && debug.warn(`[Profiling] Unknown profiler mode: ${this._mode}, profiler was not initialized`); break; } } // Attaches a listener to beforeSend which will add the threadId data to the event being sent. // This adds a constant overhead to all events being sent which could be improved to only attach // and detach the listener during a profiler session this._client.on('beforeSendEvent', this._onBeforeSendThreadContextAssignment.bind(this)); } /** * Initializes a new profilerId session and schedules chunk profiling. * @returns void */ start() { if (this._mode === 'current') { this._startProfiler(); return; } if (!this._client) { DEBUG_BUILD && debug.log('[Profiling] Failed to start, sentry client was never attached to the profiler.'); return; } if (this._mode !== 'legacy') { DEBUG_BUILD && debug.log('[Profiling] Continuous profiling is not supported in the current mode.'); return; } if (this._legacyProfilerMode === 'span') { DEBUG_BUILD && debug.log('[Profiling] Calls to profiler.start() are not supported in span profiling mode.'); return; } // Flush any existing chunks before starting a new one. this._stopChunkProfiling(); // Restart the profiler session this._setupSpanChunkInstrumentation(); this._restartChunkProfiling(); } /** * Stops the current chunk and flushes the profile to Sentry. * @returns void */ stop() { if (this._mode === 'current') { this._stopProfiler(); return; } if (!this._client) { DEBUG_BUILD && debug.log('[Profiling] Failed to stop, sentry client was never attached to the profiler.'); return; } if (this._mode !== 'legacy') { DEBUG_BUILD && debug.log('[Profiling] Continuous profiling is not supported in the current mode.'); return; } if (this._legacyProfilerMode === 'span') { DEBUG_BUILD && debug.log('[Profiling] Calls to profiler.stop() are not supported in span profiling mode.'); return; } this._stopChunkProfiling(); this._teardownSpanChunkInstrumentation(); } _startProfiler() { if (this._mode !== 'current') { DEBUG_BUILD && debug.log('[Profiling] Continuous profiling is not supported in the current mode.'); return; } if (this._chunkData !== undefined) { DEBUG_BUILD && debug.log('[Profiling] Profile session already running, no-op.'); return; } if (this._mode === 'current') { if (!this._sampled) { DEBUG_BUILD && debug.log('[Profiling] Profile session not sampled, no-op.'); return; } } if (this._profileLifecycle === 'trace') { DEBUG_BUILD && debug.log( '[Profiling] You are using the trace profile lifecycle, manual calls to profiler.startProfiler() and profiler.stopProfiler() will be ignored.', ); return; } this._startChunkProfiling(); } _stopProfiler() { if (this._mode !== 'current') { DEBUG_BUILD && debug.log('[Profiling] Continuous profiling is not supported in the current mode.'); return; } if (this._profileLifecycle === 'trace') { DEBUG_BUILD && debug.log( '[Profiling] You are using the trace profile lifecycle, manual calls to profiler.startProfiler() and profiler.stopProfiler() will be ignored.', ); return; } if (!this._chunkData) { DEBUG_BUILD && debug.log('[Profiling] No profile session running, no-op.'); return; } this._stopChunkProfiling(); } /** * Starts trace lifecycle profiling. Profiling will remain active as long as there is an active span. */ _startTraceLifecycleProfiling() { if (!this._client) { DEBUG_BUILD && debug.log( '[Profiling] Failed to start trace lifecycle profiling, sentry client was never attached to the profiler.', ); return; } let activeSpanCounter = 0; this._client.on('spanStart', _span => { if (activeSpanCounter === 0) { this._startChunkProfiling(); } activeSpanCounter++; }); this._client.on('spanEnd', _span => { if (activeSpanCounter === 1) { this._stopChunkProfiling(); } activeSpanCounter--; }); } _setupAutomaticSpanProfiling() { if (!this._client) { DEBUG_BUILD && debug.log( '[Profiling] Failed to setup automatic span profiling, sentry client was never attached to the profiler.', ); return; } const spanToProfileIdMap = new WeakMap(); this._client.on('spanStart', span => { if (span !== getRootSpan(span)) { return; } const profile_id = maybeProfileSpan(this._client, span); if (profile_id) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const options = this._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?.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 && debug.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(); getIsolationScope().setContext('profile', { profile_id }); spanToProfileIdMap.set(span, profile_id); } }); this._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); } } }); this._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 = profiledTransaction.contexts?.profile; const profile_id = profileContext?.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) { delete profiledTransaction.contexts?.profile; } const cpuProfile = takeFromProfileQueue(profile_id); if (!cpuProfile) { DEBUG_BUILD && debug.log(`[Profiling] Could not retrieve profile for transaction: ${profile_id}`); continue; } // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const profile = createProfilingEvent(this._client, cpuProfile, profiledTransaction); if (!profile) return; profilesToAddToEnvelope.push(profile); // @ts-expect-error profile does not inherit from Event // eslint-disable-next-line @typescript-eslint/no-non-null-assertion this._client.emit('preprocessEvent', profile, { event_id: profiledTransaction.event_id, }); // @ts-expect-error profile does not inherit from Event // eslint-disable-next-line @typescript-eslint/no-non-null-assertion this._client.emit('postprocessEvent', profile, { event_id: profiledTransaction.event_id, }); } addProfilesToEnvelope(envelope, profilesToAddToEnvelope); }); } /** * Stop profiler and initializes profiling of the next chunk */ _restartChunkProfiling() { 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 && debug.log('[Profiling] Profiler was never attached to the client.'); return; } if (this._chunkData) { DEBUG_BUILD && debug.log( `[Profiling] Chunk with chunk_id ${this._chunkData.id} is still running, current chunk will be stopped a new chunk will be started.`, ); this._stopChunkProfiling(); } this._startChunkProfiling(); } /** * Stops profiling of the current chunks and flushes the profile to Sentry */ _stopChunkProfiling() { if (!this._chunkData) { DEBUG_BUILD && debug.log('[Profiling] No chunk data found, no-op.'); return; } if (this._chunkData?.timer) { global.clearTimeout(this._chunkData.timer); this._chunkData.timer = undefined; DEBUG_BUILD && debug.log(`[Profiling] Stopping profiling chunk: ${this._chunkData.id}`); } if (!this._client) { DEBUG_BUILD && debug.log('[Profiling] Failed to collect profile, sentry client was never attached to the profiler.'); this._resetChunkData(); return; } if (!this._chunkData?.id) { DEBUG_BUILD && debug.log(`[Profiling] Failed to collect profile for: ${this._chunkData?.id}, the chunk_id is missing.`); this._resetChunkData(); return; } const profile = CpuProfilerBindings.stopProfiling(this._chunkData.id, ProfileFormat.CHUNK); if (!profile) { DEBUG_BUILD && debug.log(`[Profiling] Failed to collect profile for: ${this._chunkData.id}`); this._resetChunkData(); return; } if (!this._profilerId) { DEBUG_BUILD && debug.log('[Profiling] Profile chunk does not contain a valid profiler_id, this is a bug in the SDK'); this._resetChunkData(); return; } if (profile) { DEBUG_BUILD && debug.log(`[Profiling] Sending profile chunk ${this._chunkData.id}.`); } DEBUG_BUILD && debug.log(`[Profiling] Profile chunk ${this._chunkData.id} sent to Sentry.`); const chunk = createProfilingChunkEvent( this._client, this._client.getOptions(), profile, this._client.getSdkMetadata()?.sdk, { chunk_id: this._chunkData.id, trace_id: this._chunkData.startTraceID, profiler_id: this._profilerId, }, ); if (!chunk) { DEBUG_BUILD && debug.log(`[Profiling] Failed to create profile chunk for: ${this._chunkData.id}`); this._resetChunkData(); 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._resetChunkData(); } /** * Flushes the profile chunk to Sentry. * @param chunk */ _flush(chunk) { if (!this._client) { DEBUG_BUILD && debug.log('[Profiling] Failed to collect profile, sentry client was never attached to the profiler.'); return; } const transport = this._client.getTransport(); if (!transport) { DEBUG_BUILD && debug.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('node', chunk, metadata?.sdk, tunnel, dsn); transport.send(envelope).then(null, reason => { DEBUG_BUILD && debug.error('Error while sending profile chunk envelope:', reason); }); } /** * Starts the profiler and registers the flush timer for a given chunk. * @param chunk */ _startChunkProfiling() { if (this._chunkData) { DEBUG_BUILD && debug.log('[Profiling] Chunk is already running, no-op.'); return; } const traceId = getCurrentScope().getPropagationContext().traceId || getIsolationScope().getPropagationContext().traceId; const chunk = this._initializeChunk(traceId); CpuProfilerBindings.startProfiling(chunk.id); DEBUG_BUILD && debug.log(`[Profiling] starting profiling chunk: ${chunk.id}`); chunk.timer = global.setTimeout(() => { DEBUG_BUILD && debug.log(`[Profiling] Stopping profiling chunk: ${chunk.id}`); this._stopChunkProfiling(); DEBUG_BUILD && debug.log('[Profiling] Starting new profiling chunk.'); setImmediate(this._restartChunkProfiling.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 && debug.log('[Profiling] Failed to initialize span profiling, sentry client was never attached to the profiler.'); return; } this._profilerId = uuid4(); getGlobalScope().setContext('profile', { profiler_id: this._profilerId, }); } /** * Assigns thread_id and thread name context to a profiled event if there is an active profiler session */ _onBeforeSendThreadContextAssignment(event) { if (!this._client || !this._profilerId) return; this._assignThreadIdContext(event); } /** * Clear profiling information from global context when a profile is not running. */ _teardownSpanChunkInstrumentation() { this._profilerId = undefined; const globalScope = getGlobalScope(); globalScope.setContext('profile', {}); } /** * Initializes new profile chunk metadata */ _initializeChunk(traceId) { this._chunkData = { id: uuid4(), startTraceID: traceId, timer: undefined, }; return this._chunkData; } /** * Assigns thread_id and thread name context to a profiled event. */ _assignThreadIdContext(event) { if (!event?.contexts?.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 = { ...(event.contexts?.trace ?? {}), data: { ...(event.contexts?.trace?.data ?? {}), ['thread.id']: PROFILER_THREAD_ID_STRING, ['thread.name']: PROFILER_THREAD_NAME, }, }; } /** * Resets the current chunk state. */ _resetChunkData() { this._chunkData = undefined; } } /** Exported only for tests. */ const _nodeProfilingIntegration = (() => { if (![16, 18, 20, 22, 24].includes(NODE_MAJOR)) { consoleSandbox(() => { // eslint-disable-next-line no-console console.warn( `[Sentry 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, 24.', '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 && debug.log('[Profiling] Profiling integration setup.'); this._profiler.initialize(client); return; }, }; }) ; /** * Determines the profiling mode based on the options. * @param options * @returns 'legacy' if the options are using the legacy profiling API, 'current' if the options are using the current profiling API */ function getProfilingMode(options) { // Legacy mode takes precedence over current mode if ('profilesSampleRate' in options || 'profilesSampler' in options) { return 'legacy'; } if ('profileSessionSampleRate' in options || 'profileLifecycle' in options) { return 'current'; } // If neither are set, we are in the legacy continuous profiling mode return 'legacy'; } /** * 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