Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@sentry/profiling-node

Package Overview
Dependencies
Maintainers
1
Versions
331
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@sentry/profiling-node - npm Package Compare versions

Comparing version
10.53.1
to
10.54.0
+246
-626
build/cjs/index.js

@@ -9,20 +9,11 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });

/**
* 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 = core.parseSemver(process.versions.node) ;
const NODE_VERSION = core.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(worker_threads.threadId);
const PROFILER_THREAD_NAME = worker_threads.isMainThread ? 'main' : 'worker';
const FORMAT_VERSION = '1';
const CONTINUOUS_FORMAT_VERSION = '2';
// Machine properties (eval only once)
const PROFILER_THREAD_NAME = worker_threads.isMainThread ? "main" : "worker";
const FORMAT_VERSION = "1";
const CONTINUOUS_FORMAT_VERSION = "2";
const PLATFORM = os.platform();

@@ -34,28 +25,9 @@ const RELEASE = os.release();

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);
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,
) {
function enrichWithThreadInformation(profile) {
if (!isRawThreadCpuProfile(profile)) {
return profile;
}
return {

@@ -67,14 +39,7 @@ samples: profile.samples,

[PROFILER_THREAD_ID_STRING]: {
name: PROFILER_THREAD_NAME,
},
},
} ;
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) {

@@ -84,55 +49,36 @@ if (!isValidProfile(profile)) {

}
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,
release: event.release ?? "",
environment: event.environment ?? "",
event_id: event.event_id ?? "",
transaction: event.transaction ?? "",
start_timestamp: event.start_timestamp ? event.start_timestamp * 1e3 : 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
function createProfilePayload(client, cpuProfile, {
release,
environment,
event_id,
transaction,
start_timestamp,
trace_id,
profile_id
}) {
if (trace_id?.length !== 32) {
DEBUG_BUILD && core.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',
platform: "node",
version: FORMAT_VERSION,
release: release,
environment: environment,
release,
environment,
measurements: cpuProfile.measurements,
runtime: {
name: 'node',
version: process$1.versions.node || '',
name: "node",
version: process$1.versions.node || ""
},

@@ -142,123 +88,80 @@ os: {

version: RELEASE,
build_number: VERSION,
build_number: VERSION
},
device: {
locale: process$1.env['LC_ALL'] || process$1.env['LC_MESSAGES'] || process$1.env['LANG'] || process$1.env['LANGUAGE'] || '',
locale: process$1.env["LC_ALL"] || process$1.env["LC_MESSAGES"] || process$1.env["LANG"] || process$1.env["LANGUAGE"] || "",
model: MODEL,
manufacturer: TYPE,
architecture: ARCH,
is_emulator: false,
is_emulator: false
},
debug_meta: {
images: applyDebugMetadata(client, cpuProfile.resources),
images: applyDebugMetadata(client, cpuProfile.resources)
},
profile: enrichedThreadProfile ,
profile: enrichedThreadProfile,
transaction: {
name: transaction,
id: event_id,
trace_id: trace_id || '',
active_thread_id: PROFILER_THREAD_ID_STRING,
},
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
function createProfileChunkPayload(client, cpuProfile, {
release,
environment,
trace_id,
profiler_id,
chunk_id,
sdk
}) {
if (trace_id?.length !== 32) {
DEBUG_BUILD && core.debug.log(`[Profiling] Invalid traceId: ${trace_id} on profiled event`);
}
const enrichedThreadProfile = enrichWithThreadInformation(cpuProfile);
const profile = {
chunk_id: chunk_id,
chunk_id,
client_sdk: {
name: sdk?.name ?? 'sentry.javascript.node',
version: sdk?.version ?? '0.0.0',
name: sdk?.name ?? "sentry.javascript.node",
version: sdk?.version ?? "0.0.0"
},
profiler_id: profiler_id,
platform: 'node',
profiler_id,
platform: "node",
version: CONTINUOUS_FORMAT_VERSION,
release: release,
environment: environment,
release,
environment,
measurements: cpuProfile.measurements,
debug_meta: {
images: applyDebugMetadata(client, cpuProfile.resources),
images: applyDebugMetadata(client, cpuProfile.resources)
},
profile: enrichedThreadProfile ,
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,
) {
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 ?? '',
release: options.release ?? "",
environment: options.environment ?? "",
trace_id: identifiers.trace_id ?? "",
chunk_id: identifiers.chunk_id,
profiler_id: identifiers.profiler_id,
sdk,
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 &&
core.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)}.`,
);
if (typeof rate !== "number" && typeof rate !== "boolean" || typeof rate === "number" && isNaN(rate)) {
DEBUG_BUILD && core.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) {

@@ -270,49 +173,25 @@ DEBUG_BUILD && core.debug.warn(`[Profiling] Invalid sample rate. Sample rate must be between 0 and 1. Got ${rate}.`);

}
/**
* 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.
core.debug.log('[Profiling] Discarding profile because it contains less than 2 samples');
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.
core.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.
core.debug.log('[Profiling] Discarding profile chunk because it contains less than 2 samples');
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.
core.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) {

@@ -322,34 +201,19 @@ if (!profiles.length) {

}
for (const profile of profiles) {
// @ts-expect-error untyped envelope
envelope[1].push([{ type: 'profile' }, profile]);
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 = [];
core.forEachEnvelopeItem(envelope, (item, type) => {
if (type !== 'transaction') {
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] ;
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) {

@@ -360,104 +224,52 @@ 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,
) {
function createEventEnvelopeHeaders(sdkInfo, tunnel, dsn) {
return {
event_id: core.uuid4(),
sent_at: new Date().toISOString(),
...(sdkInfo && { sdk: sdkInfo }),
...(!!tunnel && dsn && { dsn: core.dsnToString(dsn) }),
sent_at: (/* @__PURE__ */ new Date()).toISOString(),
...sdkInfo && { sdk: sdkInfo },
...!!tunnel && dsn && { dsn: core.dsnToString(dsn) }
};
}
/**
* Creates a standalone profile_chunk envelope.
*/
function makeProfileChunkEnvelope(
platform,
chunk,
sdkInfo,
tunnel,
dsn,
) {
function makeProfileChunkEnvelope(platform, chunk, sdkInfo, tunnel, dsn) {
const profileChunkHeader = {
type: 'profile_chunk',
platform,
type: "profile_chunk",
platform
};
return core.createEnvelope(createEventEnvelopeHeaders(sdkInfo, tunnel, dsn), [
[profileChunkHeader, chunk],
[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 core.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,
) {
// Profiling is not supported in worker threads as the native CPU profiler's
// sampling thread can race with V8's GC across isolates, causing heap corruption.
const MAX_PROFILE_DURATION_MS = 30 * 1e3;
function maybeProfileSpan(client, span, customSamplingContext) {
if (!worker_threads.isMainThread) {
DEBUG_BUILD && core.debug.log('[Profiling] Skipping span profiling in worker thread.');
DEBUG_BUILD && core.debug.log("[Profiling] Skipping span profiling in worker thread.");
return;
}
// 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 (!core.spanIsSampled(span)) {
return;
}
// Client and options are required for profiling
if (!client) {
DEBUG_BUILD && core.debug.log('[Profiling] Profiling disabled, no client found.');
DEBUG_BUILD && core.debug.log("[Profiling] Profiling disabled, no client found.");
return;
}
const options = client.getOptions();
if (!options) {
DEBUG_BUILD && core.debug.log('[Profiling] Profiling disabled, no options found.');
DEBUG_BUILD && core.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 } = core.spanToJSON(span);
// We bail out early if that is not the case
if (typeof profilesSampler === "function") {
const { description: spanName = "<unknown>", data } = core.spanToJSON(span);
const parentSampled = true;
profilesSampleRate = profilesSampler({

@@ -467,75 +279,42 @@ name: spanName,

parentSampled,
...customSamplingContext,
...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 && core.debug.warn('[Profiling] Discarding profile because of invalid sample rate.');
DEBUG_BUILD && core.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 &&
core.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'
}`,
);
DEBUG_BUILD && core.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 &&
core.debug.log(
`[Profiling] Discarding profile because it's not included in the random sample (sampling rate = ${Number(
profilesSampleRate,
)})`,
);
DEBUG_BUILD && core.debug.log(
`[Profiling] Discarding profile because it's not included in the random sample (sampling rate = ${Number(
profilesSampleRate
)})`
);
return;
}
const profile_id = core.uuid4();
nodeCpuProfiler.CpuProfilerBindings.startProfiling(profile_id);
DEBUG_BUILD && core.debug.log(`[Profiling] started profiling transaction: ${core.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 = nodeCpuProfiler.CpuProfilerBindings.stopProfiling(profile_id, 0);
DEBUG_BUILD && core.debug.log(`[Profiling] stopped profiling of transaction: ${core.spanToJSON(span).description}`);
// In case of an overlapping span, stopProfiling may return null and silently ignore the overlapping profile.
if (!profile) {
DEBUG_BUILD &&
core.debug.log(
`[Profiling] profiler returned null profile for: ${core.spanToJSON(span).description}`,
'this may indicate an overlapping span or a call to stopProfiling with a profile title that was never started',
);
DEBUG_BUILD && core.debug.log(
`[Profiling] profiler returned null profile for: ${core.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;

@@ -545,10 +324,8 @@ return profile;

const CHUNK_INTERVAL_MS = 1000 * 60;
const CHUNK_INTERVAL_MS = 1e3 * 60;
const PROFILE_MAP = new core.LRUMap(50);
const PROFILE_TIMEOUTS = {};
function addToProfileQueue(profile_id, profile) {
PROFILE_MAP.set(profile_id, profile);
}
function takeFromProfileQueue(profile_id) {

@@ -559,12 +336,12 @@ const profile = PROFILE_MAP.get(profile_id);

}
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;}
class ContinuousProfiler {
constructor() {
this._client = void 0;
this._chunkData = void 0;
this._mode = void 0;
this._legacyProfilerMode = void 0;
this._profileLifecycle = void 0;
this._sampled = void 0;
this._sessionSamplingRate = void 0;
}
/**

@@ -575,40 +352,31 @@ * Called when the profiler is attached to the client (continuous mode is enabled). If of the profiler

*/
initialize(client) {
initialize(client) {
if (!worker_threads.isMainThread) {
DEBUG_BUILD &&
core.debug.warn(
'[Profiling] nodeProfilingIntegration() does not support worker threads — profiling will be disabled for this thread.',
);
DEBUG_BUILD && core.debug.warn(
"[Profiling] nodeProfilingIntegration() does not support worker threads \u2014 profiling will be disabled for this thread."
);
return;
}
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';
this._profileLifecycle = options.profileLifecycle ?? "manual";
switch (this._mode) {
case 'legacy': {
this._legacyProfilerMode =
'profilesSampleRate' in options || 'profilesSampler' in options ? 'span' : 'continuous';
case "legacy": {
this._legacyProfilerMode = "profilesSampleRate" in options || "profilesSampler" in options ? "span" : "continuous";
DEBUG_BUILD && core.debug.log(`[Profiling] Profiling mode is ${this._legacyProfilerMode}.`);
switch (this._legacyProfilerMode) {
case 'span': {
case "span": {
this._setupAutomaticSpanProfiling();
break;
}
case 'continuous': {
// Continous mode requires manual calls to profiler.start() and profiler.stop()
case "continuous": {
break;
}
default: {
DEBUG_BUILD &&
core.debug.warn(
`[Profiling] Unknown profiler mode: ${this._legacyProfilerMode}, profiler was not initialized`,
);
DEBUG_BUILD && core.debug.warn(
`[Profiling] Unknown profiler mode: ${this._legacyProfilerMode}, profiler was not initialized`
);
break;

@@ -619,20 +387,15 @@ }

}
case 'current': {
case "current": {
this._setupSpanChunkInstrumentation();
DEBUG_BUILD && core.debug.log(`[Profiling] Profiling mode is ${this._profileLifecycle}.`);
switch (this._profileLifecycle) {
case 'trace': {
case "trace": {
this._startTraceLifecycleProfiling();
break;
}
case 'manual': {
// Manual mode requires manual calls to profiler.startProfiler() and profiler.stopProfiler()
case "manual": {
break;
}
default: {
DEBUG_BUILD &&
core.debug.warn(`[Profiling] Unknown profiler mode: ${this._profileLifecycle}, profiler was not initialized`);
DEBUG_BUILD && core.debug.warn(`[Profiling] Unknown profiler mode: ${this._profileLifecycle}, profiler was not initialized`);
break;

@@ -648,9 +411,4 @@ }

}
// 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));
this._client.on("beforeSendEvent", this._onBeforeSendThreadContextAssignment.bind(this));
}
/**

@@ -660,32 +418,23 @@ * Initializes a new profilerId session and schedules chunk profiling.

*/
start() {
if (this._mode === 'current') {
start() {
if (this._mode === "current") {
this._startProfiler();
return;
}
if (!this._client) {
DEBUG_BUILD && core.debug.log('[Profiling] Failed to start, sentry client was never attached to the profiler.');
DEBUG_BUILD && core.debug.log("[Profiling] Failed to start, sentry client was never attached to the profiler.");
return;
}
if (this._mode !== 'legacy') {
DEBUG_BUILD && core.debug.log('[Profiling] Continuous profiling is not supported in the current mode.');
if (this._mode !== "legacy") {
DEBUG_BUILD && core.debug.log("[Profiling] Continuous profiling is not supported in the current mode.");
return;
}
if (this._legacyProfilerMode === 'span') {
DEBUG_BUILD && core.debug.log('[Profiling] Calls to profiler.start() are not supported in span profiling mode.');
if (this._legacyProfilerMode === "span") {
DEBUG_BUILD && core.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();
}
/**

@@ -695,92 +444,74 @@ * Stops the current chunk and flushes the profile to Sentry.

*/
stop() {
if (this._mode === 'current') {
stop() {
if (this._mode === "current") {
this._stopProfiler();
return;
}
if (!this._client) {
DEBUG_BUILD && core.debug.log('[Profiling] Failed to stop, sentry client was never attached to the profiler.');
DEBUG_BUILD && core.debug.log("[Profiling] Failed to stop, sentry client was never attached to the profiler.");
return;
}
if (this._mode !== 'legacy') {
DEBUG_BUILD && core.debug.log('[Profiling] Continuous profiling is not supported in the current mode.');
if (this._mode !== "legacy") {
DEBUG_BUILD && core.debug.log("[Profiling] Continuous profiling is not supported in the current mode.");
return;
}
if (this._legacyProfilerMode === 'span') {
DEBUG_BUILD && core.debug.log('[Profiling] Calls to profiler.stop() are not supported in span profiling mode.');
if (this._legacyProfilerMode === "span") {
DEBUG_BUILD && core.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 && core.debug.log('[Profiling] Continuous profiling is not supported in the current mode.');
_startProfiler() {
if (this._mode !== "current") {
DEBUG_BUILD && core.debug.log("[Profiling] Continuous profiling is not supported in the current mode.");
return;
}
if (this._chunkData !== undefined) {
DEBUG_BUILD && core.debug.log('[Profiling] Profile session already running, no-op.');
if (this._chunkData !== void 0) {
DEBUG_BUILD && core.debug.log("[Profiling] Profile session already running, no-op.");
return;
}
if (this._mode === 'current') {
if (this._mode === "current") {
if (!this._sampled) {
DEBUG_BUILD && core.debug.log('[Profiling] Profile session not sampled, no-op.');
DEBUG_BUILD && core.debug.log("[Profiling] Profile session not sampled, no-op.");
return;
}
}
if (this._profileLifecycle === 'trace') {
DEBUG_BUILD &&
core.debug.log(
'[Profiling] You are using the trace profile lifecycle, manual calls to profiler.startProfiler() and profiler.stopProfiler() will be ignored.',
);
if (this._profileLifecycle === "trace") {
DEBUG_BUILD && core.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 && core.debug.log('[Profiling] Continuous profiling is not supported in the current mode.');
_stopProfiler() {
if (this._mode !== "current") {
DEBUG_BUILD && core.debug.log("[Profiling] Continuous profiling is not supported in the current mode.");
return;
}
if (this._profileLifecycle === 'trace') {
DEBUG_BUILD &&
core.debug.log(
'[Profiling] You are using the trace profile lifecycle, manual calls to profiler.startProfiler() and profiler.stopProfiler() will be ignored.',
);
if (this._profileLifecycle === "trace") {
DEBUG_BUILD && core.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 && core.debug.log('[Profiling] No profile session running, no-op.');
DEBUG_BUILD && core.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() {
_startTraceLifecycleProfiling() {
if (!this._client) {
DEBUG_BUILD &&
core.debug.log(
'[Profiling] Failed to start trace lifecycle profiling, sentry client was never attached to the profiler.',
);
DEBUG_BUILD && core.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 => {
this._client.on("spanStart", (_span) => {
if (activeSpanCounter === 0) {

@@ -791,4 +522,3 @@ this._startChunkProfiling();

});
this._client.on('spanEnd', _span => {
this._client.on("spanEnd", (_span) => {
if (activeSpanCounter === 1) {

@@ -800,42 +530,27 @@ this._stopChunkProfiling();

}
_setupAutomaticSpanProfiling() {
_setupAutomaticSpanProfiling() {
if (!this._client) {
DEBUG_BUILD &&
core.debug.log(
'[Profiling] Failed to setup automatic span profiling, sentry client was never attached to the profiler.',
);
DEBUG_BUILD && core.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 => {
const spanToProfileIdMap = /* @__PURE__ */ new WeakMap();
this._client.on("spanStart", (span) => {
if (span !== core.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 &&
core.debug.log(
'[Profiling] max profile duration elapsed, stopping profiling for:',
core.spanToJSON(span).description,
);
DEBUG_BUILD && core.debug.log(
"[Profiling] max profile duration elapsed, stopping profiling for:",
core.spanToJSON(span).description
);
const profile = stopSpanProfile(span, profile_id);

@@ -846,22 +561,15 @@ if (profile) {

}, maxProfileDurationMs);
// Unref timeout so it doesn't keep the process alive.
timeout.unref();
core.getIsolationScope().setContext('profile', { profile_id });
core.getIsolationScope().setContext("profile", { profile_id });
spanToProfileIdMap.set(span, profile_id);
}
});
this._client.on('spanEnd', span => {
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) {

@@ -872,9 +580,6 @@ addToProfileQueue(profile_id, profile);

});
this._client.on('beforeEnvelope', (envelope) => {
// if not profiles are in queue, there is nothing to add to the envelope.
this._client.on("beforeEnvelope", (envelope) => {
if (!PROFILE_MAP.size) {
return;
}
const profiledTransactionEvents = findProfiledTransactionsFromEnvelope(envelope);

@@ -884,18 +589,12 @@ if (!profiledTransactionEvents.length) {

}
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');
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);

@@ -906,80 +605,55 @@ if (!cpuProfile) {

}
// 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,
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,
this._client.emit("postprocessEvent", profile, {
event_id: profiledTransaction.event_id
});
}
addProfilesToEnvelope(envelope, profilesToAddToEnvelope);
});
}
/**
* Stop profiler and initializes profiling of the next chunk
*/
_restartChunkProfiling() {
_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 && core.debug.log('[Profiling] Profiler was never attached to the client.');
DEBUG_BUILD && core.debug.log("[Profiling] Profiler was never attached to the client.");
return;
}
if (this._chunkData) {
DEBUG_BUILD &&
core.debug.log(
`[Profiling] Chunk with chunk_id ${this._chunkData.id} is still running, current chunk will be stopped a new chunk will be started.`,
);
DEBUG_BUILD && core.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() {
_stopChunkProfiling() {
if (!this._chunkData) {
DEBUG_BUILD && core.debug.log('[Profiling] No chunk data found, no-op.');
DEBUG_BUILD && core.debug.log("[Profiling] No chunk data found, no-op.");
return;
}
if (this._chunkData?.timer) {
global.clearTimeout(this._chunkData.timer);
this._chunkData.timer = undefined;
this._chunkData.timer = void 0;
DEBUG_BUILD && core.debug.log(`[Profiling] Stopping profiling chunk: ${this._chunkData.id}`);
}
if (!this._client) {
DEBUG_BUILD &&
core.debug.log('[Profiling] Failed to collect profile, sentry client was never attached to the profiler.');
DEBUG_BUILD && core.debug.log("[Profiling] Failed to collect profile, sentry client was never attached to the profiler.");
this._resetChunkData();
return;
}
if (!this._chunkData?.id) {
DEBUG_BUILD &&
core.debug.log(`[Profiling] Failed to collect profile for: ${this._chunkData?.id}, the chunk_id is missing.`);
DEBUG_BUILD && core.debug.log(`[Profiling] Failed to collect profile for: ${this._chunkData?.id}, the chunk_id is missing.`);
this._resetChunkData();
return;
}
const profile = nodeCpuProfiler.CpuProfilerBindings.stopProfiling(this._chunkData.id, nodeCpuProfiler.ProfileFormat.CHUNK);
if (!profile) {

@@ -990,6 +664,4 @@ DEBUG_BUILD && core.debug.log(`[Profiling] Failed to collect profile for: ${this._chunkData.id}`);

}
if (!this._profilerId) {
DEBUG_BUILD &&
core.debug.log('[Profiling] Profile chunk does not contain a valid profiler_id, this is a bug in the SDK');
DEBUG_BUILD && core.debug.log("[Profiling] Profile chunk does not contain a valid profiler_id, this is a bug in the SDK");
this._resetChunkData();

@@ -1001,3 +673,2 @@ return;

}
DEBUG_BUILD && core.debug.log(`[Profiling] Profile chunk ${this._chunkData.id} sent to Sentry.`);

@@ -1012,6 +683,5 @@ const chunk = createProfilingChunkEvent(

trace_id: this._chunkData.startTraceID,
profiler_id: this._profilerId,
},
profiler_id: this._profilerId
}
);
if (!chunk) {

@@ -1022,11 +692,5 @@ DEBUG_BUILD && core.debug.log(`[Profiling] Failed to create profile chunk for: ${this._chunkData.id}`);

}
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();
}
/**

@@ -1036,25 +700,20 @@ * Flushes the profile chunk to Sentry.

*/
_flush(chunk) {
_flush(chunk) {
if (!this._client) {
DEBUG_BUILD &&
core.debug.log('[Profiling] Failed to collect profile, sentry client was never attached to the profiler.');
DEBUG_BUILD && core.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 && core.debug.log('[Profiling] No transport available to send profile chunk.');
DEBUG_BUILD && core.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 && core.debug.error('Error while sending profile chunk envelope:', reason);
const envelope = makeProfileChunkEnvelope("node", chunk, metadata?.sdk, tunnel, dsn);
transport.send(envelope).then(null, (reason) => {
DEBUG_BUILD && core.debug.error("Error while sending profile chunk envelope:", reason);
});
}
/**

@@ -1064,26 +723,19 @@ * Starts the profiler and registers the flush timer for a given chunk.

*/
_startChunkProfiling() {
_startChunkProfiling() {
if (this._chunkData) {
DEBUG_BUILD && core.debug.log('[Profiling] Chunk is already running, no-op.');
DEBUG_BUILD && core.debug.log("[Profiling] Chunk is already running, no-op.");
return;
}
const traceId =
core.getCurrentScope().getPropagationContext().traceId || core.getIsolationScope().getPropagationContext().traceId;
const traceId = core.getCurrentScope().getPropagationContext().traceId || core.getIsolationScope().getPropagationContext().traceId;
const chunk = this._initializeChunk(traceId);
nodeCpuProfiler.CpuProfilerBindings.startProfiling(chunk.id);
DEBUG_BUILD && core.debug.log(`[Profiling] starting profiling chunk: ${chunk.id}`);
chunk.timer = global.setTimeout(() => {
DEBUG_BUILD && core.debug.log(`[Profiling] Stopping profiling chunk: ${chunk.id}`);
this._stopChunkProfiling();
DEBUG_BUILD && core.debug.log('[Profiling] Starting new profiling chunk.');
DEBUG_BUILD && core.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();
}
/**

@@ -1093,126 +745,94 @@ * Attaches profiling information to spans that were started

*/
_setupSpanChunkInstrumentation() {
_setupSpanChunkInstrumentation() {
if (!this._client) {
DEBUG_BUILD &&
core.debug.log('[Profiling] Failed to initialize span profiling, sentry client was never attached to the profiler.');
DEBUG_BUILD && core.debug.log("[Profiling] Failed to initialize span profiling, sentry client was never attached to the profiler.");
return;
}
this._profilerId = core.uuid4();
core.getGlobalScope().setContext('profile', {
profiler_id: this._profilerId,
core.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) {
_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;
_teardownSpanChunkInstrumentation() {
this._profilerId = void 0;
const globalScope = core.getGlobalScope();
globalScope.setContext('profile', {});
globalScope.setContext("profile", {});
}
/**
* Initializes new profile chunk metadata
*/
_initializeChunk(traceId) {
_initializeChunk(traceId) {
this._chunkData = {
id: core.uuid4(),
startTraceID: traceId,
timer: undefined,
timer: void 0
};
return this._chunkData;
}
/**
* Assigns thread_id and thread name context to a profiled event.
*/
_assignThreadIdContext(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 ?? {}),
...event.contexts?.trace ?? {},
data: {
...(event.contexts?.trace?.data ?? {}),
['thread.id']: PROFILER_THREAD_ID_STRING,
['thread.name']: PROFILER_THREAD_NAME,
},
...event.contexts?.trace?.data ?? {},
["thread.id"]: PROFILER_THREAD_ID_STRING,
["thread.name"]: PROFILER_THREAD_NAME
}
};
}
/**
* Resets the current chunk state.
*/
_resetChunkData() {
this._chunkData = undefined;
_resetChunkData() {
this._chunkData = void 0;
}
}
/** Exported only for tests. */
const _nodeProfilingIntegration = (() => {
if (![16, 18, 20, 22, 24, 26].includes(NODE_MAJOR)) {
core.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_MAJOR}).`,
'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',
"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',
name: "ProfilingIntegration",
_profiler: new ContinuousProfiler(),
setup(client) {
DEBUG_BUILD && core.debug.log('[Profiling] Profiling integration setup.');
DEBUG_BUILD && core.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 ("profilesSampleRate" in options || "profilesSampler" in options) {
return "legacy";
}
if ('profileSessionSampleRate' in options || 'profileLifecycle' in options) {
return 'current';
if ("profileSessionSampleRate" in options || "profileLifecycle" in options) {
return "current";
}
// If neither are set, we are in the legacy continuous profiling mode
return 'legacy';
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 = core.defineIntegration(_nodeProfilingIntegration);

@@ -1219,0 +839,0 @@

@@ -7,20 +7,11 @@ import { parseSemver, forEachEnvelopeItem, createEnvelope, debug, dsnToString, uuid4, getDebugImagesForResources, spanIsSampled, spanToJSON, LRUMap, defineIntegration, consoleSandbox, getRootSpan, getIsolationScope, getCurrentScope, getGlobalScope } from '@sentry/core';

/**
* 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_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 PROFILER_THREAD_NAME = isMainThread ? "main" : "worker";
const FORMAT_VERSION = "1";
const CONTINUOUS_FORMAT_VERSION = "2";
const PLATFORM = os.platform();

@@ -32,28 +23,9 @@ const RELEASE = os.release();

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);
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,
) {
function enrichWithThreadInformation(profile) {
if (!isRawThreadCpuProfile(profile)) {
return profile;
}
return {

@@ -65,14 +37,7 @@ samples: profile.samples,

[PROFILER_THREAD_ID_STRING]: {
name: PROFILER_THREAD_NAME,
},
},
} ;
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) {

@@ -82,55 +47,36 @@ if (!isValidProfile(profile)) {

}
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,
release: event.release ?? "",
environment: event.environment ?? "",
event_id: event.event_id ?? "",
transaction: event.transaction ?? "",
start_timestamp: event.start_timestamp ? event.start_timestamp * 1e3 : 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
function createProfilePayload(client, cpuProfile, {
release,
environment,
event_id,
transaction,
start_timestamp,
trace_id,
profile_id
}) {
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',
platform: "node",
version: FORMAT_VERSION,
release: release,
environment: environment,
release,
environment,
measurements: cpuProfile.measurements,
runtime: {
name: 'node',
version: versions.node || '',
name: "node",
version: versions.node || ""
},

@@ -140,123 +86,80 @@ os: {

version: RELEASE,
build_number: VERSION,
build_number: VERSION
},
device: {
locale: env['LC_ALL'] || env['LC_MESSAGES'] || env['LANG'] || env['LANGUAGE'] || '',
locale: env["LC_ALL"] || env["LC_MESSAGES"] || env["LANG"] || env["LANGUAGE"] || "",
model: MODEL,
manufacturer: TYPE,
architecture: ARCH,
is_emulator: false,
is_emulator: false
},
debug_meta: {
images: applyDebugMetadata(client, cpuProfile.resources),
images: applyDebugMetadata(client, cpuProfile.resources)
},
profile: enrichedThreadProfile ,
profile: enrichedThreadProfile,
transaction: {
name: transaction,
id: event_id,
trace_id: trace_id || '',
active_thread_id: PROFILER_THREAD_ID_STRING,
},
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
function createProfileChunkPayload(client, cpuProfile, {
release,
environment,
trace_id,
profiler_id,
chunk_id,
sdk
}) {
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,
chunk_id,
client_sdk: {
name: sdk?.name ?? 'sentry.javascript.node',
version: sdk?.version ?? '0.0.0',
name: sdk?.name ?? "sentry.javascript.node",
version: sdk?.version ?? "0.0.0"
},
profiler_id: profiler_id,
platform: 'node',
profiler_id,
platform: "node",
version: CONTINUOUS_FORMAT_VERSION,
release: release,
environment: environment,
release,
environment,
measurements: cpuProfile.measurements,
debug_meta: {
images: applyDebugMetadata(client, cpuProfile.resources),
images: applyDebugMetadata(client, cpuProfile.resources)
},
profile: enrichedThreadProfile ,
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,
) {
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 ?? '',
release: options.release ?? "",
environment: options.environment ?? "",
trace_id: identifiers.trace_id ?? "",
chunk_id: identifiers.chunk_id,
profiler_id: identifiers.profiler_id,
sdk,
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)}.`,
);
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) {

@@ -268,49 +171,25 @@ DEBUG_BUILD && debug.warn(`[Profiling] Invalid sample rate. Sample rate must be between 0 and 1. Got ${rate}.`);

}
/**
* 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');
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');
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) {

@@ -320,34 +199,19 @@ if (!profiles.length) {

}
for (const profile of profiles) {
// @ts-expect-error untyped envelope
envelope[1].push([{ type: 'profile' }, profile]);
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') {
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] ;
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) {

@@ -358,104 +222,52 @@ 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,
) {
function createEventEnvelopeHeaders(sdkInfo, tunnel, dsn) {
return {
event_id: uuid4(),
sent_at: new Date().toISOString(),
...(sdkInfo && { sdk: sdkInfo }),
...(!!tunnel && dsn && { dsn: dsnToString(dsn) }),
sent_at: (/* @__PURE__ */ new Date()).toISOString(),
...sdkInfo && { sdk: sdkInfo },
...!!tunnel && dsn && { dsn: dsnToString(dsn) }
};
}
/**
* Creates a standalone profile_chunk envelope.
*/
function makeProfileChunkEnvelope(
platform,
chunk,
sdkInfo,
tunnel,
dsn,
) {
function makeProfileChunkEnvelope(platform, chunk, sdkInfo, tunnel, dsn) {
const profileChunkHeader = {
type: 'profile_chunk',
platform,
type: "profile_chunk",
platform
};
return createEnvelope(createEventEnvelopeHeaders(sdkInfo, tunnel, dsn), [
[profileChunkHeader, chunk],
[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,
) {
// Profiling is not supported in worker threads as the native CPU profiler's
// sampling thread can race with V8's GC across isolates, causing heap corruption.
const MAX_PROFILE_DURATION_MS = 30 * 1e3;
function maybeProfileSpan(client, span, customSamplingContext) {
if (!isMainThread) {
DEBUG_BUILD && debug.log('[Profiling] Skipping span profiling in worker thread.');
DEBUG_BUILD && debug.log("[Profiling] Skipping span profiling in worker thread.");
return;
}
// 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.');
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.');
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
if (typeof profilesSampler === "function") {
const { description: spanName = "<unknown>", data } = spanToJSON(span);
const parentSampled = true;
profilesSampleRate = profilesSampler({

@@ -465,75 +277,42 @@ name: spanName,

parentSampled,
...customSamplingContext,
...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.');
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'
}`,
);
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,
)})`,
);
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',
);
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;

@@ -543,10 +322,8 @@ return profile;

const CHUNK_INTERVAL_MS = 1000 * 60;
const CHUNK_INTERVAL_MS = 1e3 * 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) {

@@ -557,12 +334,12 @@ const profile = PROFILE_MAP.get(profile_id);

}
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;}
class ContinuousProfiler {
constructor() {
this._client = void 0;
this._chunkData = void 0;
this._mode = void 0;
this._legacyProfilerMode = void 0;
this._profileLifecycle = void 0;
this._sampled = void 0;
this._sessionSamplingRate = void 0;
}
/**

@@ -573,40 +350,31 @@ * Called when the profiler is attached to the client (continuous mode is enabled). If of the profiler

*/
initialize(client) {
initialize(client) {
if (!isMainThread) {
DEBUG_BUILD &&
debug.warn(
'[Profiling] nodeProfilingIntegration() does not support worker threads — profiling will be disabled for this thread.',
);
DEBUG_BUILD && debug.warn(
"[Profiling] nodeProfilingIntegration() does not support worker threads \u2014 profiling will be disabled for this thread."
);
return;
}
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';
this._profileLifecycle = options.profileLifecycle ?? "manual";
switch (this._mode) {
case 'legacy': {
this._legacyProfilerMode =
'profilesSampleRate' in options || 'profilesSampler' in options ? 'span' : 'continuous';
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': {
case "span": {
this._setupAutomaticSpanProfiling();
break;
}
case 'continuous': {
// Continous mode requires manual calls to profiler.start() and profiler.stop()
case "continuous": {
break;
}
default: {
DEBUG_BUILD &&
debug.warn(
`[Profiling] Unknown profiler mode: ${this._legacyProfilerMode}, profiler was not initialized`,
);
DEBUG_BUILD && debug.warn(
`[Profiling] Unknown profiler mode: ${this._legacyProfilerMode}, profiler was not initialized`
);
break;

@@ -617,20 +385,15 @@ }

}
case 'current': {
case "current": {
this._setupSpanChunkInstrumentation();
DEBUG_BUILD && debug.log(`[Profiling] Profiling mode is ${this._profileLifecycle}.`);
switch (this._profileLifecycle) {
case 'trace': {
case "trace": {
this._startTraceLifecycleProfiling();
break;
}
case 'manual': {
// Manual mode requires manual calls to profiler.startProfiler() and profiler.stopProfiler()
case "manual": {
break;
}
default: {
DEBUG_BUILD &&
debug.warn(`[Profiling] Unknown profiler mode: ${this._profileLifecycle}, profiler was not initialized`);
DEBUG_BUILD && debug.warn(`[Profiling] Unknown profiler mode: ${this._profileLifecycle}, profiler was not initialized`);
break;

@@ -646,9 +409,4 @@ }

}
// 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));
this._client.on("beforeSendEvent", this._onBeforeSendThreadContextAssignment.bind(this));
}
/**

@@ -658,32 +416,23 @@ * Initializes a new profilerId session and schedules chunk profiling.

*/
start() {
if (this._mode === 'current') {
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.');
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.');
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.');
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();
}
/**

@@ -693,92 +442,74 @@ * Stops the current chunk and flushes the profile to Sentry.

*/
stop() {
if (this._mode === 'current') {
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.');
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.');
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.');
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.');
_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.');
if (this._chunkData !== void 0) {
DEBUG_BUILD && debug.log("[Profiling] Profile session already running, no-op.");
return;
}
if (this._mode === 'current') {
if (this._mode === "current") {
if (!this._sampled) {
DEBUG_BUILD && debug.log('[Profiling] Profile session not sampled, no-op.');
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.',
);
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.');
_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.',
);
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.');
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() {
_startTraceLifecycleProfiling() {
if (!this._client) {
DEBUG_BUILD &&
debug.log(
'[Profiling] Failed to start trace lifecycle profiling, sentry client was never attached to the profiler.',
);
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 => {
this._client.on("spanStart", (_span) => {
if (activeSpanCounter === 0) {

@@ -789,4 +520,3 @@ this._startChunkProfiling();

});
this._client.on('spanEnd', _span => {
this._client.on("spanEnd", (_span) => {
if (activeSpanCounter === 1) {

@@ -798,42 +528,27 @@ this._stopChunkProfiling();

}
_setupAutomaticSpanProfiling() {
_setupAutomaticSpanProfiling() {
if (!this._client) {
DEBUG_BUILD &&
debug.log(
'[Profiling] Failed to setup automatic span profiling, sentry client was never attached to the profiler.',
);
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 => {
const spanToProfileIdMap = /* @__PURE__ */ 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,
);
DEBUG_BUILD && debug.log(
"[Profiling] max profile duration elapsed, stopping profiling for:",
spanToJSON(span).description
);
const profile = stopSpanProfile(span, profile_id);

@@ -844,22 +559,15 @@ if (profile) {

}, maxProfileDurationMs);
// Unref timeout so it doesn't keep the process alive.
timeout.unref();
getIsolationScope().setContext('profile', { profile_id });
getIsolationScope().setContext("profile", { profile_id });
spanToProfileIdMap.set(span, profile_id);
}
});
this._client.on('spanEnd', span => {
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) {

@@ -870,9 +578,6 @@ addToProfileQueue(profile_id, profile);

});
this._client.on('beforeEnvelope', (envelope) => {
// if not profiles are in queue, there is nothing to add to the envelope.
this._client.on("beforeEnvelope", (envelope) => {
if (!PROFILE_MAP.size) {
return;
}
const profiledTransactionEvents = findProfiledTransactionsFromEnvelope(envelope);

@@ -882,18 +587,12 @@ if (!profiledTransactionEvents.length) {

}
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');
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);

@@ -904,80 +603,55 @@ if (!cpuProfile) {

}
// 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,
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,
this._client.emit("postprocessEvent", profile, {
event_id: profiledTransaction.event_id
});
}
addProfilesToEnvelope(envelope, profilesToAddToEnvelope);
});
}
/**
* Stop profiler and initializes profiling of the next chunk
*/
_restartChunkProfiling() {
_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.');
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.`,
);
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() {
_stopChunkProfiling() {
if (!this._chunkData) {
DEBUG_BUILD && debug.log('[Profiling] No chunk data found, no-op.');
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;
this._chunkData.timer = void 0;
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.');
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.`);
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) {

@@ -988,6 +662,4 @@ DEBUG_BUILD && debug.log(`[Profiling] Failed to collect profile for: ${this._chunkData.id}`);

}
if (!this._profilerId) {
DEBUG_BUILD &&
debug.log('[Profiling] Profile chunk does not contain a valid profiler_id, this is a bug in the SDK');
DEBUG_BUILD && debug.log("[Profiling] Profile chunk does not contain a valid profiler_id, this is a bug in the SDK");
this._resetChunkData();

@@ -999,3 +671,2 @@ return;

}
DEBUG_BUILD && debug.log(`[Profiling] Profile chunk ${this._chunkData.id} sent to Sentry.`);

@@ -1010,6 +681,5 @@ const chunk = createProfilingChunkEvent(

trace_id: this._chunkData.startTraceID,
profiler_id: this._profilerId,
},
profiler_id: this._profilerId
}
);
if (!chunk) {

@@ -1020,11 +690,5 @@ DEBUG_BUILD && debug.log(`[Profiling] Failed to create profile chunk for: ${this._chunkData.id}`);

}
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();
}
/**

@@ -1034,25 +698,20 @@ * Flushes the profile chunk to Sentry.

*/
_flush(chunk) {
_flush(chunk) {
if (!this._client) {
DEBUG_BUILD &&
debug.log('[Profiling] Failed to collect profile, sentry client was never attached to the profiler.');
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.');
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);
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);
});
}
/**

@@ -1062,26 +721,19 @@ * Starts the profiler and registers the flush timer for a given chunk.

*/
_startChunkProfiling() {
_startChunkProfiling() {
if (this._chunkData) {
DEBUG_BUILD && debug.log('[Profiling] Chunk is already running, no-op.');
DEBUG_BUILD && debug.log("[Profiling] Chunk is already running, no-op.");
return;
}
const traceId =
getCurrentScope().getPropagationContext().traceId || getIsolationScope().getPropagationContext().traceId;
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.');
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();
}
/**

@@ -1091,126 +743,94 @@ * Attaches profiling information to spans that were started

*/
_setupSpanChunkInstrumentation() {
_setupSpanChunkInstrumentation() {
if (!this._client) {
DEBUG_BUILD &&
debug.log('[Profiling] Failed to initialize span profiling, sentry client was never attached to the profiler.');
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,
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) {
_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;
_teardownSpanChunkInstrumentation() {
this._profilerId = void 0;
const globalScope = getGlobalScope();
globalScope.setContext('profile', {});
globalScope.setContext("profile", {});
}
/**
* Initializes new profile chunk metadata
*/
_initializeChunk(traceId) {
_initializeChunk(traceId) {
this._chunkData = {
id: uuid4(),
startTraceID: traceId,
timer: undefined,
timer: void 0
};
return this._chunkData;
}
/**
* Assigns thread_id and thread name context to a profiled event.
*/
_assignThreadIdContext(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 ?? {}),
...event.contexts?.trace ?? {},
data: {
...(event.contexts?.trace?.data ?? {}),
['thread.id']: PROFILER_THREAD_ID_STRING,
['thread.name']: PROFILER_THREAD_NAME,
},
...event.contexts?.trace?.data ?? {},
["thread.id"]: PROFILER_THREAD_ID_STRING,
["thread.name"]: PROFILER_THREAD_NAME
}
};
}
/**
* Resets the current chunk state.
*/
_resetChunkData() {
this._chunkData = undefined;
_resetChunkData() {
this._chunkData = void 0;
}
}
/** Exported only for tests. */
const _nodeProfilingIntegration = (() => {
if (![16, 18, 20, 22, 24, 26].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_MAJOR}).`,
'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',
"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',
name: "ProfilingIntegration",
_profiler: new ContinuousProfiler(),
setup(client) {
DEBUG_BUILD && debug.log('[Profiling] Profiling integration setup.');
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 ("profilesSampleRate" in options || "profilesSampler" in options) {
return "legacy";
}
if ('profileSessionSampleRate' in options || 'profileLifecycle' in options) {
return 'current';
if ("profileSessionSampleRate" in options || "profileLifecycle" in options) {
return "current";
}
// If neither are set, we are in the legacy continuous profiling mode
return 'legacy';
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);

@@ -1217,0 +837,0 @@

@@ -1,1 +0,1 @@

{"type":"module","version":"10.53.1","sideEffects":false}
{"type":"module","version":"10.54.0","sideEffects":false}
{
"name": "@sentry/profiling-node",
"version": "10.53.1",
"version": "10.54.0",
"description": "Official Sentry SDK for Node.js Profiling",

@@ -65,4 +65,4 @@ "repository": "git://github.com/getsentry/sentry-javascript.git",

"@sentry-internal/node-cpu-profiler": "^2.4.0",
"@sentry/core": "10.53.1",
"@sentry/node": "10.53.1"
"@sentry/core": "10.54.0",
"@sentry/node": "10.54.0"
},

@@ -69,0 +69,0 @@ "devDependencies": {

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display