@sentry/profiling-node
Advanced tools
+246
-626
@@ -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 @@ |
+246
-626
@@ -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} |
+3
-3
| { | ||
| "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
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
3
-40%222235
-10.18%2021
-18.21%+ Added
+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
Updated
Updated