From cbc5acbdf5cc41f38aff7ed2789249dd659b0a44 Mon Sep 17 00:00:00 2001 From: Francesco Gringl-Novy Date: Tue, 1 Jul 2025 12:52:14 +0200 Subject: [PATCH 01/16] fix(browser): Ensure explicit `parentSpan` is considered (#16776) Even if `parentSpanIsAlwaysRootSpan=true` is configured. Fixes https://github.com/getsentry/sentry-javascript/issues/16769 --- .size-limit.js | 2 +- packages/core/src/tracing/trace.ts | 18 ++- packages/core/test/lib/tracing/trace.test.ts | 116 +++++++++++++++++++ 3 files changed, 131 insertions(+), 5 deletions(-) diff --git a/.size-limit.js b/.size-limit.js index 61fb027289d3..dc8e4f9df560 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -120,7 +120,7 @@ module.exports = [ import: createImport('init', 'ErrorBoundary', 'reactRouterV6BrowserTracingIntegration'), ignore: ['react/jsx-runtime'], gzip: true, - limit: '41 KB', + limit: '42 KB', }, // Vue SDK (ESM) { diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index 7a2229090e97..bbb956acf042 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -58,7 +58,7 @@ export function startSpan(options: StartSpanOptions, callback: (span: Span) = return wrapper(() => { const scope = getCurrentScope(); - const parentSpan = getParentSpan(scope); + const parentSpan = getParentSpan(scope, customParentSpan); const shouldSkipSpan = options.onlyIfParent && !parentSpan; const activeSpan = shouldSkipSpan @@ -116,7 +116,7 @@ export function startSpanManual(options: StartSpanOptions, callback: (span: S return wrapper(() => { const scope = getCurrentScope(); - const parentSpan = getParentSpan(scope); + const parentSpan = getParentSpan(scope, customParentSpan); const shouldSkipSpan = options.onlyIfParent && !parentSpan; const activeSpan = shouldSkipSpan @@ -176,7 +176,7 @@ export function startInactiveSpan(options: StartSpanOptions): Span { return wrapper(() => { const scope = getCurrentScope(); - const parentSpan = getParentSpan(scope); + const parentSpan = getParentSpan(scope, customParentSpan); const shouldSkipSpan = options.onlyIfParent && !parentSpan; @@ -489,7 +489,17 @@ function _startChildSpan(parentSpan: Span, scope: Scope, spanArguments: SentrySp return childSpan; } -function getParentSpan(scope: Scope): SentrySpan | undefined { +function getParentSpan(scope: Scope, customParentSpan: Span | null | undefined): SentrySpan | undefined { + // always use the passed in span directly + if (customParentSpan) { + return customParentSpan as SentrySpan; + } + + // This is different from `undefined` as it means the user explicitly wants no parent span + if (customParentSpan === null) { + return undefined; + } + const span = _getSpanForScope(scope) as SentrySpan | undefined; if (!span) { diff --git a/packages/core/test/lib/tracing/trace.test.ts b/packages/core/test/lib/tracing/trace.test.ts index 6d25afe13d3e..eccbb57f1610 100644 --- a/packages/core/test/lib/tracing/trace.test.ts +++ b/packages/core/test/lib/tracing/trace.test.ts @@ -620,6 +620,44 @@ describe('startSpan', () => { }); }); }); + + it('explicit parentSpan takes precedence over parentSpanIsAlwaysRootSpan=true', () => { + const options = getDefaultTestClientOptions({ + tracesSampleRate: 1, + parentSpanIsAlwaysRootSpan: true, + }); + client = new TestClient(options); + setCurrentClient(client); + client.init(); + + const parentSpan = startInactiveSpan({ name: 'parent span' }); + + startSpan({ name: 'parent span' }, () => { + startSpan({ name: 'child span' }, () => { + startSpan({ name: 'grand child span', parentSpan }, grandChildSpan => { + expect(spanToJSON(grandChildSpan).parent_span_id).toBe(parentSpan.spanContext().spanId); + }); + }); + }); + }); + + it('explicit parentSpan=null takes precedence over parentSpanIsAlwaysRootSpan=true', () => { + const options = getDefaultTestClientOptions({ + tracesSampleRate: 1, + parentSpanIsAlwaysRootSpan: true, + }); + client = new TestClient(options); + setCurrentClient(client); + client.init(); + + startSpan({ name: 'parent span' }, () => { + startSpan({ name: 'child span' }, () => { + startSpan({ name: 'grand child span', parentSpan: null }, grandChildSpan => { + expect(spanToJSON(grandChildSpan).parent_span_id).toBe(undefined); + }); + }); + }); + }); }); it('samples with a tracesSampler', () => { @@ -1174,6 +1212,46 @@ describe('startSpanManual', () => { span.end(); }); }); + + it('explicit parentSpan takes precedence over parentSpanIsAlwaysRootSpan=true', () => { + const options = getDefaultTestClientOptions({ + tracesSampleRate: 1, + parentSpanIsAlwaysRootSpan: true, + }); + client = new TestClient(options); + setCurrentClient(client); + client.init(); + + const parentSpan = startInactiveSpan({ name: 'parent span' }); + + startSpan({ name: 'parent span' }, () => { + startSpan({ name: 'child span' }, () => { + startSpanManual({ name: 'grand child span', parentSpan }, grandChildSpan => { + expect(spanToJSON(grandChildSpan).parent_span_id).toBe(parentSpan.spanContext().spanId); + grandChildSpan.end(); + }); + }); + }); + }); + + it('explicit parentSpan=null takes precedence over parentSpanIsAlwaysRootSpan=true', () => { + const options = getDefaultTestClientOptions({ + tracesSampleRate: 1, + parentSpanIsAlwaysRootSpan: true, + }); + client = new TestClient(options); + setCurrentClient(client); + client.init(); + + startSpan({ name: 'parent span' }, () => { + startSpan({ name: 'child span' }, () => { + startSpanManual({ name: 'grand child span', parentSpan: null }, grandChildSpan => { + expect(spanToJSON(grandChildSpan).parent_span_id).toBe(undefined); + grandChildSpan.end(); + }); + }); + }); + }); }); it('sets a child span reference on the parent span', () => { @@ -1543,6 +1621,44 @@ describe('startInactiveSpan', () => { }); }); }); + + it('explicit parentSpan takes precedence over parentSpanIsAlwaysRootSpan=true', () => { + const options = getDefaultTestClientOptions({ + tracesSampleRate: 1, + parentSpanIsAlwaysRootSpan: true, + }); + client = new TestClient(options); + setCurrentClient(client); + client.init(); + + const parentSpan = startInactiveSpan({ name: 'parent span' }); + + startSpan({ name: 'parent span' }, () => { + startSpan({ name: 'child span' }, () => { + const grandChildSpan = startInactiveSpan({ name: 'grand child span', parentSpan }); + expect(spanToJSON(grandChildSpan).parent_span_id).toBe(parentSpan.spanContext().spanId); + grandChildSpan.end(); + }); + }); + }); + + it('explicit parentSpan=null takes precedence over parentSpanIsAlwaysRootSpan=true', () => { + const options = getDefaultTestClientOptions({ + tracesSampleRate: 1, + parentSpanIsAlwaysRootSpan: true, + }); + client = new TestClient(options); + setCurrentClient(client); + client.init(); + + startSpan({ name: 'parent span' }, () => { + startSpan({ name: 'child span' }, () => { + const grandChildSpan = startInactiveSpan({ name: 'grand child span', parentSpan: null }); + expect(spanToJSON(grandChildSpan).parent_span_id).toBe(undefined); + grandChildSpan.end(); + }); + }); + }); }); it('includes the scope at the time the span was started when finished', async () => { From f2f8e1f0d44da2cfc3f04472da144e9ba7a824ab Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 1 Jul 2025 16:10:02 +0200 Subject: [PATCH 02/16] feat(browser): Add ElementTiming instrumentation and spans (#16589) This PR adds support for instrumenting and sending spans from [`ElementTiming` API ](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/elementtiming)entries. Just like with web vitals and long tasks/animation frames, we register a `PerformanceObserver` and extract spans from newly emitted ET entries. Important: - We'll by default emit ET spans. Users can opt out by setting `enableElementTiming: false` in `browserTracingIntegration` - We for now only emit an ET span, if there is an active parent span. Happy to adjust this but considering the limitations from below I'm not sure if we actually want all spans after the pageload span. For now, we'll also emit spans on other transactions that pageload (most prominently `navigation` spans as well). We could also go the route of only sending until the first navigation as with standalone CLS/LCP spans. Happy to accept any direction we wanna take this. Some noteworthy findings while working on this: - ET is only emitted for text and image nodes. - For image nodes, we get the `loadTime` which is the relative timestamp to the browser's `timeOrigin`, when the image _finished_ loading. For text nodes, `loadTime` is always `0`, since nothing needs to be loaded. - For all nodes, we get `renderTime` which is the relative timestamp to the browser's `timeOrigin`, when the node finished rendering (i.e. was painted by the browser). - In any case, we do not get start times for rendering or loading. Consequently, the span duration is - `renderTime - loadTime` for image nodes - `0` for text nodes - The span start time is: - `timeOrigin + loadTime` for image nodes - `timeOrigin + renderTime` for text nodes In addition to the raw span and conventional attributes, we also collect a bunch of ET-specific attributes: - `element.type` - tag name of the element (e.g. `img` or `p`) - `element.size` - width x height of the element - `element.render-time` - `entry.renderTime` - `element.load-time` - `entry.loadTime` - `element.url` - url of the loaded image (`undefined` for text nodes) - `element.identifier` - the identifier passed to the `elementtiming=identifier` HTML attribute - `element.paint-type` - the node paint type (`image-paint` or `text-paint`) also some additional sentry-sepcific attributes: - `route` - the route name, either from the active root span (if available) or from the scope's `transactionName` - `sentry.span-start-time-source` - the data point we used as the span start time More than happy to adjust any of this logic or attribute names, based on review feedback :) closes #13675 also ref https://github.com/getsentry/sentry-javascript/issues/7292 --------- Co-authored-by: Abhijeet Prasad Co-authored-by: s1gr1d --- .size-limit.js | 10 +- .../assets/sentry-logo-600x179.png | Bin 0 -> 16118 bytes .../tracing/metrics/element-timing/init.js | 10 + .../tracing/metrics/element-timing/subject.js | 27 ++ .../metrics/element-timing/template.html | 49 +++ .../tracing/metrics/element-timing/test.ts | 232 +++++++++++ packages/browser-utils/src/index.ts | 2 + .../src/metrics/elementTiming.ts | 121 ++++++ .../browser-utils/src/metrics/instrument.ts | 3 +- .../src/metrics/web-vitals/lib/observe.ts | 3 + .../instrument/metrics/elementTiming.test.ts | 369 ++++++++++++++++++ .../src/tracing/browserTracingIntegration.ts | 15 + 12 files changed, 835 insertions(+), 6 deletions(-) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/element-timing/assets/sentry-logo-600x179.png create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/element-timing/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/element-timing/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/element-timing/template.html create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/element-timing/test.ts create mode 100644 packages/browser-utils/src/metrics/elementTiming.ts create mode 100644 packages/browser-utils/test/instrument/metrics/elementTiming.test.ts diff --git a/.size-limit.js b/.size-limit.js index dc8e4f9df560..685b40b00fbe 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -38,7 +38,7 @@ module.exports = [ path: 'packages/browser/build/npm/esm/index.js', import: createImport('init', 'browserTracingIntegration'), gzip: true, - limit: '40 KB', + limit: '40.7 KB', }, { name: '@sentry/browser (incl. Tracing, Replay)', @@ -75,7 +75,7 @@ module.exports = [ path: 'packages/browser/build/npm/esm/index.js', import: createImport('init', 'browserTracingIntegration', 'replayIntegration', 'replayCanvasIntegration'), gzip: true, - limit: '82 KB', + limit: '83 KB', }, { name: '@sentry/browser (incl. Tracing, Replay, Feedback)', @@ -135,7 +135,7 @@ module.exports = [ path: 'packages/vue/build/esm/index.js', import: createImport('init', 'browserTracingIntegration'), gzip: true, - limit: '41 KB', + limit: '42 KB', }, // Svelte SDK (ESM) { @@ -206,7 +206,7 @@ module.exports = [ import: createImport('init'), ignore: ['next/router', 'next/constants'], gzip: true, - limit: '43 KB', + limit: '44 KB', }, // SvelteKit SDK (ESM) { @@ -215,7 +215,7 @@ module.exports = [ import: createImport('init'), ignore: ['$app/stores'], gzip: true, - limit: '40 KB', + limit: '41 KB', }, // Node SDK (ESM) { diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/element-timing/assets/sentry-logo-600x179.png b/dev-packages/browser-integration-tests/suites/tracing/metrics/element-timing/assets/sentry-logo-600x179.png new file mode 100644 index 0000000000000000000000000000000000000000..353b7233d6bfa4f026f9998cacfa4add4bba9274 GIT binary patch literal 16118 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 Cl9AH@ literal 0 HcmV?d00001 diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/element-timing/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/element-timing/init.js new file mode 100644 index 000000000000..5a4cb2dff8b7 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/element-timing/init.js @@ -0,0 +1,10 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + debug: true, + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.browserTracingIntegration()], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/element-timing/subject.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/element-timing/subject.js new file mode 100644 index 000000000000..0fff6c2a88e6 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/element-timing/subject.js @@ -0,0 +1,27 @@ +const lazyDiv = document.getElementById('content-lazy'); +const navigationButton = document.getElementById('button1'); +const navigationDiv = document.getElementById('content-navigation'); +const clickButton = document.getElementById('button2'); +const clickDiv = document.getElementById('content-click'); + +navigationButton.addEventListener('click', () => { + window.history.pushState({}, '', '/some-other-path'); + navigationDiv.innerHTML = ` + +

This is navigation content

+ `; +}); + +setTimeout(() => { + lazyDiv.innerHTML = ` + +

This is lazy loaded content

+ `; +}, 1000); + +clickButton.addEventListener('click', () => { + clickDiv.innerHTML = ` + +

This is click loaded content

+ `; +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/element-timing/template.html b/dev-packages/browser-integration-tests/suites/tracing/metrics/element-timing/template.html new file mode 100644 index 000000000000..6f536f8d2aa4 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/element-timing/template.html @@ -0,0 +1,49 @@ + + + + + + + + + + +

+ This is some text content + with another nested span + and a small text +

+ + +
+

Header with element timing

+ +
+ + + + + +
+

This div will be populated lazily

+
+ + +
+

This div will be populated after a navigation

+
+ + +
+

This div will be populated on click

+
+ + + + + + diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/element-timing/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/element-timing/test.ts new file mode 100644 index 000000000000..e17cbbbda691 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/element-timing/test.ts @@ -0,0 +1,232 @@ +import type { Page, Route } from '@playwright/test'; +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers'; + +sentryTest( + 'adds element timing spans to pageload span tree for elements rendered during pageload', + async ({ getLocalTestUrl, page, browserName }) => { + if (shouldSkipTracingTest() || browserName === 'webkit') { + sentryTest.skip(); + } + + const pageloadEventPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'pageload'); + + serveAssets(page); + + const url = await getLocalTestUrl({ testDir: __dirname }); + + await page.goto(url); + + const eventData = envelopeRequestParser(await pageloadEventPromise); + + const elementTimingSpans = eventData.spans?.filter(({ op }) => op === 'ui.elementtiming'); + + expect(elementTimingSpans?.length).toEqual(8); + + // Check image-fast span (this is served with a 100ms delay) + const imageFastSpan = elementTimingSpans?.find(({ description }) => description === 'element[image-fast]'); + const imageFastRenderTime = imageFastSpan?.data['element.render_time']; + const imageFastLoadTime = imageFastSpan?.data['element.load_time']; + const duration = imageFastSpan!.timestamp! - imageFastSpan!.start_timestamp!; + + expect(imageFastSpan).toBeDefined(); + expect(imageFastSpan?.data).toEqual({ + 'sentry.op': 'ui.elementtiming', + 'sentry.origin': 'auto.ui.browser.elementtiming', + 'sentry.source': 'component', + 'sentry.span_start_time_source': 'load-time', + 'element.id': 'image-fast-id', + 'element.identifier': 'image-fast', + 'element.type': 'img', + 'element.size': '600x179', + 'element.url': 'https://sentry-test-site.example/path/to/image-fast.png', + 'element.render_time': expect.any(Number), + 'element.load_time': expect.any(Number), + 'element.paint_type': 'image-paint', + 'sentry.transaction_name': '/index.html', + }); + expect(imageFastRenderTime).toBeGreaterThan(90); + expect(imageFastRenderTime).toBeLessThan(400); + expect(imageFastLoadTime).toBeGreaterThan(90); + expect(imageFastLoadTime).toBeLessThan(400); + expect(imageFastRenderTime).toBeGreaterThan(imageFastLoadTime as number); + expect(duration).toBeGreaterThan(0); + expect(duration).toBeLessThan(20); + + // Check text1 span + const text1Span = elementTimingSpans?.find(({ data }) => data?.['element.identifier'] === 'text1'); + const text1RenderTime = text1Span?.data['element.render_time']; + const text1LoadTime = text1Span?.data['element.load_time']; + const text1Duration = text1Span!.timestamp! - text1Span!.start_timestamp!; + expect(text1Span).toBeDefined(); + expect(text1Span?.data).toEqual({ + 'sentry.op': 'ui.elementtiming', + 'sentry.origin': 'auto.ui.browser.elementtiming', + 'sentry.source': 'component', + 'sentry.span_start_time_source': 'render-time', + 'element.id': 'text1-id', + 'element.identifier': 'text1', + 'element.type': 'p', + 'element.render_time': expect.any(Number), + 'element.load_time': expect.any(Number), + 'element.paint_type': 'text-paint', + 'sentry.transaction_name': '/index.html', + }); + expect(text1RenderTime).toBeGreaterThan(0); + expect(text1RenderTime).toBeLessThan(300); + expect(text1LoadTime).toBe(0); + expect(text1RenderTime).toBeGreaterThan(text1LoadTime as number); + expect(text1Duration).toBe(0); + + // Check button1 span (no need for a full assertion) + const button1Span = elementTimingSpans?.find(({ data }) => data?.['element.identifier'] === 'button1'); + expect(button1Span).toBeDefined(); + expect(button1Span?.data).toMatchObject({ + 'element.identifier': 'button1', + 'element.type': 'button', + 'element.paint_type': 'text-paint', + 'sentry.transaction_name': '/index.html', + }); + + // Check image-slow span + const imageSlowSpan = elementTimingSpans?.find(({ data }) => data?.['element.identifier'] === 'image-slow'); + expect(imageSlowSpan).toBeDefined(); + expect(imageSlowSpan?.data).toEqual({ + 'element.id': '', + 'element.identifier': 'image-slow', + 'element.type': 'img', + 'element.size': '600x179', + 'element.url': 'https://sentry-test-site.example/path/to/image-slow.png', + 'element.paint_type': 'image-paint', + 'element.render_time': expect.any(Number), + 'element.load_time': expect.any(Number), + 'sentry.op': 'ui.elementtiming', + 'sentry.origin': 'auto.ui.browser.elementtiming', + 'sentry.source': 'component', + 'sentry.span_start_time_source': 'load-time', + 'sentry.transaction_name': '/index.html', + }); + const imageSlowRenderTime = imageSlowSpan?.data['element.render_time']; + const imageSlowLoadTime = imageSlowSpan?.data['element.load_time']; + const imageSlowDuration = imageSlowSpan!.timestamp! - imageSlowSpan!.start_timestamp!; + expect(imageSlowRenderTime).toBeGreaterThan(1400); + expect(imageSlowRenderTime).toBeLessThan(2000); + expect(imageSlowLoadTime).toBeGreaterThan(1400); + expect(imageSlowLoadTime).toBeLessThan(2000); + expect(imageSlowDuration).toBeGreaterThan(0); + expect(imageSlowDuration).toBeLessThan(20); + + // Check lazy-image span + const lazyImageSpan = elementTimingSpans?.find(({ data }) => data?.['element.identifier'] === 'lazy-image'); + expect(lazyImageSpan).toBeDefined(); + expect(lazyImageSpan?.data).toEqual({ + 'element.id': '', + 'element.identifier': 'lazy-image', + 'element.type': 'img', + 'element.size': '600x179', + 'element.url': 'https://sentry-test-site.example/path/to/image-lazy.png', + 'element.paint_type': 'image-paint', + 'element.render_time': expect.any(Number), + 'element.load_time': expect.any(Number), + 'sentry.op': 'ui.elementtiming', + 'sentry.origin': 'auto.ui.browser.elementtiming', + 'sentry.source': 'component', + 'sentry.span_start_time_source': 'load-time', + 'sentry.transaction_name': '/index.html', + }); + const lazyImageRenderTime = lazyImageSpan?.data['element.render_time']; + const lazyImageLoadTime = lazyImageSpan?.data['element.load_time']; + const lazyImageDuration = lazyImageSpan!.timestamp! - lazyImageSpan!.start_timestamp!; + expect(lazyImageRenderTime).toBeGreaterThan(1000); + expect(lazyImageRenderTime).toBeLessThan(1500); + expect(lazyImageLoadTime).toBeGreaterThan(1000); + expect(lazyImageLoadTime).toBeLessThan(1500); + expect(lazyImageDuration).toBeGreaterThan(0); + expect(lazyImageDuration).toBeLessThan(20); + + // Check lazy-text span + const lazyTextSpan = elementTimingSpans?.find(({ data }) => data?.['element.identifier'] === 'lazy-text'); + expect(lazyTextSpan?.data).toMatchObject({ + 'element.id': '', + 'element.identifier': 'lazy-text', + 'element.type': 'p', + 'sentry.transaction_name': '/index.html', + }); + const lazyTextRenderTime = lazyTextSpan?.data['element.render_time']; + const lazyTextLoadTime = lazyTextSpan?.data['element.load_time']; + const lazyTextDuration = lazyTextSpan!.timestamp! - lazyTextSpan!.start_timestamp!; + expect(lazyTextRenderTime).toBeGreaterThan(1000); + expect(lazyTextRenderTime).toBeLessThan(1500); + expect(lazyTextLoadTime).toBe(0); + expect(lazyTextDuration).toBe(0); + + // the div1 entry does not emit an elementTiming entry because it's neither a text nor an image + expect(elementTimingSpans?.find(({ description }) => description === 'element[div1]')).toBeUndefined(); + }, +); + +sentryTest('emits element timing spans on navigation', async ({ getLocalTestUrl, page, browserName }) => { + if (shouldSkipTracingTest() || browserName === 'webkit') { + sentryTest.skip(); + } + + serveAssets(page); + + const url = await getLocalTestUrl({ testDir: __dirname }); + + await page.goto(url); + + const pageloadEventPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'pageload'); + + const navigationEventPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'navigation'); + + await pageloadEventPromise; + + await page.locator('#button1').click(); + + const navigationTransactionEvent = envelopeRequestParser(await navigationEventPromise); + const pageloadTransactionEvent = envelopeRequestParser(await pageloadEventPromise); + + const navigationElementTimingSpans = navigationTransactionEvent.spans?.filter(({ op }) => op === 'ui.elementtiming'); + + expect(navigationElementTimingSpans?.length).toEqual(2); + + const navigationStartTime = navigationTransactionEvent.start_timestamp!; + const pageloadStartTime = pageloadTransactionEvent.start_timestamp!; + + const imageSpan = navigationElementTimingSpans?.find( + ({ description }) => description === 'element[navigation-image]', + ); + const textSpan = navigationElementTimingSpans?.find(({ description }) => description === 'element[navigation-text]'); + + // Image started loading after navigation, but render-time and load-time still start from the time origin + // of the pageload. This is somewhat a limitation (though by design according to the ElementTiming spec) + expect((imageSpan!.data['element.render_time']! as number) / 1000 + pageloadStartTime).toBeGreaterThan( + navigationStartTime, + ); + expect((imageSpan!.data['element.load_time']! as number) / 1000 + pageloadStartTime).toBeGreaterThan( + navigationStartTime, + ); + + expect(textSpan?.data['element.load_time']).toBe(0); + expect((textSpan!.data['element.render_time']! as number) / 1000 + pageloadStartTime).toBeGreaterThan( + navigationStartTime, + ); +}); + +function serveAssets(page: Page) { + page.route(/image-(fast|lazy|navigation|click)\.png/, async (route: Route) => { + await new Promise(resolve => setTimeout(resolve, 100)); + return route.fulfill({ + path: `${__dirname}/assets/sentry-logo-600x179.png`, + }); + }); + + page.route('**/image-slow.png', async (route: Route) => { + await new Promise(resolve => setTimeout(resolve, 1500)); + return route.fulfill({ + path: `${__dirname}/assets/sentry-logo-600x179.png`, + }); + }); +} diff --git a/packages/browser-utils/src/index.ts b/packages/browser-utils/src/index.ts index f66446ea5159..0a2d9e85ade9 100644 --- a/packages/browser-utils/src/index.ts +++ b/packages/browser-utils/src/index.ts @@ -17,6 +17,8 @@ export { registerInpInteractionListener, } from './metrics/browserMetrics'; +export { startTrackingElementTiming } from './metrics/elementTiming'; + export { extractNetworkProtocol } from './metrics/utils'; export { addClickKeypressInstrumentationHandler } from './instrument/dom'; diff --git a/packages/browser-utils/src/metrics/elementTiming.ts b/packages/browser-utils/src/metrics/elementTiming.ts new file mode 100644 index 000000000000..f746b16645af --- /dev/null +++ b/packages/browser-utils/src/metrics/elementTiming.ts @@ -0,0 +1,121 @@ +import type { SpanAttributes } from '@sentry/core'; +import { + browserPerformanceTimeOrigin, + getActiveSpan, + getCurrentScope, + getRootSpan, + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + spanToJSON, + startSpan, + timestampInSeconds, +} from '@sentry/core'; +import { addPerformanceInstrumentationHandler } from './instrument'; +import { getBrowserPerformanceAPI, msToSec } from './utils'; + +// ElementTiming interface based on the W3C spec +interface PerformanceElementTiming extends PerformanceEntry { + renderTime: number; + loadTime: number; + intersectionRect: DOMRectReadOnly; + identifier: string; + naturalWidth: number; + naturalHeight: number; + id: string; + element: Element | null; + url?: string; +} + +/** + * Start tracking ElementTiming performance entries. + */ +export function startTrackingElementTiming(): () => void { + const performance = getBrowserPerformanceAPI(); + if (performance && browserPerformanceTimeOrigin()) { + return addPerformanceInstrumentationHandler('element', _onElementTiming); + } + + return () => undefined; +} + +/** + * exported only for testing + */ +export const _onElementTiming = ({ entries }: { entries: PerformanceEntry[] }): void => { + const activeSpan = getActiveSpan(); + const rootSpan = activeSpan ? getRootSpan(activeSpan) : undefined; + const transactionName = rootSpan + ? spanToJSON(rootSpan).description + : getCurrentScope().getScopeData().transactionName; + + entries.forEach(entry => { + const elementEntry = entry as PerformanceElementTiming; + + // Skip entries without identifier (elementtiming attribute) + if (!elementEntry.identifier) { + return; + } + + // `name` contains the type of the element paint. Can be `'image-paint'` or `'text-paint'`. + // https://developer.mozilla.org/en-US/docs/Web/API/PerformanceElementTiming#instance_properties + const paintType = elementEntry.name as 'image-paint' | 'text-paint' | undefined; + + const renderTime = elementEntry.renderTime; + const loadTime = elementEntry.loadTime; + + // starting the span at: + // - `loadTime` if available (should be available for all "image-paint" entries, 0 otherwise) + // - `renderTime` if available (available for all entries, except 3rd party images, but these should be covered by `loadTime`, 0 otherwise) + // - `timestampInSeconds()` as a safeguard + // see https://developer.mozilla.org/en-US/docs/Web/API/PerformanceElementTiming/renderTime#cross-origin_image_render_time + const [spanStartTime, spanStartTimeSource] = loadTime + ? [msToSec(loadTime), 'load-time'] + : renderTime + ? [msToSec(renderTime), 'render-time'] + : [timestampInSeconds(), 'entry-emission']; + + const duration = + paintType === 'image-paint' + ? // for image paints, we can acually get a duration because image-paint entries also have a `loadTime` + // and `renderTime`. `loadTime` is the time when the image finished loading and `renderTime` is the + // time when the image finished rendering. + msToSec(Math.max(0, (renderTime ?? 0) - (loadTime ?? 0))) + : // for `'text-paint'` entries, we can't get a duration because the `loadTime` is always zero. + 0; + + const attributes: SpanAttributes = { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.elementtiming', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.elementtiming', + // name must be user-entered, so we can assume low cardinality + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + // recording the source of the span start time, as it varies depending on available data + 'sentry.span_start_time_source': spanStartTimeSource, + 'sentry.transaction_name': transactionName, + 'element.id': elementEntry.id, + 'element.type': elementEntry.element?.tagName?.toLowerCase() || 'unknown', + 'element.size': + elementEntry.naturalWidth && elementEntry.naturalHeight + ? `${elementEntry.naturalWidth}x${elementEntry.naturalHeight}` + : undefined, + 'element.render_time': renderTime, + 'element.load_time': loadTime, + // `url` is `0`(number) for text paints (hence we fall back to undefined) + 'element.url': elementEntry.url || undefined, + 'element.identifier': elementEntry.identifier, + 'element.paint_type': paintType, + }; + + startSpan( + { + name: `element[${elementEntry.identifier}]`, + attributes, + startTime: spanStartTime, + onlyIfParent: true, + }, + span => { + span.end(spanStartTime + duration); + }, + ); + }); +}; diff --git a/packages/browser-utils/src/metrics/instrument.ts b/packages/browser-utils/src/metrics/instrument.ts index cb84908ce55b..9fbf075a7712 100644 --- a/packages/browser-utils/src/metrics/instrument.ts +++ b/packages/browser-utils/src/metrics/instrument.ts @@ -13,7 +13,8 @@ type InstrumentHandlerTypePerformanceObserver = | 'navigation' | 'paint' | 'resource' - | 'first-input'; + | 'first-input' + | 'element'; type InstrumentHandlerTypeMetric = 'cls' | 'lcp' | 'fid' | 'ttfb' | 'inp'; diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/observe.ts b/packages/browser-utils/src/metrics/web-vitals/lib/observe.ts index 9af0116cd0b1..6071893dfa8e 100644 --- a/packages/browser-utils/src/metrics/web-vitals/lib/observe.ts +++ b/packages/browser-utils/src/metrics/web-vitals/lib/observe.ts @@ -28,6 +28,9 @@ interface PerformanceEntryMap { // our `instrumentPerformanceObserver` function also observes 'longtask' // entries. longtask: PerformanceEntry[]; + // Sentry-specific change: + // We add element as a supported entry type for ElementTiming API + element: PerformanceEntry[]; } /** diff --git a/packages/browser-utils/test/instrument/metrics/elementTiming.test.ts b/packages/browser-utils/test/instrument/metrics/elementTiming.test.ts new file mode 100644 index 000000000000..04456ceadc44 --- /dev/null +++ b/packages/browser-utils/test/instrument/metrics/elementTiming.test.ts @@ -0,0 +1,369 @@ +import * as sentryCore from '@sentry/core'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { _onElementTiming, startTrackingElementTiming } from '../../../src/metrics/elementTiming'; +import * as browserMetricsInstrumentation from '../../../src/metrics/instrument'; +import * as browserMetricsUtils from '../../../src/metrics/utils'; + +describe('_onElementTiming', () => { + const spanEndSpy = vi.fn(); + const startSpanSpy = vi.spyOn(sentryCore, 'startSpan').mockImplementation((opts, cb) => { + // @ts-expect-error - only passing a partial span. This is fine for the test. + cb({ + end: spanEndSpy, + }); + }); + + beforeEach(() => { + startSpanSpy.mockClear(); + spanEndSpy.mockClear(); + }); + + it('does nothing if the ET entry has no identifier', () => { + const entry = { + name: 'image-paint', + entryType: 'element', + startTime: 0, + duration: 0, + renderTime: 100, + } as Partial; + + // @ts-expect-error - only passing a partial entry. This is fine for the test. + _onElementTiming({ entries: [entry] }); + + expect(startSpanSpy).not.toHaveBeenCalled(); + }); + + describe('span start time', () => { + it('uses the load time as span start time if available', () => { + const entry = { + name: 'image-paint', + entryType: 'element', + startTime: 0, + duration: 0, + renderTime: 100, + loadTime: 50, + identifier: 'test-element', + } as Partial; + + // @ts-expect-error - only passing a partial entry. This is fine for the test. + _onElementTiming({ entries: [entry] }); + + expect(startSpanSpy).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'element[test-element]', + startTime: 0.05, + attributes: expect.objectContaining({ + 'sentry.op': 'ui.elementtiming', + 'sentry.origin': 'auto.ui.browser.elementtiming', + 'sentry.source': 'component', + 'sentry.span_start_time_source': 'load-time', + 'element.render_time': 100, + 'element.load_time': 50, + 'element.identifier': 'test-element', + 'element.paint_type': 'image-paint', + }), + }), + expect.any(Function), + ); + }); + + it('uses the render time as span start time if load time is not available', () => { + const entry = { + name: 'image-paint', + entryType: 'element', + startTime: 0, + duration: 0, + renderTime: 100, + identifier: 'test-element', + } as Partial; + + // @ts-expect-error - only passing a partial entry. This is fine for the test. + _onElementTiming({ entries: [entry] }); + + expect(startSpanSpy).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'element[test-element]', + startTime: 0.1, + attributes: expect.objectContaining({ + 'sentry.op': 'ui.elementtiming', + 'sentry.origin': 'auto.ui.browser.elementtiming', + 'sentry.source': 'component', + 'sentry.span_start_time_source': 'render-time', + 'element.render_time': 100, + 'element.load_time': undefined, + 'element.identifier': 'test-element', + 'element.paint_type': 'image-paint', + }), + }), + expect.any(Function), + ); + }); + + it('falls back to the time of handling the entry if load and render time are not available', () => { + const entry = { + name: 'image-paint', + entryType: 'element', + startTime: 0, + duration: 0, + identifier: 'test-element', + } as Partial; + + // @ts-expect-error - only passing a partial entry. This is fine for the test. + _onElementTiming({ entries: [entry] }); + + expect(startSpanSpy).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'element[test-element]', + startTime: expect.any(Number), + attributes: expect.objectContaining({ + 'sentry.op': 'ui.elementtiming', + 'sentry.origin': 'auto.ui.browser.elementtiming', + 'sentry.source': 'component', + 'sentry.span_start_time_source': 'entry-emission', + 'element.render_time': undefined, + 'element.load_time': undefined, + 'element.identifier': 'test-element', + 'element.paint_type': 'image-paint', + }), + }), + expect.any(Function), + ); + }); + }); + + describe('span duration', () => { + it('uses (render-load) time as duration for image paints', () => { + const entry = { + name: 'image-paint', + entryType: 'element', + startTime: 0, + duration: 0, + renderTime: 1505, + loadTime: 1500, + identifier: 'test-element', + } as Partial; + + // @ts-expect-error - only passing a partial entry. This is fine for the test. + _onElementTiming({ entries: [entry] }); + + expect(startSpanSpy).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'element[test-element]', + startTime: 1.5, + attributes: expect.objectContaining({ + 'element.render_time': 1505, + 'element.load_time': 1500, + 'element.paint_type': 'image-paint', + }), + }), + expect.any(Function), + ); + + expect(spanEndSpy).toHaveBeenCalledWith(1.505); + }); + + it('uses 0 as duration for text paints', () => { + const entry = { + name: 'text-paint', + entryType: 'element', + startTime: 0, + duration: 0, + loadTime: 0, + renderTime: 1600, + identifier: 'test-element', + } as Partial; + + // @ts-expect-error - only passing a partial entry. This is fine for the test. + _onElementTiming({ entries: [entry] }); + + expect(startSpanSpy).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'element[test-element]', + startTime: 1.6, + attributes: expect.objectContaining({ + 'element.paint_type': 'text-paint', + 'element.render_time': 1600, + 'element.load_time': 0, + }), + }), + expect.any(Function), + ); + + expect(spanEndSpy).toHaveBeenCalledWith(1.6); + }); + + // per spec, no other kinds are supported but let's make sure we're defensive + it('uses 0 as duration for other kinds of entries', () => { + const entry = { + name: 'somethingelse', + entryType: 'element', + startTime: 0, + duration: 0, + loadTime: 0, + renderTime: 1700, + identifier: 'test-element', + } as Partial; + + // @ts-expect-error - only passing a partial entry. This is fine for the test. + _onElementTiming({ entries: [entry] }); + + expect(startSpanSpy).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'element[test-element]', + startTime: 1.7, + attributes: expect.objectContaining({ + 'element.paint_type': 'somethingelse', + 'element.render_time': 1700, + 'element.load_time': 0, + }), + }), + expect.any(Function), + ); + + expect(spanEndSpy).toHaveBeenCalledWith(1.7); + }); + }); + + describe('span attributes', () => { + it('sets element type, identifier, paint type, load and render time', () => { + const entry = { + name: 'image-paint', + entryType: 'element', + startTime: 0, + duration: 0, + renderTime: 100, + identifier: 'my-image', + element: { + tagName: 'IMG', + }, + } as Partial; + + // @ts-expect-error - only passing a partial entry. This is fine for the test. + _onElementTiming({ entries: [entry] }); + + expect(startSpanSpy).toHaveBeenCalledWith( + expect.objectContaining({ + attributes: expect.objectContaining({ + 'element.type': 'img', + 'element.identifier': 'my-image', + 'element.paint_type': 'image-paint', + 'element.render_time': 100, + 'element.load_time': undefined, + 'element.size': undefined, + 'element.url': undefined, + }), + }), + expect.any(Function), + ); + }); + + it('sets element size if available', () => { + const entry = { + name: 'image-paint', + entryType: 'element', + startTime: 0, + duration: 0, + renderTime: 100, + naturalWidth: 512, + naturalHeight: 256, + identifier: 'my-image', + } as Partial; + + // @ts-expect-error - only passing a partial entry. This is fine for the test. + _onElementTiming({ entries: [entry] }); + + expect(startSpanSpy).toHaveBeenCalledWith( + expect.objectContaining({ + attributes: expect.objectContaining({ + 'element.size': '512x256', + 'element.identifier': 'my-image', + }), + }), + expect.any(Function), + ); + }); + + it('sets element url if available', () => { + const entry = { + name: 'image-paint', + entryType: 'element', + startTime: 0, + duration: 0, + url: 'https://santry.com/image.png', + identifier: 'my-image', + } as Partial; + + // @ts-expect-error - only passing a partial entry. This is fine for the test. + _onElementTiming({ entries: [entry] }); + + expect(startSpanSpy).toHaveBeenCalledWith( + expect.objectContaining({ + attributes: expect.objectContaining({ + 'element.identifier': 'my-image', + 'element.url': 'https://santry.com/image.png', + }), + }), + expect.any(Function), + ); + }); + + it('sets sentry attributes', () => { + const entry = { + name: 'image-paint', + entryType: 'element', + startTime: 0, + duration: 0, + renderTime: 100, + identifier: 'my-image', + } as Partial; + + // @ts-expect-error - only passing a partial entry. This is fine for the test. + _onElementTiming({ entries: [entry] }); + + expect(startSpanSpy).toHaveBeenCalledWith( + expect.objectContaining({ + attributes: expect.objectContaining({ + 'sentry.op': 'ui.elementtiming', + 'sentry.origin': 'auto.ui.browser.elementtiming', + 'sentry.source': 'component', + 'sentry.span_start_time_source': 'render-time', + 'sentry.transaction_name': undefined, + }), + }), + expect.any(Function), + ); + }); + }); +}); + +describe('startTrackingElementTiming', () => { + const addInstrumentationHandlerSpy = vi.spyOn(browserMetricsInstrumentation, 'addPerformanceInstrumentationHandler'); + + beforeEach(() => { + addInstrumentationHandlerSpy.mockClear(); + }); + + it('returns a function that does nothing if the browser does not support the performance API', () => { + vi.spyOn(browserMetricsUtils, 'getBrowserPerformanceAPI').mockReturnValue(undefined); + expect(typeof startTrackingElementTiming()).toBe('function'); + + expect(addInstrumentationHandlerSpy).not.toHaveBeenCalled(); + }); + + it('adds an instrumentation handler for elementtiming entries, if the browser supports the performance API', () => { + vi.spyOn(browserMetricsUtils, 'getBrowserPerformanceAPI').mockReturnValue({ + getEntriesByType: vi.fn().mockReturnValue([]), + } as unknown as Performance); + + const addInstrumentationHandlerSpy = vi.spyOn( + browserMetricsInstrumentation, + 'addPerformanceInstrumentationHandler', + ); + + const stopTracking = startTrackingElementTiming(); + + expect(typeof stopTracking).toBe('function'); + + expect(addInstrumentationHandlerSpy).toHaveBeenCalledWith('element', expect.any(Function)); + }); +}); diff --git a/packages/browser/src/tracing/browserTracingIntegration.ts b/packages/browser/src/tracing/browserTracingIntegration.ts index af742310c37f..e1815cc7bf39 100644 --- a/packages/browser/src/tracing/browserTracingIntegration.ts +++ b/packages/browser/src/tracing/browserTracingIntegration.ts @@ -26,6 +26,7 @@ import { addHistoryInstrumentationHandler, addPerformanceEntries, registerInpInteractionListener, + startTrackingElementTiming, startTrackingINP, startTrackingInteractions, startTrackingLongAnimationFrames, @@ -115,6 +116,14 @@ export interface BrowserTracingOptions { */ enableInp: boolean; + /** + * If true, Sentry will capture [element timing](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceElementTiming) + * information and add it to the corresponding transaction. + * + * Default: true + */ + enableElementTiming: boolean; + /** * Flag to disable patching all together for fetch requests. * @@ -269,6 +278,7 @@ const DEFAULT_BROWSER_TRACING_OPTIONS: BrowserTracingOptions = { enableLongTask: true, enableLongAnimationFrame: true, enableInp: true, + enableElementTiming: true, ignoreResourceSpans: [], ignorePerformanceApiSpans: [], linkPreviousTrace: 'in-memory', @@ -300,6 +310,7 @@ export const browserTracingIntegration = ((_options: Partial Date: Wed, 2 Jul 2025 13:51:35 +0200 Subject: [PATCH 03/16] feat(node): Export server-side feature flag integration shims (#16735) --- .../app/server-component/featureFlag/page.tsx | 16 ++++++ .../tests/server-components.test.ts | 6 +++ packages/astro/src/index.server.ts | 6 +++ packages/astro/src/index.types.ts | 7 +++ packages/aws-serverless/src/index.ts | 6 +++ packages/bun/src/index.ts | 6 +++ packages/google-cloud-serverless/src/index.ts | 6 +++ packages/integration-shims/src/index.ts | 1 + .../integration-shims/src/launchDarkly.ts | 38 ++++++++++++++ packages/nextjs/src/index.types.ts | 7 +++ packages/node/src/index.ts | 8 +++ .../integrations/featureFlagShims/index.ts | 13 +++++ .../featureFlagShims/launchDarkly.ts | 37 ++++++++++++++ .../featureFlagShims/openFeature.ts | 49 +++++++++++++++++++ .../integrations/featureFlagShims/statsig.ts | 18 +++++++ .../integrations/featureFlagShims/unleash.ts | 18 +++++++ packages/nuxt/src/index.types.ts | 7 +++ packages/react-router/src/index.types.ts | 7 +++ packages/remix/src/index.types.ts | 7 +++ packages/solidstart/src/index.types.ts | 7 +++ packages/sveltekit/src/index.types.ts | 7 +++ .../tanstackstart-react/src/index.types.ts | 7 +++ 22 files changed, 284 insertions(+) create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/server-component/featureFlag/page.tsx create mode 100644 packages/integration-shims/src/launchDarkly.ts create mode 100644 packages/node/src/integrations/featureFlagShims/index.ts create mode 100644 packages/node/src/integrations/featureFlagShims/launchDarkly.ts create mode 100644 packages/node/src/integrations/featureFlagShims/openFeature.ts create mode 100644 packages/node/src/integrations/featureFlagShims/statsig.ts create mode 100644 packages/node/src/integrations/featureFlagShims/unleash.ts diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/server-component/featureFlag/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/server-component/featureFlag/page.tsx new file mode 100644 index 000000000000..3db71ae022ef --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/server-component/featureFlag/page.tsx @@ -0,0 +1,16 @@ +import * as Sentry from '@sentry/nextjs'; + +export const dynamic = 'force-dynamic'; + +export default async function FeatureFlagServerComponent() { + Sentry.buildLaunchDarklyFlagUsedHandler(); + Sentry.launchDarklyIntegration(); + Sentry.openFeatureIntegration(); + new Sentry.OpenFeatureIntegrationHook(); + // @ts-ignore - we just want to test that the statsigIntegration is imported + Sentry.statsigIntegration(); + // @ts-ignore - we just want to test that the unleashIntegration is imported + Sentry.unleashIntegration(); + + return
FeatureFlagServerComponent
; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/server-components.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/server-components.test.ts index 0a32972b0e6a..52f6ae13875a 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/server-components.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/server-components.test.ts @@ -133,3 +133,9 @@ test('Should capture an error and transaction for a app router page', async ({ p }), ); }); + +test('Should not throw error on server component when importing shimmed feature flag function', async ({ page }) => { + await page.goto('/server-component/featureFlag'); + // tests that none of the feature flag functions throw an error when imported in a node environment + await expect(page.locator('body')).toContainText('FeatureFlagServerComponent'); +}); diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index 83a135e71f21..90ea06e0ef07 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -138,6 +138,12 @@ export { NODE_VERSION, featureFlagsIntegration, type FeatureFlagsIntegration, + launchDarklyIntegration, + buildLaunchDarklyFlagUsedHandler, + openFeatureIntegration, + OpenFeatureIntegrationHook, + statsigIntegration, + unleashIntegration, } from '@sentry/node'; export { init } from './server/sdk'; diff --git a/packages/astro/src/index.types.ts b/packages/astro/src/index.types.ts index d74a885c2b37..ceb4fc6d8a51 100644 --- a/packages/astro/src/index.types.ts +++ b/packages/astro/src/index.types.ts @@ -29,4 +29,11 @@ export declare const Span: clientSdk.Span; export declare const logger: typeof clientSdk.logger | typeof serverSdk.logger; +export declare const launchDarklyIntegration: typeof clientSdk.launchDarklyIntegration; +export declare const buildLaunchDarklyFlagUsedHandler: typeof clientSdk.buildLaunchDarklyFlagUsedHandler; +export declare const openFeatureIntegration: typeof clientSdk.openFeatureIntegration; +export declare const OpenFeatureIntegrationHook: typeof clientSdk.OpenFeatureIntegrationHook; +export declare const statsigIntegration: typeof clientSdk.statsigIntegration; +export declare const unleashIntegration: typeof clientSdk.unleashIntegration; + export default sentryAstro; diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts index f64ee53dc47c..f7a5f77ac0fb 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -124,6 +124,12 @@ export { NODE_VERSION, featureFlagsIntegration, type FeatureFlagsIntegration, + launchDarklyIntegration, + buildLaunchDarklyFlagUsedHandler, + openFeatureIntegration, + OpenFeatureIntegrationHook, + statsigIntegration, + unleashIntegration, } from '@sentry/node'; export { diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index 4a9d7fd9d71c..38bd74fda8c2 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -142,6 +142,12 @@ export { createSentryWinstonTransport, wrapMcpServerWithSentry, featureFlagsIntegration, + launchDarklyIntegration, + buildLaunchDarklyFlagUsedHandler, + openFeatureIntegration, + OpenFeatureIntegrationHook, + statsigIntegration, + unleashIntegration, } from '@sentry/node'; export { diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts index f0bed369acee..14797a9af008 100644 --- a/packages/google-cloud-serverless/src/index.ts +++ b/packages/google-cloud-serverless/src/index.ts @@ -124,6 +124,12 @@ export { NODE_VERSION, featureFlagsIntegration, type FeatureFlagsIntegration, + launchDarklyIntegration, + buildLaunchDarklyFlagUsedHandler, + openFeatureIntegration, + OpenFeatureIntegrationHook, + statsigIntegration, + unleashIntegration, } from '@sentry/node'; export { diff --git a/packages/integration-shims/src/index.ts b/packages/integration-shims/src/index.ts index 510b26ddbb76..e887ac725023 100644 --- a/packages/integration-shims/src/index.ts +++ b/packages/integration-shims/src/index.ts @@ -1,3 +1,4 @@ export { feedbackIntegrationShim } from './Feedback'; export { replayIntegrationShim } from './Replay'; export { browserTracingIntegrationShim } from './BrowserTracing'; +export { launchDarklyIntegrationShim, buildLaunchDarklyFlagUsedHandlerShim } from './launchDarkly'; diff --git a/packages/integration-shims/src/launchDarkly.ts b/packages/integration-shims/src/launchDarkly.ts new file mode 100644 index 000000000000..76750f5c863c --- /dev/null +++ b/packages/integration-shims/src/launchDarkly.ts @@ -0,0 +1,38 @@ +import { consoleSandbox, defineIntegration, isBrowser } from '@sentry/core'; +import { FAKE_FUNCTION } from './common'; + +/** + * This is a shim for the LaunchDarkly integration. + * We need this in order to not throw runtime errors when accidentally importing this on the server through a meta framework like Next.js. + */ +export const launchDarklyIntegrationShim = defineIntegration((_options?: unknown) => { + if (!isBrowser()) { + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn('The launchDarklyIntegration() can only be used in the browser.'); + }); + } + + return { + name: 'LaunchDarkly', + }; +}); + +/** + * This is a shim for the LaunchDarkly flag used handler. + */ +export function buildLaunchDarklyFlagUsedHandlerShim(): unknown { + if (!isBrowser()) { + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn('The buildLaunchDarklyFlagUsedHandler() can only be used in the browser.'); + }); + } + + return { + name: 'sentry-flag-auditor', + type: 'flag-used', + synchronous: true, + method: FAKE_FUNCTION, + }; +} diff --git a/packages/nextjs/src/index.types.ts b/packages/nextjs/src/index.types.ts index 04b73ea4c83e..fe5a75bd5c8b 100644 --- a/packages/nextjs/src/index.types.ts +++ b/packages/nextjs/src/index.types.ts @@ -141,3 +141,10 @@ export declare function wrapApiHandlerWithSentryVercelCrons(WrappingTarget: C): C; export { captureRequestError } from './common/captureRequestError'; + +export declare const launchDarklyIntegration: typeof clientSdk.launchDarklyIntegration; +export declare const buildLaunchDarklyFlagUsedHandler: typeof clientSdk.buildLaunchDarklyFlagUsedHandler; +export declare const openFeatureIntegration: typeof clientSdk.openFeatureIntegration; +export declare const OpenFeatureIntegrationHook: typeof clientSdk.OpenFeatureIntegrationHook; +export declare const statsigIntegration: typeof clientSdk.statsigIntegration; +export declare const unleashIntegration: typeof clientSdk.unleashIntegration; diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 1c02da9fff2e..71970174721c 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -37,6 +37,14 @@ export { amqplibIntegration } from './integrations/tracing/amqplib'; export { vercelAIIntegration } from './integrations/tracing/vercelai'; export { childProcessIntegration } from './integrations/childProcess'; export { createSentryWinstonTransport } from './integrations/winston'; +export { + launchDarklyIntegration, + buildLaunchDarklyFlagUsedHandler, + openFeatureIntegration, + OpenFeatureIntegrationHook, + statsigIntegration, + unleashIntegration, +} from './integrations/featureFlagShims'; export { SentryContextManager } from './otel/contextManager'; export { generateInstrumentOnce } from './otel/instrument'; diff --git a/packages/node/src/integrations/featureFlagShims/index.ts b/packages/node/src/integrations/featureFlagShims/index.ts new file mode 100644 index 000000000000..230dbaeeb7e8 --- /dev/null +++ b/packages/node/src/integrations/featureFlagShims/index.ts @@ -0,0 +1,13 @@ +export { + launchDarklyIntegrationShim as launchDarklyIntegration, + buildLaunchDarklyFlagUsedHandlerShim as buildLaunchDarklyFlagUsedHandler, +} from './launchDarkly'; + +export { + openFeatureIntegrationShim as openFeatureIntegration, + OpenFeatureIntegrationHookShim as OpenFeatureIntegrationHook, +} from './openFeature'; + +export { statsigIntegrationShim as statsigIntegration } from './statsig'; + +export { unleashIntegrationShim as unleashIntegration } from './unleash'; diff --git a/packages/node/src/integrations/featureFlagShims/launchDarkly.ts b/packages/node/src/integrations/featureFlagShims/launchDarkly.ts new file mode 100644 index 000000000000..c525e2f366ac --- /dev/null +++ b/packages/node/src/integrations/featureFlagShims/launchDarkly.ts @@ -0,0 +1,37 @@ +import { consoleSandbox, defineIntegration, isBrowser } from '@sentry/core'; + +/** + * This is a shim for the LaunchDarkly integration. + * We need this in order to not throw runtime errors when accidentally importing this on the server through a meta framework like Next.js. + */ +export const launchDarklyIntegrationShim = defineIntegration((_options?: unknown) => { + if (!isBrowser()) { + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn('The launchDarklyIntegration() can only be used in the browser.'); + }); + } + + return { + name: 'LaunchDarkly', + }; +}); + +/** + * This is a shim for the LaunchDarkly flag used handler. + */ +export function buildLaunchDarklyFlagUsedHandlerShim(): unknown { + if (!isBrowser()) { + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn('The buildLaunchDarklyFlagUsedHandler() can only be used in the browser.'); + }); + } + + return { + name: 'sentry-flag-auditor', + type: 'flag-used', + synchronous: true, + method: () => null, + }; +} diff --git a/packages/node/src/integrations/featureFlagShims/openFeature.ts b/packages/node/src/integrations/featureFlagShims/openFeature.ts new file mode 100644 index 000000000000..d0768fb618de --- /dev/null +++ b/packages/node/src/integrations/featureFlagShims/openFeature.ts @@ -0,0 +1,49 @@ +import { consoleSandbox, defineIntegration, isBrowser } from '@sentry/core'; + +/** + * This is a shim for the OpenFeature integration. + * We need this in order to not throw runtime errors when accidentally importing this on the server through a meta framework like Next.js. + */ +export const openFeatureIntegrationShim = defineIntegration((_options?: unknown) => { + if (!isBrowser()) { + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn('The openFeatureIntegration() can only be used in the browser.'); + }); + } + + return { + name: 'OpenFeature', + }; +}); + +/** + * This is a shim for the OpenFeature integration hook. + */ +export class OpenFeatureIntegrationHookShim { + /** + * + */ + public constructor() { + if (!isBrowser()) { + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn('The OpenFeatureIntegrationHook can only be used in the browser.'); + }); + } + } + + /** + * + */ + public after(): void { + // No-op + } + + /** + * + */ + public error(): void { + // No-op + } +} diff --git a/packages/node/src/integrations/featureFlagShims/statsig.ts b/packages/node/src/integrations/featureFlagShims/statsig.ts new file mode 100644 index 000000000000..8a74170d2b1c --- /dev/null +++ b/packages/node/src/integrations/featureFlagShims/statsig.ts @@ -0,0 +1,18 @@ +import { consoleSandbox, defineIntegration, isBrowser } from '@sentry/core'; + +/** + * This is a shim for the Statsig integration. + * We need this in order to not throw runtime errors when accidentally importing this on the server through a meta framework like Next.js. + */ +export const statsigIntegrationShim = defineIntegration((_options?: unknown) => { + if (!isBrowser()) { + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn('The statsigIntegration() can only be used in the browser.'); + }); + } + + return { + name: 'Statsig', + }; +}); diff --git a/packages/node/src/integrations/featureFlagShims/unleash.ts b/packages/node/src/integrations/featureFlagShims/unleash.ts new file mode 100644 index 000000000000..748e63b71040 --- /dev/null +++ b/packages/node/src/integrations/featureFlagShims/unleash.ts @@ -0,0 +1,18 @@ +import { consoleSandbox, defineIntegration, isBrowser } from '@sentry/core'; + +/** + * This is a shim for the Unleash integration. + * We need this in order to not throw runtime errors when accidentally importing this on the server through a meta framework like Next.js. + */ +export const unleashIntegrationShim = defineIntegration((_options?: unknown) => { + if (!isBrowser()) { + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn('The unleashIntegration() can only be used in the browser.'); + }); + } + + return { + name: 'Unleash', + }; +}); diff --git a/packages/nuxt/src/index.types.ts b/packages/nuxt/src/index.types.ts index c6cdb01d280e..4f006e0b5b07 100644 --- a/packages/nuxt/src/index.types.ts +++ b/packages/nuxt/src/index.types.ts @@ -18,3 +18,10 @@ export declare const getDefaultIntegrations: (options: Options) => Integration[] export declare const defaultStackParser: StackParser; export declare const logger: typeof clientSdk.logger | typeof serverSdk.logger; + +export declare const launchDarklyIntegration: typeof clientSdk.launchDarklyIntegration; +export declare const buildLaunchDarklyFlagUsedHandler: typeof clientSdk.buildLaunchDarklyFlagUsedHandler; +export declare const openFeatureIntegration: typeof clientSdk.openFeatureIntegration; +export declare const OpenFeatureIntegrationHook: typeof clientSdk.OpenFeatureIntegrationHook; +export declare const statsigIntegration: typeof clientSdk.statsigIntegration; +export declare const unleashIntegration: typeof clientSdk.unleashIntegration; diff --git a/packages/react-router/src/index.types.ts b/packages/react-router/src/index.types.ts index 45f4fe10fa31..150fc45a1e63 100644 --- a/packages/react-router/src/index.types.ts +++ b/packages/react-router/src/index.types.ts @@ -18,3 +18,10 @@ export declare const defaultStackParser: StackParser; export declare const getDefaultIntegrations: (options: Options) => Integration[]; export declare const logger: typeof clientSdk.logger | typeof serverSdk.logger; + +export declare const launchDarklyIntegration: typeof clientSdk.launchDarklyIntegration; +export declare const buildLaunchDarklyFlagUsedHandler: typeof clientSdk.buildLaunchDarklyFlagUsedHandler; +export declare const openFeatureIntegration: typeof clientSdk.openFeatureIntegration; +export declare const OpenFeatureIntegrationHook: typeof clientSdk.OpenFeatureIntegrationHook; +export declare const statsigIntegration: typeof clientSdk.statsigIntegration; +export declare const unleashIntegration: typeof clientSdk.unleashIntegration; diff --git a/packages/remix/src/index.types.ts b/packages/remix/src/index.types.ts index 697fc3813045..d0df7397f612 100644 --- a/packages/remix/src/index.types.ts +++ b/packages/remix/src/index.types.ts @@ -32,3 +32,10 @@ declare const runtime: 'client' | 'server'; export const close = runtime === 'client' ? clientSdk.close : serverSdk.close; export const flush = runtime === 'client' ? clientSdk.flush : serverSdk.flush; export const lastEventId = runtime === 'client' ? clientSdk.lastEventId : serverSdk.lastEventId; + +export declare const launchDarklyIntegration: typeof clientSdk.launchDarklyIntegration; +export declare const buildLaunchDarklyFlagUsedHandler: typeof clientSdk.buildLaunchDarklyFlagUsedHandler; +export declare const openFeatureIntegration: typeof clientSdk.openFeatureIntegration; +export declare const OpenFeatureIntegrationHook: typeof clientSdk.OpenFeatureIntegrationHook; +export declare const statsigIntegration: typeof clientSdk.statsigIntegration; +export declare const unleashIntegration: typeof clientSdk.unleashIntegration; diff --git a/packages/solidstart/src/index.types.ts b/packages/solidstart/src/index.types.ts index e4cd974ed00e..7725d1ad3d3c 100644 --- a/packages/solidstart/src/index.types.ts +++ b/packages/solidstart/src/index.types.ts @@ -25,3 +25,10 @@ export declare function flush(timeout?: number | undefined): PromiseLike; export declare const logger: typeof clientSdk.logger | typeof serverSdk.logger; + +export declare const launchDarklyIntegration: typeof clientSdk.launchDarklyIntegration; +export declare const buildLaunchDarklyFlagUsedHandler: typeof clientSdk.buildLaunchDarklyFlagUsedHandler; +export declare const openFeatureIntegration: typeof clientSdk.openFeatureIntegration; +export declare const OpenFeatureIntegrationHook: typeof clientSdk.OpenFeatureIntegrationHook; +export declare const statsigIntegration: typeof clientSdk.statsigIntegration; +export declare const unleashIntegration: typeof clientSdk.unleashIntegration; diff --git a/packages/tanstackstart-react/src/index.types.ts b/packages/tanstackstart-react/src/index.types.ts index 85bbe9df63fd..448ea35f637b 100644 --- a/packages/tanstackstart-react/src/index.types.ts +++ b/packages/tanstackstart-react/src/index.types.ts @@ -28,3 +28,10 @@ export declare const showReportDialog: typeof clientSdk.showReportDialog; export declare const withErrorBoundary: typeof clientSdk.withErrorBoundary; export declare const logger: typeof clientSdk.logger | typeof serverSdk.logger; + +export declare const launchDarklyIntegration: typeof clientSdk.launchDarklyIntegration; +export declare const buildLaunchDarklyFlagUsedHandler: typeof clientSdk.buildLaunchDarklyFlagUsedHandler; +export declare const openFeatureIntegration: typeof clientSdk.openFeatureIntegration; +export declare const OpenFeatureIntegrationHook: typeof clientSdk.OpenFeatureIntegrationHook; +export declare const statsigIntegration: typeof clientSdk.statsigIntegration; +export declare const unleashIntegration: typeof clientSdk.unleashIntegration; From 8601855dd416a3d1a237af13cd16a78a6c1088e6 Mon Sep 17 00:00:00 2001 From: Sigrid Huemer <32902192+s1gr1d@users.noreply.github.com> Date: Wed, 2 Jul 2025 14:00:02 +0200 Subject: [PATCH 04/16] fix(nuxt): Add `@sentry/cloudflare` as optional peerDependency (#16782) In order to not have all users of the SDK auto-install the `@sentry/cloudflare` dependency, it's added as `peerDependency`. ref https://github.com/getsentry/sentry-javascript/pull/15597 --- docs/creating-a-new-sdk.md | 31 +++++++++++++++++++++++++++++++ packages/nuxt/package.json | 10 ++++++++-- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/docs/creating-a-new-sdk.md b/docs/creating-a-new-sdk.md index 549f224d5cc4..5ed201681d54 100644 --- a/docs/creating-a-new-sdk.md +++ b/docs/creating-a-new-sdk.md @@ -144,3 +144,34 @@ provide an abstraction layer of options that we expose on top of that. We generally want to support Node runtimes for the server. However, sometimes there may be alternate runtimes that may be supported, e.g. Cloudflare Workers or Vercel Edge Functions. We generally do not need to support these in an MVP, but may decide to support them later. + +#### Cloudflare Workers/Pages + +To add support for Cloudflare Workers or Pages in a specific SDK, you need to do the following: + +1. Add `@sentry/cloudflare` as an optional peer dependency to the `package.json` of the SDK. + This ensures that users who want to use the SDK with Cloudflare will install the necessary package, but it won't be a requirement for users on other platforms. + + ```json + "peerDependencies": { + "@sentry/cloudflare": ">=9.33.0" + }, + "peerDependenciesMeta": { + "@sentry/cloudflare": { + "optional": true + } + } + ``` + +2. Add `@sentry/cloudflare` to the `devDependencies` in the SDK's `package.json`. + This is necessary for local development and testing, allowing you to use the Cloudflare-specific APIs in the development environment. + + ```json + "devDependencies": { + "@sentry/cloudflare": "9.33.0", + } + ``` + +3. Add documentation to the [Cloudflare Frameworks docs](https://docs.sentry.io/platforms/javascript/guides/cloudflare/frameworks/) explaining how to set up the SDK for Cloudflare Workers/Pages. + This documentation should include instructions for users to add the `@sentry/cloudflare` package to their project. + You can then link from the framework-specific docs pages to the Cloudflare SDK docs page by adding an entry to "Next Steps" on the "Getting Started" and "Manual Setup" pages. diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index 47386983dfd7..47beff4a0884 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -43,12 +43,17 @@ "access": "public" }, "peerDependencies": { - "nuxt": ">=3.7.0 || 4.x" + "nuxt": ">=3.7.0 || 4.x", + "@sentry/cloudflare": ">=9.34.0" + }, + "peerDependenciesMeta": { + "@sentry/cloudflare": { + "optional": true + } }, "dependencies": { "@nuxt/kit": "^3.13.2", "@sentry/browser": "9.34.0", - "@sentry/cloudflare": "9.34.0", "@sentry/core": "9.34.0", "@sentry/node": "9.34.0", "@sentry/rollup-plugin": "^3.5.0", @@ -57,6 +62,7 @@ }, "devDependencies": { "@nuxt/module-builder": "^0.8.4", + "@sentry/cloudflare": "9.34.0", "nuxt": "^3.13.2" }, "scripts": { From 39137547f7032c9b711da378aa5e93e60746a502 Mon Sep 17 00:00:00 2001 From: Francesco Gringl-Novy Date: Wed, 2 Jul 2025 14:11:20 +0200 Subject: [PATCH 05/16] test: Avoid publishing old tarballs for E2E tests (#16784) I noticed that locally I sometimes have old tarballs lying around, which are all published for E2E tests. This is unnecessary, and can also make it a bit unreliable which version is used for tests (as we generally have the dependencies as `*` which could be anything). This changes it so we only publish the current version. --- dev-packages/e2e-tests/publish-packages.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/dev-packages/e2e-tests/publish-packages.ts b/dev-packages/e2e-tests/publish-packages.ts index d342470bd06d..5ade5b1d735c 100644 --- a/dev-packages/e2e-tests/publish-packages.ts +++ b/dev-packages/e2e-tests/publish-packages.ts @@ -1,15 +1,26 @@ import * as childProcess from 'child_process'; +import { readFileSync } from 'fs'; import * as glob from 'glob'; import * as path from 'path'; const repositoryRoot = path.resolve(__dirname, '../..'); +const version = (JSON.parse(readFileSync(path.join(__dirname, './package.json'), 'utf8')) as { version: string }) + .version; + // Get absolute paths of all the packages we want to publish to the fake registry -const packageTarballPaths = glob.sync('packages/*/sentry-*.tgz', { +// Only include the current versions, to avoid getting old tarballs published as well +const packageTarballPaths = glob.sync(`packages/*/sentry-*-${version}.tgz`, { cwd: repositoryRoot, absolute: true, }); +if (packageTarballPaths.length === 0) { + // eslint-disable-next-line no-console + console.log(`No packages to publish for version ${version}, did you run "yarn build:tarballs"?`); + process.exit(1); +} + // Publish built packages to the fake registry packageTarballPaths.forEach(tarballPath => { // eslint-disable-next-line no-console From d668714c52beb6c7745954ee469a468550c360fd Mon Sep 17 00:00:00 2001 From: Francesco Gringl-Novy Date: Wed, 2 Jul 2025 15:00:25 +0200 Subject: [PATCH 06/16] fix(node): Avoid using dynamic `require` for fastify integration (#16789) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Not sure why we even have this, but this somehow breaks cloudflare-pages E2E tests in some scenarios. It also seems simply unnecessary - or is there a reason we have this? 🤔 Extracted this out from https://github.com/getsentry/sentry-javascript/pull/16783 --- .../integrations/tracing/fastify/fastify-otel/index.js | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/node/src/integrations/tracing/fastify/fastify-otel/index.js b/packages/node/src/integrations/tracing/fastify/fastify-otel/index.js index d4f0638cb30a..334223d697a5 100644 --- a/packages/node/src/integrations/tracing/fastify/fastify-otel/index.js +++ b/packages/node/src/integrations/tracing/fastify/fastify-otel/index.js @@ -30,7 +30,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/* eslint-disable @typescript-eslint/explicit-member-accessibility */ /* eslint-disable jsdoc/require-jsdoc */ /* eslint-disable max-lines */ /* eslint-disable no-param-reassign */ @@ -44,6 +43,7 @@ import { ATTR_HTTP_ROUTE, ATTR_SERVICE_NAME, } from '@opentelemetry/semantic-conventions'; +import * as minimatch from 'minimatch'; // SENTRY VENDOR NOTE // Instead of using the package.json file, we hard code the package name and version here. @@ -97,18 +97,12 @@ export class FastifyOtelInstrumentation extends InstrumentationBase { throw new TypeError('ignorePaths must be a string or a function'); } - let globMatcher = null; + const globMatcher = minimatch.minimatch; this[kIgnorePaths] = routeOptions => { if (typeof ignorePaths === 'function') { return ignorePaths(routeOptions); } else { - // Using minimatch to match the path until path.matchesGlob is out of experimental - // path.matchesGlob uses minimatch internally - if (globMatcher == null) { - globMatcher = require('minimatch').minimatch; - } - return globMatcher(routeOptions.url, ignorePaths); } }; From 86d899483391ed0ec6df3f64d5225512feef5da4 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Wed, 2 Jul 2025 14:58:43 +0100 Subject: [PATCH 07/16] feat(node): Add `eventLoopBlockIntegration` (#16709) This PR: - Adds `eventLoopBlockIntegration` which uses `@sentry-internal/node-native-stacktrace` to detect and capture blocked event loops - Adds suite of integration tests to test all the functionality - Bundling - Unlike the existing ANR integration, we can't bundle the worker code into a base64 string because there is a native module and all threads need to load the same native module - The worker is loaded via `new Worker(new URL('./thread-blocked-watchdog.js', import.meta.url))` which will be bundled correctly by latest Vite and Webpack 5 - For other bundlers you can add an extra entry point pointing at `@sentry/node-native/thread-blocked-watchdog` which should output to`thread-blocked-watchdog.js` next to your bundle. ## Usage If you instrument your application via the Node.js `--import` flag, this instrumentation will be automatically applied to all worker threads. `instrument.mjs` ```javascript import * as Sentry from '@sentry/node'; import { eventLoopBlockIntegration } from '@sentry/node-native'; Sentry.init({ dsn: '__YOUR_DSN__', // Capture stack traces when the event loop is blocked for more than 500ms integrations: [eventLoopBlockIntegration({ threshold: 500, // defaults to 1000 ms maxEventsPerHour: 5, // defaults to 1 })], }); ``` `app.mjs` ```javascript import { Worker } from 'worker_threads'; const worker = new Worker(new URL('./worker.mjs', import.meta.url)); // This main thread will be monitored for blocked event loops ``` `worker.mjs` ```javascript // This worker thread will also be monitored for blocked event loops too setTimeout(() => { longWork(); }, 2_000); ``` Start your application: ```bash node --import instrument.mjs app.mjs ``` Blocked events are captured with stack traces for all threads: image --------- Co-authored-by: Abhijeet Prasad --- .../suites/thread-blocked-native/app-path.mjs | 23 ++ .../thread-blocked-native/basic-multiple.mjs | 23 ++ .../suites/thread-blocked-native/basic.js | 24 ++ .../suites/thread-blocked-native/basic.mjs | 24 ++ .../thread-blocked-native/indefinite.mjs | 27 ++ .../thread-blocked-native/instrument.mjs | 9 + .../suites/thread-blocked-native/long-work.js | 12 + .../should-exit-forced.js | 19 ++ .../thread-blocked-native/should-exit.js | 18 ++ .../suites/thread-blocked-native/test.ts | 200 ++++++++++++ .../thread-blocked-native/worker-block.mjs | 5 + .../thread-blocked-native/worker-main.mjs | 14 + dev-packages/node-integration-tests/test.txt | 213 ------------- .../node-integration-tests/utils/runner.ts | 9 + packages/core/src/types-hoist/exception.ts | 2 +- packages/core/src/types-hoist/thread.ts | 3 +- packages/node-native/README.md | 47 +++ packages/node-native/package.json | 9 + packages/node-native/rollup.npm.config.mjs | 2 +- packages/node-native/src/common.ts | 44 +++ .../src/event-loop-block-integration.ts | 167 ++++++++++ .../src/event-loop-block-watchdog.ts | 286 ++++++++++++++++++ packages/node-native/src/index.ts | 2 +- yarn.lock | 10 +- 24 files changed, 974 insertions(+), 218 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/thread-blocked-native/app-path.mjs create mode 100644 dev-packages/node-integration-tests/suites/thread-blocked-native/basic-multiple.mjs create mode 100644 dev-packages/node-integration-tests/suites/thread-blocked-native/basic.js create mode 100644 dev-packages/node-integration-tests/suites/thread-blocked-native/basic.mjs create mode 100644 dev-packages/node-integration-tests/suites/thread-blocked-native/indefinite.mjs create mode 100644 dev-packages/node-integration-tests/suites/thread-blocked-native/instrument.mjs create mode 100644 dev-packages/node-integration-tests/suites/thread-blocked-native/long-work.js create mode 100644 dev-packages/node-integration-tests/suites/thread-blocked-native/should-exit-forced.js create mode 100644 dev-packages/node-integration-tests/suites/thread-blocked-native/should-exit.js create mode 100644 dev-packages/node-integration-tests/suites/thread-blocked-native/test.ts create mode 100644 dev-packages/node-integration-tests/suites/thread-blocked-native/worker-block.mjs create mode 100644 dev-packages/node-integration-tests/suites/thread-blocked-native/worker-main.mjs delete mode 100644 dev-packages/node-integration-tests/test.txt create mode 100644 packages/node-native/src/common.ts create mode 100644 packages/node-native/src/event-loop-block-integration.ts create mode 100644 packages/node-native/src/event-loop-block-watchdog.ts diff --git a/dev-packages/node-integration-tests/suites/thread-blocked-native/app-path.mjs b/dev-packages/node-integration-tests/suites/thread-blocked-native/app-path.mjs new file mode 100644 index 000000000000..c561b221d95f --- /dev/null +++ b/dev-packages/node-integration-tests/suites/thread-blocked-native/app-path.mjs @@ -0,0 +1,23 @@ +import * as Sentry from '@sentry/node'; +import { eventLoopBlockIntegration } from '@sentry/node-native'; +import * as path from 'path'; +import * as url from 'url'; +import { longWork } from './long-work.js'; + +global._sentryDebugIds = { [new Error().stack]: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa' }; + +const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); + +setTimeout(() => { + process.exit(); +}, 10000); + +Sentry.init({ + dsn: process.env.SENTRY_DSN, + release: '1.0', + integrations: [eventLoopBlockIntegration({ appRootPath: __dirname })], +}); + +setTimeout(() => { + longWork(); +}, 1000); diff --git a/dev-packages/node-integration-tests/suites/thread-blocked-native/basic-multiple.mjs b/dev-packages/node-integration-tests/suites/thread-blocked-native/basic-multiple.mjs new file mode 100644 index 000000000000..32135d2246f2 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/thread-blocked-native/basic-multiple.mjs @@ -0,0 +1,23 @@ +import * as Sentry from '@sentry/node'; +import { eventLoopBlockIntegration } from '@sentry/node-native'; +import { longWork } from './long-work.js'; + +global._sentryDebugIds = { [new Error().stack]: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa' }; + +setTimeout(() => { + process.exit(); +}, 10000); + +Sentry.init({ + dsn: process.env.SENTRY_DSN, + release: '1.0', + integrations: [eventLoopBlockIntegration({ maxEventsPerHour: 2 })], +}); + +setTimeout(() => { + longWork(); +}, 1000); + +setTimeout(() => { + longWork(); +}, 4000); diff --git a/dev-packages/node-integration-tests/suites/thread-blocked-native/basic.js b/dev-packages/node-integration-tests/suites/thread-blocked-native/basic.js new file mode 100644 index 000000000000..30740bbd031b --- /dev/null +++ b/dev-packages/node-integration-tests/suites/thread-blocked-native/basic.js @@ -0,0 +1,24 @@ +const Sentry = require('@sentry/node'); +const { eventLoopBlockIntegration } = require('@sentry/node-native'); +const { longWork } = require('./long-work.js'); + +global._sentryDebugIds = { [new Error().stack]: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa' }; + +setTimeout(() => { + process.exit(); +}, 10000); + +Sentry.init({ + dsn: process.env.SENTRY_DSN, + release: '1.0', + integrations: [eventLoopBlockIntegration()], +}); + +setTimeout(() => { + longWork(); +}, 2000); + +// Ensure we only send one event even with multiple blocking events +setTimeout(() => { + longWork(); +}, 5000); diff --git a/dev-packages/node-integration-tests/suites/thread-blocked-native/basic.mjs b/dev-packages/node-integration-tests/suites/thread-blocked-native/basic.mjs new file mode 100644 index 000000000000..273760a6db39 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/thread-blocked-native/basic.mjs @@ -0,0 +1,24 @@ +import * as Sentry from '@sentry/node'; +import { eventLoopBlockIntegration } from '@sentry/node-native'; +import { longWork } from './long-work.js'; + +global._sentryDebugIds = { [new Error().stack]: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa' }; + +setTimeout(() => { + process.exit(); +}, 12000); + +Sentry.init({ + dsn: process.env.SENTRY_DSN, + release: '1.0', + integrations: [eventLoopBlockIntegration()], +}); + +setTimeout(() => { + longWork(); +}, 2000); + +// Ensure we only send one event even with multiple blocking events +setTimeout(() => { + longWork(); +}, 5000); diff --git a/dev-packages/node-integration-tests/suites/thread-blocked-native/indefinite.mjs b/dev-packages/node-integration-tests/suites/thread-blocked-native/indefinite.mjs new file mode 100644 index 000000000000..55eecb5c23ec --- /dev/null +++ b/dev-packages/node-integration-tests/suites/thread-blocked-native/indefinite.mjs @@ -0,0 +1,27 @@ +import * as Sentry from '@sentry/node'; +import { eventLoopBlockIntegration } from '@sentry/node-native'; +import * as assert from 'assert'; +import * as crypto from 'crypto'; + +setTimeout(() => { + process.exit(); +}, 10000); + +Sentry.init({ + dsn: process.env.SENTRY_DSN, + release: '1.0', + integrations: [eventLoopBlockIntegration()], +}); + +function longWork() { + // This loop will run almost indefinitely + for (let i = 0; i < 2000000000; i++) { + const salt = crypto.randomBytes(128).toString('base64'); + const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512'); + assert.ok(hash); + } +} + +setTimeout(() => { + longWork(); +}, 1000); diff --git a/dev-packages/node-integration-tests/suites/thread-blocked-native/instrument.mjs b/dev-packages/node-integration-tests/suites/thread-blocked-native/instrument.mjs new file mode 100644 index 000000000000..ee66bf82f8bf --- /dev/null +++ b/dev-packages/node-integration-tests/suites/thread-blocked-native/instrument.mjs @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/node'; +import { eventLoopBlockIntegration } from '@sentry/node-native'; + +Sentry.init({ + debug: true, + dsn: process.env.SENTRY_DSN, + release: '1.0', + integrations: [eventLoopBlockIntegration()], +}); diff --git a/dev-packages/node-integration-tests/suites/thread-blocked-native/long-work.js b/dev-packages/node-integration-tests/suites/thread-blocked-native/long-work.js new file mode 100644 index 000000000000..55f5358a10fe --- /dev/null +++ b/dev-packages/node-integration-tests/suites/thread-blocked-native/long-work.js @@ -0,0 +1,12 @@ +const crypto = require('crypto'); +const assert = require('assert'); + +function longWork() { + for (let i = 0; i < 200; i++) { + const salt = crypto.randomBytes(128).toString('base64'); + const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512'); + assert.ok(hash); + } +} + +exports.longWork = longWork; diff --git a/dev-packages/node-integration-tests/suites/thread-blocked-native/should-exit-forced.js b/dev-packages/node-integration-tests/suites/thread-blocked-native/should-exit-forced.js new file mode 100644 index 000000000000..71622bdbe083 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/thread-blocked-native/should-exit-forced.js @@ -0,0 +1,19 @@ +const Sentry = require('@sentry/node'); +const { eventLoopBlockIntegration } = require('@sentry/node-native'); + +function configureSentry() { + Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + debug: true, + integrations: [eventLoopBlockIntegration()], + }); +} + +async function main() { + configureSentry(); + await new Promise(resolve => setTimeout(resolve, 1000)); + process.exit(0); +} + +main(); diff --git a/dev-packages/node-integration-tests/suites/thread-blocked-native/should-exit.js b/dev-packages/node-integration-tests/suites/thread-blocked-native/should-exit.js new file mode 100644 index 000000000000..cda4c0e10d3a --- /dev/null +++ b/dev-packages/node-integration-tests/suites/thread-blocked-native/should-exit.js @@ -0,0 +1,18 @@ +const Sentry = require('@sentry/node'); +const { eventLoopBlockIntegration } = require('@sentry/node-native'); + +function configureSentry() { + Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + debug: true, + integrations: [eventLoopBlockIntegration()], + }); +} + +async function main() { + configureSentry(); + await new Promise(resolve => setTimeout(resolve, 1000)); +} + +main(); diff --git a/dev-packages/node-integration-tests/suites/thread-blocked-native/test.ts b/dev-packages/node-integration-tests/suites/thread-blocked-native/test.ts new file mode 100644 index 000000000000..6798882015f1 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/thread-blocked-native/test.ts @@ -0,0 +1,200 @@ +import { join } from 'node:path'; +import type { Event } from '@sentry/core'; +import { afterAll, describe, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../utils/runner'; + +function EXCEPTION(thread_id = '0') { + return { + values: [ + { + type: 'EventLoopBlocked', + value: 'Event Loop Blocked for at least 1000 ms', + mechanism: { type: 'ANR' }, + thread_id, + stacktrace: { + frames: expect.arrayContaining([ + expect.objectContaining({ + colno: expect.any(Number), + lineno: expect.any(Number), + filename: expect.any(String), + function: '?', + in_app: true, + }), + expect.objectContaining({ + colno: expect.any(Number), + lineno: expect.any(Number), + filename: expect.any(String), + function: 'longWork', + in_app: true, + }), + ]), + }, + }, + ], + }; +} + +const ANR_EVENT = { + // Ensure we have context + contexts: { + device: { + arch: expect.any(String), + }, + app: { + app_start_time: expect.any(String), + }, + os: { + name: expect.any(String), + }, + culture: { + timezone: expect.any(String), + }, + }, + threads: { + values: [ + { + id: '0', + name: 'main', + crashed: true, + current: true, + main: true, + }, + ], + }, + // and an exception that is our ANR + exception: EXCEPTION(), +}; + +function ANR_EVENT_WITH_DEBUG_META(file: string): Event { + return { + ...ANR_EVENT, + debug_meta: { + images: [ + { + type: 'sourcemap', + debug_id: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa', + code_file: expect.stringContaining(file), + }, + ], + }, + }; +} + +describe('Thread Blocked Native', { timeout: 30_000 }, () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + test('CJS', async () => { + await createRunner(__dirname, 'basic.js') + .withMockSentryServer() + .expect({ event: ANR_EVENT_WITH_DEBUG_META('basic') }) + .start() + .completed(); + }); + + test('ESM', async () => { + await createRunner(__dirname, 'basic.mjs') + .withMockSentryServer() + .expect({ event: ANR_EVENT_WITH_DEBUG_META('basic') }) + .start() + .completed(); + }); + + test('Custom appRootPath', async () => { + const ANR_EVENT_WITH_SPECIFIC_DEBUG_META: Event = { + ...ANR_EVENT, + debug_meta: { + images: [ + { + type: 'sourcemap', + debug_id: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa', + code_file: 'app:///app-path.mjs', + }, + ], + }, + }; + + await createRunner(__dirname, 'app-path.mjs') + .withMockSentryServer() + .expect({ event: ANR_EVENT_WITH_SPECIFIC_DEBUG_META }) + .start() + .completed(); + }); + + test('multiple events via maxEventsPerHour', async () => { + await createRunner(__dirname, 'basic-multiple.mjs') + .withMockSentryServer() + .expect({ event: ANR_EVENT_WITH_DEBUG_META('basic-multiple') }) + .expect({ event: ANR_EVENT_WITH_DEBUG_META('basic-multiple') }) + .start() + .completed(); + }); + + test('blocked indefinitely', async () => { + await createRunner(__dirname, 'indefinite.mjs') + .withMockSentryServer() + .expect({ event: ANR_EVENT }) + .start() + .completed(); + }); + + test('should exit', async () => { + const runner = createRunner(__dirname, 'should-exit.js').start(); + + await new Promise(resolve => setTimeout(resolve, 5_000)); + + expect(runner.childHasExited()).toBe(true); + }); + + test('should exit forced', async () => { + const runner = createRunner(__dirname, 'should-exit-forced.js').start(); + + await new Promise(resolve => setTimeout(resolve, 5_000)); + + expect(runner.childHasExited()).toBe(true); + }); + + test('worker thread', async () => { + const instrument = join(__dirname, 'instrument.mjs'); + await createRunner(__dirname, 'worker-main.mjs') + .withMockSentryServer() + .withFlags('--import', instrument) + .expect({ + event: event => { + const crashedThread = event.threads?.values?.find(thread => thread.crashed)?.id as string; + expect(crashedThread).toBeDefined(); + + expect(event).toMatchObject({ + ...ANR_EVENT, + exception: { + ...EXCEPTION(crashedThread), + }, + threads: { + values: [ + { + id: '0', + name: 'main', + crashed: false, + current: true, + main: true, + stacktrace: { + frames: expect.any(Array), + }, + }, + { + id: crashedThread, + name: `worker-${crashedThread}`, + crashed: true, + current: true, + main: false, + }, + ], + }, + }); + }, + }) + .start() + .completed(); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/thread-blocked-native/worker-block.mjs b/dev-packages/node-integration-tests/suites/thread-blocked-native/worker-block.mjs new file mode 100644 index 000000000000..274a4ce9e3a9 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/thread-blocked-native/worker-block.mjs @@ -0,0 +1,5 @@ +import { longWork } from './long-work.js'; + +setTimeout(() => { + longWork(); +}, 2000); diff --git a/dev-packages/node-integration-tests/suites/thread-blocked-native/worker-main.mjs b/dev-packages/node-integration-tests/suites/thread-blocked-native/worker-main.mjs new file mode 100644 index 000000000000..8591be4197e3 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/thread-blocked-native/worker-main.mjs @@ -0,0 +1,14 @@ +import { Worker } from 'node:worker_threads'; +import * as path from 'path'; +import * as url from 'url'; + +const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); + +const workerPath = path.join(__dirname, 'worker-block.mjs'); + +const thread = new Worker(workerPath, { stdout: 'inherit' }); +thread.unref(); + +setInterval(() => { + // This keeps the main thread alive to allow the worker to run indefinitely +}, 1000); diff --git a/dev-packages/node-integration-tests/test.txt b/dev-packages/node-integration-tests/test.txt deleted file mode 100644 index 0a0fa7f94de9..000000000000 --- a/dev-packages/node-integration-tests/test.txt +++ /dev/null @@ -1,213 +0,0 @@ -yarn run v1.22.22 -$ /Users/abhijeetprasad/workspace/sentry-javascript/node_modules/.bin/jest contextLines/memory-leak - console.log - starting scenario /Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts [ '-r', 'ts-node/register' ] undefined - - at log (utils/runner.ts:462:11) - - console.log - line COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad cwd DIR 1,16 608 107673020 /Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad txt REG 1,16 88074480 114479727 /Users/abhijeetprasad/.volta/tools/image/node/18.20.5/bin/node - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad 0u unix 0x6a083c8cc83ea8db 0t0 ->0xf2cacdd1d3a0ebec - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad 1u unix 0xd99cc422a76ba47f 0t0 ->0x542148981a0b9ef2 - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad 2u unix 0x97e70527ed5803f8 0t0 ->0xbafdaf00ef20de83 - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad 3u KQUEUE count=0, state=0 - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad 4 PIPE 0x271836c29e42bc67 16384 ->0x16ac23fcfd4fe1a3 - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad 5 PIPE 0x16ac23fcfd4fe1a3 16384 ->0x271836c29e42bc67 - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad 6 PIPE 0xd76fcd4ca2a35fcf 16384 ->0x30d26cd4f0e069b2 - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad 7 PIPE 0x30d26cd4f0e069b2 16384 ->0xd76fcd4ca2a35fcf - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad 8 PIPE 0x37691847717c3d6 16384 ->0x966eedd79d018252 - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad 9 PIPE 0x966eedd79d018252 16384 ->0x37691847717c3d6 - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad 10u KQUEUE count=0, state=0xa - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad 11 PIPE 0x99c1186f14b865be 16384 ->0xe88675eb1eefb2b - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad 12 PIPE 0xe88675eb1eefb2b 16384 ->0x99c1186f14b865be - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad 13 PIPE 0x52173210451cdda9 16384 ->0x50bbc31a0f1cc1af - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad 14 PIPE 0x50bbc31a0f1cc1af 16384 ->0x52173210451cdda9 - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad 15u KQUEUE count=0, state=0 - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad 16 PIPE 0xa115aa0653327e72 16384 ->0x100525c465ee1eb0 - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad 17 PIPE 0x100525c465ee1eb0 16384 ->0xa115aa0653327e72 - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad 18 PIPE 0x41945cf9fe740277 16384 ->0x8791d18eade5b1e0 - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad 19 PIPE 0x8791d18eade5b1e0 16384 ->0x41945cf9fe740277 - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad 20r CHR 3,2 0t0 333 /dev/null - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad 21u KQUEUE count=0, state=0xa - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad 22 PIPE 0xf4c6a2f47fb0bff5 16384 ->0xa00185e1c59cedbe - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad 23 PIPE 0xa00185e1c59cedbe 16384 ->0xf4c6a2f47fb0bff5 - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad 24 PIPE 0x4ac25a99f45f7ca4 16384 ->0x2032aef840c94700 - - at log (utils/runner.ts:462:11) - - console.log - line node 90932 abhijeetprasad 25 PIPE 0x2032aef840c94700 16384 ->0x4ac25a99f45f7ca4 - - at log (utils/runner.ts:462:11) - - console.log - line null - - at log (utils/runner.ts:462:11) - - console.log - line [{"sent_at":"2025-01-13T21:47:47.663Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"}},[[{"type":"session"},{"sid":"0ae9ef2ac2ba49dd92b6dab9d81444ac","init":true,"started":"2025-01-13T21:47:47.502Z","timestamp":"2025-01-13T21:47:47.663Z","status":"ok","errors":1,"duration":0.16146087646484375,"attrs":{"release":"1.0","environment":"production"}}]]] - - at log (utils/runner.ts:462:11) - - console.log - line [{"event_id":"2626269e3c634fc289338c441e76412c","sent_at":"2025-01-13T21:47:47.663Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 0","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"2626269e3c634fc289338c441e76412c","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"b1e1b8a0d410ef14"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.528,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ChildProcess","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]] - - at log (utils/runner.ts:462:11) - - console.log - line [{"event_id":"f58236bf0a7f4a999f7daf5283f0400f","sent_at":"2025-01-13T21:47:47.664Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 1","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"f58236bf0a7f4a999f7daf5283f0400f","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"9b6ccaf59536bcb4"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.531,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ChildProcess","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]] - - at log (utils/runner.ts:462:11) - - console.log - line [{"event_id":"d4d1b66dc41b44b98df2d2ff5d5370a2","sent_at":"2025-01-13T21:47:47.665Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 2","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"d4d1b66dc41b44b98df2d2ff5d5370a2","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"82d56f443d3f01f9"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.532,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ChildProcess","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]] - - at log (utils/runner.ts:462:11) - - console.log - line [{"event_id":"293d7c8c731c48eca30735b41efd40ba","sent_at":"2025-01-13T21:47:47.665Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 3","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"293d7c8c731c48eca30735b41efd40ba","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"8be46494d3555ddb"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.533,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ChildProcess","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]] - - at log (utils/runner.ts:462:11) - - console.log - line [{"event_id":"e9273b56624d4261b00f5431852da167","sent_at":"2025-01-13T21:47:47.666Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 4","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"e9273b56624d4261b00f5431852da167","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"9a067a8906c8c147"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.533,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ChildProcess","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]] - - at log (utils/runner.ts:462:11) - - console.log - line [{"event_id":"cf92173285aa49b8bdb3fe31a5de6c90","sent_at":"2025-01-13T21:47:47.667Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 5","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"cf92173285aa49b8bdb3fe31a5de6c90","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"ac2ad9041812f9d9"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.534,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ChildProcess","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]] - - at log (utils/runner.ts:462:11) - - console.log - line [{"event_id":"65224267e02049daadbc577de86960f3","sent_at":"2025-01-13T21:47:47.667Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 6","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"65224267e02049daadbc577de86960f3","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"b12818330e05cd2f"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.535,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ChildProcess","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]] - - at log (utils/runner.ts:462:11) - - console.log - line [{"event_id":"b9e96b480e1a4e74a2ecebde9f0400a9","sent_at":"2025-01-13T21:47:47.668Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 7","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"b9e96b480e1a4e74a2ecebde9f0400a9","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"83cb86896d96bbf6"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.536,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ChildProcess","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]] - - at log (utils/runner.ts:462:11) - - console.log - line [{"event_id":"c541f2c0a31345b78f93f69ffe5e0fc6","sent_at":"2025-01-13T21:47:47.668Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 8","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"c541f2c0a31345b78f93f69ffe5e0fc6","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"a0e8e199fcf05714"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.536,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ChildProcess","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]] - - at log (utils/runner.ts:462:11) - - console.log - line [{"event_id":"dc08b3fe26e94759817c7b5e95469727","sent_at":"2025-01-13T21:47:47.669Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 9","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"dc08b3fe26e94759817c7b5e95469727","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"8ec7d145c5362df0"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270106624},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.537,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ChildProcess","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]] - - at log (utils/runner.ts:462:11) - -Done in 4.21s. diff --git a/dev-packages/node-integration-tests/utils/runner.ts b/dev-packages/node-integration-tests/utils/runner.ts index 97b1efa2dbb4..1006d71bf3f0 100644 --- a/dev-packages/node-integration-tests/utils/runner.ts +++ b/dev-packages/node-integration-tests/utils/runner.ts @@ -15,6 +15,7 @@ import { normalize } from '@sentry/core'; import { execSync, spawn, spawnSync } from 'child_process'; import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'fs'; import { join } from 'path'; +import { inspect } from 'util'; import { afterAll, beforeAll, describe, test } from 'vitest'; import { assertEnvelopeHeader, @@ -338,6 +339,8 @@ export function createRunner(...paths: string[]) { } function newEnvelope(envelope: Envelope): void { + if (process.env.DEBUG) log('newEnvelope', inspect(envelope, false, null, true)); + for (const item of envelope[1]) { const envelopeItemType = item[0].type; @@ -449,6 +452,12 @@ export function createRunner(...paths: string[]) { child = spawn('node', [...flags, testPath], { env }); + child.on('error', e => { + // eslint-disable-next-line no-console + console.error('Error starting child process:', e); + complete(e); + }); + CLEANUP_STEPS.add(() => { child?.kill(); }); diff --git a/packages/core/src/types-hoist/exception.ts b/packages/core/src/types-hoist/exception.ts index a74adf6c1603..27b320363a82 100644 --- a/packages/core/src/types-hoist/exception.ts +++ b/packages/core/src/types-hoist/exception.ts @@ -7,6 +7,6 @@ export interface Exception { value?: string; mechanism?: Mechanism; module?: string; - thread_id?: number; + thread_id?: number | string; stacktrace?: Stacktrace; } diff --git a/packages/core/src/types-hoist/thread.ts b/packages/core/src/types-hoist/thread.ts index 1cfad253a299..76f5592ef401 100644 --- a/packages/core/src/types-hoist/thread.ts +++ b/packages/core/src/types-hoist/thread.ts @@ -2,8 +2,9 @@ import type { Stacktrace } from './stacktrace'; /** JSDoc */ export interface Thread { - id?: number; + id?: number | string; name?: string; + main?: boolean; stacktrace?: Stacktrace; crashed?: boolean; current?: boolean; diff --git a/packages/node-native/README.md b/packages/node-native/README.md index 00779a75666e..4ff7b6fdab45 100644 --- a/packages/node-native/README.md +++ b/packages/node-native/README.md @@ -19,3 +19,50 @@ yarn add @sentry/node @sentry/node-native # Using npm npm install --save @sentry/node @sentry/node-native ``` + +## `eventLoopBlockIntegration` + +The `eventLoopBlockIntegration` can be used to monitor for blocked event loops in +all threads of a Node.js application. + +If you instrument your application via the Node.js `--import` flag, Sentry will +be started and this instrumentation will be automatically applied to all worker +threads. + +`instrument.mjs` + +```javascript +import * as Sentry from '@sentry/node'; +import { eventLoopBlockIntegration } from '@sentry/node-native'; + +Sentry.init({ + dsn: '__YOUR_DSN__', + // Capture stack traces when the event loop is blocked for more than 500ms + integrations: [eventLoopBlockIntegration({ threshold: 500 })], +}); +``` + +`app.mjs` + +```javascript +import { Worker } from 'worker_threads'; + +const worker = new Worker(new URL('./worker.mjs', import.meta.url)); + +// This main thread will be monitored for blocked event loops +``` + +`worker.mjs` + +```javascript +// This worker thread will also be monitored for blocked event loops too +``` + +Start your application: + +```bash +node --import instrument.mjs app.mjs +``` + +If a thread is blocked for more than the configured threshold, stack traces will +be captured for all threads and sent to Sentry. diff --git a/packages/node-native/package.json b/packages/node-native/package.json index 79788f1b6c65..bfa03ea947c2 100644 --- a/packages/node-native/package.json +++ b/packages/node-native/package.json @@ -20,6 +20,14 @@ "types": "./build/types/index.d.ts", "default": "./build/cjs/index.js" } + }, + "./event-loop-block-watchdog": { + "import": { + "default": "./build/esm/event-loop-block-watchdog.js" + }, + "require": { + "default": "./build/cjs/event-loop-block-watchdog.js" + } } }, "typesVersions": { @@ -55,6 +63,7 @@ "build:tarball": "npm pack" }, "dependencies": { + "@sentry-internal/node-native-stacktrace": "^0.1.0", "@sentry/core": "9.34.0", "@sentry/node": "9.34.0" }, diff --git a/packages/node-native/rollup.npm.config.mjs b/packages/node-native/rollup.npm.config.mjs index b58b8e8ac027..ce79b0ac9bbb 100644 --- a/packages/node-native/rollup.npm.config.mjs +++ b/packages/node-native/rollup.npm.config.mjs @@ -2,7 +2,7 @@ import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollu export default makeNPMConfigVariants( makeBaseNPMConfig({ - entrypoints: ['src/index.ts'], + entrypoints: ['src/index.ts', 'src/event-loop-block-watchdog.ts'], packageSpecificConfig: { output: { dir: 'build', diff --git a/packages/node-native/src/common.ts b/packages/node-native/src/common.ts new file mode 100644 index 000000000000..2a96050dbc34 --- /dev/null +++ b/packages/node-native/src/common.ts @@ -0,0 +1,44 @@ +import type { Contexts, DsnComponents, Primitive, SdkMetadata, Session } from '@sentry/core'; + +export const POLL_RATIO = 2; + +export interface ThreadBlockedIntegrationOptions { + /** + * Threshold in milliseconds to trigger an event. + * + * Defaults to 1000ms. + */ + threshold: number; + /** + * Maximum number of blocked events to send per clock hour. + * + * Defaults to 1. + */ + maxEventsPerHour: number; + /** + * Tags to include with blocked events. + */ + staticTags: { [key: string]: Primitive }; + /** + * @ignore Internal use only. + * + * If this is supplied, stack frame filenames will be rewritten to be relative to this path. + */ + appRootPath: string | undefined; +} + +export interface WorkerStartData extends ThreadBlockedIntegrationOptions { + debug: boolean; + sdkMetadata: SdkMetadata; + dsn: DsnComponents; + tunnel: string | undefined; + release: string | undefined; + environment: string; + dist: string | undefined; + contexts: Contexts; +} + +export interface ThreadState { + session: Session | undefined; + debugImages: Record; +} diff --git a/packages/node-native/src/event-loop-block-integration.ts b/packages/node-native/src/event-loop-block-integration.ts new file mode 100644 index 000000000000..6b643e944adf --- /dev/null +++ b/packages/node-native/src/event-loop-block-integration.ts @@ -0,0 +1,167 @@ +import { Worker } from 'node:worker_threads'; +import type { Contexts, Event, EventHint, IntegrationFn } from '@sentry/core'; +import { defineIntegration, getFilenameToDebugIdMap, getIsolationScope, logger } from '@sentry/core'; +import type { NodeClient } from '@sentry/node'; +import { registerThread, threadPoll } from '@sentry-internal/node-native-stacktrace'; +import type { ThreadBlockedIntegrationOptions, WorkerStartData } from './common'; +import { POLL_RATIO } from './common'; + +const DEFAULT_THRESHOLD_MS = 1_000; + +function log(message: string, ...args: unknown[]): void { + logger.log(`[Sentry Block Event Loop] ${message}`, ...args); +} + +/** + * Gets contexts by calling all event processors. This shouldn't be called until all integrations are setup + */ +async function getContexts(client: NodeClient): Promise { + let event: Event | null = { message: INTEGRATION_NAME }; + const eventHint: EventHint = {}; + + for (const processor of client.getEventProcessors()) { + if (event === null) break; + event = await processor(event, eventHint); + } + + return event?.contexts || {}; +} + +const INTEGRATION_NAME = 'ThreadBlocked'; + +const _eventLoopBlockIntegration = ((options: Partial = {}) => { + return { + name: INTEGRATION_NAME, + afterAllSetup(client: NodeClient) { + registerThread(); + _startWorker(client, options).catch(err => { + log('Failed to start event loop block worker', err); + }); + }, + }; +}) satisfies IntegrationFn; + +/** + * Monitors the Node.js event loop for blocking behavior and reports blocked events to Sentry. + * + * Uses a background worker thread to detect when the main thread is blocked for longer than + * the configured threshold (default: 1 second). + * + * When instrumenting via the `--import` flag, this integration will + * automatically monitor all worker threads as well. + * + * ```js + * // instrument.mjs + * import * as Sentry from '@sentry/node'; + * import { eventLoopBlockIntegration } from '@sentry/node-native'; + * + * Sentry.init({ + * dsn: '__YOUR_DSN__', + * integrations: [ + * eventLoopBlockIntegration({ + * threshold: 500, // Report blocks longer than 500ms + * }), + * ], + * }); + * ``` + * + * Start your application with: + * ```bash + * node --import instrument.mjs app.mjs + * ``` + */ +export const eventLoopBlockIntegration = defineIntegration(_eventLoopBlockIntegration); + +/** + * Starts the worker thread + * + * @returns A function to stop the worker + */ +async function _startWorker( + client: NodeClient, + integrationOptions: Partial, +): Promise<() => void> { + const dsn = client.getDsn(); + + if (!dsn) { + return () => { + // + }; + } + + const contexts = await getContexts(client); + + // These will not be accurate if sent later from the worker thread + delete contexts.app?.app_memory; + delete contexts.device?.free_memory; + + const initOptions = client.getOptions(); + + const sdkMetadata = client.getSdkMetadata() || {}; + if (sdkMetadata.sdk) { + sdkMetadata.sdk.integrations = initOptions.integrations.map(i => i.name); + } + + const options: WorkerStartData = { + debug: logger.isEnabled(), + dsn, + tunnel: initOptions.tunnel, + environment: initOptions.environment || 'production', + release: initOptions.release, + dist: initOptions.dist, + sdkMetadata, + appRootPath: integrationOptions.appRootPath, + threshold: integrationOptions.threshold || DEFAULT_THRESHOLD_MS, + maxEventsPerHour: integrationOptions.maxEventsPerHour || 1, + staticTags: integrationOptions.staticTags || {}, + contexts, + }; + + const pollInterval = options.threshold / POLL_RATIO; + + const worker = new Worker(new URL('./event-loop-block-watchdog.js', import.meta.url), { + workerData: options, + // We don't want any Node args like --import to be passed to the worker + execArgv: [], + env: { ...process.env, NODE_OPTIONS: undefined }, + }); + + process.on('exit', () => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + worker.terminate(); + }); + + const timer = setInterval(() => { + try { + const currentSession = getIsolationScope().getSession(); + // We need to copy the session object and remove the toJSON method so it can be sent to the worker + // serialized without making it a SerializedSession + const session = currentSession ? { ...currentSession, toJSON: undefined } : undefined; + // message the worker to tell it the main event loop is still running + threadPoll({ session, debugImages: getFilenameToDebugIdMap(initOptions.stackParser) }); + } catch (_) { + // + } + }, pollInterval); + // Timer should not block exit + timer.unref(); + + worker.once('error', (err: Error) => { + clearInterval(timer); + log('watchdog worker error', err); + }); + + worker.once('exit', (code: number) => { + clearInterval(timer); + log('watchdog worker exit', code); + }); + + // Ensure this thread can't block app exit + worker.unref(); + + return () => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + worker.terminate(); + clearInterval(timer); + }; +} diff --git a/packages/node-native/src/event-loop-block-watchdog.ts b/packages/node-native/src/event-loop-block-watchdog.ts new file mode 100644 index 000000000000..8909c00d1ea7 --- /dev/null +++ b/packages/node-native/src/event-loop-block-watchdog.ts @@ -0,0 +1,286 @@ +import { workerData } from 'node:worker_threads'; +import type { DebugImage, Event, Session, StackFrame, Thread } from '@sentry/core'; +import { + createEventEnvelope, + createSessionEnvelope, + filenameIsInApp, + getEnvelopeEndpointWithUrlEncodedAuth, + makeSession, + normalizeUrlToBase, + stripSentryFramesAndReverse, + updateSession, + uuid4, +} from '@sentry/core'; +import { makeNodeTransport } from '@sentry/node'; +import { captureStackTrace, getThreadsLastSeen } from '@sentry-internal/node-native-stacktrace'; +import type { ThreadState, WorkerStartData } from './common'; +import { POLL_RATIO } from './common'; + +const { + threshold, + appRootPath, + contexts, + debug, + dist, + dsn, + environment, + maxEventsPerHour, + release, + sdkMetadata, + staticTags: tags, + tunnel, +} = workerData as WorkerStartData; + +const pollInterval = threshold / POLL_RATIO; +const triggeredThreads = new Set(); + +function log(...msg: unknown[]): void { + if (debug) { + // eslint-disable-next-line no-console + console.log('[Sentry Block Event Loop Watchdog]', ...msg); + } +} + +function createRateLimiter(maxEventsPerHour: number): () => boolean { + let currentHour = 0; + let currentCount = 0; + + return function isRateLimited(): boolean { + const hour = new Date().getHours(); + + if (hour !== currentHour) { + currentHour = hour; + currentCount = 0; + } + + if (currentCount >= maxEventsPerHour) { + if (currentCount === maxEventsPerHour) { + currentCount += 1; + log(`Rate limit reached: ${currentCount} events in this hour`); + } + return true; + } + + currentCount += 1; + return false; + }; +} + +const url = getEnvelopeEndpointWithUrlEncodedAuth(dsn, tunnel, sdkMetadata.sdk); +const transport = makeNodeTransport({ + url, + recordDroppedEvent: () => { + // + }, +}); +const isRateLimited = createRateLimiter(maxEventsPerHour); + +async function sendAbnormalSession(serializedSession: Session | undefined): Promise { + if (!serializedSession) { + return; + } + + log('Sending abnormal session'); + const session = makeSession(serializedSession); + + updateSession(session, { + status: 'abnormal', + abnormal_mechanism: 'anr_foreground', + release, + environment, + }); + + const envelope = createSessionEnvelope(session, dsn, sdkMetadata, tunnel); + // Log the envelope so to aid in testing + log(JSON.stringify(envelope)); + + await transport.send(envelope); +} + +log('Started'); + +function prepareStackFrames(stackFrames: StackFrame[] | undefined): StackFrame[] | undefined { + if (!stackFrames) { + return undefined; + } + + // Strip Sentry frames and reverse the stack frames so they are in the correct order + const strippedFrames = stripSentryFramesAndReverse(stackFrames); + + for (const frame of strippedFrames) { + if (!frame.filename) { + continue; + } + + frame.in_app = filenameIsInApp(frame.filename); + + // If we have an app root path, rewrite the filenames to be relative to the app root + if (appRootPath) { + frame.filename = normalizeUrlToBase(frame.filename, appRootPath); + } + } + + return strippedFrames; +} + +function stripFileProtocol(filename: string | undefined): string | undefined { + if (!filename) { + return undefined; + } + return filename.replace(/^file:\/\//, ''); +} + +// eslint-disable-next-line complexity +function applyDebugMeta(event: Event, debugImages: Record): void { + if (Object.keys(debugImages).length === 0) { + return; + } + + const normalisedDebugImages = appRootPath ? {} : debugImages; + if (appRootPath) { + for (const [path, debugId] of Object.entries(debugImages)) { + normalisedDebugImages[normalizeUrlToBase(path, appRootPath)] = debugId; + } + } + + const filenameToDebugId = new Map(); + + for (const exception of event.exception?.values || []) { + for (const frame of exception.stacktrace?.frames || []) { + const filename = stripFileProtocol(frame.abs_path || frame.filename); + if (filename && normalisedDebugImages[filename]) { + filenameToDebugId.set(filename, normalisedDebugImages[filename] as string); + } + } + } + + for (const thread of event.threads?.values || []) { + for (const frame of thread.stacktrace?.frames || []) { + const filename = stripFileProtocol(frame.abs_path || frame.filename); + if (filename && normalisedDebugImages[filename]) { + filenameToDebugId.set(filename, normalisedDebugImages[filename] as string); + } + } + } + + if (filenameToDebugId.size > 0) { + const images: DebugImage[] = []; + for (const [code_file, debug_id] of filenameToDebugId.entries()) { + images.push({ + type: 'sourcemap', + code_file, + debug_id, + }); + } + event.debug_meta = { images }; + } +} + +function getExceptionAndThreads( + crashedThreadId: string, + threads: ReturnType>, +): Event { + const crashedThread = threads[crashedThreadId]; + + return { + exception: { + values: [ + { + type: 'EventLoopBlocked', + value: `Event Loop Blocked for at least ${threshold} ms`, + stacktrace: { frames: prepareStackFrames(crashedThread?.frames) }, + // This ensures the UI doesn't say 'Crashed in' for the stack trace + mechanism: { type: 'ANR' }, + thread_id: crashedThreadId, + }, + ], + }, + threads: { + values: Object.entries(threads).map(([threadId, threadState]) => { + const crashed = threadId === crashedThreadId; + + const thread: Thread = { + id: threadId, + name: threadId === '0' ? 'main' : `worker-${threadId}`, + crashed, + current: true, + main: threadId === '0', + }; + + if (!crashed) { + thread.stacktrace = { frames: prepareStackFrames(threadState.frames) }; + } + + return thread; + }), + }, + }; +} + +async function sendBlockEvent(crashedThreadId: string): Promise { + if (isRateLimited()) { + return; + } + + const threads = captureStackTrace(); + const crashedThread = threads[crashedThreadId]; + + if (!crashedThread) { + log(`No thread found with ID '${crashedThreadId}'`); + return; + } + + try { + await sendAbnormalSession(crashedThread.state?.session); + } catch (error) { + log(`Failed to send abnormal session for thread '${crashedThreadId}':`, error); + } + + log('Sending event'); + + const event: Event = { + event_id: uuid4(), + contexts, + release, + environment, + dist, + platform: 'node', + level: 'error', + tags, + ...getExceptionAndThreads(crashedThreadId, threads), + }; + + const allDebugImages: Record = Object.values(threads).reduce((acc, threadState) => { + return { ...acc, ...threadState.state?.debugImages }; + }, {}); + + applyDebugMeta(event, allDebugImages); + + const envelope = createEventEnvelope(event, dsn, sdkMetadata, tunnel); + // Log the envelope to aid in testing + log(JSON.stringify(envelope)); + + await transport.send(envelope); + await transport.flush(2000); +} + +setInterval(async () => { + for (const [threadId, time] of Object.entries(getThreadsLastSeen())) { + if (time > threshold) { + if (triggeredThreads.has(threadId)) { + continue; + } + + log(`Blocked thread detected '${threadId}' last polled ${time} ms ago.`); + triggeredThreads.add(threadId); + + try { + await sendBlockEvent(threadId); + } catch (error) { + log(`Failed to send event for thread '${threadId}':`, error); + } + } else { + triggeredThreads.delete(threadId); + } + } +}, pollInterval); diff --git a/packages/node-native/src/index.ts b/packages/node-native/src/index.ts index cb0ff5c3b541..454be4eb8ad2 100644 --- a/packages/node-native/src/index.ts +++ b/packages/node-native/src/index.ts @@ -1 +1 @@ -export {}; +export { eventLoopBlockIntegration } from './event-loop-block-integration'; diff --git a/yarn.lock b/yarn.lock index a44f353e348e..7575b784e07f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6473,6 +6473,14 @@ detect-libc "^2.0.3" node-abi "^3.73.0" +"@sentry-internal/node-native-stacktrace@^0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/node-native-stacktrace/-/node-native-stacktrace-0.1.0.tgz#fa0eaf1e66245f463ca2294ff63da74c56d1a052" + integrity sha512-dWkxhDdjcRdEOTk1acrdBledqIroaYJrOSbecx5tJ/m9DiWZ1Oa4eNi/sI2SHLT+hKmsBBxrychf6+Iitz5Bzw== + dependencies: + detect-libc "^2.0.4" + node-abi "^3.73.0" + "@sentry-internal/rrdom@2.34.0": version "2.34.0" resolved "https://registry.yarnpkg.com/@sentry-internal/rrdom/-/rrdom-2.34.0.tgz#fccc9fe211c3995d4200abafbe8d75b671961ee9" @@ -13334,7 +13342,7 @@ detect-libc@^1.0.3: resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg== -detect-libc@^2.0.0, detect-libc@^2.0.2, detect-libc@^2.0.3: +detect-libc@^2.0.0, detect-libc@^2.0.2, detect-libc@^2.0.3, detect-libc@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.4.tgz#f04715b8ba815e53b4d8109655b6508a6865a7e8" integrity sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA== From 04ecbc3f1d109d85548e7370c6f2277028b9dc0e Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Wed, 2 Jul 2025 15:51:07 -0400 Subject: [PATCH 08/16] feat(node): Update vercel ai integration attributes (#16721) Make changes to the Vercel AI integration as per https://www.notion.so/sentry/Agent-Monitoring-SDK-differences-21c8b10e4b5d80bcab51f72ae1418ea8 AI summary: ### Key Improvements: 1. **Eliminated duplicate attributes** - Tool call and prompt attributes are now renamed instead of duplicated 2. **Added tool input/output support** - New `gen_ai.tool.input` and `gen_ai.tool.output` attributes 3. **Auto-sets tool type** - Automatically adds `gen_ai.tool.type: 'function'` for tool calls 4. **Better OpenTelemetry compliance** - Cleaner attribute mapping following semantic conventions ### Technical Changes: - Uses `renameAttributeKey()` consistently instead of duplicating attributes - Removes old `ai.*` attributes after creating new `gen_ai.*` ones --- .../suites/tracing/vercelai/test.ts | 10 ++++------ packages/core/src/utils/vercel-ai.ts | 20 +++++++++++++++---- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts b/dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts index a10d602ee93a..3566d40322de 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts @@ -182,10 +182,9 @@ describe('Vercel AI integration', () => { expect.objectContaining({ data: { 'ai.operationId': 'ai.toolCall', - 'ai.toolCall.id': 'call-1', - 'ai.toolCall.name': 'getWeather', 'gen_ai.tool.call.id': 'call-1', 'gen_ai.tool.name': 'getWeather', + 'gen_ai.tool.type': 'function', 'operation.name': 'ai.toolCall', 'sentry.op': 'gen_ai.execute_tool', 'sentry.origin': 'auto.vercelai.otel', @@ -389,12 +388,11 @@ describe('Vercel AI integration', () => { expect.objectContaining({ data: { 'ai.operationId': 'ai.toolCall', - 'ai.toolCall.args': expect.any(String), - 'ai.toolCall.id': 'call-1', - 'ai.toolCall.name': 'getWeather', - 'ai.toolCall.result': expect.any(String), 'gen_ai.tool.call.id': 'call-1', 'gen_ai.tool.name': 'getWeather', + 'gen_ai.tool.input': expect.any(String), + 'gen_ai.tool.output': expect.any(String), + 'gen_ai.tool.type': 'function', 'operation.name': 'ai.toolCall', 'sentry.op': 'gen_ai.execute_tool', 'sentry.origin': 'auto.vercelai.otel', diff --git a/packages/core/src/utils/vercel-ai.ts b/packages/core/src/utils/vercel-ai.ts index 2a653addd805..401c295c97c9 100644 --- a/packages/core/src/utils/vercel-ai.ts +++ b/packages/core/src/utils/vercel-ai.ts @@ -12,8 +12,10 @@ import { AI_RESPONSE_TEXT_ATTRIBUTE, AI_RESPONSE_TOOL_CALLS_ATTRIBUTE, AI_TELEMETRY_FUNCTION_ID_ATTRIBUTE, + AI_TOOL_CALL_ARGS_ATTRIBUTE, AI_TOOL_CALL_ID_ATTRIBUTE, AI_TOOL_CALL_NAME_ATTRIBUTE, + AI_TOOL_CALL_RESULT_ATTRIBUTE, AI_USAGE_COMPLETION_TOKENS_ATTRIBUTE, AI_USAGE_PROMPT_TOKENS_ATTRIBUTE, GEN_AI_RESPONSE_MODEL_ATTRIBUTE, @@ -94,6 +96,9 @@ function processEndedVercelAiSpan(span: SpanJSON): void { renameAttributeKey(attributes, AI_RESPONSE_TEXT_ATTRIBUTE, 'gen_ai.response.text'); renameAttributeKey(attributes, AI_RESPONSE_TOOL_CALLS_ATTRIBUTE, 'gen_ai.response.tool_calls'); renameAttributeKey(attributes, AI_PROMPT_TOOLS_ATTRIBUTE, 'gen_ai.request.available_tools'); + + renameAttributeKey(attributes, AI_TOOL_CALL_ARGS_ATTRIBUTE, 'gen_ai.tool.input'); + renameAttributeKey(attributes, AI_TOOL_CALL_RESULT_ATTRIBUTE, 'gen_ai.tool.output'); } /** @@ -111,9 +116,16 @@ function renameAttributeKey(attributes: Record, oldKey: string, function processToolCallSpan(span: Span, attributes: SpanAttributes): void { addOriginToSpan(span, 'auto.vercelai.otel'); span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'gen_ai.execute_tool'); - span.setAttribute('gen_ai.tool.call.id', attributes[AI_TOOL_CALL_ID_ATTRIBUTE]); - span.setAttribute('gen_ai.tool.name', attributes[AI_TOOL_CALL_NAME_ATTRIBUTE]); - span.updateName(`execute_tool ${attributes[AI_TOOL_CALL_NAME_ATTRIBUTE]}`); + renameAttributeKey(attributes, AI_TOOL_CALL_NAME_ATTRIBUTE, 'gen_ai.tool.name'); + renameAttributeKey(attributes, AI_TOOL_CALL_ID_ATTRIBUTE, 'gen_ai.tool.call.id'); + // https://opentelemetry.io/docs/specs/semconv/registry/attributes/gen-ai/#gen-ai-tool-type + if (!attributes['gen_ai.tool.type']) { + span.setAttribute('gen_ai.tool.type', 'function'); + } + const toolName = attributes['gen_ai.tool.name']; + if (toolName) { + span.updateName(`execute_tool ${toolName}`); + } } function processGenerateSpan(span: Span, name: string, attributes: SpanAttributes): void { @@ -127,7 +139,7 @@ function processGenerateSpan(span: Span, name: string, attributes: SpanAttribute const functionId = attributes[AI_TELEMETRY_FUNCTION_ID_ATTRIBUTE]; if (functionId && typeof functionId === 'string' && name.split('.').length - 1 === 1) { span.updateName(`${nameWthoutAi} ${functionId}`); - span.setAttribute('ai.pipeline.name', functionId); + span.setAttribute('gen_ai.function_id', functionId); } if (attributes[AI_PROMPT_ATTRIBUTE]) { From 7f3f6f78112c4712405c1ef47ca24496c85032d7 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Wed, 2 Jul 2025 15:52:52 -0400 Subject: [PATCH 09/16] feat(cloudflare): Add user agent to cloudflare spans (#16793) resolves https://github.com/getsentry/sentry-javascript/issues/16766 --- packages/cloudflare/src/request.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/cloudflare/src/request.ts b/packages/cloudflare/src/request.ts index d403501f3ed4..1ad2e1a8eb91 100644 --- a/packages/cloudflare/src/request.ts +++ b/packages/cloudflare/src/request.ts @@ -48,15 +48,18 @@ export function wrapRequestHandler( attributes['http.request.body.size'] = parseInt(contentLength, 10); } + const userAgentHeader = request.headers.get('user-agent'); + if (userAgentHeader) { + attributes['user_agent.original'] = userAgentHeader; + } + attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP] = 'http.server'; addCloudResourceContext(isolationScope); - if (request) { - addRequest(isolationScope, request); - if (request.cf) { - addCultureContext(isolationScope, request.cf); - attributes['network.protocol.name'] = request.cf.httpProtocol; - } + addRequest(isolationScope, request); + if (request.cf) { + addCultureContext(isolationScope, request.cf); + attributes['network.protocol.name'] = request.cf.httpProtocol; } // Do not capture spans for OPTIONS and HEAD requests From a75f456eb83b8547ab6798f5d987a248a6898b0a Mon Sep 17 00:00:00 2001 From: 0xbad0c0d3 <0xbad0c0d3@gmail.com> Date: Thu, 3 Jul 2025 12:11:18 +0300 Subject: [PATCH 10/16] Fix/nestjs cron issues (#16792) Nestjs sync task has the same issue as async tasks - they don't care about exceptions because nestjs `@Cron()` decorator automatically wraps it to `try-catch` block and logs an error. That's why we should capture exception in our `@SentryCron()` decorator. Fixes also #16749 - [ ] If you've added code that should be tested, please add tests. - [x] Ensure your code lints and the test suite passes (`yarn lint`) & (`yarn test`). --------- Co-authored-by: cod1k Co-authored-by: Andrei <168741329+andreiborza@users.noreply.github.com> --- dev-packages/e2e-tests/run.ts | 1 + .../nestjs-basic/src/app.service.ts | 17 ++++++++++--- .../nestjs-basic/tests/cron-decorator.test.ts | 24 +++++++++++++++---- .../test-applications/node-express/src/app.ts | 12 ++++++++++ .../node-express/tests/errors.test.ts | 15 ++++++++++-- packages/core/src/exports.ts | 11 +++++---- packages/nestjs/src/decorators.ts | 17 +++++++++++-- 7 files changed, 81 insertions(+), 16 deletions(-) diff --git a/dev-packages/e2e-tests/run.ts b/dev-packages/e2e-tests/run.ts index e8901eede1b9..44f0bc06dca7 100644 --- a/dev-packages/e2e-tests/run.ts +++ b/dev-packages/e2e-tests/run.ts @@ -66,6 +66,7 @@ async function run(): Promise { } await asyncExec('pnpm clean:test-applications', { env, cwd: __dirname }); + await asyncExec('pnpm cache delete "@sentry/*"', { env, cwd: __dirname }); const testAppPaths = appName ? [appName.trim()] : globSync('*', { cwd: `${__dirname}/test-applications/` }); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic/src/app.service.ts b/dev-packages/e2e-tests/test-applications/nestjs-basic/src/app.service.ts index 242b4c778a0e..a9f89152d56d 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-basic/src/app.service.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-basic/src/app.service.ts @@ -85,10 +85,21 @@ export class AppService { only supports minute granularity, but we don't want to wait (worst case) a full minute for the tests to finish. */ - @Cron('*/5 * * * * *', { name: 'test-cron-error' }) - @SentryCron('test-cron-error-slug', monitorConfig) + @Cron('*/5 * * * * *', { name: 'test-async-cron-error' }) + @SentryCron('test-async-cron-error-slug', monitorConfig) async testCronError() { - throw new Error('Test error from cron job'); + throw new Error('Test error from cron async job'); + } + + /* + Actual cron schedule differs from schedule defined in config because Sentry + only supports minute granularity, but we don't want to wait (worst case) a + full minute for the tests to finish. + */ + @Cron('*/5 * * * * *', { name: 'test-sync-cron-error' }) + @SentryCron('test-sync-cron-error-slug', monitorConfig) + testSyncCronError() { + throw new Error('Test error from cron sync job'); } async killTestCron(job: string) { diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/cron-decorator.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/cron-decorator.test.ts index 7896603b3bd9..c193a94911c1 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/cron-decorator.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/cron-decorator.test.ts @@ -62,20 +62,36 @@ test('Cron job triggers send of in_progress envelope', async ({ baseURL }) => { await fetch(`${baseURL}/kill-test-cron/test-cron-job`); }); -test('Sends exceptions to Sentry on error in cron job', async ({ baseURL }) => { +test('Sends exceptions to Sentry on error in async cron job', async ({ baseURL }) => { const errorEventPromise = waitForError('nestjs-basic', event => { - return !event.type && event.exception?.values?.[0]?.value === 'Test error from cron job'; + return !event.type && event.exception?.values?.[0]?.value === 'Test error from cron async job'; }); const errorEvent = await errorEventPromise; expect(errorEvent.exception?.values).toHaveLength(1); - expect(errorEvent.exception?.values?.[0]?.value).toBe('Test error from cron job'); expect(errorEvent.contexts?.trace).toEqual({ trace_id: expect.stringMatching(/[a-f0-9]{32}/), span_id: expect.stringMatching(/[a-f0-9]{16}/), }); // kill cron so tests don't get stuck - await fetch(`${baseURL}/kill-test-cron/test-cron-error`); + await fetch(`${baseURL}/kill-test-cron/test-async-cron-error`); +}); + +test('Sends exceptions to Sentry on error in sync cron job', async ({ baseURL }) => { + const errorEventPromise = waitForError('nestjs-basic', event => { + return !event.type && event.exception?.values?.[0]?.value === 'Test error from cron sync job'; + }); + + const errorEvent = await errorEventPromise; + + expect(errorEvent.exception?.values).toHaveLength(1); + expect(errorEvent.contexts?.trace).toEqual({ + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + }); + + // kill cron so tests don't get stuck + await fetch(`${baseURL}/kill-test-cron/test-sync-cron-error`); }); diff --git a/dev-packages/e2e-tests/test-applications/node-express/src/app.ts b/dev-packages/e2e-tests/test-applications/node-express/src/app.ts index 76a02ea2c255..35b21a97b9aa 100644 --- a/dev-packages/e2e-tests/test-applications/node-express/src/app.ts +++ b/dev-packages/e2e-tests/test-applications/node-express/src/app.ts @@ -29,6 +29,18 @@ const port = 3030; app.use(mcpRouter); +app.get('/crash-in-with-monitor/:id', async (req, res) => { + try { + await Sentry.withMonitor('express-crash', async () => { + throw new Error(`This is an exception withMonitor: ${req.params.id}`); + }); + res.sendStatus(200); + } catch (error: any) { + res.status(500); + res.send({ message: error.message, pid: process.pid }); + } +}); + app.get('/test-success', function (req, res) { res.send({ version: 'v1' }); }); diff --git a/dev-packages/e2e-tests/test-applications/node-express/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-express/tests/errors.test.ts index bf0c5c5fb6b2..a4faaf137eb7 100644 --- a/dev-packages/e2e-tests/test-applications/node-express/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-express/tests/errors.test.ts @@ -37,6 +37,17 @@ test('Should record caught exceptions with local variable', async ({ baseURL }) const errorEvent = await errorEventPromise; - const frames = errorEvent.exception?.values?.[0].stacktrace?.frames; - expect(frames?.[frames.length - 1].vars?.randomVariableToRecord).toBeDefined(); + const frames = errorEvent.exception?.values?.[0]?.stacktrace?.frames; + expect(frames?.[frames.length - 1]?.vars?.randomVariableToRecord).toBeDefined(); +}); + +test('To not crash app from withMonitor', async ({ baseURL }) => { + const doRequest = async (id: number) => { + const response = await fetch(`${baseURL}/crash-in-with-monitor/${id}`) + return response.json(); + } + const [response1, response2] = await Promise.all([doRequest(1), doRequest(2)]) + expect(response1.message).toBe('This is an exception withMonitor: 1') + expect(response2.message).toBe('This is an exception withMonitor: 2') + expect(response1.pid).toBe(response2.pid) //Just to double-check, TBS }); diff --git a/packages/core/src/exports.ts b/packages/core/src/exports.ts index 92e4d09d4d81..5d36ffedf43f 100644 --- a/packages/core/src/exports.ts +++ b/packages/core/src/exports.ts @@ -150,6 +150,7 @@ export function captureCheckIn(checkIn: CheckIn, upsertMonitorConfig?: MonitorCo * Wraps a callback with a cron monitor check in. The check in will be sent to Sentry when the callback finishes. * * @param monitorSlug The distinct slug of the monitor. + * @param callback Callback to be monitored * @param upsertMonitorConfig An optional object that describes a monitor config. Use this if you want * to create a monitor automatically when sending a check in. */ @@ -175,18 +176,18 @@ export function withMonitor( } if (isThenable(maybePromiseResult)) { - Promise.resolve(maybePromiseResult).then( - () => { + return maybePromiseResult.then( + r => { finishCheckIn('ok'); + return r; }, e => { finishCheckIn('error'); throw e; }, - ); - } else { - finishCheckIn('ok'); + ) as T; } + finishCheckIn('ok'); return maybePromiseResult; }); diff --git a/packages/nestjs/src/decorators.ts b/packages/nestjs/src/decorators.ts index b11b69046c8c..9ac7315dabd8 100644 --- a/packages/nestjs/src/decorators.ts +++ b/packages/nestjs/src/decorators.ts @@ -1,5 +1,5 @@ import type { MonitorConfig } from '@sentry/core'; -import { captureException } from '@sentry/core'; +import { captureException, isThenable } from '@sentry/core'; import * as Sentry from '@sentry/node'; import { startSpan } from '@sentry/node'; import { isExpectedError } from './helpers'; @@ -15,7 +15,20 @@ export const SentryCron = (monitorSlug: string, monitorConfig?: MonitorConfig): return Sentry.withMonitor( monitorSlug, () => { - return originalMethod.apply(this, args); + let result; + try { + result = originalMethod.apply(this, args); + } catch (e) { + captureException(e); + throw e; + } + if (isThenable(result)) { + return result.then(undefined, e => { + captureException(e); + throw e; + }); + } + return result; }, monitorConfig, ); From d0a68fb9aa07f454dfbc78baf32b5db01f18531a Mon Sep 17 00:00:00 2001 From: Francesco Gringl-Novy Date: Thu, 3 Jul 2025 14:34:03 +0200 Subject: [PATCH 11/16] fix(nuxt): Ensure order of plugins is consistent (#16798) Noticed https://github.com/getsentry/sentry-javascript/pull/16783 in the nuxt-3-min test, that the plugins are not consistently run apparently. In that test, the client integrations plugin was run before the client config plugin, leading to the client not existing yet, and thus not adding the browser tracing integration. This PR changes this to ensure we have a consistent order of these plugins. --- packages/nuxt/src/module.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module.ts index bb9843bd6ce2..5392774d330d 100644 --- a/packages/nuxt/src/module.ts +++ b/packages/nuxt/src/module.ts @@ -42,6 +42,7 @@ export default defineNuxtModule({ addPluginTemplate({ mode: 'client', filename: 'sentry-client-config.mjs', + order: 0, // Dynamic import of config file to wrap it within a Nuxt context (here: defineNuxtPlugin) // Makes it possible to call useRuntimeConfig() in the user-defined sentry config file @@ -56,7 +57,13 @@ export default defineNuxtModule({ });`, }); - addPlugin({ src: moduleDirResolver.resolve('./runtime/plugins/sentry.client'), mode: 'client' }); + // Add the plugin which loads client integrations etc. - + // this must run after the sentry-client-config plugin has run, and the client is initialized! + addPlugin({ + src: moduleDirResolver.resolve('./runtime/plugins/sentry.client'), + mode: 'client', + order: 1, + }); } const serverConfigFile = findDefaultSdkInitFile('server', nuxt); From 6df4acdc889b9355ec1f7764b89beec07b17dfed Mon Sep 17 00:00:00 2001 From: Alekhin Sergey Date: Thu, 3 Jul 2025 16:47:47 +0300 Subject: [PATCH 12/16] feat(browser): Export `Context` and `Contexts` types (#16763) --- packages/browser/src/exports.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/browser/src/exports.ts b/packages/browser/src/exports.ts index 51e26fdf95b9..2a45880de82b 100644 --- a/packages/browser/src/exports.ts +++ b/packages/browser/src/exports.ts @@ -1,6 +1,8 @@ export type { Breadcrumb, BreadcrumbHint, + Context, + Contexts, RequestEventData, SdkInfo, Event, From 9c6852ab1dd88f8b4a7efec425b174bfd70a19d1 Mon Sep 17 00:00:00 2001 From: Daniel Griesser Date: Thu, 3 Jul 2025 16:00:09 +0200 Subject: [PATCH 13/16] chore: Add external contributor to CHANGELOG.md (#16800) This PR adds the external contributor to the CHANGELOG.md file, so that they are credited for their contribution. See #16792 Co-authored-by: andreiborza <168741329+andreiborza@users.noreply.github.com> --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 956152978b88..96efe8e1c890 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +Work in this release was contributed by @0xbad0c0d3. Thank you for your contribution! + ## 9.34.0 ### Important Changes From 2c50d740c784e2c035b550a57979b0b093db0db1 Mon Sep 17 00:00:00 2001 From: Francesco Gringl-Novy Date: Thu, 3 Jul 2025 16:03:22 +0200 Subject: [PATCH 14/16] fix(astro): Handle errors in middlewares better (#16693) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hopefully fixes https://github.com/getsentry/sentry-javascript/issues/16491 I could not get a reproducing test there for this 🤔 I figure this is "safe" to do, but not quite sure how/when this would happen. I would assume this was "introduced" by https://github.com/getsentry/sentry-javascript/pull/15995, maybe @Fryuni has a clue how/when that could happen and if this is a reasonable change 🤔 --- packages/astro/src/server/middleware.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/astro/src/server/middleware.ts b/packages/astro/src/server/middleware.ts index f339f7b6f979..5aabfa8d7351 100644 --- a/packages/astro/src/server/middleware.ts +++ b/packages/astro/src/server/middleware.ts @@ -184,14 +184,28 @@ async function instrumentRequest( const newResponseStream = new ReadableStream({ start: async controller => { + // Assign to a new variable to avoid TS losing the narrower type checked above. + const body = originalBody; + + async function* bodyReporter(): AsyncGenerator { + try { + for await (const chunk of body) { + yield chunk; + } + } catch (e) { + // Report stream errors coming from user code or Astro rendering. + sendErrorToSentry(e); + throw e; + } + } + try { - for await (const chunk of originalBody) { + for await (const chunk of bodyReporter()) { const html = typeof chunk === 'string' ? chunk : decoder.decode(chunk, { stream: true }); const modifiedHtml = addMetaTagToHead(html); controller.enqueue(new TextEncoder().encode(modifiedHtml)); } } catch (e) { - sendErrorToSentry(e); controller.error(e); } finally { controller.close(); From a1c3067bf5dc1e0db5dff34bd325ffe24abba8df Mon Sep 17 00:00:00 2001 From: Daniel Griesser Date: Thu, 3 Jul 2025 16:07:53 +0200 Subject: [PATCH 15/16] chore: Add external contributor to CHANGELOG.md (#16802) This PR adds the external contributor to the CHANGELOG.md file, so that they are credited for their contribution. See #16763 Co-authored-by: chargome <20254395+chargome@users.noreply.github.com> Co-authored-by: Francesco Gringl-Novy --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96efe8e1c890..44666cd1802a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott -Work in this release was contributed by @0xbad0c0d3. Thank you for your contribution! +Work in this release was contributed by @0xbad0c0d3 and @alSergey. Thank you for your contributions! ## 9.34.0 From 1a1c19d97f8aa5031339018b6450762e6c5c6826 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Fri, 4 Jul 2025 12:18:27 +0200 Subject: [PATCH 16/16] meta(changelog): Update changelog for 9.35.0 --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44666cd1802a..09f630d5aedf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,21 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 9.35.0 + +- feat(browser): Add ElementTiming instrumentation and spans ([#16589](https://github.com/getsentry/sentry-javascript/pull/16589)) +- feat(browser): Export `Context` and `Contexts` types ([#16763](https://github.com/getsentry/sentry-javascript/pull/16763)) +- feat(cloudflare): Add user agent to cloudflare spans ([#16793](https://github.com/getsentry/sentry-javascript/pull/16793)) +- feat(node): Add `eventLoopBlockIntegration` ([#16709](https://github.com/getsentry/sentry-javascript/pull/16709)) +- feat(node): Export server-side feature flag integration shims ([#16735](https://github.com/getsentry/sentry-javascript/pull/16735)) +- feat(node): Update vercel ai integration attributes ([#16721](https://github.com/getsentry/sentry-javascript/pull/16721)) +- fix(astro): Handle errors in middlewares better ([#16693](https://github.com/getsentry/sentry-javascript/pull/16693)) +- fix(browser): Ensure explicit `parentSpan` is considered ([#16776](https://github.com/getsentry/sentry-javascript/pull/16776)) +- fix(node): Avoid using dynamic `require` for fastify integration ([#16789](https://github.com/getsentry/sentry-javascript/pull/16789)) +- fix(nuxt): Add `@sentry/cloudflare` as optional peerDependency ([#16782](https://github.com/getsentry/sentry-javascript/pull/16782)) +- fix(nuxt): Ensure order of plugins is consistent ([#16798](https://github.com/getsentry/sentry-javascript/pull/16798)) +- fix(nestjs): Fix exception handling in `@Cron` decorated tasks ([#16792](https://github.com/getsentry/sentry-javascript/pull/16792)) + Work in this release was contributed by @0xbad0c0d3 and @alSergey. Thank you for your contributions! ## 9.34.0