diff --git a/.size-limit.js b/.size-limit.js index 61fb027289d3..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)', @@ -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) { @@ -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/CHANGELOG.md b/CHANGELOG.md index 956152978b88..09f630d5aedf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,23 @@ - "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 ### Important Changes 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 000000000000..353b7233d6bf Binary files /dev/null and b/dev-packages/browser-integration-tests/suites/tracing/metrics/element-timing/assets/sentry-logo-600x179.png differ 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/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 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/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/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/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/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/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/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/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/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(); 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/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/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, 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( } 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/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/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/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]) { 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 () => { 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/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, ); 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-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/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/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); } }; 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": { 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/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); 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; 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==