@sentry/profiling-node
Version:
Official Sentry SDK for Node.js Profiling
1,186 lines (1,039 loc) • 39.2 kB
JavaScript
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