diff --git a/.craft.yml b/.craft.yml index 44d245311312..ffc85c13c90b 100644 --- a/.craft.yml +++ b/.craft.yml @@ -142,26 +142,11 @@ targets: id: '@sentry-internal/eslint-config-sdk' includeNames: /^sentry-internal-eslint-config-sdk-\d.*\.tgz$/ - # TODO(v9): Remove this target # NOTE: We publish the v8 layer under its own name so people on v8 can still get patches # whenever we release a new v8 version—otherwise we would overwrite the current major lambda layer. - name: aws-lambda-layer includeNames: /^sentry-node-serverless-\d+.\d+.\d+(-(beta|alpha|rc)\.\d+)?\.zip$/ - layerName: SentryNodeServerlessSDKv8 - compatibleRuntimes: - - name: node - versions: - - nodejs10.x - - nodejs12.x - - nodejs14.x - - nodejs16.x - - nodejs18.x - - nodejs20.x - license: MIT - - # AWS Lambda Layer target - - name: aws-lambda-layer - includeNames: /^sentry-node-serverless-\d+.\d+.\d+(-(beta|alpha|rc)\.\d+)?\.zip$/ + # TODO(v9): change to `SentryNodeServerlessSDKv8` once v9 is released layerName: SentryNodeServerlessSDK compatibleRuntimes: - name: node diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bf9ba21376bb..d633b9bc784d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,6 +4,8 @@ on: branches: - develop - master + - v9 + - v8 - release/** pull_request: merge_group: @@ -105,7 +107,7 @@ jobs: outputs: commit_label: '${{ env.COMMIT_SHA }}: ${{ env.COMMIT_MESSAGE }}' # Note: These next three have to be checked as strings ('true'/'false')! - is_develop: ${{ github.ref == 'refs/heads/develop' }} + is_base_branch: ${{ github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/v9' || github.ref == 'refs/heads/v8'}} is_release: ${{ startsWith(github.ref, 'refs/heads/release/') }} changed_profiling_node: ${{ steps.changed.outputs.profiling_node == 'true' }} changed_ci: ${{ steps.changed.outputs.workflow == 'true' }} @@ -126,7 +128,7 @@ jobs: timeout-minutes: 15 if: | needs.job_get_metadata.outputs.changed_any_code == 'true' || - needs.job_get_metadata.outputs.is_develop == 'true' || + needs.job_get_metadata.outputs.is_base_branch == 'true' || needs.job_get_metadata.outputs.is_release == 'true' || (needs.job_get_metadata.outputs.is_gitflow_sync == 'false' && needs.job_get_metadata.outputs.has_gitflow_label == 'false') steps: @@ -171,7 +173,7 @@ jobs: key: nx-Linux-${{ github.ref }}-${{ env.HEAD_COMMIT || github.sha }} # On develop branch, we want to _store_ the cache (so it can be used by other branches), but never _restore_ from it restore-keys: - ${{needs.job_get_metadata.outputs.is_develop == 'false' && env.NX_CACHE_RESTORE_KEYS || 'nx-never-restore'}} + ${{needs.job_get_metadata.outputs.is_base_branch == 'false' && env.NX_CACHE_RESTORE_KEYS || 'nx-never-restore'}} - name: Build packages # Set the CODECOV_TOKEN for Bundle Analysis @@ -219,7 +221,7 @@ jobs: timeout-minutes: 15 runs-on: ubuntu-20.04 if: - github.event_name == 'pull_request' || needs.job_get_metadata.outputs.is_develop == 'true' || + github.event_name == 'pull_request' || needs.job_get_metadata.outputs.is_base_branch == 'true' || needs.job_get_metadata.outputs.is_release == 'true' steps: - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) @@ -1207,7 +1209,8 @@ jobs: - name: Run E2E test working-directory: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }} timeout-minutes: 10 - run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn test:assert + run: | + xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn test:assert job_required_jobs_passed: name: All required jobs passed or were skipped diff --git a/.github/workflows/enforce-license-compliance.yml b/.github/workflows/enforce-license-compliance.yml index 8f63b6ca063b..776f8135178d 100644 --- a/.github/workflows/enforce-license-compliance.yml +++ b/.github/workflows/enforce-license-compliance.yml @@ -2,9 +2,18 @@ name: "CI: Enforce License Compliance" on: push: - branches: [master, develop, release/*] + branches: + - develop + - master + - v9 + - v8 + - release/** pull_request: - branches: [master, develop] + branches: + - develop + - master + - v9 + - v8 jobs: enforce-license-compliance: diff --git a/.size-limit.js b/.size-limit.js index 6e73c9234c09..6fd3277c7580 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -47,7 +47,7 @@ module.exports = [ path: 'packages/browser/build/npm/esm/index.js', import: createImport('init', 'browserTracingIntegration', 'replayIntegration'), gzip: true, - limit: '75 KB', + limit: '76 KB', }, { name: '@sentry/browser (incl. Tracing, Replay) - with treeshaking flags', diff --git a/CHANGELOG.md b/CHANGELOG.md index dd6332f4c125..09eea57101f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,72 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 8.50.0 + +- feat(v8/react): Add support for React Router `createMemoryRouter` ([#14985](https://github.com/getsentry/sentry-javascript/pull/14985)) + +## 8.49.0 + +- feat(v8/browser): Flush offline queue on flush and browser online event ([#14969](https://github.com/getsentry/sentry-javascript/pull/14969)) +- feat(v8/react): Add a `handled` prop to ErrorBoundary ([#14978](https://github.com/getsentry/sentry-javascript/pull/14978)) +- fix(profiling/v8): Don't put `require`, `__filename` and `__dirname` on global object ([#14952](https://github.com/getsentry/sentry-javascript/pull/14952)) +- fix(v8/node): Enforce that ContextLines integration does not leave open file handles ([#14997](https://github.com/getsentry/sentry-javascript/pull/14997)) +- fix(v8/replay): Disable mousemove sampling in rrweb for iOS browsers ([#14944](https://github.com/getsentry/sentry-javascript/pull/14944)) +- fix(v8/sveltekit): Ensure source maps deletion is called after source ma… ([#14963](https://github.com/getsentry/sentry-javascript/pull/14963)) +- fix(v8/vue): Re-throw error when no errorHandler exists ([#14943](https://github.com/getsentry/sentry-javascript/pull/14943)) + +Work in this release was contributed by @HHK1 and @mstrokin. Thank you for your contribution! + +## 8.48.0 + +### Deprecations + +- **feat(v8/core): Deprecate `getDomElement` method ([#14799](https://github.com/getsentry/sentry-javascript/pull/14799))** + + Deprecates `getDomElement`. There is no replacement. + +### Other changes + +- fix(nestjs/v8): Use correct main/module path in package.json ([#14791](https://github.com/getsentry/sentry-javascript/pull/14791)) +- fix(v8/core): Use consistent `continueTrace` implementation in core ([#14819](https://github.com/getsentry/sentry-javascript/pull/14819)) +- fix(v8/node): Correctly resolve debug IDs for ANR events with custom appRoot ([#14823](https://github.com/getsentry/sentry-javascript/pull/14823)) +- fix(v8/node): Ensure `NODE_OPTIONS` is not passed to worker threads ([#14825](https://github.com/getsentry/sentry-javascript/pull/14825)) +- fix(v8/angular): Fall back to element `tagName` when name is not provided to `TraceDirective` ([#14828](https://github.com/getsentry/sentry-javascript/pull/14828)) +- fix(aws-lambda): Remove version suffix from lambda layer ([#14843](https://github.com/getsentry/sentry-javascript/pull/14843)) +- fix(v8/node): Ensure express requests are properly handled ([#14851](https://github.com/getsentry/sentry-javascript/pull/14851)) +- feat(v8/node): Add `openTelemetrySpanProcessors` option ([#14853](https://github.com/getsentry/sentry-javascript/pull/14853)) +- fix(v8/react): Use `Set` as the `allRoutes` container. ([#14878](https://github.com/getsentry/sentry-javascript/pull/14878)) (#14884) +- fix(v8/react): Improve handling of routes nested under path="/" ([#14897](https://github.com/getsentry/sentry-javascript/pull/14897)) +- feat(v8/core): Add `normalizedRequest` to `samplingContext` ([#14903](https://github.com/getsentry/sentry-javascript/pull/14903)) +- fix(v8/feedback): Avoid lazy loading code for `syncFeedbackIntegration` ([#14918](https://github.com/getsentry/sentry-javascript/pull/14918)) + +Work in this release was contributed by @arturovt. Thank you for your contribution! + +## 8.47.0 + +- feat(v8/core): Add `updateSpanName` helper function (#14736) +- feat(v8/node): Do not overwrite prisma `db.system` in newer Prisma versions (#14772) +- feat(v8/node/deps): Bump @prisma/instrumentation from 5.19.1 to 5.22.0 (#14755) +- feat(v8/replay): Mask srcdoc iframe contents per default (#14779) +- ref(v8/nextjs): Fix typo in source maps deletion warning (#14776) + +Work in this release was contributed by @aloisklink and @benjick. Thank you for your contributions! + +## 8.46.0 + +- feat: Allow capture of more than 1 ANR event [v8] ([#14713](https://github.com/getsentry/sentry-javascript/pull/14713)) +- feat(node): Detect Railway release name [v8] ([#14714](https://github.com/getsentry/sentry-javascript/pull/14714)) +- fix: Normalise ANR debug image file paths if appRoot was supplied [v8] ([#14709](https://github.com/getsentry/sentry-javascript/pull/14709)) +- fix(nuxt): Remove build config from tsconfig ([#14737](https://github.com/getsentry/sentry-javascript/pull/14737)) + +Work in this release was contributed by @conor-ob. Thank you for your contribution! + +## 8.45.1 + +- fix(feedback): Return when the `sendFeedback` promise resolves ([#14683](https://github.com/getsentry/sentry-javascript/pull/14683)) + +Work in this release was contributed by @antonis. Thank you for your contribution! + ## 8.45.0 - feat(core): Add `handled` option to `captureConsoleIntegration` ([#14664](https://github.com/getsentry/sentry-javascript/pull/14664)) diff --git a/dev-packages/browser-integration-tests/package.json b/dev-packages/browser-integration-tests/package.json index 31a7ce76727e..7585706fd4ce 100644 --- a/dev-packages/browser-integration-tests/package.json +++ b/dev-packages/browser-integration-tests/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/browser-integration-tests", - "version": "8.45.0", + "version": "8.50.0", "main": "index.js", "license": "MIT", "engines": { @@ -43,7 +43,7 @@ "@babel/preset-typescript": "^7.16.7", "@playwright/test": "^1.44.1", "@sentry-internal/rrweb": "2.31.0", - "@sentry/browser": "8.45.0", + "@sentry/browser": "8.50.0", "axios": "1.7.7", "babel-loader": "^8.2.2", "html-webpack-plugin": "^5.5.0", diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/badSignature/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/badSignature/init.js new file mode 100644 index 000000000000..dc92fbc296a4 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/badSignature/init.js @@ -0,0 +1,17 @@ +import * as Sentry from '@sentry/browser'; + +window.UnleashClient = class { + isEnabled(x) { + return x; + } +}; + +window.Sentry = Sentry; +window.sentryUnleashIntegration = Sentry.unleashIntegration({ unleashClientClass: window.UnleashClient }); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 1.0, + integrations: [window.sentryUnleashIntegration], + debug: true, // Required to test logging. +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/badSignature/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/badSignature/test.ts new file mode 100644 index 000000000000..9b95d4d51c81 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/badSignature/test.ts @@ -0,0 +1,59 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../../../utils/fixtures'; + +import { shouldSkipFeatureFlagsTest } from '../../../../../utils/helpers'; + +sentryTest('Logs and returns if isEnabled does not match expected signature', async ({ getLocalTestUrl, page }) => { + if (shouldSkipFeatureFlagsTest()) { + sentryTest.skip(); + } + const bundleKey = process.env.PW_BUNDLE || ''; + const hasDebug = !bundleKey.includes('_min'); + + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); + }); + + const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true }); + await page.goto(url); + + const errorLogs: string[] = []; + page.on('console', msg => { + if (msg.type() == 'error') { + errorLogs.push(msg.text()); + } + }); + + const results = await page.evaluate(() => { + const unleash = new (window as any).UnleashClient(); + const res1 = unleash.isEnabled('my-feature'); + const res2 = unleash.isEnabled(999); + const res3 = unleash.isEnabled({}); + return [res1, res2, res3]; + }); + + // Test that the expected results are still returned. Note isEnabled is identity function for this test. + expect(results).toEqual(['my-feature', 999, {}]); + + // Expected error logs. + if (hasDebug) { + expect(errorLogs).toEqual( + expect.arrayContaining([ + expect.stringContaining( + '[Feature Flags] UnleashClient.isEnabled does not match expected signature. arg0: my-feature (string), result: my-feature (string)', + ), + expect.stringContaining( + '[Feature Flags] UnleashClient.isEnabled does not match expected signature. arg0: 999 (number), result: 999 (number)', + ), + expect.stringContaining( + '[Feature Flags] UnleashClient.isEnabled does not match expected signature. arg0: [object Object] (object), result: [object Object] (object)', + ), + ]), + ); + } +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/basic/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/basic/test.ts new file mode 100644 index 000000000000..5bb72caddd24 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/basic/test.ts @@ -0,0 +1,58 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../../../utils/fixtures'; + +import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers'; + +const FLAG_BUFFER_SIZE = 100; // Corresponds to constant in featureFlags.ts, in browser utils. + +sentryTest('Basic test with eviction, update, and no async tasks', async ({ getLocalTestUrl, page }) => { + if (shouldSkipFeatureFlagsTest()) { + sentryTest.skip(); + } + + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); + }); + + const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true }); + await page.goto(url); + + await page.evaluate(bufferSize => { + const client = new (window as any).UnleashClient(); + + client.isEnabled('feat1'); + client.isEnabled('strFeat'); + client.isEnabled('noPayloadFeat'); + client.isEnabled('jsonFeat'); + client.isEnabled('noVariantFeat'); + client.isEnabled('disabledFeat'); + + for (let i = 7; i <= bufferSize; i++) { + client.isEnabled(`feat${i}`); + } + client.isEnabled(`feat${bufferSize + 1}`); // eviction + client.isEnabled('noPayloadFeat'); // update (move to tail) + }, FLAG_BUFFER_SIZE); + + const reqPromise = waitForErrorRequest(page); + await page.locator('#error').click(); + const req = await reqPromise; + const event = envelopeRequestParser(req); + + const expectedFlags = [{ flag: 'strFeat', result: true }]; + expectedFlags.push({ flag: 'jsonFeat', result: true }); + expectedFlags.push({ flag: 'noVariantFeat', result: true }); + expectedFlags.push({ flag: 'disabledFeat', result: false }); + for (let i = 7; i <= FLAG_BUFFER_SIZE; i++) { + expectedFlags.push({ flag: `feat${i}`, result: false }); + } + expectedFlags.push({ flag: `feat${FLAG_BUFFER_SIZE + 1}`, result: false }); + expectedFlags.push({ flag: 'noPayloadFeat', result: true }); + + expect(event.contexts?.flags?.values).toEqual(expectedFlags); +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/init.js new file mode 100644 index 000000000000..9f1f28730cf7 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/init.js @@ -0,0 +1,50 @@ +import * as Sentry from '@sentry/browser'; + +window.UnleashClient = class { + constructor() { + this._featureToVariant = { + strFeat: { name: 'variant1', enabled: true, feature_enabled: true, payload: { type: 'string', value: 'test' } }, + noPayloadFeat: { name: 'eu-west', enabled: true, feature_enabled: true }, + jsonFeat: { + name: 'paid-orgs', + enabled: true, + feature_enabled: true, + payload: { + type: 'json', + value: '{"foo": {"bar": "baz"}, "hello": [1, 2, 3]}', + }, + }, + + // Enabled feature with no configured variants. + noVariantFeat: { name: 'disabled', enabled: false, feature_enabled: true }, + + // Disabled feature. + disabledFeat: { name: 'disabled', enabled: false, feature_enabled: false }, + }; + + // Variant returned for features that don't exist. + // `feature_enabled` may be defined in prod, but we want to test the undefined case. + this._fallbackVariant = { + name: 'disabled', + enabled: false, + }; + } + + isEnabled(toggleName) { + const variant = this._featureToVariant[toggleName] || this._fallbackVariant; + return variant.feature_enabled || false; + } + + getVariant(toggleName) { + return this._featureToVariant[toggleName] || this._fallbackVariant; + } +}; + +window.Sentry = Sentry; +window.sentryUnleashIntegration = Sentry.unleashIntegration({ unleashClientClass: window.UnleashClient }); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 1.0, + integrations: [window.sentryUnleashIntegration], +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/subject.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/subject.js new file mode 100644 index 000000000000..e6697408128c --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/subject.js @@ -0,0 +1,3 @@ +document.getElementById('error').addEventListener('click', () => { + throw new Error('Button triggered error'); +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/template.html b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/template.html new file mode 100644 index 000000000000..9330c6c679f4 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/template.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/withScope/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/withScope/test.ts new file mode 100644 index 000000000000..2d821bf6c81d --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/withScope/test.ts @@ -0,0 +1,65 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../../../utils/fixtures'; + +import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers'; + +import type { Scope } from '@sentry/browser'; + +sentryTest('Flag evaluations in forked scopes are stored separately.', async ({ getLocalTestUrl, page }) => { + if (shouldSkipFeatureFlagsTest()) { + sentryTest.skip(); + } + + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); + }); + + const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true }); + await page.goto(url); + + const forkedReqPromise = waitForErrorRequest(page, event => !!event.tags && event.tags.isForked === true); + const mainReqPromise = waitForErrorRequest(page, event => !!event.tags && event.tags.isForked === false); + + await page.evaluate(() => { + const Sentry = (window as any).Sentry; + const errorButton = document.querySelector('#error') as HTMLButtonElement; + const unleash = new (window as any).UnleashClient(); + + unleash.isEnabled('strFeat'); + + Sentry.withScope((scope: Scope) => { + unleash.isEnabled('disabledFeat'); + unleash.isEnabled('strFeat'); + scope.setTag('isForked', true); + if (errorButton) { + errorButton.click(); + } + }); + + unleash.isEnabled('noPayloadFeat'); + Sentry.getCurrentScope().setTag('isForked', false); + errorButton.click(); + return true; + }); + + const forkedReq = await forkedReqPromise; + const forkedEvent = envelopeRequestParser(forkedReq); + + const mainReq = await mainReqPromise; + const mainEvent = envelopeRequestParser(mainReq); + + expect(forkedEvent.contexts?.flags?.values).toEqual([ + { flag: 'disabledFeat', result: false }, + { flag: 'strFeat', result: true }, + ]); + + expect(mainEvent.contexts?.flags?.values).toEqual([ + { flag: 'strFeat', result: true }, + { flag: 'noPayloadFeat', result: true }, + ]); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-update-txn-name/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-update-txn-name/test.ts index 6226ff75dbb9..7d2d949898c2 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-update-txn-name/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-update-txn-name/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; +import { type Event, SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME } from '@sentry/core'; import { SEMANTIC_ATTRIBUTE_SENTRY_OP, @@ -10,27 +10,34 @@ import { import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; -sentryTest('sets the source to custom when updating the transaction name', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } +sentryTest( + 'sets the source to custom when updating the transaction name with `span.updateName`', + async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } - const url = await getLocalTestUrl({ testDir: __dirname }); + const url = await getLocalTestUrl({ testDir: __dirname }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); + const eventData = await getFirstSentryEnvelopeRequest(page, url); - const traceContextData = eventData.contexts?.trace?.data; + const traceContextData = eventData.contexts?.trace?.data; - expect(traceContextData).toMatchObject({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', - [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', - }); + expect(traceContextData).toBeDefined(); - expect(eventData.transaction).toBe('new name'); + expect(eventData.transaction).toBe('new name'); - expect(eventData.contexts?.trace?.op).toBe('pageload'); - expect(eventData.spans?.length).toBeGreaterThan(0); - expect(eventData.transaction_info?.source).toEqual('custom'); -}); + expect(traceContextData).toMatchObject({ + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + }); + + expect(traceContextData![SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]).toBeUndefined(); + + expect(eventData.contexts?.trace?.op).toBe('pageload'); + expect(eventData.spans?.length).toBeGreaterThan(0); + expect(eventData.transaction_info?.source).toEqual('custom'); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName/init.js new file mode 100644 index 000000000000..1f0b64911a75 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName/init.js @@ -0,0 +1,10 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; +window._testBaseTimestamp = performance.timeOrigin / 1000; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.browserTracingIntegration()], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName/subject.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName/subject.js new file mode 100644 index 000000000000..7f0ad0fea340 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName/subject.js @@ -0,0 +1,4 @@ +const activeSpan = Sentry.getActiveSpan(); +const rootSpan = activeSpan && Sentry.getRootSpan(activeSpan); + +Sentry.updateSpanName(rootSpan, 'new name'); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName/test.ts new file mode 100644 index 000000000000..69094b38e4dd --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-updateSpanName/test.ts @@ -0,0 +1,43 @@ +import { expect } from '@playwright/test'; +import { type Event, SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME } from '@sentry/core'; + +import { + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, +} from '@sentry/browser'; +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest( + 'sets the source to custom when updating the transaction name with Sentry.updateSpanName', + async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + + const traceContextData = eventData.contexts?.trace?.data; + + expect(traceContextData).toBeDefined(); + + expect(traceContextData).toMatchObject({ + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.browser', + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', + }); + + expect(traceContextData![SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]).toBeUndefined(); + + expect(eventData.transaction).toBe('new name'); + + expect(eventData.contexts?.trace?.op).toBe('pageload'); + expect(eventData.spans?.length).toBeGreaterThan(0); + expect(eventData.transaction_info?.source).toEqual('custom'); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/dsc-txn-name-update/test.ts b/dev-packages/browser-integration-tests/suites/tracing/dsc-txn-name-update/test.ts index 7ce5f7195a5b..34e15d1be573 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/dsc-txn-name-update/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/dsc-txn-name-update/test.ts @@ -181,5 +181,9 @@ async function captureErrorAndGetEnvelopeTraceHeader(page: Page): Promise=18" diff --git a/dev-packages/e2e-tests/package.json b/dev-packages/e2e-tests/package.json index 6452d7752eba..11d9116c8e79 100644 --- a/dev-packages/e2e-tests/package.json +++ b/dev-packages/e2e-tests/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/e2e-tests", - "version": "8.45.0", + "version": "8.50.0", "license": "MIT", "private": true, "scripts": { diff --git a/dev-packages/e2e-tests/test-applications/angular-17/src/app/component-tracking/component-tracking.components.ts b/dev-packages/e2e-tests/test-applications/angular-17/src/app/component-tracking/component-tracking.components.ts index d437a1d43fdd..1e43d5c6c096 100644 --- a/dev-packages/e2e-tests/test-applications/angular-17/src/app/component-tracking/component-tracking.components.ts +++ b/dev-packages/e2e-tests/test-applications/angular-17/src/app/component-tracking/component-tracking.components.ts @@ -6,7 +6,10 @@ import { SampleComponent } from '../sample-component/sample-component.components selector: 'app-cancel', standalone: true, imports: [TraceModule, SampleComponent], - template: ``, + template: ` + + + `, }) @TraceClass({ name: 'ComponentTrackingComponent' }) export class ComponentTrackingComponent implements OnInit, AfterViewInit { diff --git a/dev-packages/e2e-tests/test-applications/angular-17/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/angular-17/tests/performance.test.ts index 29c88a6108e2..03a715ce646c 100644 --- a/dev-packages/e2e-tests/test-applications/angular-17/tests/performance.test.ts +++ b/dev-packages/e2e-tests/test-applications/angular-17/tests/performance.test.ts @@ -191,7 +191,7 @@ test.describe('finish routing span', () => { }); test.describe('TraceDirective', () => { - test('creates a child tracingSpan with component name as span name on ngOnInit', async ({ page }) => { + test('creates a child span with the component name as span name on ngOnInit', async ({ page }) => { const navigationTxnPromise = waitForTransaction('angular-17', async transactionEvent => { return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; }); @@ -201,23 +201,36 @@ test.describe('TraceDirective', () => { // immediately navigate to a different route const [_, navigationTxn] = await Promise.all([page.locator('#componentTracking').click(), navigationTxnPromise]); - const traceDirectiveSpan = navigationTxn.spans?.find( + const traceDirectiveSpans = navigationTxn.spans?.filter( span => span?.data && span?.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] === 'auto.ui.angular.trace_directive', ); - expect(traceDirectiveSpan).toBeDefined(); - expect(traceDirectiveSpan).toEqual( - expect.objectContaining({ - data: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.angular.init', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_directive', - }, - description: '', - op: 'ui.angular.init', - origin: 'auto.ui.angular.trace_directive', - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - }), + expect(traceDirectiveSpans).toHaveLength(2); + expect(traceDirectiveSpans).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.angular.init', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_directive', + }, + description: '', // custom component name passed to trace directive + op: 'ui.angular.init', + origin: 'auto.ui.angular.trace_directive', + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + }), + expect.objectContaining({ + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.angular.init', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_directive', + }, + description: '', // fallback selector name + op: 'ui.angular.init', + origin: 'auto.ui.angular.trace_directive', + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + }), + ]), ); }); }); diff --git a/dev-packages/e2e-tests/test-applications/angular-19/.npmrc b/dev-packages/e2e-tests/test-applications/angular-19/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-19/.npmrc @@ -0,0 +1,2 @@ +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/dev-packages/e2e-tests/test-applications/angular-19/src/app/component-tracking/component-tracking.components.ts b/dev-packages/e2e-tests/test-applications/angular-19/src/app/component-tracking/component-tracking.components.ts index d437a1d43fdd..a82e5b1acce6 100644 --- a/dev-packages/e2e-tests/test-applications/angular-19/src/app/component-tracking/component-tracking.components.ts +++ b/dev-packages/e2e-tests/test-applications/angular-19/src/app/component-tracking/component-tracking.components.ts @@ -3,10 +3,13 @@ import { TraceClass, TraceMethod, TraceModule } from '@sentry/angular'; import { SampleComponent } from '../sample-component/sample-component.components'; @Component({ - selector: 'app-cancel', + selector: 'app-component-tracking', standalone: true, imports: [TraceModule, SampleComponent], - template: ``, + template: ` + + + `, }) @TraceClass({ name: 'ComponentTrackingComponent' }) export class ComponentTrackingComponent implements OnInit, AfterViewInit { diff --git a/dev-packages/e2e-tests/test-applications/angular-19/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/angular-19/tests/performance.test.ts index af85b8ffc405..c2cb2eca34b6 100644 --- a/dev-packages/e2e-tests/test-applications/angular-19/tests/performance.test.ts +++ b/dev-packages/e2e-tests/test-applications/angular-19/tests/performance.test.ts @@ -191,7 +191,7 @@ test.describe('finish routing span', () => { }); test.describe('TraceDirective', () => { - test('creates a child tracingSpan with component name as span name on ngOnInit', async ({ page }) => { + test('creates a child span with the component name as span name on ngOnInit', async ({ page }) => { const navigationTxnPromise = waitForTransaction('angular-18', async transactionEvent => { return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; }); @@ -201,23 +201,36 @@ test.describe('TraceDirective', () => { // immediately navigate to a different route const [_, navigationTxn] = await Promise.all([page.locator('#componentTracking').click(), navigationTxnPromise]); - const traceDirectiveSpan = navigationTxn.spans?.find( + const traceDirectiveSpans = navigationTxn.spans?.filter( span => span?.data && span?.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] === 'auto.ui.angular.trace_directive', ); - expect(traceDirectiveSpan).toBeDefined(); - expect(traceDirectiveSpan).toEqual( - expect.objectContaining({ - data: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.angular.init', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_directive', - }, - description: '', - op: 'ui.angular.init', - origin: 'auto.ui.angular.trace_directive', - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - }), + expect(traceDirectiveSpans).toHaveLength(2); + expect(traceDirectiveSpans).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.angular.init', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_directive', + }, + description: '', // custom component name passed to trace directive + op: 'ui.angular.init', + origin: 'auto.ui.angular.trace_directive', + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + }), + expect.objectContaining({ + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.angular.init', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_directive', + }, + description: '', // fallback selector name + op: 'ui.angular.init', + origin: 'auto.ui.angular.trace_directive', + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + }), + ]), ); }); }); diff --git a/dev-packages/e2e-tests/test-applications/node-express/package.json b/dev-packages/e2e-tests/test-applications/node-express/package.json index bc0b9b4dead7..b42a17fd5438 100644 --- a/dev-packages/e2e-tests/test-applications/node-express/package.json +++ b/dev-packages/e2e-tests/test-applications/node-express/package.json @@ -15,9 +15,9 @@ "@sentry/node": "latest || *", "@trpc/server": "10.45.2", "@trpc/client": "10.45.2", - "@types/express": "4.17.17", - "@types/node": "18.15.1", - "express": "4.20.0", + "@types/express": "^4.17.21", + "@types/node": "^18.19.1", + "express": "^4.21.2", "typescript": "4.9.5", "zod": "~3.22.4" }, @@ -25,6 +25,9 @@ "@playwright/test": "^1.44.1", "@sentry-internal/test-utils": "link:../../../test-utils" }, + "resolutions": { + "@types/qs": "6.9.17" + }, "volta": { "extends": "../../package.json" } diff --git a/dev-packages/e2e-tests/test-applications/node-express/tsconfig.json b/dev-packages/e2e-tests/test-applications/node-express/tsconfig.json index 8cb64e989ed9..ce4fafb745ad 100644 --- a/dev-packages/e2e-tests/test-applications/node-express/tsconfig.json +++ b/dev-packages/e2e-tests/test-applications/node-express/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "types": ["node"], "esModuleInterop": true, - "lib": ["es2018"], + "lib": ["es2020"], "strict": true, "outDir": "dist" }, diff --git a/dev-packages/e2e-tests/test-applications/node-otel/.gitignore b/dev-packages/e2e-tests/test-applications/node-otel/.gitignore new file mode 100644 index 000000000000..1521c8b7652b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-otel/.gitignore @@ -0,0 +1 @@ +dist diff --git a/dev-packages/e2e-tests/test-applications/node-otel/.npmrc b/dev-packages/e2e-tests/test-applications/node-otel/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-otel/.npmrc @@ -0,0 +1,2 @@ +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/dev-packages/e2e-tests/test-applications/node-otel/package.json b/dev-packages/e2e-tests/test-applications/node-otel/package.json new file mode 100644 index 000000000000..70d97d1fa502 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-otel/package.json @@ -0,0 +1,31 @@ +{ + "name": "node-otel", + "version": "1.0.0", + "private": true, + "scripts": { + "build": "tsc", + "start": "node dist/app.js", + "test": "playwright test", + "clean": "npx rimraf node_modules pnpm-lock.yaml", + "test:build": "pnpm install && pnpm build", + "test:assert": "pnpm test" + }, + "dependencies": { + "@opentelemetry/sdk-node": "0.52.1", + "@opentelemetry/exporter-trace-otlp-http": "0.52.1", + "@sentry/core": "latest || *", + "@sentry/node": "latest || *", + "@sentry/opentelemetry": "latest || *", + "@types/express": "4.17.17", + "@types/node": "^18.19.1", + "express": "4.19.2", + "typescript": "~5.0.0" + }, + "devDependencies": { + "@playwright/test": "^1.44.1", + "@sentry-internal/test-utils": "link:../../../test-utils" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/dev-packages/e2e-tests/test-applications/node-otel/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/node-otel/playwright.config.mjs new file mode 100644 index 000000000000..888e61cfb2dc --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-otel/playwright.config.mjs @@ -0,0 +1,34 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; + +const config = getPlaywrightConfig( + { + startCommand: `pnpm start`, + }, + { + webServer: [ + { + command: `node ./start-event-proxy.mjs`, + port: 3031, + stdout: 'pipe', + stderr: 'pipe', + }, + { + command: `node ./start-otel-proxy.mjs`, + port: 3032, + stdout: 'pipe', + stderr: 'pipe', + }, + { + command: 'pnpm start', + port: 3030, + stdout: 'pipe', + stderr: 'pipe', + env: { + PORT: 3030, + }, + }, + ], + }, +); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/node-otel/src/app.ts b/dev-packages/e2e-tests/test-applications/node-otel/src/app.ts new file mode 100644 index 000000000000..26779990f6d1 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-otel/src/app.ts @@ -0,0 +1,53 @@ +import './instrument'; + +// Other imports below +import * as Sentry from '@sentry/node'; +import express from 'express'; + +const app = express(); +const port = 3030; + +app.get('/test-success', function (req, res) { + res.send({ version: 'v1' }); +}); + +app.get('/test-param/:param', function (req, res) { + res.send({ paramWas: req.params.param }); +}); + +app.get('/test-transaction', function (req, res) { + Sentry.withActiveSpan(null, async () => { + Sentry.startSpan({ name: 'test-transaction', op: 'e2e-test' }, () => { + Sentry.startSpan({ name: 'test-span' }, () => undefined); + }); + + await Sentry.flush(); + + res.send({}); + }); +}); + +app.get('/test-error', async function (req, res) { + const exceptionId = Sentry.captureException(new Error('This is an error')); + + await Sentry.flush(2000); + + res.send({ exceptionId }); +}); + +app.get('/test-exception/:id', function (req, _res) { + throw new Error(`This is an exception with id ${req.params.id}`); +}); + +Sentry.setupExpressErrorHandler(app); + +app.use(function onError(err: unknown, req: any, res: any, next: any) { + // The error id is attached to `res.sentry` to be returned + // and optionally displayed to the user for support. + res.statusCode = 500; + res.end(res.sentry + '\n'); +}); + +app.listen(port, () => { + console.log(`Example app listening on port ${port}`); +}); diff --git a/dev-packages/e2e-tests/test-applications/node-otel/src/instrument.ts b/dev-packages/e2e-tests/test-applications/node-otel/src/instrument.ts new file mode 100644 index 000000000000..bbc5ddf9c30f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-otel/src/instrument.ts @@ -0,0 +1,22 @@ +const opentelemetry = require('@opentelemetry/sdk-node'); +const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http'); +const Sentry = require('@sentry/node'); + +const sentryClient = Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: + process.env.E2E_TEST_DSN || + 'https://3b6c388182fb435097f41d181be2b2ba@o4504321058471936.ingest.sentry.io/4504321066008576', + debug: !!process.env.DEBUG, + tunnel: `http://localhost:3031/`, // proxy server + tracesSampleRate: 1, + + // Additional OTEL options + openTelemetrySpanProcessors: [ + new opentelemetry.node.BatchSpanProcessor( + new OTLPTraceExporter({ + url: 'http://localhost:3032/', + }), + ), + ], +}); diff --git a/dev-packages/e2e-tests/test-applications/node-otel/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/node-otel/start-event-proxy.mjs new file mode 100644 index 000000000000..e82b876a4979 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-otel/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'node-otel', +}); diff --git a/dev-packages/e2e-tests/test-applications/node-otel/start-otel-proxy.mjs b/dev-packages/e2e-tests/test-applications/node-otel/start-otel-proxy.mjs new file mode 100644 index 000000000000..df546bf5ff77 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-otel/start-otel-proxy.mjs @@ -0,0 +1,6 @@ +import { startProxyServer } from '@sentry-internal/test-utils'; + +startProxyServer({ + port: 3032, + proxyServerName: 'node-otel-otel', +}); diff --git a/dev-packages/e2e-tests/test-applications/node-otel/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-otel/tests/errors.test.ts new file mode 100644 index 000000000000..e5b2d5ff6836 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-otel/tests/errors.test.ts @@ -0,0 +1,29 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test('Sends correct error event', async ({ baseURL }) => { + const errorEventPromise = waitForError('node-otel', event => { + return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123'; + }); + + await fetch(`${baseURL}/test-exception/123`); + + const errorEvent = await errorEventPromise; + + expect(errorEvent.exception?.values).toHaveLength(1); + expect(errorEvent.exception?.values?.[0]?.value).toBe('This is an exception with id 123'); + + expect(errorEvent.request).toEqual({ + method: 'GET', + cookies: {}, + headers: expect.any(Object), + url: 'http://localhost:3030/test-exception/123', + }); + + expect(errorEvent.transaction).toEqual('GET /test-exception/:id'); + + expect(errorEvent.contexts?.trace).toEqual({ + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/node-otel/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-otel/tests/transactions.test.ts new file mode 100644 index 000000000000..de68adf681b7 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-otel/tests/transactions.test.ts @@ -0,0 +1,207 @@ +import { expect, test } from '@playwright/test'; +import { waitForPlainRequest, waitForTransaction } from '@sentry-internal/test-utils'; + +test('Sends an API route transaction', async ({ baseURL }) => { + const pageloadTransactionEventPromise = waitForTransaction('node-otel', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /test-transaction' + ); + }); + + // Ensure we also send data to the OTLP endpoint + const otelPromise = waitForPlainRequest('node-otel-otel', data => { + const json = JSON.parse(data) as any; + + return json.resourceSpans.length > 0; + }); + + await fetch(`${baseURL}/test-transaction`); + + const transactionEvent = await pageloadTransactionEventPromise; + + const otelData = await otelPromise; + + // For now we do not test the actual shape of this, but only existence + expect(otelData).toBeDefined(); + + expect(transactionEvent.contexts?.trace).toEqual({ + data: { + 'sentry.source': 'route', + 'sentry.origin': 'auto.http.otel.http', + 'sentry.op': 'http.server', + 'sentry.sample_rate': 1, + url: 'http://localhost:3030/test-transaction', + 'otel.kind': 'SERVER', + 'http.response.status_code': 200, + 'http.url': 'http://localhost:3030/test-transaction', + 'http.host': 'localhost:3030', + 'net.host.name': 'localhost', + 'http.method': 'GET', + 'http.scheme': 'http', + 'http.target': '/test-transaction', + 'http.user_agent': 'node', + 'http.flavor': '1.1', + 'net.transport': 'ip_tcp', + 'net.host.ip': expect.any(String), + 'net.host.port': expect.any(Number), + 'net.peer.ip': expect.any(String), + 'net.peer.port': expect.any(Number), + 'http.status_code': 200, + 'http.status_text': 'OK', + 'http.route': '/test-transaction', + }, + op: 'http.server', + span_id: expect.stringMatching(/[a-f0-9]{16}/), + status: 'ok', + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + origin: 'auto.http.otel.http', + }); + + expect(transactionEvent).toEqual( + expect.objectContaining({ + transaction: 'GET /test-transaction', + type: 'transaction', + transaction_info: { + source: 'route', + }, + }), + ); + + const spans = transactionEvent.spans || []; + + expect(spans).toContainEqual({ + data: { + 'sentry.origin': 'auto.http.otel.express', + 'sentry.op': 'middleware.express', + 'http.route': '/', + 'express.name': 'query', + 'express.type': 'middleware', + }, + description: 'query', + op: 'middleware.express', + origin: 'auto.http.otel.express', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + }); + + expect(spans).toContainEqual({ + data: { + 'sentry.origin': 'auto.http.otel.express', + 'sentry.op': 'middleware.express', + 'http.route': '/', + 'express.name': 'expressInit', + 'express.type': 'middleware', + }, + description: 'expressInit', + op: 'middleware.express', + origin: 'auto.http.otel.express', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + }); + + expect(spans).toContainEqual({ + data: { + 'sentry.origin': 'auto.http.otel.express', + 'sentry.op': 'request_handler.express', + 'http.route': '/test-transaction', + 'express.name': '/test-transaction', + 'express.type': 'request_handler', + }, + description: '/test-transaction', + op: 'request_handler.express', + origin: 'auto.http.otel.express', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + }); +}); + +test('Sends an API route transaction for an errored route', async ({ baseURL }) => { + const transactionEventPromise = waitForTransaction('node-otel', transactionEvent => { + return ( + transactionEvent.contexts?.trace?.op === 'http.server' && + transactionEvent.transaction === 'GET /test-exception/:id' && + transactionEvent.request?.url === 'http://localhost:3030/test-exception/777' + ); + }); + + await fetch(`${baseURL}/test-exception/777`); + + const transactionEvent = await transactionEventPromise; + + expect(transactionEvent.contexts?.trace?.op).toEqual('http.server'); + expect(transactionEvent.transaction).toEqual('GET /test-exception/:id'); + expect(transactionEvent.contexts?.trace?.status).toEqual('internal_error'); + expect(transactionEvent.contexts?.trace?.data?.['http.status_code']).toEqual(500); + + const spans = transactionEvent.spans || []; + + expect(spans).toContainEqual({ + data: { + 'sentry.origin': 'auto.http.otel.express', + 'sentry.op': 'middleware.express', + 'http.route': '/', + 'express.name': 'query', + 'express.type': 'middleware', + }, + description: 'query', + op: 'middleware.express', + origin: 'auto.http.otel.express', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + }); + + expect(spans).toContainEqual({ + data: { + 'sentry.origin': 'auto.http.otel.express', + 'sentry.op': 'middleware.express', + 'http.route': '/', + 'express.name': 'expressInit', + 'express.type': 'middleware', + }, + description: 'expressInit', + op: 'middleware.express', + origin: 'auto.http.otel.express', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + }); + + expect(spans).toContainEqual({ + data: { + 'sentry.origin': 'auto.http.otel.express', + 'sentry.op': 'request_handler.express', + 'http.route': '/test-exception/:id', + 'express.name': '/test-exception/:id', + 'express.type': 'request_handler', + }, + description: '/test-exception/:id', + op: 'request_handler.express', + origin: 'auto.http.otel.express', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/node-otel/tsconfig.json b/dev-packages/e2e-tests/test-applications/node-otel/tsconfig.json new file mode 100644 index 000000000000..8cb64e989ed9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-otel/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "types": ["node"], + "esModuleInterop": true, + "lib": ["es2018"], + "strict": true, + "outDir": "dist" + }, + "include": ["src/**/*.ts"] +} diff --git a/dev-packages/e2e-tests/test-applications/node-profiling/build.mjs b/dev-packages/e2e-tests/test-applications/node-profiling/build-cjs.mjs similarity index 90% rename from dev-packages/e2e-tests/test-applications/node-profiling/build.mjs rename to dev-packages/e2e-tests/test-applications/node-profiling/build-cjs.mjs index 55ec0b5fae52..4a9aa83d0eec 100644 --- a/dev-packages/e2e-tests/test-applications/node-profiling/build.mjs +++ b/dev-packages/e2e-tests/test-applications/node-profiling/build-cjs.mjs @@ -11,9 +11,10 @@ console.log('Running build using esbuild version', esbuild.version); esbuild.buildSync({ platform: 'node', entryPoints: ['./index.ts'], - outdir: './dist', + outfile: './dist/cjs/index.js', target: 'esnext', format: 'cjs', bundle: true, loader: { '.node': 'copy' }, + external: ['@sentry/node', '@sentry/profiling-node'], }); diff --git a/dev-packages/e2e-tests/test-applications/node-profiling/build.shimmed.mjs b/dev-packages/e2e-tests/test-applications/node-profiling/build-esm.mjs similarity index 68% rename from dev-packages/e2e-tests/test-applications/node-profiling/build.shimmed.mjs rename to dev-packages/e2e-tests/test-applications/node-profiling/build-esm.mjs index c45e30539bc0..294e53d50635 100644 --- a/dev-packages/e2e-tests/test-applications/node-profiling/build.shimmed.mjs +++ b/dev-packages/e2e-tests/test-applications/node-profiling/build-esm.mjs @@ -11,19 +11,10 @@ console.log('Running build using esbuild version', esbuild.version); esbuild.buildSync({ platform: 'node', entryPoints: ['./index.ts'], - outfile: './dist/index.shimmed.mjs', + outfile: './dist/esm/index.mjs', target: 'esnext', format: 'esm', bundle: true, loader: { '.node': 'copy' }, - banner: { - js: ` - import { dirname } from 'node:path'; - import { fileURLToPath } from 'node:url'; - import { createRequire } from 'node:module'; - const require = createRequire(import.meta.url); - const __filename = fileURLToPath(import.meta.url); - const __dirname = dirname(__filename); - `, - }, + external: ['@sentry/node', '@sentry/profiling-node'], }); diff --git a/dev-packages/e2e-tests/test-applications/node-profiling/index.ts b/dev-packages/e2e-tests/test-applications/node-profiling/index.ts index d49add80955c..e956a1d9de33 100644 --- a/dev-packages/e2e-tests/test-applications/node-profiling/index.ts +++ b/dev-packages/e2e-tests/test-applications/node-profiling/index.ts @@ -1,5 +1,5 @@ -const Sentry = require('@sentry/node'); -const { nodeProfilingIntegration } = require('@sentry/profiling-node'); +import * as Sentry from '@sentry/node'; +import { nodeProfilingIntegration } from '@sentry/profiling-node'; const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); diff --git a/dev-packages/e2e-tests/test-applications/node-profiling/package.json b/dev-packages/e2e-tests/test-applications/node-profiling/package.json index 8aede827a1f3..d78ca10fa25d 100644 --- a/dev-packages/e2e-tests/test-applications/node-profiling/package.json +++ b/dev-packages/e2e-tests/test-applications/node-profiling/package.json @@ -4,8 +4,8 @@ "private": true, "scripts": { "typecheck": "tsc --noEmit", - "build": "node build.mjs && node build.shimmed.mjs", - "test": "node dist/index.js && node --experimental-require-module dist/index.js && node dist/index.shimmed.mjs", + "build": "node build-cjs.mjs && node build-esm.mjs", + "test": "node dist/cjs/index.js && node --experimental-require-module dist/cjs/index.js && node dist/esm/index.mjs", "clean": "npx rimraf node_modules dist", "test:electron": "$(pnpm bin)/electron-rebuild && playwright test", "test:build": "pnpm run typecheck && pnpm run build", @@ -17,9 +17,9 @@ "@sentry/electron": "latest || *", "@sentry/node": "latest || *", "@sentry/profiling-node": "latest || *", - "electron": "^33.2.0" + "electron": "^33.2.0", + "esbuild": "0.20.0" }, - "devDependencies": {}, "volta": { "extends": "../../package.json" }, diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/.gitignore b/dev-packages/e2e-tests/test-applications/react-create-browser-router/.gitignore new file mode 100644 index 000000000000..84634c973eeb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/.gitignore @@ -0,0 +1,29 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +/test-results/ +/playwright-report/ +/playwright/.cache/ + +!*.d.ts diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/.npmrc b/dev-packages/e2e-tests/test-applications/react-create-browser-router/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/.npmrc @@ -0,0 +1,2 @@ +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/package.json b/dev-packages/e2e-tests/test-applications/react-create-browser-router/package.json new file mode 100644 index 000000000000..a4e7dae6d1e2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/package.json @@ -0,0 +1,40 @@ +{ + "name": "react-create-browser-router-test", + "version": "0.1.0", + "private": true, + "dependencies": { + "@sentry/react": "latest || *", + "@types/node": "^18.19.1", + "@types/react": "18.0.0", + "@types/react-dom": "18.0.0", + "react": "18.2.0", + "react-dom": "18.2.0", + "react-router-dom": "^6.4.1", + "react-scripts": "5.0.1", + "typescript": "~5.0.0" + }, + "scripts": { + "build": "react-scripts build", + "start": "serve -s build", + "test": "playwright test", + "clean": "npx rimraf node_modules pnpm-lock.yaml", + "test:build": "pnpm install && pnpm build", + "test:build-canary": "pnpm install && pnpm add react@canary react-dom@canary && pnpm build", + "test:assert": "pnpm test" + }, + "eslintConfig": { + "extends": ["react-app", "react-app/jest"] + }, + "browserslist": { + "production": [">0.2%", "not dead", "not op_mini all"], + "development": ["last 1 chrome version", "last 1 firefox version", "last 1 safari version"] + }, + "devDependencies": { + "@playwright/test": "^1.44.1", + "@sentry-internal/test-utils": "link:../../../test-utils", + "serve": "14.0.1" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/react-create-browser-router/playwright.config.mjs new file mode 100644 index 000000000000..31f2b913b58b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/playwright.config.mjs @@ -0,0 +1,7 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; + +const config = getPlaywrightConfig({ + startCommand: `pnpm start`, +}); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/public/index.html b/dev-packages/e2e-tests/test-applications/react-create-browser-router/public/index.html new file mode 100644 index 000000000000..39da76522bea --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/public/index.html @@ -0,0 +1,24 @@ + + + + + + + + React App + + + +
+ + + diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/globals.d.ts b/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/globals.d.ts new file mode 100644 index 000000000000..ffa61ca49acc --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/globals.d.ts @@ -0,0 +1,5 @@ +interface Window { + recordedTransactions?: string[]; + capturedExceptionId?: string; + sentryReplayId?: string; +} diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/index.tsx b/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/index.tsx new file mode 100644 index 000000000000..88f8cfa502ec --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/index.tsx @@ -0,0 +1,66 @@ +import * as Sentry from '@sentry/react'; +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { + RouterProvider, + createBrowserRouter, + createRoutesFromChildren, + matchRoutes, + useLocation, + useNavigationType, +} from 'react-router-dom'; +import Index from './pages/Index'; +import User from './pages/User'; + +const replay = Sentry.replayIntegration(); + +Sentry.init({ + // environment: 'qa', // dynamic sampling bias to keep transactions + dsn: process.env.REACT_APP_E2E_TEST_DSN, + integrations: [ + Sentry.reactRouterV6BrowserTracingIntegration({ + useEffect: React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + }), + replay, + ], + // We recommend adjusting this value in production, or using tracesSampler + // for finer control + tracesSampleRate: 1.0, + release: 'e2e-test', + + tunnel: 'http://localhost:3031', + + // Always capture replays, so we can test this properly + replaysSessionSampleRate: 1.0, + replaysOnErrorSampleRate: 0.0, + + debug: !!process.env.DEBUG, +}); + +const sentryCreateBrowserRouter = Sentry.wrapCreateBrowserRouterV6(createBrowserRouter); + +const router = sentryCreateBrowserRouter( + [ + { + path: '/', + element: , + }, + { + path: '/user/:id', + element: , + }, + ], + { + // We're testing whether this option is avoided in the integration + // We expect this to be ignored + initialEntries: ['/user/1'], + }, +); + +const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); + +root.render(); diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/pages/Index.tsx b/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/pages/Index.tsx new file mode 100644 index 000000000000..d6b71a1d1279 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/pages/Index.tsx @@ -0,0 +1,23 @@ +// biome-ignore lint/nursery/noUnusedImports: Need React import for JSX +import * as React from 'react'; +import { Link } from 'react-router-dom'; + +const Index = () => { + return ( + <> + { + throw new Error('I am an error!'); + }} + /> + + navigate + + + ); +}; + +export default Index; diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/pages/User.tsx b/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/pages/User.tsx new file mode 100644 index 000000000000..62f0c2d17533 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/pages/User.tsx @@ -0,0 +1,8 @@ +// biome-ignore lint/nursery/noUnusedImports: Need React import for JSX +import * as React from 'react'; + +const User = () => { + return

I am a blank page :)

; +}; + +export default User; diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/react-app-env.d.ts b/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/react-app-env.d.ts new file mode 100644 index 000000000000..6431bc5fc6b2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/react-app-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/react-create-browser-router/start-event-proxy.mjs new file mode 100644 index 000000000000..be93e129284f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'react-create-browser-router', +}); diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/react-create-browser-router/tests/errors.test.ts new file mode 100644 index 000000000000..4a11f07410ab --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/tests/errors.test.ts @@ -0,0 +1,30 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test('Captures exception correctly', async ({ page }) => { + const errorEventPromise = waitForError('react-create-browser-router', event => { + return !event.type && event.exception?.values?.[0]?.value === 'I am an error!'; + }); + + await page.goto('/'); + + const exceptionButton = page.locator('id=exception-button'); + await exceptionButton.click(); + + const errorEvent = await errorEventPromise; + + expect(errorEvent.exception?.values).toHaveLength(1); + expect(errorEvent.exception?.values?.[0]?.value).toBe('I am an error!'); + + expect(errorEvent.request).toEqual({ + headers: expect.any(Object), + url: 'http://localhost:3030/', + }); + + expect(errorEvent.transaction).toEqual('/'); + + expect(errorEvent.contexts?.trace).toEqual({ + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/react-create-browser-router/tests/transactions.test.ts new file mode 100644 index 000000000000..5ecd098daf94 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/tests/transactions.test.ts @@ -0,0 +1,78 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('Captures a pageload transaction', async ({ page }) => { + const transactionEventPromise = waitForTransaction('react-create-browser-router', event => { + return event.contexts?.trace?.op === 'pageload'; + }); + + await page.goto('/'); + + const transactionEvent = await transactionEventPromise; + + expect(transactionEvent).toEqual( + expect.objectContaining({ + transaction: '/', + type: 'transaction', + transaction_info: { + source: 'route', + }, + }), + ); + + expect(transactionEvent.contexts?.trace).toEqual( + expect.objectContaining({ + data: expect.objectContaining({ + deviceMemory: expect.any(String), + effectiveConnectionType: expect.any(String), + hardwareConcurrency: expect.any(String), + 'sentry.idle_span_finish_reason': 'idleTimeout', + 'sentry.op': 'pageload', + 'sentry.origin': 'auto.pageload.react.reactrouter_v6', + 'sentry.sample_rate': 1, + 'sentry.source': 'route', + }), + op: 'pageload', + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + origin: 'auto.pageload.react.reactrouter_v6', + }), + ); +}); + +test('Captures a navigation transaction', async ({ page }) => { + const transactionEventPromise = waitForTransaction('react-create-browser-router', event => { + return event.contexts?.trace?.op === 'navigation'; + }); + + await page.goto('/'); + const linkElement = page.locator('id=navigation'); + await linkElement.click(); + + const transactionEvent = await transactionEventPromise; + expect(transactionEvent.contexts?.trace).toEqual({ + data: expect.objectContaining({ + 'sentry.idle_span_finish_reason': 'idleTimeout', + 'sentry.op': 'navigation', + 'sentry.origin': 'auto.navigation.react.reactrouter_v6', + 'sentry.sample_rate': 1, + 'sentry.source': 'route', + }), + op: 'navigation', + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + origin: 'auto.navigation.react.reactrouter_v6', + }); + + expect(transactionEvent).toEqual( + expect.objectContaining({ + transaction: '/user/:id', + type: 'transaction', + transaction_info: { + source: 'route', + }, + }), + ); + + expect(transactionEvent.spans).toEqual([]); +}); diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/tsconfig.json b/dev-packages/e2e-tests/test-applications/react-create-browser-router/tsconfig.json new file mode 100644 index 000000000000..4cc95dc2689a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es2018", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react" + }, + "include": ["src", "tests"] +} diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/.gitignore b/dev-packages/e2e-tests/test-applications/react-create-memory-router/.gitignore new file mode 100644 index 000000000000..84634c973eeb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/.gitignore @@ -0,0 +1,29 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +/test-results/ +/playwright-report/ +/playwright/.cache/ + +!*.d.ts diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/.npmrc b/dev-packages/e2e-tests/test-applications/react-create-memory-router/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/.npmrc @@ -0,0 +1,2 @@ +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/package.json b/dev-packages/e2e-tests/test-applications/react-create-memory-router/package.json new file mode 100644 index 000000000000..dc6c9b4340f0 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/package.json @@ -0,0 +1,40 @@ +{ + "name": "react-create-memory-router-test", + "version": "0.1.0", + "private": true, + "dependencies": { + "@sentry/react": "latest || *", + "@types/node": "^18.19.1", + "@types/react": "18.0.0", + "@types/react-dom": "18.0.0", + "react": "18.2.0", + "react-dom": "18.2.0", + "react-router-dom": "^6.4.1", + "react-scripts": "5.0.1", + "typescript": "~5.0.0" + }, + "scripts": { + "build": "react-scripts build", + "start": "serve -s build", + "test": "playwright test", + "clean": "npx rimraf node_modules pnpm-lock.yaml", + "test:build": "pnpm install && pnpm build", + "test:build-canary": "pnpm install && pnpm add react@canary react-dom@canary && pnpm build", + "test:assert": "pnpm test" + }, + "eslintConfig": { + "extends": ["react-app", "react-app/jest"] + }, + "browserslist": { + "production": [">0.2%", "not dead", "not op_mini all"], + "development": ["last 1 chrome version", "last 1 firefox version", "last 1 safari version"] + }, + "devDependencies": { + "@playwright/test": "^1.44.1", + "@sentry-internal/test-utils": "link:../../../test-utils", + "serve": "14.0.1" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/react-create-memory-router/playwright.config.mjs new file mode 100644 index 000000000000..31f2b913b58b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/playwright.config.mjs @@ -0,0 +1,7 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; + +const config = getPlaywrightConfig({ + startCommand: `pnpm start`, +}); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/public/index.html b/dev-packages/e2e-tests/test-applications/react-create-memory-router/public/index.html new file mode 100644 index 000000000000..39da76522bea --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/public/index.html @@ -0,0 +1,24 @@ + + + + + + + + React App + + + +
+ + + diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/globals.d.ts b/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/globals.d.ts new file mode 100644 index 000000000000..ffa61ca49acc --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/globals.d.ts @@ -0,0 +1,5 @@ +interface Window { + recordedTransactions?: string[]; + capturedExceptionId?: string; + sentryReplayId?: string; +} diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/index.tsx b/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/index.tsx new file mode 100644 index 000000000000..f71572f9dc1f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/index.tsx @@ -0,0 +1,65 @@ +import * as Sentry from '@sentry/react'; +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { + RouterProvider, + createMemoryRouter, + createRoutesFromChildren, + matchRoutes, + useLocation, + useNavigationType, +} from 'react-router-dom'; +import Index from './pages/Index'; +import User from './pages/User'; + +const replay = Sentry.replayIntegration(); + +Sentry.init({ + // environment: 'qa', // dynamic sampling bias to keep transactions + dsn: process.env.REACT_APP_E2E_TEST_DSN, + integrations: [ + Sentry.reactRouterV6BrowserTracingIntegration({ + useEffect: React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + }), + replay, + ], + // We recommend adjusting this value in production, or using tracesSampler + // for finer control + tracesSampleRate: 1.0, + release: 'e2e-test', + + tunnel: 'http://localhost:3031', + + // Always capture replays, so we can test this properly + replaysSessionSampleRate: 1.0, + replaysOnErrorSampleRate: 0.0, + + debug: !!process.env.DEBUG, +}); + +const sentryCreateMemoryRouter = Sentry.wrapCreateMemoryRouterV6(createMemoryRouter); + +const router = sentryCreateMemoryRouter( + [ + { + path: '/', + element: , + }, + { + path: '/user/:id', + element: , + }, + ], + { + initialEntries: ['/', '/user/1', '/user/2'], + initialIndex: 2, + }, +); + +const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); + +root.render(); diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/pages/Index.tsx b/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/pages/Index.tsx new file mode 100644 index 000000000000..b025f721e100 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/pages/Index.tsx @@ -0,0 +1,19 @@ +// biome-ignore lint/nursery/noUnusedImports: Need React import for JSX +import * as React from 'react'; + +const Index = () => { + return ( + <> + { + throw new Error('I am an error!'); + }} + /> + + ); +}; + +export default Index; diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/pages/User.tsx b/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/pages/User.tsx new file mode 100644 index 000000000000..e54d6c604e2d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/pages/User.tsx @@ -0,0 +1,19 @@ +// biome-ignore lint/nursery/noUnusedImports: Need React import for JSX +import * as React from 'react'; +import { Link } from 'react-router-dom'; + +const User = () => { + return ( +
+ + Home + + + navigate + +

I am a blank page :)

; +
+ ); +}; + +export default User; diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/react-app-env.d.ts b/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/react-app-env.d.ts new file mode 100644 index 000000000000..6431bc5fc6b2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/react-app-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/react-create-memory-router/start-event-proxy.mjs new file mode 100644 index 000000000000..9c451610f4c7 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'react-create-memory-router', +}); diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/react-create-memory-router/tests/errors.test.ts new file mode 100644 index 000000000000..9406ca63e30c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/tests/errors.test.ts @@ -0,0 +1,34 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test('Captures exception correctly', async ({ page }) => { + const errorEventPromise = waitForError('react-create-memory-router', event => { + return !event.type && event.exception?.values?.[0]?.value === 'I am an error!'; + }); + + await page.goto('/'); + + // We're on the user page, navigate back to the home page + const homeButton = page.locator('id=home-button'); + await homeButton.click(); + + const exceptionButton = page.locator('id=exception-button'); + await exceptionButton.click(); + + const errorEvent = await errorEventPromise; + + expect(errorEvent.exception?.values).toHaveLength(1); + expect(errorEvent.exception?.values?.[0]?.value).toBe('I am an error!'); + + expect(errorEvent.request).toEqual({ + headers: expect.any(Object), + url: 'http://localhost:3030/', + }); + + expect(errorEvent.transaction).toEqual('/'); + + expect(errorEvent.contexts?.trace).toEqual({ + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/react-create-memory-router/tests/transactions.test.ts new file mode 100644 index 000000000000..7c75c395c3af --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/tests/transactions.test.ts @@ -0,0 +1,75 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('Captures a pageload transaction', async ({ page }) => { + const transactionEventPromise = waitForTransaction('react-create-memory-router', event => { + return event.contexts?.trace?.op === 'pageload'; + }); + + await page.goto('/'); + + const transactionEvent = await transactionEventPromise; + + expect(transactionEvent).toEqual( + expect.objectContaining({ + transaction: '/user/:id', + type: 'transaction', + transaction_info: { + source: 'route', + }, + }), + ); + + expect(transactionEvent.contexts?.trace).toEqual( + expect.objectContaining({ + data: expect.objectContaining({ + 'sentry.idle_span_finish_reason': 'idleTimeout', + 'sentry.op': 'pageload', + 'sentry.origin': 'auto.pageload.react.reactrouter_v6', + 'sentry.sample_rate': 1, + 'sentry.source': 'route', + }), + op: 'pageload', + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + origin: 'auto.pageload.react.reactrouter_v6', + }), + ); +}); + +test('Captures a navigation transaction', async ({ page }) => { + const transactionEventPromise = waitForTransaction('react-create-memory-router', event => { + return event.contexts?.trace?.op === 'navigation'; + }); + + await page.goto('/'); + const linkElement = page.locator('id=navigation-button'); + await linkElement.click(); + + const transactionEvent = await transactionEventPromise; + expect(transactionEvent.contexts?.trace).toEqual({ + data: expect.objectContaining({ + 'sentry.idle_span_finish_reason': 'idleTimeout', + 'sentry.op': 'navigation', + 'sentry.origin': 'auto.navigation.react.reactrouter_v6', + 'sentry.sample_rate': 1, + 'sentry.source': 'route', + }), + op: 'navigation', + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + origin: 'auto.navigation.react.reactrouter_v6', + }); + + expect(transactionEvent).toEqual( + expect.objectContaining({ + transaction: '/user/:id', + type: 'transaction', + transaction_info: { + source: 'route', + }, + }), + ); + + expect(transactionEvent.spans).toEqual([]); +}); diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/tsconfig.json b/dev-packages/e2e-tests/test-applications/react-create-memory-router/tsconfig.json new file mode 100644 index 000000000000..4cc95dc2689a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es2018", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react" + }, + "include": ["src", "tests"] +} diff --git a/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/src/index.tsx b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/src/index.tsx index f6694a954915..581014169a78 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/src/index.tsx +++ b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/src/index.tsx @@ -3,6 +3,7 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import { BrowserRouter, + Outlet, Route, Routes, createRoutesFromChildren, @@ -48,17 +49,28 @@ const DetailsRoutes = () => ( ); +const DetailsRoutesAlternative = () => ( + + Details} /> + +); + const ViewsRoutes = () => ( Views} /> } /> + } /> ); const ProjectsRoutes = () => ( - }> - No Match Page} /> + }> + Project Page Root} /> + }> + } /> + + ); @@ -67,7 +79,7 @@ root.render( } /> - }> + } /> , ); diff --git a/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/src/pages/Index.tsx b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/src/pages/Index.tsx index aa99b61f89ea..d2362c149f84 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/src/pages/Index.tsx +++ b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/src/pages/Index.tsx @@ -8,6 +8,9 @@ const Index = () => { navigate + + navigate old + ); }; diff --git a/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/tests/transactions.test.ts index 23bc0aaabe95..2f13b7cc1eac 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/tests/transactions.test.ts @@ -10,6 +10,7 @@ test('sends a pageload transaction with a parameterized URL', async ({ page }) = const rootSpan = await transactionPromise; + expect((await page.innerHTML('#root')).includes('Details')).toBe(true); expect(rootSpan).toMatchObject({ contexts: { trace: { @@ -24,6 +25,30 @@ test('sends a pageload transaction with a parameterized URL', async ({ page }) = }); }); +test('sends a pageload transaction with a parameterized URL - alternative route', async ({ page }) => { + const transactionPromise = waitForTransaction('react-router-6-descendant-routes', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + await page.goto(`/projects/234/old-views/234/567`); + + const rootSpan = await transactionPromise; + + expect((await page.innerHTML('#root')).includes('Details')).toBe(true); + expect(rootSpan).toMatchObject({ + contexts: { + trace: { + op: 'pageload', + origin: 'auto.pageload.react.reactrouter_v6', + }, + }, + transaction: '/projects/:projectId/old-views/:viewId/:detailId', + transaction_info: { + source: 'route', + }, + }); +}); + test('sends a navigation transaction with a parameterized URL', async ({ page }) => { const pageloadTxnPromise = waitForTransaction('react-router-6-descendant-routes', async transactionEvent => { return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; @@ -52,6 +77,8 @@ test('sends a navigation transaction with a parameterized URL', async ({ page }) const linkElement = page.locator('id=navigation'); const [_, navigationTxn] = await Promise.all([linkElement.click(), navigationTxnPromise]); + + expect((await page.innerHTML('#root')).includes('Details')).toBe(true); expect(navigationTxn).toMatchObject({ contexts: { trace: { @@ -65,3 +92,47 @@ test('sends a navigation transaction with a parameterized URL', async ({ page }) }, }); }); + +test('sends a navigation transaction with a parameterized URL - alternative route', async ({ page }) => { + const pageloadTxnPromise = waitForTransaction('react-router-6-descendant-routes', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + const navigationTxnPromise = waitForTransaction('react-router-6-descendant-routes', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/`); + const pageloadTxn = await pageloadTxnPromise; + + expect(pageloadTxn).toMatchObject({ + contexts: { + trace: { + op: 'pageload', + origin: 'auto.pageload.react.reactrouter_v6', + }, + }, + transaction: '/', + transaction_info: { + source: 'route', + }, + }); + + const linkElement = page.locator('id=old-navigation'); + + const [_, navigationTxn] = await Promise.all([linkElement.click(), navigationTxnPromise]); + + expect((await page.innerHTML('#root')).includes('Details')).toBe(true); + expect(navigationTxn).toMatchObject({ + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.react.reactrouter_v6', + }, + }, + transaction: '/projects/:projectId/old-views/:viewId/:detailId', + transaction_info: { + source: 'route', + }, + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/vue-3/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/vue-3/tests/errors.test.ts index 262cda11b366..b86e56eb4b83 100644 --- a/dev-packages/e2e-tests/test-applications/vue-3/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/vue-3/tests/errors.test.ts @@ -19,7 +19,7 @@ test('sends an error', async ({ page }) => { type: 'Error', value: 'This is a Vue test error', mechanism: { - type: 'generic', + type: 'vue', handled: false, }, }, @@ -47,7 +47,7 @@ test('sends an error with a parameterized transaction name', async ({ page }) => type: 'Error', value: 'This is a Vue test error', mechanism: { - type: 'generic', + type: 'vue', handled: false, }, }, diff --git a/dev-packages/external-contributor-gh-action/package.json b/dev-packages/external-contributor-gh-action/package.json index a4857bd41082..0a748440c262 100644 --- a/dev-packages/external-contributor-gh-action/package.json +++ b/dev-packages/external-contributor-gh-action/package.json @@ -1,7 +1,7 @@ { "name": "@sentry-internal/external-contributor-gh-action", "description": "An internal Github Action to add external contributors to the CHANGELOG.md file.", - "version": "8.45.0", + "version": "8.50.0", "license": "MIT", "engines": { "node": ">=18" diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index 24be97583c1c..c9f306d24bb8 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/node-integration-tests", - "version": "8.45.0", + "version": "8.50.0", "license": "MIT", "engines": { "node": ">=14.18" @@ -30,10 +30,10 @@ "@nestjs/common": "10.4.6", "@nestjs/core": "10.4.6", "@nestjs/platform-express": "10.4.6", - "@prisma/client": "5.9.1", - "@sentry/aws-serverless": "8.45.0", - "@sentry/core": "8.45.0", - "@sentry/node": "8.45.0", + "@prisma/client": "5.22.0", + "@sentry/aws-serverless": "8.50.0", + "@sentry/core": "8.50.0", + "@sentry/node": "8.50.0", "@types/mongodb": "^3.6.20", "@types/mysql": "^2.15.21", "@types/pg": "^8.6.5", diff --git a/dev-packages/node-integration-tests/suites/anr/app-path.mjs b/dev-packages/node-integration-tests/suites/anr/app-path.mjs new file mode 100644 index 000000000000..b7d32e1aa9b2 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/anr/app-path.mjs @@ -0,0 +1,36 @@ +import * as assert from 'assert'; +import * as crypto from 'crypto'; +import * as path from 'path'; +import * as url from 'url'; + +import * as Sentry from '@sentry/node'; + +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', + autoSessionTracking: false, + integrations: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100, appRootPath: __dirname })], +}); + +Sentry.setUser({ email: 'person@home.com' }); +Sentry.addBreadcrumb({ message: 'important message!' }); + +function longWork() { + for (let i = 0; i < 20; 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/anr/basic-multiple.mjs b/dev-packages/node-integration-tests/suites/anr/basic-multiple.mjs new file mode 100644 index 000000000000..f58eb87f8237 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/anr/basic-multiple.mjs @@ -0,0 +1,36 @@ +import * as assert from 'assert'; +import * as crypto from 'crypto'; + +import * as Sentry from '@sentry/node'; + +global._sentryDebugIds = { [new Error().stack]: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa' }; + +setTimeout(() => { + process.exit(); +}, 10000); + +Sentry.init({ + dsn: process.env.SENTRY_DSN, + release: '1.0', + autoSessionTracking: false, + integrations: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100, maxAnrEvents: 2 })], +}); + +Sentry.setUser({ email: 'person@home.com' }); +Sentry.addBreadcrumb({ message: 'important message!' }); + +function longWork() { + for (let i = 0; i < 20; i++) { + const salt = crypto.randomBytes(128).toString('base64'); + const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512'); + assert.ok(hash); + } +} + +setTimeout(() => { + longWork(); +}, 1000); + +setTimeout(() => { + longWork(); +}, 4000); diff --git a/dev-packages/node-integration-tests/suites/anr/basic.mjs b/dev-packages/node-integration-tests/suites/anr/basic.mjs index 18777e5ecdbd..454a35605925 100644 --- a/dev-packages/node-integration-tests/suites/anr/basic.mjs +++ b/dev-packages/node-integration-tests/suites/anr/basic.mjs @@ -30,3 +30,8 @@ function longWork() { setTimeout(() => { longWork(); }, 1000); + +// Ensure we only send one event even with multiple blocking events +setTimeout(() => { + longWork(); +}, 4000); diff --git a/dev-packages/node-integration-tests/suites/anr/test.ts b/dev-packages/node-integration-tests/suites/anr/test.ts index b1750b308d28..cd7df2a86314 100644 --- a/dev-packages/node-integration-tests/suites/anr/test.ts +++ b/dev-packages/node-integration-tests/suites/anr/test.ts @@ -31,20 +31,20 @@ const ANR_EVENT = { mechanism: { type: 'ANR' }, 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, - }, + }), ]), }, }, @@ -101,7 +101,7 @@ const ANR_EVENT_WITH_DEBUG_META: Event = { { type: 'sourcemap', debug_id: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa', - code_file: expect.stringContaining('basic.'), + code_file: expect.stringContaining('basic'), }, ], }, @@ -123,6 +123,34 @@ conditionalTest({ min: 16 })('should report ANR when event loop blocked', () => .start(done); }); + test('Custom appRootPath', done => { + const ANR_EVENT_WITH_SPECIFIC_DEBUG_META: Event = { + ...ANR_EVENT_WITH_SCOPE, + debug_meta: { + images: [ + { + type: 'sourcemap', + debug_id: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa', + code_file: 'app:///app-path.mjs', + }, + ], + }, + }; + + createRunner(__dirname, 'app-path.mjs') + .withMockSentryServer() + .expect({ event: ANR_EVENT_WITH_SPECIFIC_DEBUG_META }) + .start(done); + }); + + test('multiple events via maxAnrEvents', done => { + createRunner(__dirname, 'basic-multiple.mjs') + .withMockSentryServer() + .expect({ event: ANR_EVENT_WITH_DEBUG_META }) + .expect({ event: ANR_EVENT_WITH_DEBUG_META }) + .start(done); + }); + test('blocked indefinitely', done => { createRunner(__dirname, 'indefinite.mjs').withMockSentryServer().expect({ event: ANR_EVENT }).start(done); }); diff --git a/dev-packages/node-integration-tests/suites/contextLines/instrument.mjs b/dev-packages/node-integration-tests/suites/contextLines/filename-with-spaces/instrument.mjs similarity index 100% rename from dev-packages/node-integration-tests/suites/contextLines/instrument.mjs rename to dev-packages/node-integration-tests/suites/contextLines/filename-with-spaces/instrument.mjs diff --git a/dev-packages/node-integration-tests/suites/contextLines/scenario with space.cjs b/dev-packages/node-integration-tests/suites/contextLines/filename-with-spaces/scenario with space.cjs similarity index 100% rename from dev-packages/node-integration-tests/suites/contextLines/scenario with space.cjs rename to dev-packages/node-integration-tests/suites/contextLines/filename-with-spaces/scenario with space.cjs diff --git a/dev-packages/node-integration-tests/suites/contextLines/scenario with space.mjs b/dev-packages/node-integration-tests/suites/contextLines/filename-with-spaces/scenario with space.mjs similarity index 100% rename from dev-packages/node-integration-tests/suites/contextLines/scenario with space.mjs rename to dev-packages/node-integration-tests/suites/contextLines/filename-with-spaces/scenario with space.mjs diff --git a/dev-packages/node-integration-tests/suites/contextLines/test.ts b/dev-packages/node-integration-tests/suites/contextLines/filename-with-spaces/test.ts similarity index 96% rename from dev-packages/node-integration-tests/suites/contextLines/test.ts rename to dev-packages/node-integration-tests/suites/contextLines/filename-with-spaces/test.ts index 1912f0b57f04..87f3c1f6fda0 100644 --- a/dev-packages/node-integration-tests/suites/contextLines/test.ts +++ b/dev-packages/node-integration-tests/suites/contextLines/filename-with-spaces/test.ts @@ -1,6 +1,6 @@ import { join } from 'path'; -import { conditionalTest } from '../../utils'; -import { createRunner } from '../../utils/runner'; +import { conditionalTest } from '../../../utils'; +import { createRunner } from '../../../utils/runner'; conditionalTest({ min: 18 })('ContextLines integration in ESM', () => { test('reads encoded context lines from filenames with spaces', done => { diff --git a/dev-packages/node-integration-tests/suites/contextLines/memory-leak/nested-file.ts b/dev-packages/node-integration-tests/suites/contextLines/memory-leak/nested-file.ts new file mode 100644 index 000000000000..47aec48484b7 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/contextLines/memory-leak/nested-file.ts @@ -0,0 +1,5 @@ +import * as Sentry from '@sentry/node'; + +export function captureException(i: number): void { + Sentry.captureException(new Error(`error in loop ${i}`)); +} diff --git a/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts b/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts new file mode 100644 index 000000000000..c48fae3e2e2e --- /dev/null +++ b/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts @@ -0,0 +1,7 @@ +import { captureException } from './nested-file'; + +export function runSentry(): void { + for (let i = 0; i < 10; i++) { + captureException(i); + } +} diff --git a/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts b/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts new file mode 100644 index 000000000000..0ca16a75fae2 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts @@ -0,0 +1,30 @@ +import { execSync } from 'node:child_process'; +import * as path from 'node:path'; + +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +import { runSentry } from './other-file'; + +runSentry(); + +const lsofOutput = execSync(`lsof -p ${process.pid}`, { encoding: 'utf8' }); +const lsofTable = lsofOutput.split('\n'); +const mainPath = __dirname.replace(`${path.sep}suites${path.sep}contextLines${path.sep}memory-leak`, ''); +const numberOfLsofEntriesWithMainPath = lsofTable.filter(entry => entry.includes(mainPath)); + +// There should only be a single entry with the main path, otherwise we are leaking file handles from the +// context lines integration. +if (numberOfLsofEntriesWithMainPath.length > 1) { + // eslint-disable-next-line no-console + console.error('Leaked file handles detected'); + // eslint-disable-next-line no-console + console.error(lsofTable); + process.exit(1); +} diff --git a/dev-packages/node-integration-tests/suites/contextLines/memory-leak/test.ts b/dev-packages/node-integration-tests/suites/contextLines/memory-leak/test.ts new file mode 100644 index 000000000000..a8437163de07 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/contextLines/memory-leak/test.ts @@ -0,0 +1,17 @@ +import { conditionalTest } from '../../../utils'; +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +conditionalTest({ min: 18 })('ContextLines integration in CJS', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + // Regression test for: https://github.com/getsentry/sentry-javascript/issues/14892 + test('does not leak open file handles', done => { + createRunner(__dirname, 'scenario.ts') + .expectN(10, { + event: {}, + }) + .start(done); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/express/requestUser/server.js b/dev-packages/node-integration-tests/suites/express/requestUser/server.js new file mode 100644 index 000000000000..d93d22905506 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express/requestUser/server.js @@ -0,0 +1,49 @@ +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); +const Sentry = require('@sentry/node'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, + debug: true, +}); + +// express must be required after Sentry is initialized +const express = require('express'); +const cors = require('cors'); +const { startExpressServerAndSendPortToRunner } = require('@sentry-internal/node-integration-tests'); + +const app = express(); + +app.use(cors()); + +app.use((req, _res, next) => { + // We simulate this, which would in other cases be done by some middleware + req.user = { + id: '1', + email: 'test@sentry.io', + }; + + next(); +}); + +app.get('/test1', (_req, _res) => { + throw new Error('error_1'); +}); + +app.use((_req, _res, next) => { + Sentry.setUser({ + id: '2', + email: 'test2@sentry.io', + }); + + next(); +}); + +app.get('/test2', (_req, _res) => { + throw new Error('error_2'); +}); + +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/requestUser/test.ts b/dev-packages/node-integration-tests/suites/express/requestUser/test.ts new file mode 100644 index 000000000000..ff32e2b96c89 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express/requestUser/test.ts @@ -0,0 +1,49 @@ +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +describe('express user handling', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + test('picks user from request', done => { + createRunner(__dirname, 'server.js') + .expect({ + event: { + user: { + id: '1', + email: 'test@sentry.io', + }, + exception: { + values: [ + { + value: 'error_1', + }, + ], + }, + }, + }) + .start(done) + .makeRequest('get', '/test1', { expectError: true }); + }); + + test('setUser overwrites user from request', done => { + createRunner(__dirname, 'server.js') + .expect({ + event: { + user: { + id: '2', + email: 'test2@sentry.io', + }, + exception: { + values: [ + { + value: 'error_2', + }, + ], + }, + }, + }) + .start(done) + .makeRequest('get', '/test2', { expectError: true }); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/express/tracing/tracesSampler/scenario-normalizedRequest.js b/dev-packages/node-integration-tests/suites/express/tracing/tracesSampler/scenario-normalizedRequest.js new file mode 100644 index 000000000000..da31780f2c5f --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express/tracing/tracesSampler/scenario-normalizedRequest.js @@ -0,0 +1,34 @@ +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); +const Sentry = require('@sentry/node'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, + tracesSampler: samplingContext => { + // The sampling decision is based on whether the data in `normalizedRequest` is available --> this is what we want to test for + return ( + samplingContext.normalizedRequest.url.includes('/test-normalized-request?query=123') && + samplingContext.normalizedRequest.method && + samplingContext.normalizedRequest.query_string === 'query=123' && + !!samplingContext.normalizedRequest.headers + ); + }, +}); + +// express must be required after Sentry is initialized +const express = require('express'); +const cors = require('cors'); +const { startExpressServerAndSendPortToRunner } = require('@sentry-internal/node-integration-tests'); + +const app = express(); + +app.use(cors()); + +app.get('/test-normalized-request', (_req, res) => { + res.send('Success'); +}); + +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/tracing/tracesSampler/server.js b/dev-packages/node-integration-tests/suites/express/tracing/tracesSampler/server.js index c096871cb755..b60ea07b636f 100644 --- a/dev-packages/node-integration-tests/suites/express/tracing/tracesSampler/server.js +++ b/dev-packages/node-integration-tests/suites/express/tracing/tracesSampler/server.js @@ -15,7 +15,6 @@ Sentry.init({ samplingContext.attributes['http.method'] === 'GET' ); }, - debug: true, }); // express must be required after Sentry is initialized diff --git a/dev-packages/node-integration-tests/suites/express/tracing/tracesSampler/test.ts b/dev-packages/node-integration-tests/suites/express/tracing/tracesSampler/test.ts index a19299787f91..07cc8d094d8f 100644 --- a/dev-packages/node-integration-tests/suites/express/tracing/tracesSampler/test.ts +++ b/dev-packages/node-integration-tests/suites/express/tracing/tracesSampler/test.ts @@ -22,3 +22,23 @@ describe('express tracesSampler', () => { }); }); }); + +describe('express tracesSampler includes normalizedRequest data', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + describe('CJS', () => { + test('correctly samples & passes data to tracesSampler', done => { + const runner = createRunner(__dirname, 'scenario-normalizedRequest.js') + .expect({ + transaction: { + transaction: 'GET /test-normalized-request', + }, + }) + .start(done); + + runner.makeRequest('get', '/test-normalized-request?query=123'); + }); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/express/tracing/updateName/server.js b/dev-packages/node-integration-tests/suites/express/tracing/updateName/server.js new file mode 100644 index 000000000000..c98e17276d92 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express/tracing/updateName/server.js @@ -0,0 +1,58 @@ +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); +const Sentry = require('@sentry/node'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + // disable attaching headers to /test/* endpoints + tracePropagationTargets: [/^(?!.*test).*$/], + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +// express must be required after Sentry is initialized +const express = require('express'); +const cors = require('cors'); +const bodyParser = require('body-parser'); +const { startExpressServerAndSendPortToRunner } = require('@sentry-internal/node-integration-tests'); + +const app = express(); + +app.use(cors()); +app.use(bodyParser.json()); +app.use(bodyParser.text()); +app.use(bodyParser.raw()); + +app.get('/test/:id/span-updateName', (_req, res) => { + const span = Sentry.getActiveSpan(); + const rootSpan = Sentry.getRootSpan(span); + rootSpan.updateName('new-name'); + res.send({ response: 'response 1' }); +}); + +app.get('/test/:id/span-updateName-source', (_req, res) => { + const span = Sentry.getActiveSpan(); + const rootSpan = Sentry.getRootSpan(span); + rootSpan.updateName('new-name'); + rootSpan.setAttribute(Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'custom'); + res.send({ response: 'response 2' }); +}); + +app.get('/test/:id/updateSpanName', (_req, res) => { + const span = Sentry.getActiveSpan(); + const rootSpan = Sentry.getRootSpan(span); + Sentry.updateSpanName(rootSpan, 'new-name'); + res.send({ response: 'response 3' }); +}); + +app.get('/test/:id/updateSpanNameAndSource', (_req, res) => { + const span = Sentry.getActiveSpan(); + const rootSpan = Sentry.getRootSpan(span); + Sentry.updateSpanName(rootSpan, 'new-name'); + rootSpan.setAttribute(Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'component'); + res.send({ response: 'response 4' }); +}); + +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/tracing/updateName/test.ts b/dev-packages/node-integration-tests/suites/express/tracing/updateName/test.ts new file mode 100644 index 000000000000..c6345713fd7e --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express/tracing/updateName/test.ts @@ -0,0 +1,94 @@ +import { SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/node'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +describe('express tracing', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + describe('CJS', () => { + // This test documents the unfortunate behaviour of using `span.updateName` on the server-side. + // For http.server root spans (which is the root span on the server 99% of the time), Otel's http instrumentation + // calls `span.updateName` and overwrites whatever the name was set to before (by us or by users). + test("calling just `span.updateName` doesn't update the final name in express (missing source)", done => { + createRunner(__dirname, 'server.js') + .expect({ + transaction: { + transaction: 'GET /test/:id/span-updateName', + transaction_info: { + source: 'route', + }, + }, + }) + .start(done) + .makeRequest('get', '/test/123/span-updateName'); + }); + + // Also calling `updateName` AND setting a source doesn't change anything - Otel has no concept of source, this is sentry-internal. + // Therefore, only the source is updated but the name is still overwritten by Otel. + test("calling `span.updateName` and setting attribute source doesn't update the final name in express but it updates the source", done => { + createRunner(__dirname, 'server.js') + .expect({ + transaction: { + transaction: 'GET /test/:id/span-updateName-source', + transaction_info: { + source: 'custom', + }, + }, + }) + .start(done) + .makeRequest('get', '/test/123/span-updateName-source'); + }); + + // This test documents the correct way to update the span name (and implicitly the source) in Node: + test('calling `Sentry.updateSpanName` updates the final name and source in express', done => { + createRunner(__dirname, 'server.js') + .expect({ + transaction: txnEvent => { + expect(txnEvent).toMatchObject({ + transaction: 'new-name', + transaction_info: { + source: 'custom', + }, + contexts: { + trace: { + op: 'http.server', + data: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom' }, + }, + }, + }); + // ensure we delete the internal attribute once we're done with it + expect(txnEvent.contexts?.trace?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]).toBeUndefined(); + }, + }) + .start(done) + .makeRequest('get', '/test/123/updateSpanName'); + }); + }); + + // This test documents the correct way to update the span name (and implicitly the source) in Node: + test('calling `Sentry.updateSpanName` and setting source subsequently updates the final name and sets correct source', done => { + createRunner(__dirname, 'server.js') + .expect({ + transaction: txnEvent => { + expect(txnEvent).toMatchObject({ + transaction: 'new-name', + transaction_info: { + source: 'component', + }, + contexts: { + trace: { + op: 'http.server', + data: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component' }, + }, + }, + }); + // ensure we delete the internal attribute once we're done with it + expect(txnEvent.contexts?.trace?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]).toBeUndefined(); + }, + }) + .start(done) + .makeRequest('get', '/test/123/updateSpanNameAndSource'); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage/test.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage/test.ts index 86b3bf6d9d22..0370b123cab2 100644 --- a/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage/test.ts @@ -1,11 +1,42 @@ +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/node'; import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; afterAll(() => { cleanupChildProcesses(); }); -test('should send a manually started root span', done => { +test('sends a manually started root span with source custom', done => { createRunner(__dirname, 'scenario.ts') - .expect({ transaction: { transaction: 'test_span' } }) + .expect({ + transaction: { + transaction: 'test_span', + transaction_info: { source: 'custom' }, + contexts: { + trace: { + span_id: expect.any(String), + trace_id: expect.any(String), + data: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom' }, + }, + }, + }, + }) + .start(done); +}); + +test("doesn't change the name for manually started spans even if attributes triggering inference are set", done => { + createRunner(__dirname, 'scenario.ts') + .expect({ + transaction: { + transaction: 'test_span', + transaction_info: { source: 'custom' }, + contexts: { + trace: { + span_id: expect.any(String), + trace_id: expect.any(String), + data: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom' }, + }, + }, + }, + }) .start(done); }); diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/updateName-method/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/updateName-method/scenario.ts new file mode 100644 index 000000000000..ea30608c1c5c --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/updateName-method/scenario.ts @@ -0,0 +1,16 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +Sentry.startSpan( + { name: 'test_span', attributes: { [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' } }, + (span: Sentry.Span) => { + span.updateName('new name'); + }, +); diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/updateName-method/test.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/updateName-method/test.ts new file mode 100644 index 000000000000..676071926b91 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/updateName-method/test.ts @@ -0,0 +1,24 @@ +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/node'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('updates the span name when calling `span.updateName`', done => { + createRunner(__dirname, 'scenario.ts') + .expect({ + transaction: { + transaction: 'new name', + transaction_info: { source: 'url' }, + contexts: { + trace: { + span_id: expect.any(String), + trace_id: expect.any(String), + data: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' }, + }, + }, + }, + }) + .start(done); +}); diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/updateSpanName-function/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/updateSpanName-function/scenario.ts new file mode 100644 index 000000000000..ecf7670fa23d --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/updateSpanName-function/scenario.ts @@ -0,0 +1,16 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +Sentry.startSpan( + { name: 'test_span', attributes: { [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' } }, + (span: Sentry.Span) => { + Sentry.updateSpanName(span, 'new name'); + }, +); diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/updateSpanName-function/test.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/updateSpanName-function/test.ts new file mode 100644 index 000000000000..c5b325fc0ea2 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/updateSpanName-function/test.ts @@ -0,0 +1,24 @@ +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/node'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('updates the span name and source when calling `updateSpanName`', done => { + createRunner(__dirname, 'scenario.ts') + .expect({ + transaction: { + transaction: 'new name', + transaction_info: { source: 'custom' }, + contexts: { + trace: { + span_id: expect.any(String), + trace_id: expect.any(String), + data: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom' }, + }, + }, + }, + }) + .start(done); +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/package.json b/dev-packages/node-integration-tests/suites/tracing/prisma-orm/package.json index b40c92b4356e..70e2c29be629 100644 --- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/package.json +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm/package.json @@ -16,7 +16,7 @@ "author": "", "license": "ISC", "dependencies": { - "@prisma/client": "5.9.1", - "prisma": "^5.9.1" + "@prisma/client": "5.22.0", + "prisma": "5.22.0" } } diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/test.ts b/dev-packages/node-integration-tests/suites/tracing/prisma-orm/test.ts index dd92de5d0292..4cc1757c0d19 100644 --- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm/test.ts @@ -3,103 +3,137 @@ import { createRunner } from '../../../utils/runner'; conditionalTest({ min: 16 })('Prisma ORM Tests', () => { test('CJS - should instrument PostgreSQL queries from Prisma ORM', done => { - const EXPECTED_TRANSACTION = { - transaction: 'Test Transaction', - spans: expect.arrayContaining([ - expect.objectContaining({ - data: { - method: 'create', - model: 'User', - name: 'User.create', - 'sentry.origin': 'auto.db.otel.prisma', - }, - description: 'prisma:client:operation', - status: 'ok', - }), - expect.objectContaining({ - data: { - 'sentry.origin': 'auto.db.otel.prisma', - }, - description: 'prisma:client:serialize', - status: 'ok', - }), - expect.objectContaining({ - data: { - 'sentry.origin': 'auto.db.otel.prisma', - }, - description: 'prisma:client:connect', - status: 'ok', - }), - expect.objectContaining({ - data: { - 'sentry.origin': 'auto.db.otel.prisma', - }, - description: 'prisma:engine', - status: 'ok', - }), - expect.objectContaining({ - data: { - 'db.type': 'postgres', - 'sentry.origin': 'auto.db.otel.prisma', - }, - description: 'prisma:engine:connection', - status: 'ok', - }), - expect.objectContaining({ - data: { - 'db.statement': expect.stringContaining( - 'INSERT INTO "public"."User" ("createdAt","email","name") VALUES ($1,$2,$3) RETURNING "public"."User"."id", "public"."User"."createdAt", "public"."User"."email", "public"."User"."name" /* traceparent', - ), - 'sentry.origin': 'auto.db.otel.prisma', - 'db.system': 'prisma', - 'sentry.op': 'db', - }, - description: expect.stringContaining( - 'INSERT INTO "public"."User" ("createdAt","email","name") VALUES ($1,$2,$3) RETURNING "public"."User"."id", "public"."User"."createdAt", "public"."User"."email", "public"."User"."name" /* traceparent', - ), - status: 'ok', - }), - expect.objectContaining({ - data: { - 'sentry.origin': 'auto.db.otel.prisma', - }, - description: 'prisma:engine:serialize', - status: 'ok', - }), - expect.objectContaining({ - data: { - 'sentry.origin': 'auto.db.otel.prisma', - }, - description: 'prisma:engine:response_json_serialization', - status: 'ok', - }), - expect.objectContaining({ - data: { - method: 'findMany', - model: 'User', - name: 'User.findMany', - 'sentry.origin': 'auto.db.otel.prisma', - }, - description: 'prisma:client:operation', - status: 'ok', - }), - expect.objectContaining({ - data: { - 'sentry.origin': 'auto.db.otel.prisma', - }, - description: 'prisma:client:serialize', - status: 'ok', - }), - expect.objectContaining({ - data: { - 'sentry.origin': 'auto.db.otel.prisma', - }, - description: 'prisma:engine', - status: 'ok', - }), - ]), - }; + createRunner(__dirname, 'scenario.js') + .expect({ + transaction: transaction => { + expect(transaction.transaction).toBe('Test Transaction'); - createRunner(__dirname, 'scenario.js').expect({ transaction: EXPECTED_TRANSACTION }).start(done); + const spans = transaction.spans || []; + expect(spans.length).toBeGreaterThanOrEqual(5); + + expect(spans).toContainEqual( + expect.objectContaining({ + data: { + method: 'create', + model: 'User', + name: 'User.create', + 'sentry.origin': 'auto.db.otel.prisma', + }, + description: 'prisma:client:operation', + status: 'ok', + }), + ); + + expect(spans).toContainEqual( + expect.objectContaining({ + data: { + 'sentry.origin': 'auto.db.otel.prisma', + }, + description: 'prisma:client:serialize', + status: 'ok', + }), + ); + + expect(spans).toContainEqual( + expect.objectContaining({ + data: { + 'sentry.origin': 'auto.db.otel.prisma', + }, + description: 'prisma:client:connect', + status: 'ok', + }), + ); + + expect(spans).toContainEqual( + expect.objectContaining({ + data: { + 'sentry.origin': 'auto.db.otel.prisma', + }, + description: 'prisma:engine', + status: 'ok', + }), + ); + expect(spans).toContainEqual( + expect.objectContaining({ + data: { + 'sentry.origin': 'auto.db.otel.prisma', + 'sentry.op': 'db', + 'db.system': 'postgresql', + }, + description: 'prisma:engine:connection', + status: 'ok', + op: 'db', + }), + ); + + expect(spans).toContainEqual( + expect.objectContaining({ + data: { + 'db.statement': expect.stringContaining( + 'INSERT INTO "public"."User" ("createdAt","email","name") VALUES ($1,$2,$3) RETURNING "public"."User"."id", "public"."User"."createdAt", "public"."User"."email", "public"."User"."name" /* traceparent', + ), + 'sentry.origin': 'auto.db.otel.prisma', + 'sentry.op': 'db', + 'db.system': 'postgresql', + 'otel.kind': 'CLIENT', + }, + description: expect.stringContaining( + 'INSERT INTO "public"."User" ("createdAt","email","name") VALUES ($1,$2,$3) RETURNING "public"."User"."id", "public"."User"."createdAt", "public"."User"."email", "public"."User"."name" /* traceparent', + ), + status: 'ok', + op: 'db', + }), + ); + expect(spans).toContainEqual( + expect.objectContaining({ + data: { + 'sentry.origin': 'auto.db.otel.prisma', + }, + description: 'prisma:engine:serialize', + status: 'ok', + }), + ); + expect(spans).toContainEqual( + expect.objectContaining({ + data: { + 'sentry.origin': 'auto.db.otel.prisma', + }, + description: 'prisma:engine:response_json_serialization', + status: 'ok', + }), + ); + expect(spans).toContainEqual( + expect.objectContaining({ + data: { + method: 'findMany', + model: 'User', + name: 'User.findMany', + 'sentry.origin': 'auto.db.otel.prisma', + }, + description: 'prisma:client:operation', + status: 'ok', + }), + ); + expect(spans).toContainEqual( + expect.objectContaining({ + data: { + 'sentry.origin': 'auto.db.otel.prisma', + }, + description: 'prisma:client:serialize', + status: 'ok', + }), + ); + expect(spans).toContainEqual( + expect.objectContaining({ + data: { + 'sentry.origin': 'auto.db.otel.prisma', + }, + description: 'prisma:engine', + status: 'ok', + }), + ); + }, + }) + .start(done); }); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/yarn.lock b/dev-packages/node-integration-tests/suites/tracing/prisma-orm/yarn.lock index 9c0fc47be4be..860aa032d6cc 100644 --- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/yarn.lock +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm/yarn.lock @@ -2,50 +2,57 @@ # yarn lockfile v1 -"@prisma/client@5.9.1": - version "5.9.1" - resolved "https://registry.yarnpkg.com/@prisma/client/-/client-5.9.1.tgz#d92bd2f7f006e0316cb4fda9d73f235965cf2c64" - integrity sha512-caSOnG4kxcSkhqC/2ShV7rEoWwd3XrftokxJqOCMVvia4NYV/TPtJlS9C2os3Igxw/Qyxumj9GBQzcStzECvtQ== - -"@prisma/debug@5.9.1": - version "5.9.1" - resolved "https://registry.yarnpkg.com/@prisma/debug/-/debug-5.9.1.tgz#906274e73d3267f88b69459199fa3c51cd9511a3" - integrity sha512-yAHFSFCg8KVoL0oRUno3m60GAjsUKYUDkQ+9BA2X2JfVR3kRVSJFc/GpQ2fSORi4pSHZR9orfM4UC9OVXIFFTA== - -"@prisma/engines-version@5.9.0-32.23fdc5965b1e05fc54e5f26ed3de66776b93de64": - version "5.9.0-32.23fdc5965b1e05fc54e5f26ed3de66776b93de64" - resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-5.9.0-32.23fdc5965b1e05fc54e5f26ed3de66776b93de64.tgz#54d2164f28d23e09d41cf9eb0bddbbe7f3aaa660" - integrity sha512-HFl7275yF0FWbdcNvcSRbbu9JCBSLMcurYwvWc8WGDnpu7APxQo2ONtZrUggU3WxLxUJ2uBX+0GOFIcJeVeOOQ== - -"@prisma/engines@5.9.1": - version "5.9.1" - resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-5.9.1.tgz#767539afc6f193a182d0495b30b027f61f279073" - integrity sha512-gkdXmjxQ5jktxWNdDA5aZZ6R8rH74JkoKq6LD5mACSvxd2vbqWeWIOV0Py5wFC8vofOYShbt6XUeCIUmrOzOnQ== +"@prisma/client@5.22.0": + version "5.22.0" + resolved "https://registry.yarnpkg.com/@prisma/client/-/client-5.22.0.tgz#da1ca9c133fbefe89e0da781c75e1c59da5f8802" + integrity sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA== + +"@prisma/debug@5.22.0": + version "5.22.0" + resolved "https://registry.yarnpkg.com/@prisma/debug/-/debug-5.22.0.tgz#58af56ed7f6f313df9fb1042b6224d3174bbf412" + integrity sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ== + +"@prisma/engines-version@5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2": + version "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2" + resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz#d534dd7235c1ba5a23bacd5b92cc0ca3894c28f4" + integrity sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ== + +"@prisma/engines@5.22.0": + version "5.22.0" + resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-5.22.0.tgz#28f3f52a2812c990a8b66eb93a0987816a5b6d84" + integrity sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA== dependencies: - "@prisma/debug" "5.9.1" - "@prisma/engines-version" "5.9.0-32.23fdc5965b1e05fc54e5f26ed3de66776b93de64" - "@prisma/fetch-engine" "5.9.1" - "@prisma/get-platform" "5.9.1" - -"@prisma/fetch-engine@5.9.1": - version "5.9.1" - resolved "https://registry.yarnpkg.com/@prisma/fetch-engine/-/fetch-engine-5.9.1.tgz#5d3b2c9af54a242e37b3f9561b59ab72f8e92818" - integrity sha512-l0goQOMcNVOJs1kAcwqpKq3ylvkD9F04Ioe1oJoCqmz05mw22bNAKKGWuDd3zTUoUZr97va0c/UfLNru+PDmNA== + "@prisma/debug" "5.22.0" + "@prisma/engines-version" "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2" + "@prisma/fetch-engine" "5.22.0" + "@prisma/get-platform" "5.22.0" + +"@prisma/fetch-engine@5.22.0": + version "5.22.0" + resolved "https://registry.yarnpkg.com/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz#4fb691b483a450c5548aac2f837b267dd50ef52e" + integrity sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA== dependencies: - "@prisma/debug" "5.9.1" - "@prisma/engines-version" "5.9.0-32.23fdc5965b1e05fc54e5f26ed3de66776b93de64" - "@prisma/get-platform" "5.9.1" - -"@prisma/get-platform@5.9.1": - version "5.9.1" - resolved "https://registry.yarnpkg.com/@prisma/get-platform/-/get-platform-5.9.1.tgz#a66bb46ab4d30db786c84150ef074ab0aad4549e" - integrity sha512-6OQsNxTyhvG+T2Ksr8FPFpuPeL4r9u0JF0OZHUBI/Uy9SS43sPyAIutt4ZEAyqWQt104ERh70EZedkHZKsnNbg== + "@prisma/debug" "5.22.0" + "@prisma/engines-version" "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2" + "@prisma/get-platform" "5.22.0" + +"@prisma/get-platform@5.22.0": + version "5.22.0" + resolved "https://registry.yarnpkg.com/@prisma/get-platform/-/get-platform-5.22.0.tgz#fc675bc9d12614ca2dade0506c9c4a77e7dddacd" + integrity sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q== dependencies: - "@prisma/debug" "5.9.1" + "@prisma/debug" "5.22.0" -prisma@^5.9.1: - version "5.9.1" - resolved "https://registry.yarnpkg.com/prisma/-/prisma-5.9.1.tgz#baa3dd635fbf71504980978f10f55ea11068f6aa" - integrity sha512-Hy/8KJZz0ELtkw4FnG9MS9rNWlXcJhf98Z2QMqi0QiVMoS8PzsBkpla0/Y5hTlob8F3HeECYphBjqmBxrluUrQ== +fsevents@2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +prisma@5.22.0: + version "5.22.0" + resolved "https://registry.yarnpkg.com/prisma/-/prisma-5.22.0.tgz#1f6717ff487cdef5f5799cc1010459920e2e6197" + integrity sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A== dependencies: - "@prisma/engines" "5.9.1" + "@prisma/engines" "5.22.0" + optionalDependencies: + fsevents "2.3.3" diff --git a/dev-packages/node-integration-tests/test.txt b/dev-packages/node-integration-tests/test.txt new file mode 100644 index 000000000000..64dae8790895 --- /dev/null +++ b/dev-packages/node-integration-tests/test.txt @@ -0,0 +1,213 @@ +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","ProcessAndThreadBreadcrumbs","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","ProcessAndThreadBreadcrumbs","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","ProcessAndThreadBreadcrumbs","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","ProcessAndThreadBreadcrumbs","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","ProcessAndThreadBreadcrumbs","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","ProcessAndThreadBreadcrumbs","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","ProcessAndThreadBreadcrumbs","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","ProcessAndThreadBreadcrumbs","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","ProcessAndThreadBreadcrumbs","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","ProcessAndThreadBreadcrumbs","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 bc4fb901e2db..a3fe726767b4 100644 --- a/dev-packages/node-integration-tests/utils/runner.ts +++ b/dev-packages/node-integration-tests/utils/runner.ts @@ -168,6 +168,12 @@ export function createRunner(...paths: string[]) { expectedEnvelopes.push(expected); return this; }, + expectN: function (n: number, expected: Expected) { + for (let i = 0; i < n; i++) { + expectedEnvelopes.push(expected); + } + return this; + }, expectHeader: function (expected: ExpectedEnvelopeHeader) { if (!expectedEnvelopeHeaders) { expectedEnvelopeHeaders = []; diff --git a/dev-packages/rollup-utils/package.json b/dev-packages/rollup-utils/package.json index 763f043da327..a180aaed32c3 100644 --- a/dev-packages/rollup-utils/package.json +++ b/dev-packages/rollup-utils/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/rollup-utils", - "version": "8.45.0", + "version": "8.50.0", "description": "Rollup utilities used at Sentry for the Sentry JavaScript SDK", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/rollup-utils", diff --git a/dev-packages/size-limit-gh-action/package.json b/dev-packages/size-limit-gh-action/package.json index c40c18645d6a..dc9f08e66405 100644 --- a/dev-packages/size-limit-gh-action/package.json +++ b/dev-packages/size-limit-gh-action/package.json @@ -1,7 +1,7 @@ { "name": "@sentry-internal/size-limit-gh-action", "description": "An internal Github Action to compare the current size of a PR against the one on develop.", - "version": "8.45.0", + "version": "8.50.0", "license": "MIT", "engines": { "node": ">=18" diff --git a/dev-packages/test-utils/package.json b/dev-packages/test-utils/package.json index 09ad4cf5a55d..2a613f4fd88f 100644 --- a/dev-packages/test-utils/package.json +++ b/dev-packages/test-utils/package.json @@ -1,6 +1,6 @@ { "private": true, - "version": "8.45.0", + "version": "8.50.0", "name": "@sentry-internal/test-utils", "author": "Sentry", "license": "MIT", @@ -45,7 +45,7 @@ }, "devDependencies": { "@playwright/test": "^1.44.1", - "@sentry/core": "8.45.0" + "@sentry/core": "8.50.0" }, "volta": { "extends": "../../package.json" diff --git a/docs/assets/run-release-workflow.png b/docs/assets/run-release-workflow.png new file mode 100644 index 000000000000..50af8d111fe8 Binary files /dev/null and b/docs/assets/run-release-workflow.png differ diff --git a/docs/changelog/v7.md b/docs/changelog/v7.md index e784702015e0..cef925871efa 100644 --- a/docs/changelog/v7.md +++ b/docs/changelog/v7.md @@ -3,13 +3,49 @@ Support for Sentry SDK v7 will be dropped soon. We recommend migrating to the latest version of the SDK. You can migrate from `v7` of the SDK to `v8` by following the [migration guide](../../MIGRATION.md#upgrading-from-7x-to-8x). +## 7.120.3 + +- fix(v7/publish): Ensure discontinued packages are published with `latest` tag (#14926) + +## 7.120.2 + +- fix(tracing-internal): Fix case when lrp keys offset is 0 (#14615) + +Work in this release contributed by @LubomirIgonda1. Thank you for your contribution! + +## 7.120.1 + +- fix(v7/cdn): Ensure `_sentryModuleMetadata` is not mangled (#14357) + +Work in this release contributed by @gilisho. Thank you for your contribution! + +## 7.120.0 + +- feat(v7/browser): Add moduleMetadataIntegration lazy loading support (#13822) + +Work in this release contributed by @gilisho. Thank you for your contribution! + +## 7.119.2 + +- chore(nextjs/v7): Bump rollup to 2.79.2 + +## 7.119.1 + +- fix(browser/v7): Ensure wrap() only returns functions (#13838 backport) + +Work in this release contributed by @legobeat. Thank you for your contribution! + +## 7.119.0 + +- backport(tracing): Report dropped spans for transactions (#13343) + ## 7.118.0 - fix(v7/bundle): Ensure CDN bundles do not overwrite `window.Sentry` (#12579) ## 7.117.0 -- feat(browser/v7): Publish browserprofling CDN bundle (#12224) +- feat(browser/v7): Publish browser profiling CDN bundle (#12224) - fix(v7/publish): Add `v7` tag to `@sentry/replay` (#12304) ## 7.116.0 diff --git a/docs/publishing-a-release.md b/docs/publishing-a-release.md index 7b88d6bd41b8..a3fd5b64f0ea 100644 --- a/docs/publishing-a-release.md +++ b/docs/publishing-a-release.md @@ -20,6 +20,21 @@ _These steps are only relevant to Sentry employees when preparing and publishing [@getsentry/releases-approvers](https://github.com/orgs/getsentry/teams/release-approvers) to approve the release. a. Once the release is completed, a sync from `master` ->` develop` will be automatically triggered +## Publishing a release for previous majors + +1. Run `yarn changelog` on the major branch (e.g. `v8`) and determine what version will be released (we use + [semver](https://semver.org)) +2. Create a branch, e.g. `changelog-8.45.1`, off the major branch (e.g. `v8`) +3. Update `CHANGELOG.md` to add an entry for the next release number and a list of changes since the + last release. (See details below.) +4. Open a PR with the title `meta(changelog): Update changelog for VERSION` against the major branch. +5. Once the PR is merged, open the [Prepare Release workflow](https://github.com/getsentry/sentry-javascript/actions/workflows/release.yml) and + fill in ![run-release-workflow.png](./assets/run-release-workflow.png) + 1. The major branch you want to release for, e.g. `v8` + 2. The version you want to release, e.g. `8.45.1` + 3. The major branch to merge into, e.g. `v8` +6. Run the release workflow + ## Updating the Changelog 1. Run `yarn changelog` and copy everything. diff --git a/lerna.json b/lerna.json index febf4090d555..a050816fc520 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "8.45.0", + "version": "8.50.0", "npmClient": "yarn" } diff --git a/packages/angular/package.json b/packages/angular/package.json index 06bb0492c2f7..3a4a17cf0fcb 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/angular", - "version": "8.45.0", + "version": "8.50.0", "description": "Official Sentry SDK for Angular", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/angular", @@ -21,8 +21,8 @@ "rxjs": "^6.5.5 || ^7.x" }, "dependencies": { - "@sentry/browser": "8.45.0", - "@sentry/core": "8.45.0", + "@sentry/browser": "8.50.0", + "@sentry/core": "8.50.0", "tslib": "^2.4.1" }, "devDependencies": { diff --git a/packages/angular/src/tracing.ts b/packages/angular/src/tracing.ts index c347a5e19b2e..a5b9391e6ee4 100644 --- a/packages/angular/src/tracing.ts +++ b/packages/angular/src/tracing.ts @@ -1,3 +1,5 @@ +// eslint-disable-next-line @typescript-eslint/consistent-type-imports +import { ElementRef } from '@angular/core'; import type { AfterViewInit, OnDestroy, OnInit } from '@angular/core'; import { Directive, Injectable, Input, NgModule } from '@angular/core'; import type { ActivatedRouteSnapshot, Event, RouterState } from '@angular/router'; @@ -235,10 +237,17 @@ export class TraceService implements OnDestroy { } } -const UNKNOWN_COMPONENT = 'unknown'; - /** - * A directive that can be used to capture initialization lifecycle of the whole component. + * Captures the initialization lifecycle of the component this directive is applied to. + * Specifically, this directive measures the time between `ngOnInit` and `ngAfterViewInit` + * of the component. + * + * Falls back to the component's selector if no name is provided. + * + * @example + * ```html + * + * ``` */ @Directive({ selector: '[trace]' }) export class TraceDirective implements OnInit, AfterViewInit { @@ -246,13 +255,19 @@ export class TraceDirective implements OnInit, AfterViewInit { private _tracingSpan?: Span; + public constructor(private readonly _host: ElementRef) {} + /** * Implementation of OnInit lifecycle method * @inheritdoc */ public ngOnInit(): void { if (!this.componentName) { - this.componentName = UNKNOWN_COMPONENT; + // Technically, the `trace` binding should always be provided. + // However, if it is incorrectly declared on the element without a + // value (e.g., ``), we fall back to using `tagName` + // (which is e.g. `APP-COMPONENT`). + this.componentName = this._host.nativeElement.tagName.toLowerCase(); } if (getActiveSpan()) { diff --git a/packages/astro/package.json b/packages/astro/package.json index 43c374a766cc..a705f918f9cb 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/astro", - "version": "8.45.0", + "version": "8.50.0", "description": "Official Sentry SDK for Astro", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/astro", @@ -56,9 +56,9 @@ "astro": ">=3.x || >=4.0.0-beta || >=5.x" }, "dependencies": { - "@sentry/browser": "8.45.0", - "@sentry/core": "8.45.0", - "@sentry/node": "8.45.0", + "@sentry/browser": "8.50.0", + "@sentry/core": "8.50.0", + "@sentry/node": "8.50.0", "@sentry/vite-plugin": "^2.22.6" }, "devDependencies": { diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index 7eca9de9a41a..ad7481816b4d 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -138,6 +138,7 @@ export { startSpanManual, tediousIntegration, trpcMiddleware, + updateSpanName, withActiveSpan, withIsolationScope, withMonitor, diff --git a/packages/astro/src/index.types.ts b/packages/astro/src/index.types.ts index ce87a51c3af7..cfcce38d624d 100644 --- a/packages/astro/src/index.types.ts +++ b/packages/astro/src/index.types.ts @@ -27,8 +27,6 @@ export declare function flush(timeout?: number | undefined): PromiseLike feedbackModalIntegration, getScreenshotIntegration: () => feedbackScreenshotIntegration, }); diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index e6f57c13fe6b..e6e089cbe04c 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -75,3 +75,4 @@ export { } from './integrations/featureFlags'; export { launchDarklyIntegration, buildLaunchDarklyFlagUsedHandler } from './integrations/featureFlags/launchdarkly'; export { openFeatureIntegration, OpenFeatureIntegrationHook } from './integrations/featureFlags/openfeature'; +export { unleashIntegration } from './integrations/featureFlags/unleash'; diff --git a/packages/browser/src/integrations/featureFlags/unleash/index.ts b/packages/browser/src/integrations/featureFlags/unleash/index.ts new file mode 100644 index 000000000000..934ff196ee95 --- /dev/null +++ b/packages/browser/src/integrations/featureFlags/unleash/index.ts @@ -0,0 +1 @@ +export { unleashIntegration } from './integration'; diff --git a/packages/browser/src/integrations/featureFlags/unleash/integration.ts b/packages/browser/src/integrations/featureFlags/unleash/integration.ts new file mode 100644 index 000000000000..9b97f11f9228 --- /dev/null +++ b/packages/browser/src/integrations/featureFlags/unleash/integration.ts @@ -0,0 +1,75 @@ +import type { Client, Event, EventHint, IntegrationFn } from '@sentry/core'; + +import { defineIntegration, fill, logger } from '@sentry/core'; +import { DEBUG_BUILD } from '../../../debug-build'; +import { copyFlagsFromScopeToEvent, insertFlagToScope } from '../../../utils/featureFlags'; +import type { UnleashClient, UnleashClientClass } from './types'; + +/** + * Sentry integration for capturing feature flag evaluations from the Unleash SDK. + * + * See the [feature flag documentation](https://develop.sentry.dev/sdk/expected-features/#feature-flags) for more information. + * + * @example + * ``` + * import { UnleashClient } from 'unleash-proxy-client'; + * import * as Sentry from '@sentry/browser'; + * + * const unleashIntegration = Sentry.unleashIntegration({unleashClientClass: UnleashClient}); + * + * Sentry.init({ + * dsn: '___PUBLIC_DSN___', + * integrations: [unleashIntegration], + * }); + * + * const unleash = new UnleashClient(...); + * unleash.start(); + * + * unleash.isEnabled('my-feature'); + * unleash.getVariant('other-feature'); + * Sentry.captureException(new Error('something went wrong')); + * ``` + */ +export const unleashIntegration = defineIntegration( + ({ unleashClientClass }: { unleashClientClass: UnleashClientClass }) => { + return { + name: 'Unleash', + + processEvent(event: Event, _hint: EventHint, _client: Client): Event { + return copyFlagsFromScopeToEvent(event); + }, + + setupOnce() { + const unleashClientPrototype = unleashClientClass.prototype as UnleashClient; + fill(unleashClientPrototype, 'isEnabled', _wrappedIsEnabled); + }, + }; + }, +) satisfies IntegrationFn; + +/** + * Wraps the UnleashClient.isEnabled method to capture feature flag evaluations. Its only side effect is writing to Sentry scope. + * + * This wrapper is safe for all isEnabled signatures. If the signature does not match (this: UnleashClient, toggleName: string, ...args: unknown[]) => boolean, + * we log an error and return the original result. + * + * @param original - The original method. + * @returns Wrapped method. Results should match the original. + */ +function _wrappedIsEnabled( + original: (this: UnleashClient, ...args: unknown[]) => unknown, +): (this: UnleashClient, ...args: unknown[]) => unknown { + return function (this: UnleashClient, ...args: unknown[]): unknown { + const toggleName = args[0]; + const result = original.apply(this, args); + + if (typeof toggleName === 'string' && typeof result === 'boolean') { + insertFlagToScope(toggleName, result); + } else if (DEBUG_BUILD) { + logger.error( + `[Feature Flags] UnleashClient.isEnabled does not match expected signature. arg0: ${toggleName} (${typeof toggleName}), result: ${result} (${typeof result})`, + ); + } + return result; + }; +} diff --git a/packages/browser/src/integrations/featureFlags/unleash/types.ts b/packages/browser/src/integrations/featureFlags/unleash/types.ts new file mode 100644 index 000000000000..8fc3c57843dd --- /dev/null +++ b/packages/browser/src/integrations/featureFlags/unleash/types.ts @@ -0,0 +1,16 @@ +export interface IVariant { + name: string; + enabled: boolean; + feature_enabled?: boolean; + payload?: { + type: string; + value: string; + }; +} + +export interface UnleashClient { + isEnabled(this: UnleashClient, featureName: string): boolean; + getVariant(this: UnleashClient, featureName: string): IVariant; +} + +export type UnleashClientClass = new (...args: unknown[]) => UnleashClient; diff --git a/packages/browser/src/tracing/browserTracingIntegration.ts b/packages/browser/src/tracing/browserTracingIntegration.ts index 17030f2f4a43..78ead340ac12 100644 --- a/packages/browser/src/tracing/browserTracingIntegration.ts +++ b/packages/browser/src/tracing/browserTracingIntegration.ts @@ -465,6 +465,7 @@ export function getMetaContent(metaName: string): string | undefined { // Can't specify generic to `getDomElement` because tracing can be used // in a variety of environments, have to disable `no-unsafe-member-access` // as a result. + // eslint-disable-next-line deprecation/deprecation const metaTag = getDomElement(`meta[name=${metaName}]`); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access return metaTag ? metaTag.getAttribute('content') : undefined; diff --git a/packages/browser/src/transports/offline.ts b/packages/browser/src/transports/offline.ts index 372c360194c7..5fbf7fa6ffc4 100644 --- a/packages/browser/src/transports/offline.ts +++ b/packages/browser/src/transports/offline.ts @@ -1,5 +1,6 @@ import type { BaseTransportOptions, Envelope, OfflineStore, OfflineTransportOptions, Transport } from '@sentry/core'; import { makeOfflineTransport, parseEnvelope, serializeEnvelope } from '@sentry/core'; +import { WINDOW } from '../helpers'; import { makeFetchTransport } from './fetch'; // 'Store', 'promisifyRequest' and 'createStore' were originally copied from the 'idb-keyval' package before being @@ -158,7 +159,15 @@ function createIndexedDbStore(options: BrowserOfflineTransportOptions): OfflineS function makeIndexedDbOfflineTransport( createTransport: (options: T) => Transport, ): (options: T & BrowserOfflineTransportOptions) => Transport { - return options => createTransport({ ...options, createStore: createIndexedDbStore }); + return options => { + const transport = createTransport({ ...options, createStore: createIndexedDbStore }); + + WINDOW.addEventListener('online', async _ => { + await transport.flush(); + }); + + return transport; + }; } /** diff --git a/packages/browser/test/transports/offline.test.ts b/packages/browser/test/transports/offline.test.ts index a9a396949588..070d6623f967 100644 --- a/packages/browser/test/transports/offline.test.ts +++ b/packages/browser/test/transports/offline.test.ts @@ -64,6 +64,7 @@ describe('makeOfflineTransport', () => { await deleteDatabase('sentry'); (global as any).TextEncoder = TextEncoder; (global as any).TextDecoder = TextDecoder; + (global as any).addEventListener = () => {}; }); it('indexedDb wrappers push, unshift and pop', async () => { @@ -115,4 +116,32 @@ describe('makeOfflineTransport', () => { expect(queuedCount).toEqual(1); expect(getSendCount()).toEqual(2); }); + + it('flush forces retry', async () => { + const { getSendCount, baseTransport } = createTestTransport(new Error(), { statusCode: 200 }, { statusCode: 200 }); + let queuedCount = 0; + const transport = makeBrowserOfflineTransport(baseTransport)({ + ...transportOptions, + shouldStore: () => { + queuedCount += 1; + return true; + }, + url: 'http://localhost', + }); + const result = await transport.send(ERROR_ENVELOPE); + + expect(result).toEqual({}); + + await delay(MIN_DELAY * 2); + + expect(getSendCount()).toEqual(0); + expect(queuedCount).toEqual(1); + + await transport.flush(); + + await delay(MIN_DELAY * 2); + + expect(queuedCount).toEqual(1); + expect(getSendCount()).toEqual(1); + }); }); diff --git a/packages/bun/package.json b/packages/bun/package.json index ce1c85cbcd0f..b5f41faf9e2a 100644 --- a/packages/bun/package.json +++ b/packages/bun/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/bun", - "version": "8.45.0", + "version": "8.50.0", "description": "Official Sentry SDK for bun", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/bun", @@ -39,9 +39,9 @@ "access": "public" }, "dependencies": { - "@sentry/core": "8.45.0", - "@sentry/node": "8.45.0", - "@sentry/opentelemetry": "8.45.0" + "@sentry/core": "8.50.0", + "@sentry/node": "8.50.0", + "@sentry/opentelemetry": "8.50.0" }, "devDependencies": { "bun-types": "latest" diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index 1ba5f2de4786..dcae6e98aa8d 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -141,6 +141,7 @@ export { spanToTraceHeader, spanToBaggageHeader, trpcMiddleware, + updateSpanName, // eslint-disable-next-line deprecation/deprecation addOpenTelemetryInstrumentation, zodErrorsIntegration, diff --git a/packages/cloudflare/package.json b/packages/cloudflare/package.json index efec51c5c0f5..9bf9949c9cbf 100644 --- a/packages/cloudflare/package.json +++ b/packages/cloudflare/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/cloudflare", - "version": "8.45.0", + "version": "8.50.0", "description": "Official Sentry SDK for Cloudflare Workers and Pages", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/cloudflare", @@ -39,7 +39,7 @@ "access": "public" }, "dependencies": { - "@sentry/core": "8.45.0" + "@sentry/core": "8.50.0" }, "optionalDependencies": { "@cloudflare/workers-types": "^4.x" diff --git a/packages/cloudflare/src/index.ts b/packages/cloudflare/src/index.ts index f3c80b8ddf32..fb8c34694282 100644 --- a/packages/cloudflare/src/index.ts +++ b/packages/cloudflare/src/index.ts @@ -89,6 +89,7 @@ export { spanToJSON, spanToTraceHeader, spanToBaggageHeader, + updateSpanName, } from '@sentry/core'; export { withSentry } from './handler'; diff --git a/packages/core/package.json b/packages/core/package.json index ab43b79117b9..4bf08772257a 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/core", - "version": "8.45.0", + "version": "8.50.0", "description": "Base implementation for all Sentry JavaScript SDKs", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/core", diff --git a/packages/core/src/asyncContext/types.ts b/packages/core/src/asyncContext/types.ts index 7b5bf8acc54c..b830bebb0d9b 100644 --- a/packages/core/src/asyncContext/types.ts +++ b/packages/core/src/asyncContext/types.ts @@ -1,6 +1,7 @@ import type { Scope } from '../types-hoist'; import type { getTraceData } from '../utils/traceData'; import type { + continueTrace, startInactiveSpan, startSpan, startSpanManual, @@ -68,4 +69,11 @@ export interface AsyncContextStrategy { /** Get trace data as serialized string values for propagation via `sentry-trace` and `baggage`. */ getTraceData?: typeof getTraceData; + + /** + * Continue a trace from `sentry-trace` and `baggage` values. + * These values can be obtained from incoming request headers, or in the browser from `` + * and `` HTML tags. + */ + continueTrace?: typeof continueTrace; } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 77259d2434d4..5baf88f38e1c 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -81,6 +81,7 @@ export { getActiveSpan, addChildSpanToSpan, spanTimeInputToSeconds, + updateSpanName, } from './utils/spanUtils'; export { parseSampleRate } from './utils/parseSampleRate'; export { applySdkMetadata } from './utils/sdkMetadata'; diff --git a/packages/core/src/semanticAttributes.ts b/packages/core/src/semanticAttributes.ts index 2896bd81f93f..b799f5321a0e 100644 --- a/packages/core/src/semanticAttributes.ts +++ b/packages/core/src/semanticAttributes.ts @@ -29,6 +29,15 @@ export const SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT = 'sentry.measurement_un /** The value of a measurement, which may be stored as a TimedEvent. */ export const SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE = 'sentry.measurement_value'; +/** + * A custom span name set by users guaranteed to be taken over any automatically + * inferred name. This attribute is removed before the span is sent. + * + * @internal only meant for internal SDK usage + * @hidden + */ +export const SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME = 'sentry.custom_span_name'; + /** * The id of the profile that this span occurred in. */ diff --git a/packages/core/src/tracing/sampling.ts b/packages/core/src/tracing/sampling.ts index 9109e78e0343..9ad01bc98a27 100644 --- a/packages/core/src/tracing/sampling.ts +++ b/packages/core/src/tracing/sampling.ts @@ -1,5 +1,6 @@ -import type { Options, SamplingContext } from '../types-hoist'; +import type { Options, RequestEventData, SamplingContext } from '../types-hoist'; +import { getIsolationScope } from '../currentScopes'; import { DEBUG_BUILD } from '../debug-build'; import { logger } from '../utils-hoist/logger'; import { hasTracingEnabled } from '../utils/hasTracingEnabled'; @@ -20,13 +21,22 @@ export function sampleSpan( return [false]; } + // Casting this from unknown, as the type of `sdkProcessingMetadata` is only changed in v9 and `normalizedRequest` is set in SentryHttpInstrumentation + const normalizedRequest = getIsolationScope().getScopeData().sdkProcessingMetadata + .normalizedRequest as RequestEventData; + + const enhancedSamplingContext = { + ...samplingContext, + normalizedRequest: samplingContext.normalizedRequest || normalizedRequest, + }; + // we would have bailed already if neither `tracesSampler` nor `tracesSampleRate` nor `enableTracing` were defined, so one of these should // work; prefer the hook if so let sampleRate; if (typeof options.tracesSampler === 'function') { - sampleRate = options.tracesSampler(samplingContext); - } else if (samplingContext.parentSampled !== undefined) { - sampleRate = samplingContext.parentSampled; + sampleRate = options.tracesSampler(enhancedSamplingContext); + } else if (enhancedSamplingContext.parentSampled !== undefined) { + sampleRate = enhancedSamplingContext.parentSampled; } else if (typeof options.tracesSampleRate !== 'undefined') { sampleRate = options.tracesSampleRate; } else { diff --git a/packages/core/src/tracing/sentrySpan.ts b/packages/core/src/tracing/sentrySpan.ts index 126702dfad2b..9965261970f2 100644 --- a/packages/core/src/tracing/sentrySpan.ts +++ b/packages/core/src/tracing/sentrySpan.ts @@ -5,6 +5,7 @@ import { getMetricSummaryJsonForSpan } from '../metrics/metric-summary'; import { SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME, SEMANTIC_ATTRIBUTE_PROFILE_ID, + SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, @@ -355,6 +356,14 @@ export class SentrySpan implements Span { const source = this._attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] as TransactionSource | undefined; + // remove internal root span attributes we don't need to send. + /* eslint-disable @typescript-eslint/no-dynamic-delete */ + delete this._attributes[SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]; + spans.forEach(span => { + span.data && delete span.data[SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]; + }); + // eslint-enabled-next-line @typescript-eslint/no-dynamic-delete + const transaction: TransactionEvent = { contexts: { trace: spanToTransactionTraceContext(this), diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index d44d0b216db3..26fefff68174 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -191,15 +191,20 @@ export function startInactiveSpan(options: StartSpanOptions): Span { * be attached to the incoming trace. */ export const continueTrace = ( - { - sentryTrace, - baggage, - }: { + options: { sentryTrace: Parameters[0]; baggage: Parameters[1]; }, callback: () => V, ): V => { + const carrier = getMainCarrier(); + const acs = getAsyncContextStrategy(carrier); + if (acs.continueTrace) { + return acs.continueTrace(options, callback); + } + + const { sentryTrace, baggage } = options; + return withScope(scope => { const propagationContext = propagationContextFromHeaders(sentryTrace, baggage); scope.setPropagationContext(propagationContext); diff --git a/packages/core/src/transports/offline.ts b/packages/core/src/transports/offline.ts index cf8902739db7..4cdd0b4a71af 100644 --- a/packages/core/src/transports/offline.ts +++ b/packages/core/src/transports/offline.ts @@ -170,7 +170,15 @@ export function makeOfflineTransport( return { send, - flush: t => transport.flush(t), + flush: timeout => { + // If there's no timeout, we should attempt to flush the offline queue. + if (timeout === undefined) { + retryDelay = START_DELAY; + flushIn(MIN_DELAY); + } + + return transport.flush(timeout); + }, }; }; } diff --git a/packages/core/src/types-hoist/context.ts b/packages/core/src/types-hoist/context.ts index 60aa60b38868..ad6879636086 100644 --- a/packages/core/src/types-hoist/context.ts +++ b/packages/core/src/types-hoist/context.ts @@ -133,6 +133,6 @@ export interface MissingInstrumentationContext extends Record { * directly is not recommended. Use the functions in @sentry/browser * src/utils/featureFlags instead. */ -export interface FeatureFlagContext extends Record { +interface FeatureFlagContext extends Record { values: FeatureFlag[]; } diff --git a/packages/core/src/types-hoist/samplingcontext.ts b/packages/core/src/types-hoist/samplingcontext.ts index ecce87d7fbc7..1cb15490e5b2 100644 --- a/packages/core/src/types-hoist/samplingcontext.ts +++ b/packages/core/src/types-hoist/samplingcontext.ts @@ -1,3 +1,4 @@ +import type { RequestEventData } from '../types-hoist/request'; import type { ExtractedNodeRequestData, WorkerLocation } from './misc'; import type { SpanAttributes } from './span'; @@ -35,10 +36,16 @@ export interface SamplingContext extends CustomSamplingContext { location?: WorkerLocation; /** - * Object representing the incoming request to a node server. Passed by default when using the TracingHandler. + * Object representing the incoming request to a node server. + * @deprecated This attribute is currently never defined and will be removed in v9. Use `normalizedRequest` instead */ request?: ExtractedNodeRequestData; + /** + * Object representing the incoming request to a node server in a normalized format. + */ + normalizedRequest?: RequestEventData; + /** The name of the span being sampled. */ name: string; diff --git a/packages/core/src/types-hoist/span.ts b/packages/core/src/types-hoist/span.ts index a2ee74fd7cfa..cf0c3086bf88 100644 --- a/packages/core/src/types-hoist/span.ts +++ b/packages/core/src/types-hoist/span.ts @@ -234,6 +234,16 @@ export interface Span { /** * Update the name of the span. + * + * **Important:** You most likely want to use `Sentry.updateSpanName(span, name)` instead! + * + * This method will update the current span name but cannot guarantee that the new name will be + * the final name of the span. Instrumentation might still overwrite the name with an automatically + * computed name, for example in `http.server` or `db` spans. + * + * You can ensure that your name is kept and not overwritten by calling `Sentry.updateSpanName(span, name)` + * + * @param name the new name of the span */ updateName(name: string): this; diff --git a/packages/core/src/utils-hoist/browser.ts b/packages/core/src/utils-hoist/browser.ts index b3a5220e7c3c..c5d5a91ccb75 100644 --- a/packages/core/src/utils-hoist/browser.ts +++ b/packages/core/src/utils-hoist/browser.ts @@ -155,6 +155,8 @@ export function getLocationHref(): string { * `const element = getDomElement('selector');` * * @param selector the selector string passed on to document.querySelector + * + * @deprecated This method is deprecated and will be removed in the next major version. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export function getDomElement(selector: string): E | null { diff --git a/packages/core/src/utils-hoist/index.ts b/packages/core/src/utils-hoist/index.ts index e53cd0edb59b..68a52655fb3a 100644 --- a/packages/core/src/utils-hoist/index.ts +++ b/packages/core/src/utils-hoist/index.ts @@ -2,7 +2,13 @@ export { applyAggregateErrorsToEvent } from './aggregate-errors'; // eslint-disable-next-line deprecation/deprecation export { flatten } from './array'; export { getBreadcrumbLogLevelFromHttpStatusCode } from './breadcrumb-log-level'; -export { getComponentName, getDomElement, getLocationHref, htmlTreeAsString } from './browser'; +export { + getComponentName, + // eslint-disable-next-line deprecation/deprecation + getDomElement, + getLocationHref, + htmlTreeAsString, +} from './browser'; export { dsnFromString, dsnToString, makeDsn } from './dsn'; export { SentryError } from './error'; export { GLOBAL_OBJ, getGlobalSingleton } from './worldwide'; diff --git a/packages/core/src/utils-hoist/requestdata.ts b/packages/core/src/utils-hoist/requestdata.ts index bff0f3f629bd..582a8954d4c6 100644 --- a/packages/core/src/utils-hoist/requestdata.ts +++ b/packages/core/src/utils-hoist/requestdata.ts @@ -295,8 +295,8 @@ export function addNormalizedRequestDataToEvent( if (Object.keys(extractedUser).length) { event.user = { - ...event.user, ...extractedUser, + ...event.user, }; } } diff --git a/packages/core/src/utils-hoist/worldwide.ts b/packages/core/src/utils-hoist/worldwide.ts index 92018731ff4f..cb819bd92204 100644 --- a/packages/core/src/utils-hoist/worldwide.ts +++ b/packages/core/src/utils-hoist/worldwide.ts @@ -49,7 +49,7 @@ type BackwardsCompatibleSentryCarrier = SentryCarrier & { /** Internal global with common properties and Sentry extensions */ export type InternalGlobal = { - navigator?: { userAgent?: string }; + navigator?: { userAgent?: string; maxTouchPoints?: number }; console: Console; PerformanceObserver?: any; Sentry?: any; diff --git a/packages/core/src/utils/spanUtils.ts b/packages/core/src/utils/spanUtils.ts index 594a297f9395..09ba9d729449 100644 --- a/packages/core/src/utils/spanUtils.ts +++ b/packages/core/src/utils/spanUtils.ts @@ -3,7 +3,12 @@ import { getMainCarrier } from '../carrier'; import { getCurrentScope } from '../currentScopes'; import { getMetricSummaryJsonForSpan, updateMetricSummaryOnSpan } from '../metrics/metric-summary'; import type { MetricType } from '../metrics/types'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../semanticAttributes'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME, + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, +} from '../semanticAttributes'; import type { SentrySpan } from '../tracing/sentrySpan'; import { SPAN_STATUS_OK, SPAN_STATUS_UNSET } from '../tracing/spanstatus'; import type { @@ -310,3 +315,27 @@ export function showSpanDropWarning(): void { hasShownSpanDropWarning = true; } } + +/** + * Updates the name of the given span and ensures that the span name is not + * overwritten by the Sentry SDK. + * + * Use this function instead of `span.updateName()` if you want to make sure that + * your name is kept. For some spans, for example root `http.server` spans the + * Sentry SDK would otherwise overwrite the span name with a high-quality name + * it infers when the span ends. + * + * Use this function in server code or when your span is started on the server + * and on the client (browser). If you only update a span name on the client, + * you can also use `span.updateName()` the SDK does not overwrite the name. + * + * @param span - The span to update the name of. + * @param name - The name to set on the span. + */ +export function updateSpanName(span: Span, name: string): void { + span.updateName(name); + span.setAttributes({ + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', + [SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]: name, + }); +} diff --git a/packages/core/test/lib/utils/spanUtils.test.ts b/packages/core/test/lib/utils/spanUtils.test.ts index f7187695a025..aa6d4bf4cb2f 100644 --- a/packages/core/test/lib/utils/spanUtils.test.ts +++ b/packages/core/test/lib/utils/spanUtils.test.ts @@ -1,6 +1,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, SPAN_STATUS_ERROR, SPAN_STATUS_OK, SPAN_STATUS_UNSET, @@ -14,8 +15,14 @@ import { } from '../../../src'; import type { Span, SpanAttributes, SpanStatus, SpanTimeInput } from '../../../src/types-hoist'; import type { OpenTelemetrySdkTraceBaseSpan } from '../../../src/utils/spanUtils'; -import { spanToTraceContext } from '../../../src/utils/spanUtils'; -import { getRootSpan, spanIsSampled, spanTimeInputToSeconds, spanToJSON } from '../../../src/utils/spanUtils'; +import { + getRootSpan, + spanIsSampled, + spanTimeInputToSeconds, + spanToJSON, + spanToTraceContext, + updateSpanName, +} from '../../../src/utils/spanUtils'; import { TestClient, getDefaultTestClientOptions } from '../../mocks/client'; function createMockedOtelSpan({ @@ -332,3 +339,13 @@ describe('getRootSpan', () => { }); }); }); + +describe('updateSpanName', () => { + it('updates the span name and source', () => { + const span = new SentrySpan({ name: 'old-name', attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' } }); + updateSpanName(span, 'new-name'); + const spanJSON = spanToJSON(span); + expect(spanJSON.description).toBe('new-name'); + expect(spanJSON.data?.[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]).toBe('custom'); + }); +}); diff --git a/packages/core/test/utils-hoist/browser.test.ts b/packages/core/test/utils-hoist/browser.test.ts index c86570ee7fb0..ac76584908d5 100644 --- a/packages/core/test/utils-hoist/browser.test.ts +++ b/packages/core/test/utils-hoist/browser.test.ts @@ -78,6 +78,7 @@ describe('htmlTreeAsString', () => { describe('getDomElement', () => { it('returns the element for a given query selector', () => { document.head.innerHTML = '
Hello
'; + // eslint-disable-next-line deprecation/deprecation const el = getDomElement('div#mydiv'); expect(el).toBeDefined(); expect(el?.tagName).toEqual('DIV'); diff --git a/packages/deno/package.json b/packages/deno/package.json index b9f7dbdc8ea8..d6444081e21c 100644 --- a/packages/deno/package.json +++ b/packages/deno/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/deno", - "version": "8.45.0", + "version": "8.50.0", "description": "Official Sentry SDK for Deno", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/deno", @@ -24,7 +24,7 @@ "/build" ], "dependencies": { - "@sentry/core": "8.45.0" + "@sentry/core": "8.50.0" }, "devDependencies": { "@rollup/plugin-typescript": "^11.1.5", diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts index 892f6ce681c0..cea4effad4bd 100644 --- a/packages/deno/src/index.ts +++ b/packages/deno/src/index.ts @@ -89,6 +89,7 @@ export { spanToJSON, spanToTraceHeader, spanToBaggageHeader, + updateSpanName, } from '@sentry/core'; export { DenoClient } from './client'; diff --git a/packages/ember/package.json b/packages/ember/package.json index 1547eed88d94..32d777537862 100644 --- a/packages/ember/package.json +++ b/packages/ember/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/ember", - "version": "8.45.0", + "version": "8.50.0", "description": "Official Sentry SDK for Ember.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/ember", @@ -32,8 +32,8 @@ "dependencies": { "@babel/core": "^7.24.4", "@embroider/macros": "^1.16.0", - "@sentry/browser": "8.45.0", - "@sentry/core": "8.45.0", + "@sentry/browser": "8.50.0", + "@sentry/core": "8.50.0", "ember-auto-import": "^2.7.2", "ember-cli-babel": "^8.2.0", "ember-cli-htmlbars": "^6.1.1", diff --git a/packages/eslint-config-sdk/package.json b/packages/eslint-config-sdk/package.json index 246f51ed6dcf..771cf08e5ad2 100644 --- a/packages/eslint-config-sdk/package.json +++ b/packages/eslint-config-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/eslint-config-sdk", - "version": "8.45.0", + "version": "8.50.0", "description": "Official Sentry SDK eslint config", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/eslint-config-sdk", @@ -22,8 +22,8 @@ "access": "public" }, "dependencies": { - "@sentry-internal/eslint-plugin-sdk": "8.45.0", - "@sentry-internal/typescript": "8.45.0", + "@sentry-internal/eslint-plugin-sdk": "8.50.0", + "@sentry-internal/typescript": "8.50.0", "@typescript-eslint/eslint-plugin": "^5.48.0", "@typescript-eslint/parser": "^5.48.0", "eslint-config-prettier": "^6.11.0", diff --git a/packages/eslint-plugin-sdk/package.json b/packages/eslint-plugin-sdk/package.json index 7a6a729fc0cd..d6ee3aaebdcd 100644 --- a/packages/eslint-plugin-sdk/package.json +++ b/packages/eslint-plugin-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/eslint-plugin-sdk", - "version": "8.45.0", + "version": "8.50.0", "description": "Official Sentry SDK eslint plugin", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/eslint-plugin-sdk", diff --git a/packages/feedback/package.json b/packages/feedback/package.json index a5e44856378e..19596b6b0f7b 100644 --- a/packages/feedback/package.json +++ b/packages/feedback/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/feedback", - "version": "8.45.0", + "version": "8.50.0", "description": "Sentry SDK integration for user feedback", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/feedback", @@ -39,7 +39,7 @@ "access": "public" }, "dependencies": { - "@sentry/core": "8.45.0" + "@sentry/core": "8.50.0" }, "devDependencies": { "preact": "^10.19.4" diff --git a/packages/feedback/src/core/integration.ts b/packages/feedback/src/core/integration.ts index fb1bd1fc143e..1c2f5655decb 100644 --- a/packages/feedback/src/core/integration.ts +++ b/packages/feedback/src/core/integration.ts @@ -5,7 +5,7 @@ import type { Integration, IntegrationFn, } from '@sentry/core'; -import { getClient, isBrowser, logger } from '@sentry/core'; +import { addIntegration, isBrowser, logger } from '@sentry/core'; import { ADD_SCREENSHOT_LABEL, CANCEL_BUTTON_LABEL, @@ -39,16 +39,22 @@ type Unsubscribe = () => void; * Allow users to capture user feedback and send it to Sentry. */ -interface BuilderOptions { - // The type here should be `keyof typeof LazyLoadableIntegrations`, but that'll cause a cicrular - // dependency with @sentry/core - lazyLoadIntegration: ( - name: 'feedbackModalIntegration' | 'feedbackScreenshotIntegration', - scriptNonce?: string, - ) => Promise; - getModalIntegration?: null | (() => IntegrationFn); - getScreenshotIntegration?: null | (() => IntegrationFn); -} +type BuilderOptions = + | { + lazyLoadIntegration?: never; + getModalIntegration: () => IntegrationFn; + getScreenshotIntegration: () => IntegrationFn; + } + | { + // The type here should be `keyof typeof LazyLoadableIntegrations`, but that'll cause a cicrular + // dependency with @sentry/core + lazyLoadIntegration: ( + name: 'feedbackModalIntegration' | 'feedbackScreenshotIntegration', + scriptNonce?: string, + ) => Promise; + getModalIntegration?: never; + getScreenshotIntegration?: never; + }; export const buildFeedbackIntegration = ({ lazyLoadIntegration, @@ -172,45 +178,40 @@ export const buildFeedbackIntegration = ({ return _shadow as ShadowRoot; }; - const _findIntegration = async ( - integrationName: string, - getter: undefined | null | (() => IntegrationFn), - functionMethodName: 'feedbackModalIntegration' | 'feedbackScreenshotIntegration', - ): Promise => { - const client = getClient(); - const existing = client && client.getIntegrationByName(integrationName); - if (existing) { - return existing as I; - } - const integrationFn = (getter && getter()) || (await lazyLoadIntegration(functionMethodName, scriptNonce)); - const integration = integrationFn(); - client && client.addIntegration(integration); - return integration as I; - }; - const _loadAndRenderDialog = async ( options: FeedbackInternalOptions, ): Promise> => { const screenshotRequired = options.enableScreenshot && isScreenshotSupported(); - const [modalIntegration, screenshotIntegration] = await Promise.all([ - _findIntegration('FeedbackModal', getModalIntegration, 'feedbackModalIntegration'), - screenshotRequired - ? _findIntegration( - 'FeedbackScreenshot', - getScreenshotIntegration, - 'feedbackScreenshotIntegration', - ) - : undefined, - ]); - if (!modalIntegration) { - // TODO: Let the end-user retry async loading + + let modalIntegration: FeedbackModalIntegration; + let screenshotIntegration: FeedbackScreenshotIntegration | undefined; + + try { + const modalIntegrationFn = getModalIntegration + ? getModalIntegration() + : await lazyLoadIntegration('feedbackModalIntegration', scriptNonce); + modalIntegration = modalIntegrationFn() as FeedbackModalIntegration; + addIntegration(modalIntegration); + } catch { DEBUG_BUILD && logger.error( - '[Feedback] Missing feedback modal integration. Try using `feedbackSyncIntegration` in your `Sentry.init`.', + '[Feedback] Error when trying to load feedback integrations. Try using `feedbackSyncIntegration` in your `Sentry.init`.', ); throw new Error('[Feedback] Missing feedback modal integration!'); } - if (screenshotRequired && !screenshotIntegration) { + + try { + const screenshotIntegrationFn = screenshotRequired + ? getScreenshotIntegration + ? getScreenshotIntegration() + : await lazyLoadIntegration('feedbackScreenshotIntegration', scriptNonce) + : undefined; + + if (screenshotIntegrationFn) { + screenshotIntegration = screenshotIntegrationFn() as FeedbackScreenshotIntegration; + addIntegration(screenshotIntegration); + } + } catch { DEBUG_BUILD && logger.error('[Feedback] Missing feedback screenshot integration. Proceeding without screenshots.'); } @@ -227,7 +228,7 @@ export const buildFeedbackIntegration = ({ options.onFormSubmitted && options.onFormSubmitted(); }, }, - screenshotIntegration: screenshotRequired ? screenshotIntegration : undefined, + screenshotIntegration, sendFeedback, shadow: _createShadow(options), }); diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json index 2c03fae30f14..4fb85685f01b 100644 --- a/packages/gatsby/package.json +++ b/packages/gatsby/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/gatsby", - "version": "8.45.0", + "version": "8.50.0", "description": "Official Sentry SDK for Gatsby.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/gatsby", @@ -45,8 +45,8 @@ "access": "public" }, "dependencies": { - "@sentry/core": "8.45.0", - "@sentry/react": "8.45.0", + "@sentry/core": "8.50.0", + "@sentry/react": "8.50.0", "@sentry/webpack-plugin": "2.22.7" }, "peerDependencies": { diff --git a/packages/google-cloud-serverless/package.json b/packages/google-cloud-serverless/package.json index 52008df49931..0e99cccab999 100644 --- a/packages/google-cloud-serverless/package.json +++ b/packages/google-cloud-serverless/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/google-cloud-serverless", - "version": "8.45.0", + "version": "8.50.0", "description": "Official Sentry SDK for Google Cloud Functions", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/google-cloud-serverless", @@ -48,8 +48,8 @@ "access": "public" }, "dependencies": { - "@sentry/core": "8.45.0", - "@sentry/node": "8.45.0", + "@sentry/core": "8.50.0", + "@sentry/node": "8.50.0", "@types/express": "^4.17.14" }, "devDependencies": { diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts index 6f89769c2a37..95d30ec8b072 100644 --- a/packages/google-cloud-serverless/src/index.ts +++ b/packages/google-cloud-serverless/src/index.ts @@ -118,6 +118,7 @@ export { spanToTraceHeader, spanToBaggageHeader, trpcMiddleware, + updateSpanName, // eslint-disable-next-line deprecation/deprecation addOpenTelemetryInstrumentation, zodErrorsIntegration, diff --git a/packages/integration-shims/package.json b/packages/integration-shims/package.json index 322922a945a0..712d8fc27574 100644 --- a/packages/integration-shims/package.json +++ b/packages/integration-shims/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/integration-shims", - "version": "8.45.0", + "version": "8.50.0", "description": "Shims for integrations in Sentry SDK.", "main": "build/cjs/index.js", "module": "build/esm/index.js", @@ -55,7 +55,7 @@ "url": "https://github.com/getsentry/sentry-javascript/issues" }, "dependencies": { - "@sentry/core": "8.45.0" + "@sentry/core": "8.50.0" }, "engines": { "node": ">=14.18" diff --git a/packages/nestjs/package.json b/packages/nestjs/package.json index 1bef4cc56c2f..461026c95f25 100644 --- a/packages/nestjs/package.json +++ b/packages/nestjs/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/nestjs", - "version": "8.45.0", + "version": "8.50.0", "description": "Official Sentry SDK for NestJS", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/nestjs", @@ -14,8 +14,8 @@ "/*.d.ts", "/*.d.ts.map" ], - "main": "build/cjs/nestjs/index.js", - "module": "build/esm/nestjs/index.js", + "main": "build/cjs/index.js", + "module": "build/esm/index.js", "types": "build/types/index.d.ts", "exports": { "./package.json": "./package.json", @@ -44,8 +44,8 @@ "access": "public" }, "dependencies": { - "@sentry/core": "8.45.0", - "@sentry/node": "8.45.0" + "@sentry/core": "8.50.0", + "@sentry/node": "8.50.0" }, "devDependencies": { "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index baafbe149a1d..243d5126eb02 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/nextjs", - "version": "8.45.0", + "version": "8.50.0", "description": "Official Sentry SDK for Next.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/nextjs", @@ -79,12 +79,12 @@ "@opentelemetry/api": "^1.9.0", "@opentelemetry/semantic-conventions": "^1.28.0", "@rollup/plugin-commonjs": "28.0.1", - "@sentry-internal/browser-utils": "8.45.0", - "@sentry/core": "8.45.0", - "@sentry/node": "8.45.0", - "@sentry/opentelemetry": "8.45.0", - "@sentry/react": "8.45.0", - "@sentry/vercel-edge": "8.45.0", + "@sentry-internal/browser-utils": "8.50.0", + "@sentry/core": "8.50.0", + "@sentry/node": "8.50.0", + "@sentry/opentelemetry": "8.50.0", + "@sentry/react": "8.50.0", + "@sentry/vercel-edge": "8.50.0", "@sentry/webpack-plugin": "2.22.7", "chalk": "3.0.0", "resolve": "1.22.8", diff --git a/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts b/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts index 3cc5b4d340e5..856e80ee72f8 100644 --- a/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts +++ b/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts @@ -3,6 +3,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, captureException, continueTrace, + getActiveSpan, httpRequestToRequestData, isString, logger, @@ -59,7 +60,13 @@ export function wrapApiHandlerWithSentry(apiHandler: NextApiHandler, parameteriz req.__withSentry_applied__ = true; return withIsolationScope(isolationScope => { - return continueTrace( + // Normally, there is an active span here (from Next.js OTEL) and we just use that as parent + // Else, we manually continueTrace from the incoming headers + const continueTraceIfNoActiveSpan = getActiveSpan() + ? (_opts: unknown, callback: () => T) => callback() + : continueTrace; + + return continueTraceIfNoActiveSpan( { sentryTrace: req.headers && isString(req.headers['sentry-trace']) ? req.headers['sentry-trace'] : undefined, diff --git a/packages/nextjs/src/common/withServerActionInstrumentation.ts b/packages/nextjs/src/common/withServerActionInstrumentation.ts index 8d5ab14c77c3..b04d94417204 100644 --- a/packages/nextjs/src/common/withServerActionInstrumentation.ts +++ b/packages/nextjs/src/common/withServerActionInstrumentation.ts @@ -1,4 +1,5 @@ import type { RequestEventData } from '@sentry/core'; +import { getActiveSpan } from '@sentry/core'; import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, SPAN_STATUS_ERROR, @@ -95,7 +96,13 @@ async function withServerActionInstrumentationImplementation(_opts: unknown, callback: () => T) => callback() + : continueTrace; + + return continueTraceIfNoActiveSpan( { sentryTrace: sentryTraceHeader, baggage: baggageHeader, diff --git a/packages/nextjs/src/config/webpack.ts b/packages/nextjs/src/config/webpack.ts index 9fb3bef49e67..d8a29d7d025c 100644 --- a/packages/nextjs/src/config/webpack.ts +++ b/packages/nextjs/src/config/webpack.ts @@ -340,7 +340,7 @@ export function constructWebpackConfigFunction( if (!isServer && !userSentryOptions.sourcemaps?.deleteSourcemapsAfterUpload) { // eslint-disable-next-line no-console console.warn( - "[@sentry/nextjs] The Sentry SDK has enabled source map generation for your Next.js app. If you don't want to serve Source Maps to your users, either set the `deleteSourceMapsAfterUpload` option to true, or manually delete the source maps after the build. In future Sentry SDK versions `deleteSourceMapsAfterUpload` will default to `true`. If you do not want to generate and upload sourcemaps, set the `sourcemaps.disable` option in `withSentryConfig()`.", + "[@sentry/nextjs] The Sentry SDK has enabled source map generation for your Next.js app. If you don't want to serve Source Maps to your users, either set the `sourcemaps.deleteSourcemapsAfterUpload` option to true, or manually delete the source maps after the build. In future Sentry SDK versions `sourcemaps.deleteSourcemapsAfterUpload` will default to `true`. If you do not want to generate and upload sourcemaps, set the `sourcemaps.disable` option in `withSentryConfig()`.", ); } diff --git a/packages/nextjs/src/index.types.ts b/packages/nextjs/src/index.types.ts index 1b6a0e09ed85..83a5562fde35 100644 --- a/packages/nextjs/src/index.types.ts +++ b/packages/nextjs/src/index.types.ts @@ -19,10 +19,6 @@ export declare function init( options: Options | clientSdk.BrowserOptions | serverSdk.NodeOptions | edgeSdk.EdgeOptions, ): Client | undefined; -export declare const getClient: typeof clientSdk.getClient; -export declare const getRootSpan: typeof serverSdk.getRootSpan; -export declare const continueTrace: typeof clientSdk.continueTrace; - export declare const linkedErrorsIntegration: typeof clientSdk.linkedErrorsIntegration; export declare const contextLinesIntegration: typeof clientSdk.contextLinesIntegration; diff --git a/packages/nitro-utils/package.json b/packages/nitro-utils/package.json index 06ec390ef87f..b60466723dc0 100644 --- a/packages/nitro-utils/package.json +++ b/packages/nitro-utils/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/nitro-utils", - "version": "8.45.0", + "version": "8.50.0", "description": "Utilities for all Sentry SDKs with Nitro on the server-side", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/nitro-utils", @@ -37,7 +37,7 @@ } }, "dependencies": { - "@sentry/core": "8.45.0" + "@sentry/core": "8.50.0" }, "devDependencies": { "rollup": "^4.24.4" diff --git a/packages/node/package.json b/packages/node/package.json index 618b9aa89725..1e720e8a0fce 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/node", - "version": "8.45.0", + "version": "8.50.0", "description": "Sentry Node SDK using OpenTelemetry for performance instrumentation", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/node", @@ -96,9 +96,9 @@ "@opentelemetry/resources": "^1.29.0", "@opentelemetry/sdk-trace-base": "^1.29.0", "@opentelemetry/semantic-conventions": "^1.28.0", - "@prisma/instrumentation": "5.19.1", - "@sentry/core": "8.45.0", - "@sentry/opentelemetry": "8.45.0", + "@prisma/instrumentation": "5.22.0", + "@sentry/core": "8.50.0", + "@sentry/opentelemetry": "8.50.0", "import-in-the-middle": "^1.11.2" }, "devDependencies": { diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index fa16ac4e6b3d..7f1bc6c735f3 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -63,9 +63,6 @@ export { addRequestDataToEvent, DEFAULT_USER_INCLUDES, extractRequestData } from export { // eslint-disable-next-line deprecation/deprecation addOpenTelemetryInstrumentation, - // These are custom variants that need to be used instead of the core one - // As they have slightly different implementations - continueTrace, // This needs exporting so the NodeClient can be used without calling init setOpenTelemetryContextAsyncContextStrategy as setNodeAsyncContextStrategy, } from '@sentry/opentelemetry'; @@ -110,6 +107,7 @@ export { getIsolationScope, getTraceData, getTraceMetaTags, + continueTrace, withScope, withIsolationScope, captureException, @@ -142,6 +140,7 @@ export { spanToTraceHeader, spanToBaggageHeader, trpcMiddleware, + updateSpanName, zodErrorsIntegration, profiler, } from '@sentry/core'; diff --git a/packages/node/src/integrations/anr/common.ts b/packages/node/src/integrations/anr/common.ts index e2666e3ecd3e..fc1b23e35b1d 100644 --- a/packages/node/src/integrations/anr/common.ts +++ b/packages/node/src/integrations/anr/common.ts @@ -21,6 +21,12 @@ export interface AnrIntegrationOptions { * This uses the node debugger which enables the inspector API and opens the required ports. */ captureStackTrace: boolean; + /** + * Maximum number of ANR events to send. + * + * Defaults to 1. + */ + maxAnrEvents: number; /** * Tags to include with ANR events. */ diff --git a/packages/node/src/integrations/anr/index.ts b/packages/node/src/integrations/anr/index.ts index f0cef4d60831..aa903789ad12 100644 --- a/packages/node/src/integrations/anr/index.ts +++ b/packages/node/src/integrations/anr/index.ts @@ -160,6 +160,7 @@ async function _startWorker( pollInterval: integrationOptions.pollInterval || DEFAULT_INTERVAL, anrThreshold: integrationOptions.anrThreshold || DEFAULT_HANG_THRESHOLD, captureStackTrace: !!integrationOptions.captureStackTrace, + maxAnrEvents: integrationOptions.maxAnrEvents || 1, staticTags: integrationOptions.staticTags || {}, contexts, }; @@ -175,6 +176,7 @@ async function _startWorker( workerData: options, // We don't want any Node args to be passed to the worker execArgv: [], + env: { ...process.env, NODE_OPTIONS: undefined }, }); process.on('exit', () => { diff --git a/packages/node/src/integrations/anr/worker.ts b/packages/node/src/integrations/anr/worker.ts index 354cea514618..117cccfe8904 100644 --- a/packages/node/src/integrations/anr/worker.ts +++ b/packages/node/src/integrations/anr/worker.ts @@ -23,7 +23,7 @@ type VoidFunction = () => void; const options: WorkerStartData = workerData; let session: Session | undefined; -let hasSentAnrEvent = false; +let sentAnrEvents = 0; let mainDebugImages: Record = {}; function log(msg: string): void { @@ -91,24 +91,31 @@ function applyDebugMeta(event: Event): void { return; } + const normalisedDebugImages = options.appRootPath ? {} : mainDebugImages; + if (options.appRootPath) { + for (const [path, debugId] of Object.entries(mainDebugImages)) { + normalisedDebugImages[normalizeUrlToBase(path, options.appRootPath)] = debugId; + } + } + const filenameToDebugId = new Map(); for (const exception of event.exception?.values || []) { for (const frame of exception.stacktrace?.frames || []) { const filename = frame.abs_path || frame.filename; - if (filename && mainDebugImages[filename]) { - filenameToDebugId.set(filename, mainDebugImages[filename] as string); + if (filename && normalisedDebugImages[filename]) { + filenameToDebugId.set(filename, normalisedDebugImages[filename] as string); } } } if (filenameToDebugId.size > 0) { const images: DebugImage[] = []; - for (const [filename, debugId] of filenameToDebugId.entries()) { + for (const [code_file, debug_id] of filenameToDebugId.entries()) { images.push({ type: 'sourcemap', - code_file: filename, - debug_id: debugId, + code_file, + debug_id, }); } event.debug_meta = { images }; @@ -134,11 +141,11 @@ function applyScopeToEvent(event: Event, scope: ScopeData): void { } async function sendAnrEvent(frames?: StackFrame[], scope?: ScopeData): Promise { - if (hasSentAnrEvent) { + if (sentAnrEvents >= options.maxAnrEvents) { return; } - hasSentAnrEvent = true; + sentAnrEvents += 1; await sendAbnormalSession(); @@ -179,11 +186,13 @@ async function sendAnrEvent(frames?: StackFrame[], scope?: ScopeData): Promise { - process.exit(0); - }, 5_000); + if (sentAnrEvents >= options.maxAnrEvents) { + // Delay for 5 seconds so that stdio can flush if the main event loop ever restarts. + // This is mainly for the benefit of logging or debugging. + setTimeout(() => { + process.exit(0); + }, 5_000); + } } let debuggerPause: VoidFunction | undefined; diff --git a/packages/node/src/integrations/contextlines.ts b/packages/node/src/integrations/contextlines.ts index 5e1bd75913c9..2d00ad5d5c61 100644 --- a/packages/node/src/integrations/contextlines.ts +++ b/packages/node/src/integrations/contextlines.ts @@ -142,13 +142,21 @@ function getContextLinesFromFile(path: string, ranges: ReadlineRange[], output: input: stream, }); + // We need to explicitly destroy the stream to prevent memory leaks, + // removing the listeners on the readline interface is not enough. + // See: https://github.com/nodejs/node/issues/9002 and https://github.com/getsentry/sentry-javascript/issues/14892 + function destroyStreamAndResolve(): void { + stream.destroy(); + resolve(); + } + // Init at zero and increment at the start of the loop because lines are 1 indexed. let lineNumber = 0; let currentRangeIndex = 0; const range = ranges[currentRangeIndex]; if (range === undefined) { // We should never reach this point, but if we do, we should resolve the promise to prevent it from hanging. - resolve(); + destroyStreamAndResolve(); return; } let rangeStart = range[0]; @@ -162,14 +170,14 @@ function getContextLinesFromFile(path: string, ranges: ReadlineRange[], output: DEBUG_BUILD && logger.error(`Failed to read file: ${path}. Error: ${e}`); lineReaded.close(); lineReaded.removeAllListeners(); - resolve(); + destroyStreamAndResolve(); } // We need to handle the error event to prevent the process from crashing in < Node 16 // https://github.com/nodejs/node/pull/31603 stream.on('error', onStreamError); lineReaded.on('error', onStreamError); - lineReaded.on('close', resolve); + lineReaded.on('close', destroyStreamAndResolve); lineReaded.on('line', line => { lineNumber++; diff --git a/packages/node/src/integrations/local-variables/local-variables-async.ts b/packages/node/src/integrations/local-variables/local-variables-async.ts index e1e0ebadf755..c3dcb1d12450 100644 --- a/packages/node/src/integrations/local-variables/local-variables-async.ts +++ b/packages/node/src/integrations/local-variables/local-variables-async.ts @@ -81,6 +81,7 @@ export const localVariablesAsyncIntegration = defineIntegration((( workerData: options, // We don't want any Node args to be passed to the worker execArgv: [], + env: { ...process.env, NODE_OPTIONS: undefined }, }); process.on('exit', () => { diff --git a/packages/node/src/integrations/tracing/express.ts b/packages/node/src/integrations/tracing/express.ts index e6cdcf514b2c..18e50edb8e4a 100644 --- a/packages/node/src/integrations/tracing/express.ts +++ b/packages/node/src/integrations/tracing/express.ts @@ -93,7 +93,9 @@ interface MiddlewareError extends Error { }; } -type ExpressMiddleware = ( +type ExpressMiddleware = (req: http.IncomingMessage, res: http.ServerResponse, next: () => void) => void; + +type ExpressErrorMiddleware = ( error: MiddlewareError, req: http.IncomingMessage, res: http.ServerResponse, @@ -111,13 +113,17 @@ interface ExpressHandlerOptions { /** * An Express-compatible error handler. */ -export function expressErrorHandler(options?: ExpressHandlerOptions): ExpressMiddleware { +export function expressErrorHandler(options?: ExpressHandlerOptions): ExpressErrorMiddleware { return function sentryErrorMiddleware( error: MiddlewareError, - _req: http.IncomingMessage, + request: http.IncomingMessage, res: http.ServerResponse, next: (error: MiddlewareError) => void, ): void { + // Ensure we use the express-enhanced request here, instead of the plain HTTP one + // When an error happens, the `expressRequestHandler` middleware does not run, so we set it here too + getIsolationScope().setSDKProcessingMetadata({ request }); + const shouldHandleError = options?.shouldHandleError || defaultShouldHandleError; if (shouldHandleError(error)) { @@ -152,6 +158,19 @@ export function expressErrorHandler(options?: ExpressHandlerOptions): ExpressMid }; } +function expressRequestHandler(): ExpressMiddleware { + return function sentryRequestMiddleware( + request: http.IncomingMessage, + _res: http.ServerResponse, + next: () => void, + ): void { + // Ensure we use the express-enhanced request here, instead of the plain HTTP one + getIsolationScope().setSDKProcessingMetadata({ request }); + + next(); + }; +} + /** * Add an Express error handler to capture errors to Sentry. * @@ -177,9 +196,10 @@ export function expressErrorHandler(options?: ExpressHandlerOptions): ExpressMid * ``` */ export function setupExpressErrorHandler( - app: { use: (middleware: ExpressMiddleware) => unknown }, + app: { use: (middleware: ExpressMiddleware | ExpressErrorMiddleware) => unknown }, options?: ExpressHandlerOptions, ): void { + app.use(expressRequestHandler()); app.use(expressErrorHandler(options)); ensureIsWrapped(app.use, 'express'); } diff --git a/packages/node/src/integrations/tracing/prisma.ts b/packages/node/src/integrations/tracing/prisma.ts index a42d41a6b5ec..a139de3a78fb 100644 --- a/packages/node/src/integrations/tracing/prisma.ts +++ b/packages/node/src/integrations/tracing/prisma.ts @@ -29,7 +29,9 @@ const _prismaIntegration = (() => { span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.db.otel.prisma'); } - if (spanJSON.description === 'prisma:engine:db_query') { + // In Prisma v5.22+, the `db.system` attribute is automatically set + // On older versions, this is missing, so we add it here + if (spanJSON.description === 'prisma:engine:db_query' && !spanJSON.data?.['db.system']) { span.setAttribute('db.system', 'prisma'); } }); diff --git a/packages/node/src/sdk/api.ts b/packages/node/src/sdk/api.ts index dd7ccc8ca75d..7e4f200d1e06 100644 --- a/packages/node/src/sdk/api.ts +++ b/packages/node/src/sdk/api.ts @@ -68,6 +68,8 @@ export function getSentryRelease(fallback?: string): string | undefined { process.env['HEROKU_TEST_RUN_COMMIT_VERSION'] || // Heroku #2 https://docs.sentry.io/product/integrations/deployment/heroku/#configure-releases process.env['HEROKU_SLUG_COMMIT'] || + // Railway - https://docs.railway.app/reference/variables#git-variables + process.env['RAILWAY_GIT_COMMIT_SHA'] || // Render - https://render.com/docs/environment-variables process.env['RENDER_GIT_COMMIT'] || // Semaphore CI - https://docs.semaphoreci.com/ci-cd-environment/environment-variables diff --git a/packages/node/src/sdk/index.ts b/packages/node/src/sdk/index.ts index 2ce75908168d..f0e2739f2629 100644 --- a/packages/node/src/sdk/index.ts +++ b/packages/node/src/sdk/index.ts @@ -169,7 +169,9 @@ function _init( // If users opt-out of this, they _have_ to set up OpenTelemetry themselves // There is no way to use this SDK without OpenTelemetry! if (!options.skipOpenTelemetrySetup) { - initOpenTelemetry(client); + initOpenTelemetry(client, { + spanProcessors: options.openTelemetrySpanProcessors, + }); validateOpenTelemetrySetup(); } diff --git a/packages/node/src/sdk/initOtel.ts b/packages/node/src/sdk/initOtel.ts index 4f0bb444d83d..b268314485a6 100644 --- a/packages/node/src/sdk/initOtel.ts +++ b/packages/node/src/sdk/initOtel.ts @@ -1,6 +1,7 @@ import moduleModule from 'module'; import { DiagLogLevel, diag } from '@opentelemetry/api'; import { Resource } from '@opentelemetry/resources'; +import type { SpanProcessor } from '@opentelemetry/sdk-trace-base'; import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; import { ATTR_SERVICE_NAME, @@ -22,15 +23,20 @@ declare const __IMPORT_META_URL_REPLACEMENT__: string; // About 277h - this must fit into new Array(len)! const MAX_MAX_SPAN_WAIT_DURATION = 1_000_000; +interface AdditionalOpenTelemetryOptions { + /** Additional SpanProcessor instances that should be used. */ + spanProcessors?: SpanProcessor[]; +} + /** * Initialize OpenTelemetry for Node. */ -export function initOpenTelemetry(client: NodeClient): void { +export function initOpenTelemetry(client: NodeClient, options: AdditionalOpenTelemetryOptions = {}): void { if (client.getOptions().debug) { setupOpenTelemetryLogger(); } - const provider = setupOtel(client); + const provider = setupOtel(client, options); client.traceProvider = provider; } @@ -129,7 +135,7 @@ function getPreloadMethods(integrationNames?: string[]): ((() => void) & { id: s } /** Just exported for tests. */ -export function setupOtel(client: NodeClient): BasicTracerProvider { +export function setupOtel(client: NodeClient, options: AdditionalOpenTelemetryOptions = {}): BasicTracerProvider { // Create and configure NodeTracerProvider const provider = new BasicTracerProvider({ sampler: new SentrySampler(client), @@ -144,6 +150,7 @@ export function setupOtel(client: NodeClient): BasicTracerProvider { new SentrySpanProcessor({ timeout: _clampSpanProcessorTimeout(client.getOptions().maxSpanWaitDuration), }), + ...(options.spanProcessors || []), ], }); diff --git a/packages/node/src/types.ts b/packages/node/src/types.ts index ebcdee869523..c7f166ed9b4d 100644 --- a/packages/node/src/types.ts +++ b/packages/node/src/types.ts @@ -1,6 +1,6 @@ import type { Span as WriteableSpan } from '@opentelemetry/api'; import type { Instrumentation } from '@opentelemetry/instrumentation'; -import type { ReadableSpan } from '@opentelemetry/sdk-trace-base'; +import type { ReadableSpan, SpanProcessor } from '@opentelemetry/sdk-trace-base'; import type { ClientOptions, Options, SamplingContext, Scope, Span, TracePropagationTargets } from '@sentry/core'; import type { NodeTransportOptions } from './transports'; @@ -121,6 +121,11 @@ export interface BaseNodeOptions { */ openTelemetryInstrumentations?: Instrumentation[]; + /** + * Provide an array of additional OpenTelemetry SpanProcessors that should be registered. + */ + openTelemetrySpanProcessors?: SpanProcessor[]; + /** * The max. duration in seconds that the SDK will wait for parent spans to be finished before discarding a span. * The SDK will automatically clean up spans that have no finished parent after this duration. diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index 86ce75ba76f8..e86247c88b65 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/nuxt", - "version": "8.45.0", + "version": "8.50.0", "description": "Official Sentry SDK for Nuxt (EXPERIMENTAL)", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/nuxt", @@ -43,13 +43,13 @@ }, "dependencies": { "@nuxt/kit": "^3.13.2", - "@sentry/browser": "8.45.0", - "@sentry/core": "8.45.0", - "@sentry/node": "8.45.0", - "@sentry/opentelemetry": "8.45.0", + "@sentry/browser": "8.50.0", + "@sentry/core": "8.50.0", + "@sentry/node": "8.50.0", + "@sentry/opentelemetry": "8.50.0", "@sentry/rollup-plugin": "2.22.7", "@sentry/vite-plugin": "2.22.6", - "@sentry/vue": "8.45.0" + "@sentry/vue": "8.50.0" }, "devDependencies": { "@nuxt/module-builder": "^0.8.4", diff --git a/packages/nuxt/src/index.types.ts b/packages/nuxt/src/index.types.ts index fd4bd00856be..2802d3158d41 100644 --- a/packages/nuxt/src/index.types.ts +++ b/packages/nuxt/src/index.types.ts @@ -14,6 +14,5 @@ export declare const linkedErrorsIntegration: typeof clientSdk.linkedErrorsInteg export declare const contextLinesIntegration: typeof clientSdk.contextLinesIntegration; export declare const getDefaultIntegrations: (options: Options) => Integration[]; export declare const defaultStackParser: StackParser; -export declare const continueTrace: typeof clientSdk.continueTrace; // eslint-disable-next-line deprecation/deprecation export declare const metrics: typeof clientSdk.metrics & typeof serverSdk.metrics; diff --git a/packages/nuxt/tsconfig.types.json b/packages/nuxt/tsconfig.types.json index 65455f66bd75..cab81135cd7a 100644 --- a/packages/nuxt/tsconfig.types.json +++ b/packages/nuxt/tsconfig.types.json @@ -1,6 +1,6 @@ { "extends": "./tsconfig.json", - + "exclude": ["build.config.ts"], "compilerOptions": { "declaration": true, "declarationMap": true, diff --git a/packages/opentelemetry/package.json b/packages/opentelemetry/package.json index e342a8d34be8..1e0687395306 100644 --- a/packages/opentelemetry/package.json +++ b/packages/opentelemetry/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/opentelemetry", - "version": "8.45.0", + "version": "8.50.0", "description": "Official Sentry utilities for OpenTelemetry", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/opentelemetry", @@ -39,7 +39,7 @@ "access": "public" }, "dependencies": { - "@sentry/core": "8.45.0" + "@sentry/core": "8.50.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", diff --git a/packages/opentelemetry/src/asyncContextStrategy.ts b/packages/opentelemetry/src/asyncContextStrategy.ts index cfc4254819d7..695175bc3fa1 100644 --- a/packages/opentelemetry/src/asyncContextStrategy.ts +++ b/packages/opentelemetry/src/asyncContextStrategy.ts @@ -6,7 +6,7 @@ import { SENTRY_FORK_SET_ISOLATION_SCOPE_CONTEXT_KEY, SENTRY_FORK_SET_SCOPE_CONTEXT_KEY, } from './constants'; -import { startInactiveSpan, startSpan, startSpanManual, withActiveSpan } from './trace'; +import { continueTrace, startInactiveSpan, startSpan, startSpanManual, withActiveSpan } from './trace'; import type { CurrentScopes } from './types'; import { getScopesFromContext } from './utils/contextData'; import { getActiveSpan } from './utils/getActiveSpan'; @@ -103,6 +103,7 @@ export function setOpenTelemetryContextAsyncContextStrategy(): void { getActiveSpan, suppressTracing, getTraceData, + continueTrace, // The types here don't fully align, because our own `Span` type is narrower // than the OTEL one - but this is OK for here, as we now we'll only have OTEL spans passed around withActiveSpan: withActiveSpan as typeof defaultWithActiveSpan, diff --git a/packages/opentelemetry/src/spanExporter.ts b/packages/opentelemetry/src/spanExporter.ts index a8d5affa4646..bff6518eb27d 100644 --- a/packages/opentelemetry/src/spanExporter.ts +++ b/packages/opentelemetry/src/spanExporter.ts @@ -12,6 +12,7 @@ import type { TransactionSource, } from '@sentry/core'; import { + SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, @@ -392,6 +393,7 @@ function removeSentryAttributes(data: Record): Record(options: Parameters[0], callback: () => T): T { - return baseContinueTrace(options, () => { - return continueTraceAsRemoteSpan(context.active(), options, callback); - }); + return continueTraceAsRemoteSpan(context.active(), options, callback); } /** diff --git a/packages/opentelemetry/src/utils/parseSpanDescription.ts b/packages/opentelemetry/src/utils/parseSpanDescription.ts index a1aa47e5b6ce..3136b94e6bf7 100644 --- a/packages/opentelemetry/src/utils/parseSpanDescription.ts +++ b/packages/opentelemetry/src/utils/parseSpanDescription.ts @@ -15,8 +15,10 @@ import { } from '@opentelemetry/semantic-conventions'; import type { SpanAttributes, TransactionSource } from '@sentry/core'; import { + SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, getSanitizedUrlString, parseUrl, stripUrlQueryAndFragment, @@ -36,12 +38,12 @@ interface SpanDescription { /** * Infer the op & description for a set of name, attributes and kind of a span. */ -export function inferSpanData(name: string, attributes: SpanAttributes, kind: SpanKind): SpanDescription { +export function inferSpanData(spanName: string, attributes: SpanAttributes, kind: SpanKind): SpanDescription { // if http.method exists, this is an http request span // eslint-disable-next-line deprecation/deprecation const httpMethod = attributes[ATTR_HTTP_REQUEST_METHOD] || attributes[SEMATTRS_HTTP_METHOD]; if (httpMethod) { - return descriptionForHttpMethod({ attributes, name, kind }, httpMethod); + return descriptionForHttpMethod({ attributes, name: spanName, kind }, httpMethod); } // eslint-disable-next-line deprecation/deprecation @@ -53,17 +55,18 @@ export function inferSpanData(name: string, attributes: SpanAttributes, kind: Sp // If db.type exists then this is a database call span // If the Redis DB is used as a cache, the span description should not be changed if (dbSystem && !opIsCache) { - return descriptionForDbSystem({ attributes, name }); + return descriptionForDbSystem({ attributes, name: spanName }); } + const customSourceOrRoute = attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] === 'custom' ? 'custom' : 'route'; + // If rpc.service exists then this is a rpc call span. // eslint-disable-next-line deprecation/deprecation const rpcService = attributes[SEMATTRS_RPC_SERVICE]; if (rpcService) { return { + ...getUserUpdatedNameAndSource(spanName, attributes, 'route'), op: 'rpc', - description: name, - source: 'route', }; } @@ -72,9 +75,8 @@ export function inferSpanData(name: string, attributes: SpanAttributes, kind: Sp const messagingSystem = attributes[SEMATTRS_MESSAGING_SYSTEM]; if (messagingSystem) { return { + ...getUserUpdatedNameAndSource(spanName, attributes, customSourceOrRoute), op: 'message', - description: name, - source: 'route', }; } @@ -82,15 +84,22 @@ export function inferSpanData(name: string, attributes: SpanAttributes, kind: Sp // eslint-disable-next-line deprecation/deprecation const faasTrigger = attributes[SEMATTRS_FAAS_TRIGGER]; if (faasTrigger) { - return { op: faasTrigger.toString(), description: name, source: 'route' }; + return { + ...getUserUpdatedNameAndSource(spanName, attributes, customSourceOrRoute), + op: faasTrigger.toString(), + }; } - return { op: undefined, description: name, source: 'custom' }; + return { op: undefined, description: spanName, source: 'custom' }; } /** * Extract better op/description from an otel span. * + * Does not overwrite the span name if the source is already set to custom to ensure + * that user-updated span names are preserved. In this case, we only adjust the op but + * leave span description and source unchanged. + * * Based on https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/7422ce2a06337f68a59b552b8c5a2ac125d6bae5/exporter/sentryexporter/sentry_exporter.go#L306 */ export function parseSpanDescription(span: AbstractSpan): SpanDescription { @@ -102,6 +111,21 @@ export function parseSpanDescription(span: AbstractSpan): SpanDescription { } function descriptionForDbSystem({ attributes, name }: { attributes: Attributes; name: string }): SpanDescription { + // if we already have a custom name, we don't overwrite it but only set the op + const userDefinedName = attributes[SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]; + if (typeof userDefinedName === 'string') { + return { + op: 'db', + description: userDefinedName, + source: (attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] as TransactionSource) || 'custom', + }; + } + + // if we already have the source set to custom, we don't overwrite the span description but only set the op + if (attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] === 'custom') { + return { op: 'db', description: name, source: 'custom' }; + } + // Use DB statement (Ex "SELECT * FROM table") if possible as description. // eslint-disable-next-line deprecation/deprecation const statement = attributes[SEMATTRS_DB_STATEMENT]; @@ -135,7 +159,7 @@ export function descriptionForHttpMethod( const { urlPath, url, query, fragment, hasRoute } = getSanitizedUrl(attributes, kind); if (!urlPath) { - return { op: opParts.join('.'), description: name, source: 'custom' }; + return { ...getUserUpdatedNameAndSource(name, attributes), op: opParts.join('.') }; } const graphqlOperationsAttribute = attributes[SEMANTIC_ATTRIBUTE_SENTRY_GRAPHQL_OPERATION]; @@ -145,12 +169,12 @@ export function descriptionForHttpMethod( // When the http span has a graphql operation, append it to the description // We add these in the graphqlIntegration - const description = graphqlOperationsAttribute + const inferredDescription = graphqlOperationsAttribute ? `${baseDescription} (${getGraphqlOperationNamesFromAttribute(graphqlOperationsAttribute)})` : baseDescription; // If `httpPath` is a root path, then we can categorize the transaction source as route. - const source: TransactionSource = hasRoute || urlPath === '/' ? 'route' : 'url'; + const inferredSource: TransactionSource = hasRoute || urlPath === '/' ? 'route' : 'url'; const data: Record = {}; @@ -174,12 +198,21 @@ export function descriptionForHttpMethod( const origin = attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] || 'manual'; const isManualSpan = !`${origin}`.startsWith('auto'); - const useInferredDescription = isClientOrServerKind || !isManualSpan; + // If users (or in very rare occasions we) set the source to custom, we don't overwrite the name + const alreadyHasCustomSource = attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] === 'custom'; + const customSpanName = attributes[SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]; + + const useInferredDescription = + !alreadyHasCustomSource && customSpanName == null && (isClientOrServerKind || !isManualSpan); + + const { description, source } = useInferredDescription + ? { description: inferredDescription, source: inferredSource } + : getUserUpdatedNameAndSource(name, attributes); return { op: opParts.join('.'), - description: useInferredDescription ? description : name, - source: useInferredDescription ? source : 'custom', + description, + source, data, }; } @@ -244,3 +277,36 @@ export function getSanitizedUrl( return { urlPath: undefined, url, query, fragment, hasRoute: false }; } + +/** + * Because Otel instrumentation sometimes mutates span names via `span.updateName`, the only way + * to ensure that a user-set span name is preserved is to store it as a tmp attribute on the span. + * We delete this attribute once we're done with it when preparing the event envelope. + * + * This temp attribute always takes precedence over the original name. + * + * We also need to take care of setting the correct source. Users can always update the source + * after updating the name, so we need to respect that. + * + * @internal exported only for testing + */ +export function getUserUpdatedNameAndSource( + originalName: string, + attributes: Attributes, + fallbackSource: TransactionSource = 'custom', +): { + description: string; + source: TransactionSource; +} { + const source = (attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] as TransactionSource) || fallbackSource; + const description = attributes[SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]; + + if (description && typeof description === 'string') { + return { + description, + source, + }; + } + + return { description: originalName, source }; +} diff --git a/packages/opentelemetry/test/trace.test.ts b/packages/opentelemetry/test/trace.test.ts index 2c22318ec977..43ce7b0e347b 100644 --- a/packages/opentelemetry/test/trace.test.ts +++ b/packages/opentelemetry/test/trace.test.ts @@ -1574,11 +1574,8 @@ describe('continueTrace', () => { ); expect(scope.getPropagationContext()).toEqual({ - dsc: {}, // DSC should be an empty object (frozen), because there was an incoming trace - sampled: false, - parentSpanId: '1121201211212012', spanId: expect.any(String), - traceId: '12312012123120121231201212312012', + traceId: expect.any(String), }); expect(scope.getScopeData().sdkProcessingMetadata).toEqual({}); @@ -1605,14 +1602,8 @@ describe('continueTrace', () => { ); expect(scope.getPropagationContext()).toEqual({ - dsc: { - environment: 'production', - version: '1.0', - }, - sampled: true, - parentSpanId: '1121201211212012', spanId: expect.any(String), - traceId: '12312012123120121231201212312012', + traceId: expect.any(String), }); expect(scope.getScopeData().sdkProcessingMetadata).toEqual({}); @@ -1639,16 +1630,9 @@ describe('continueTrace', () => { ); expect(scope.getPropagationContext()).toEqual({ - dsc: { - environment: 'production', - version: '1.0', - }, - sampled: true, - parentSpanId: '1121201211212012', spanId: expect.any(String), - traceId: '12312012123120121231201212312012', + traceId: expect.any(String), }); - expect(scope.getScopeData().sdkProcessingMetadata).toEqual({}); }); diff --git a/packages/opentelemetry/test/utils/parseSpanDescription.test.ts b/packages/opentelemetry/test/utils/parseSpanDescription.test.ts index c44645c62888..d43dfcd9f587 100644 --- a/packages/opentelemetry/test/utils/parseSpanDescription.test.ts +++ b/packages/opentelemetry/test/utils/parseSpanDescription.test.ts @@ -15,7 +15,13 @@ import { SEMATTRS_RPC_SERVICE, } from '@opentelemetry/semantic-conventions'; -import { descriptionForHttpMethod, getSanitizedUrl, parseSpanDescription } from '../../src/utils/parseSpanDescription'; +import { SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; +import { + descriptionForHttpMethod, + getSanitizedUrl, + getUserUpdatedNameAndSource, + parseSpanDescription, +} from '../../src/utils/parseSpanDescription'; describe('parseSpanDescription', () => { it.each([ @@ -81,6 +87,53 @@ describe('parseSpanDescription', () => { source: 'task', }, ], + [ + 'works with db system and custom source', + { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', + [SEMATTRS_DB_SYSTEM]: 'mysql', + [SEMATTRS_DB_STATEMENT]: 'SELECT * from users', + }, + 'test name', + SpanKind.CLIENT, + { + description: 'test name', + op: 'db', + source: 'custom', + }, + ], + [ + 'works with db system and custom source and custom name', + { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', + [SEMATTRS_DB_SYSTEM]: 'mysql', + [SEMATTRS_DB_STATEMENT]: 'SELECT * from users', + [SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]: 'custom name', + }, + 'test name', + SpanKind.CLIENT, + { + description: 'custom name', + op: 'db', + source: 'custom', + }, + ], + [ + 'works with db system and component source and custom name', + { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + [SEMATTRS_DB_SYSTEM]: 'mysql', + [SEMATTRS_DB_STATEMENT]: 'SELECT * from users', + [SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]: 'custom name', + }, + 'test name', + SpanKind.CLIENT, + { + description: 'custom name', + op: 'db', + source: 'component', + }, + ], [ 'works with db system without statement', { @@ -107,6 +160,50 @@ describe('parseSpanDescription', () => { source: 'route', }, ], + [ + 'works with rpc service and custom source', + { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', + [SEMATTRS_RPC_SERVICE]: 'rpc-test-service', + }, + 'test name', + undefined, + { + description: 'test name', + op: 'rpc', + source: 'custom', + }, + ], + [ + 'works with rpc service and custom source and custom name', + { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', + [SEMATTRS_RPC_SERVICE]: 'rpc-test-service', + [SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]: 'custom name', + }, + 'test name', + undefined, + { + description: 'custom name', + op: 'rpc', + source: 'custom', + }, + ], + [ + 'works with rpc service and component source and custom name', + { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + [SEMATTRS_RPC_SERVICE]: 'rpc-test-service', + [SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]: 'custom name', + }, + 'test name', + undefined, + { + description: 'custom name', + op: 'rpc', + source: 'component', + }, + ], [ 'works with messaging system', { @@ -120,6 +217,50 @@ describe('parseSpanDescription', () => { source: 'route', }, ], + [ + 'works with messaging system and custom source', + { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', + [SEMATTRS_MESSAGING_SYSTEM]: 'test-messaging-system', + }, + 'test name', + undefined, + { + description: 'test name', + op: 'message', + source: 'custom', + }, + ], + [ + 'works with messaging system and custom source and custom name', + { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', + [SEMATTRS_MESSAGING_SYSTEM]: 'test-messaging-system', + [SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]: 'custom name', + }, + 'test name', + undefined, + { + description: 'custom name', + op: 'message', + source: 'custom', + }, + ], + [ + 'works with messaging system and component source and custom name', + { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + [SEMATTRS_MESSAGING_SYSTEM]: 'test-messaging-system', + [SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]: 'custom name', + }, + 'test name', + undefined, + { + description: 'custom name', + op: 'message', + source: 'component', + }, + ], [ 'works with faas trigger', { @@ -133,6 +274,50 @@ describe('parseSpanDescription', () => { source: 'route', }, ], + [ + 'works with faas trigger and custom source', + { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', + [SEMATTRS_FAAS_TRIGGER]: 'test-faas-trigger', + }, + 'test name', + undefined, + { + description: 'test name', + op: 'test-faas-trigger', + source: 'custom', + }, + ], + [ + 'works with faas trigger and custom source and custom name', + { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', + [SEMATTRS_FAAS_TRIGGER]: 'test-faas-trigger', + [SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]: 'custom name', + }, + 'test name', + undefined, + { + description: 'custom name', + op: 'test-faas-trigger', + source: 'custom', + }, + ], + [ + 'works with faas trigger and component source and custom name', + { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + [SEMATTRS_FAAS_TRIGGER]: 'test-faas-trigger', + [SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]: 'custom name', + }, + 'test name', + undefined, + { + description: 'custom name', + op: 'test-faas-trigger', + source: 'component', + }, + ], ])('%s', (_, attributes, name, kind, expected) => { const actual = parseSpanDescription({ attributes, kind, name } as unknown as Span); expect(actual).toEqual(expected); @@ -172,6 +357,26 @@ describe('descriptionForHttpMethod', () => { source: 'url', }, ], + [ + 'works with prefetch request', + 'GET', + { + [SEMATTRS_HTTP_METHOD]: 'GET', + [SEMATTRS_HTTP_URL]: 'https://www.example.com/my-path', + [SEMATTRS_HTTP_TARGET]: '/my-path', + 'sentry.http.prefetch': true, + }, + 'test name', + SpanKind.CLIENT, + { + op: 'http.client.prefetch', + description: 'GET https://www.example.com/my-path', + data: { + url: 'https://www.example.com/my-path', + }, + source: 'url', + }, + ], [ 'works with basic server POST', 'POST', @@ -230,6 +435,71 @@ describe('descriptionForHttpMethod', () => { source: 'custom', }, ], + [ + "doesn't overwrite span name with source custom", + 'GET', + { + [SEMATTRS_HTTP_METHOD]: 'GET', + [SEMATTRS_HTTP_URL]: 'https://www.example.com/my-path/123', + [SEMATTRS_HTTP_TARGET]: '/my-path/123', + [ATTR_HTTP_ROUTE]: '/my-path/:id', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', + }, + 'test name', + SpanKind.CLIENT, + { + op: 'http.client', + description: 'test name', + data: { + url: 'https://www.example.com/my-path/123', + }, + source: 'custom', + }, + ], + [ + 'takes user-passed span name (with source custom)', + 'GET', + { + [SEMATTRS_HTTP_METHOD]: 'GET', + [SEMATTRS_HTTP_URL]: 'https://www.example.com/my-path/123', + [SEMATTRS_HTTP_TARGET]: '/my-path/123', + [ATTR_HTTP_ROUTE]: '/my-path/:id', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', + [SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]: 'custom name', + }, + 'test name', + SpanKind.CLIENT, + { + op: 'http.client', + description: 'custom name', + data: { + url: 'https://www.example.com/my-path/123', + }, + source: 'custom', + }, + ], + [ + 'takes user-passed span name (with source component)', + 'GET', + { + [SEMATTRS_HTTP_METHOD]: 'GET', + [SEMATTRS_HTTP_URL]: 'https://www.example.com/my-path/123', + [SEMATTRS_HTTP_TARGET]: '/my-path/123', + [ATTR_HTTP_ROUTE]: '/my-path/:id', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + [SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]: 'custom name', + }, + 'test name', + SpanKind.CLIENT, + { + op: 'http.client', + description: 'custom name', + data: { + url: 'https://www.example.com/my-path/123', + }, + source: 'component', + }, + ], ])('%s', (_, httpMethod, attributes, name, kind, expected) => { const actual = descriptionForHttpMethod({ attributes, kind, name }, httpMethod); expect(actual).toEqual(expected); @@ -383,3 +653,38 @@ describe('getSanitizedUrl', () => { expect(actual).toEqual(expected); }); }); + +describe('getUserUpdatedNameAndSource', () => { + it('returns param name if `SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME` attribute is not set', () => { + expect(getUserUpdatedNameAndSource('base name', {})).toEqual({ description: 'base name', source: 'custom' }); + }); + + it('returns param name with custom fallback source if `SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME` attribute is not set', () => { + expect(getUserUpdatedNameAndSource('base name', {}, 'route')).toEqual({ + description: 'base name', + source: 'route', + }); + }); + + it('returns param name if `SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME` attribute is not a string', () => { + expect(getUserUpdatedNameAndSource('base name', { [SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]: 123 })).toEqual({ + description: 'base name', + source: 'custom', + }); + }); + + it.each(['custom', 'task', 'url', 'route'])( + 'returns `SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME` attribute if is a string and source is %s', + source => { + expect( + getUserUpdatedNameAndSource('base name', { + [SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]: 'custom name', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source, + }), + ).toEqual({ + description: 'custom name', + source, + }); + }, + ); +}); diff --git a/packages/profiling-node/package.json b/packages/profiling-node/package.json index 19ecb062875e..d4c952292b0b 100644 --- a/packages/profiling-node/package.json +++ b/packages/profiling-node/package.json @@ -1,12 +1,13 @@ { "name": "@sentry/profiling-node", - "version": "8.45.0", + "version": "8.50.0", "description": "Official Sentry SDK for Node.js Profiling", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/profiling-node", "author": "Sentry", "license": "MIT", "main": "lib/cjs/index.js", + "module": "lib/esm/index.js", "types": "lib/types/index.d.ts", "exports": { "./package.json": "./package.json", @@ -75,8 +76,8 @@ "test": "cross-env SENTRY_PROFILER_BINARY_DIR=lib jest --config jest.config.js" }, "dependencies": { - "@sentry/core": "8.45.0", - "@sentry/node": "8.45.0", + "@sentry/core": "8.50.0", + "@sentry/node": "8.50.0", "detect-libc": "^2.0.2", "node-abi": "^3.61.0" }, diff --git a/packages/profiling-node/rollup.npm.config.mjs b/packages/profiling-node/rollup.npm.config.mjs index 12492b7c83e8..a9c148306709 100644 --- a/packages/profiling-node/rollup.npm.config.mjs +++ b/packages/profiling-node/rollup.npm.config.mjs @@ -1,49 +1,20 @@ import commonjs from '@rollup/plugin-commonjs'; +import replace from '@rollup/plugin-replace'; import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; -export const ESMShim = ` -import cjsUrl from 'node:url'; -import cjsPath from 'node:path'; -import cjsModule from 'node:module'; - -if(typeof __filename === 'undefined'){ - globalThis.__filename = cjsUrl.fileURLToPath(import.meta.url); -} - -if(typeof __dirname === 'undefined'){ - globalThis.__dirname = cjsPath.dirname(__filename); -} - -if(typeof require === 'undefined'){ - globalThis.require = cjsModule.createRequire(import.meta.url); -} -`; - -function makeESMShimPlugin(shim) { - return { - transform(code) { - const SHIM_REGEXP = /\/\/ #START_SENTRY_ESM_SHIM[\s\S]*?\/\/ #END_SENTRY_ESM_SHIM/; - return code.replace(SHIM_REGEXP, shim); - }, - }; -} - -const variants = makeNPMConfigVariants( +export default makeNPMConfigVariants( makeBaseNPMConfig({ packageSpecificConfig: { output: { dir: 'lib', preserveModules: false }, - plugins: [commonjs()], + plugins: [ + commonjs(), + replace({ + preventAssignment: false, + values: { + __IMPORT_META_URL_REPLACEMENT__: 'import.meta.url', + }, + }), + ], }, }), ); - -for (const variant of variants) { - if (variant.output.format === 'esm') { - variant.plugins.push(makeESMShimPlugin(ESMShim)); - } else { - // Remove the ESM shim comment - variant.plugins.push(makeESMShimPlugin('')); - } -} - -export default variants; diff --git a/packages/profiling-node/src/cpu_profiler.ts b/packages/profiling-node/src/cpu_profiler.ts index ed4ad83e7b31..a9a6d65ce191 100644 --- a/packages/profiling-node/src/cpu_profiler.ts +++ b/packages/profiling-node/src/cpu_profiler.ts @@ -1,6 +1,9 @@ +import { createRequire } from 'node:module'; import { arch as _arch, platform as _platform } from 'node:os'; import { join, resolve } from 'node:path'; +import { dirname } from 'node:path'; import { env, versions } from 'node:process'; +import { fileURLToPath, pathToFileURL } from 'node:url'; import { threadId } from 'node:worker_threads'; import { familySync } from 'detect-libc'; import { getAbi } from 'node-abi'; @@ -15,11 +18,7 @@ import type { } from './types'; import type { ProfileFormat } from './types'; -// #START_SENTRY_ESM_SHIM -// When building for ESM, we shim require to use createRequire and __dirname. -// We need to do this because .node extensions in esm are not supported. -// The comment below this line exists as a placeholder for where to insert the shim. -// #END_SENTRY_ESM_SHIM +declare const __IMPORT_META_URL_REPLACEMENT__: string; const stdlib = familySync(); const platform = process.env['BUILD_PLATFORM'] || _platform(); @@ -27,23 +26,32 @@ const arch = process.env['BUILD_ARCH'] || _arch(); const abi = getAbi(versions.node, 'node'); const identifier = [platform, arch, stdlib, abi].filter(c => c !== undefined && c !== null).join('-'); -const built_from_source_path = resolve(__dirname, '..', `./sentry_cpu_profiler-${identifier}`); - /** * Imports cpp bindings based on the current platform and architecture. */ // eslint-disable-next-line complexity export function importCppBindingsModule(): PrivateV8CpuProfilerBindings { + // We need to work around using import.meta.url directly with __IMPORT_META_URL_REPLACEMENT__ because jest complains about it. + const importMetaUrl = + typeof __IMPORT_META_URL_REPLACEMENT__ !== 'undefined' + ? // This case is always hit when the SDK is built + __IMPORT_META_URL_REPLACEMENT__ + : // This case is hit when the tests are run + pathToFileURL(__filename).href; + + const createdRequire = createRequire(importMetaUrl); + const esmCompatibleDirname = dirname(fileURLToPath(importMetaUrl)); + // If a binary path is specified, use that. if (env['SENTRY_PROFILER_BINARY_PATH']) { const envPath = env['SENTRY_PROFILER_BINARY_PATH']; - return require(envPath); + return createdRequire(envPath); } // If a user specifies a different binary dir, they are in control of the binaries being moved there if (env['SENTRY_PROFILER_BINARY_DIR']) { const binaryPath = join(resolve(env['SENTRY_PROFILER_BINARY_DIR']), `sentry_cpu_profiler-${identifier}`); - return require(`${binaryPath}.node`); + return createdRequire(`${binaryPath}.node`); } // We need the fallthrough so that in the end, we can fallback to the dynamic require. @@ -51,31 +59,31 @@ export function importCppBindingsModule(): PrivateV8CpuProfilerBindings { if (platform === 'darwin') { if (arch === 'x64') { if (abi === '93') { - return require('../sentry_cpu_profiler-darwin-x64-93.node'); + return createdRequire('../sentry_cpu_profiler-darwin-x64-93.node'); } if (abi === '108') { - return require('../sentry_cpu_profiler-darwin-x64-108.node'); + return createdRequire('../sentry_cpu_profiler-darwin-x64-108.node'); } if (abi === '115') { - return require('../sentry_cpu_profiler-darwin-x64-115.node'); + return createdRequire('../sentry_cpu_profiler-darwin-x64-115.node'); } if (abi === '127') { - return require('../sentry_cpu_profiler-darwin-x64-127.node'); + return createdRequire('../sentry_cpu_profiler-darwin-x64-127.node'); } } if (arch === 'arm64') { if (abi === '93') { - return require('../sentry_cpu_profiler-darwin-arm64-93.node'); + return createdRequire('../sentry_cpu_profiler-darwin-arm64-93.node'); } if (abi === '108') { - return require('../sentry_cpu_profiler-darwin-arm64-108.node'); + return createdRequire('../sentry_cpu_profiler-darwin-arm64-108.node'); } if (abi === '115') { - return require('../sentry_cpu_profiler-darwin-arm64-115.node'); + return createdRequire('../sentry_cpu_profiler-darwin-arm64-115.node'); } if (abi === '127') { - return require('../sentry_cpu_profiler-darwin-arm64-127.node'); + return createdRequire('../sentry_cpu_profiler-darwin-arm64-127.node'); } } } @@ -83,16 +91,16 @@ export function importCppBindingsModule(): PrivateV8CpuProfilerBindings { if (platform === 'win32') { if (arch === 'x64') { if (abi === '93') { - return require('../sentry_cpu_profiler-win32-x64-93.node'); + return createdRequire('../sentry_cpu_profiler-win32-x64-93.node'); } if (abi === '108') { - return require('../sentry_cpu_profiler-win32-x64-108.node'); + return createdRequire('../sentry_cpu_profiler-win32-x64-108.node'); } if (abi === '115') { - return require('../sentry_cpu_profiler-win32-x64-115.node'); + return createdRequire('../sentry_cpu_profiler-win32-x64-115.node'); } if (abi === '127') { - return require('../sentry_cpu_profiler-win32-x64-127.node'); + return createdRequire('../sentry_cpu_profiler-win32-x64-127.node'); } } } @@ -101,66 +109,68 @@ export function importCppBindingsModule(): PrivateV8CpuProfilerBindings { if (arch === 'x64') { if (stdlib === 'musl') { if (abi === '93') { - return require('../sentry_cpu_profiler-linux-x64-musl-93.node'); + return createdRequire('../sentry_cpu_profiler-linux-x64-musl-93.node'); } if (abi === '108') { - return require('../sentry_cpu_profiler-linux-x64-musl-108.node'); + return createdRequire('../sentry_cpu_profiler-linux-x64-musl-108.node'); } if (abi === '115') { - return require('../sentry_cpu_profiler-linux-x64-musl-115.node'); + return createdRequire('../sentry_cpu_profiler-linux-x64-musl-115.node'); } if (abi === '127') { - return require('../sentry_cpu_profiler-linux-x64-musl-127.node'); + return createdRequire('../sentry_cpu_profiler-linux-x64-musl-127.node'); } } if (stdlib === 'glibc') { if (abi === '93') { - return require('../sentry_cpu_profiler-linux-x64-glibc-93.node'); + return createdRequire('../sentry_cpu_profiler-linux-x64-glibc-93.node'); } if (abi === '108') { - return require('../sentry_cpu_profiler-linux-x64-glibc-108.node'); + return createdRequire('../sentry_cpu_profiler-linux-x64-glibc-108.node'); } if (abi === '115') { - return require('../sentry_cpu_profiler-linux-x64-glibc-115.node'); + return createdRequire('../sentry_cpu_profiler-linux-x64-glibc-115.node'); } if (abi === '127') { - return require('../sentry_cpu_profiler-linux-x64-glibc-127.node'); + return createdRequire('../sentry_cpu_profiler-linux-x64-glibc-127.node'); } } } if (arch === 'arm64') { if (stdlib === 'musl') { if (abi === '93') { - return require('../sentry_cpu_profiler-linux-arm64-musl-93.node'); + return createdRequire('../sentry_cpu_profiler-linux-arm64-musl-93.node'); } if (abi === '108') { - return require('../sentry_cpu_profiler-linux-arm64-musl-108.node'); + return createdRequire('../sentry_cpu_profiler-linux-arm64-musl-108.node'); } if (abi === '115') { - return require('../sentry_cpu_profiler-linux-arm64-musl-115.node'); + return createdRequire('../sentry_cpu_profiler-linux-arm64-musl-115.node'); } if (abi === '127') { - return require('../sentry_cpu_profiler-linux-arm64-musl-127.node'); + return createdRequire('../sentry_cpu_profiler-linux-arm64-musl-127.node'); } } if (stdlib === 'glibc') { if (abi === '93') { - return require('../sentry_cpu_profiler-linux-arm64-glibc-93.node'); + return createdRequire('../sentry_cpu_profiler-linux-arm64-glibc-93.node'); } if (abi === '108') { - return require('../sentry_cpu_profiler-linux-arm64-glibc-108.node'); + return createdRequire('../sentry_cpu_profiler-linux-arm64-glibc-108.node'); } if (abi === '115') { - return require('../sentry_cpu_profiler-linux-arm64-glibc-115.node'); + return createdRequire('../sentry_cpu_profiler-linux-arm64-glibc-115.node'); } if (abi === '127') { - return require('../sentry_cpu_profiler-linux-arm64-glibc-127.node'); + return createdRequire('../sentry_cpu_profiler-linux-arm64-glibc-127.node'); } } } } - return require(`${built_from_source_path}.node`); + + const built_from_source_path = resolve(esmCompatibleDirname, '..', `sentry_cpu_profiler-${identifier}`); + return createdRequire(`${built_from_source_path}.node`); } const PrivateCpuProfilerBindings: PrivateV8CpuProfilerBindings = importCppBindingsModule(); diff --git a/packages/profiling-node/tsconfig.json b/packages/profiling-node/tsconfig.json index c53d22cf5270..68bd9a52df2a 100644 --- a/packages/profiling-node/tsconfig.json +++ b/packages/profiling-node/tsconfig.json @@ -8,4 +8,3 @@ }, "include": ["src/**/*"] } - diff --git a/packages/react/package.json b/packages/react/package.json index d14ad2f112a8..f68b3bfad560 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/react", - "version": "8.45.0", + "version": "8.50.0", "description": "Official Sentry SDK for React.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/react", @@ -39,8 +39,8 @@ "access": "public" }, "dependencies": { - "@sentry/browser": "8.45.0", - "@sentry/core": "8.45.0", + "@sentry/browser": "8.50.0", + "@sentry/core": "8.50.0", "hoist-non-react-statics": "^3.3.2" }, "peerDependencies": { diff --git a/packages/react/src/errorboundary.tsx b/packages/react/src/errorboundary.tsx index 91cc0e2cdc17..f500d79466cc 100644 --- a/packages/react/src/errorboundary.tsx +++ b/packages/react/src/errorboundary.tsx @@ -35,6 +35,12 @@ export type ErrorBoundaryProps = { * */ fallback?: React.ReactElement | FallbackRender | undefined; + /** + * If set to `true` or `false`, the error `handled` property will be set to the given value. + * If unset, the default behaviour is to rely on the presence of the `fallback` prop to determine + * if the error was handled or not. + */ + handled?: boolean | undefined; /** Called when the error boundary encounters an error */ onError?: ((error: unknown, componentStack: string | undefined, eventId: string) => void) | undefined; /** Called on componentDidMount() */ @@ -107,7 +113,8 @@ class ErrorBoundary extends React.Component & { basename?: string }): TRouter { + return function (routes: RouteObject[], opts?: Record & { basename?: string }): TRouter { const router = createRouterFunction(routes, opts); const basename = opts && opts.basename; @@ -114,6 +111,78 @@ export function createV6CompatibleWrapCreateBrowserRouter< }; } +/** + * Creates a wrapCreateMemoryRouter function that can be used with all React Router v6 compatible versions. + */ +export function createV6CompatibleWrapCreateMemoryRouter< + TState extends RouterState = RouterState, + TRouter extends Router = Router, +>( + createRouterFunction: CreateRouterFunction, + version: V6CompatibleVersion, +): CreateRouterFunction { + if (!_useEffect || !_useLocation || !_useNavigationType || !_matchRoutes) { + DEBUG_BUILD && + logger.warn( + `reactRouterV${version}Instrumentation was unable to wrap the \`createMemoryRouter\` function because of one or more missing parameters.`, + ); + + return createRouterFunction; + } + + return function ( + routes: RouteObject[], + opts?: Record & { + basename?: string; + initialEntries?: (string | { pathname: string })[]; + initialIndex?: number; + }, + ): TRouter { + const router = createRouterFunction(routes, opts); + const basename = opts ? opts.basename : undefined; + + const activeRootSpan = getActiveRootSpan(); + let initialEntry = undefined; + + const initialEntries = opts ? opts.initialEntries : undefined; + const initialIndex = opts ? opts.initialIndex : undefined; + + const hasOnlyOneInitialEntry = initialEntries && initialEntries.length === 1; + const hasIndexedEntry = initialIndex !== undefined && initialEntries && initialEntries[initialIndex]; + + initialEntry = hasOnlyOneInitialEntry + ? initialEntries[0] + : hasIndexedEntry + ? initialEntries[initialIndex] + : undefined; + + const location = initialEntry + ? typeof initialEntry === 'string' + ? { pathname: initialEntry } + : initialEntry + : router.state.location; + + if (router.state.historyAction === 'POP' && activeRootSpan) { + updatePageloadTransaction(activeRootSpan, location, routes, undefined, basename); + } + + router.subscribe((state: RouterState) => { + const location = state.location; + if (state.historyAction === 'PUSH' || state.historyAction === 'POP') { + handleNavigation({ + location, + routes, + navigationType: state.historyAction, + version, + basename, + }); + } + }); + + return router; + }; +} + /** * Creates a browser tracing integration that can be used with all React Router v6 compatible versions. */ @@ -180,7 +249,7 @@ export function createV6CompatibleWrapUseRoutes(origUseRoutes: UseRoutes, versio return origUseRoutes; } - const allRoutes: RouteObject[] = []; + const allRoutes: Set = new Set(); const SentryRoutes: React.FC<{ children?: React.ReactNode; @@ -207,10 +276,21 @@ export function createV6CompatibleWrapUseRoutes(origUseRoutes: UseRoutes, versio if (isMountRenderPass.current) { routes.forEach(route => { - allRoutes.push(...getChildRoutesRecursively(route)); + const extractedChildRoutes = getChildRoutesRecursively(route); + + extractedChildRoutes.forEach(r => { + allRoutes.add(r); + }); }); - updatePageloadTransaction(getActiveRootSpan(), normalizedLocation, routes, undefined, undefined, allRoutes); + updatePageloadTransaction( + getActiveRootSpan(), + normalizedLocation, + routes, + undefined, + undefined, + Array.from(allRoutes), + ); isMountRenderPass.current = false; } else { handleNavigation({ @@ -218,7 +298,7 @@ export function createV6CompatibleWrapUseRoutes(origUseRoutes: UseRoutes, versio routes, navigationType, version, - allRoutes, + allRoutes: Array.from(allRoutes), }); } }, [navigationType, stableLocationParam]); @@ -343,14 +423,18 @@ function locationIsInsideDescendantRoute(location: Location, routes: RouteObject return false; } -function getChildRoutesRecursively(route: RouteObject, allRoutes: RouteObject[] = []): RouteObject[] { - if (route.children && !route.index) { - route.children.forEach(child => { - allRoutes.push(...getChildRoutesRecursively(child, allRoutes)); - }); - } +function getChildRoutesRecursively(route: RouteObject, allRoutes: Set = new Set()): Set { + if (!allRoutes.has(route)) { + allRoutes.add(route); + + if (route.children && !route.index) { + route.children.forEach(child => { + const childRoutes = getChildRoutesRecursively(child, allRoutes); - allRoutes.push(route); + childRoutes.forEach(r => allRoutes.add(r)); + }); + } + } return allRoutes; } @@ -428,10 +512,10 @@ function getNormalizedName( // If path is not a wildcard and has no child routes, append the path if (path && !pathIsWildcardAndHasChildren(path, branch)) { const newPath = path[0] === '/' || pathBuilder[pathBuilder.length - 1] === '/' ? path : `/${path}`; - pathBuilder += newPath; + pathBuilder = trimSlash(pathBuilder) + prefixWithSlash(newPath); // If the path matches the current location, return the path - if (location.pathname.endsWith(basename + branch.pathname)) { + if (trimSlash(location.pathname) === trimSlash(basename + branch.pathname)) { if ( // If the route defined on the element is something like // Product} /> @@ -513,7 +597,7 @@ export function createV6CompatibleWithSentryReactRouterRouting

= new Set(); const SentryRoutes: React.FC

= (props: P) => { const isMountRenderPass = React.useRef(true); @@ -527,10 +611,14 @@ export function createV6CompatibleWithSentryReactRouterRouting

{ - allRoutes.push(...getChildRoutesRecursively(route)); + const extractedChildRoutes = getChildRoutesRecursively(route); + + extractedChildRoutes.forEach(r => { + allRoutes.add(r); + }); }); - updatePageloadTransaction(getActiveRootSpan(), location, routes, undefined, undefined, allRoutes); + updatePageloadTransaction(getActiveRootSpan(), location, routes, undefined, undefined, Array.from(allRoutes)); isMountRenderPass.current = false; } else { handleNavigation({ @@ -538,7 +626,7 @@ export function createV6CompatibleWithSentryReactRouterRouting

= Router, +>(createMemoryRouterFunction: CreateRouterFunction): CreateRouterFunction { + return createV6CompatibleWrapCreateMemoryRouter(createMemoryRouterFunction, '6'); +} + /** * A higher-order component that adds Sentry routing instrumentation to a React Router v6 Route component. * This is used to automatically capture route changes as transactions. diff --git a/packages/react/src/reactrouterv7.tsx b/packages/react/src/reactrouterv7.tsx index df2badd35e44..5a80482cd2c3 100644 --- a/packages/react/src/reactrouterv7.tsx +++ b/packages/react/src/reactrouterv7.tsx @@ -6,6 +6,7 @@ import { createReactRouterV6CompatibleTracingIntegration, createV6CompatibleWithSentryReactRouterRouting, createV6CompatibleWrapCreateBrowserRouter, + createV6CompatibleWrapCreateMemoryRouter, createV6CompatibleWrapUseRoutes, } from './reactrouterv6-compat-utils'; import type { CreateRouterFunction, Router, RouterState, UseRoutes } from './types'; @@ -40,6 +41,19 @@ export function wrapCreateBrowserRouterV7< return createV6CompatibleWrapCreateBrowserRouter(createRouterFunction, '7'); } +/** + * A wrapper function that adds Sentry routing instrumentation to a React Router v7 createMemoryRouter function. + * This is used to automatically capture route changes as transactions when using the createMemoryRouter API. + * The difference between createBrowserRouter and createMemoryRouter is that with createMemoryRouter, + * optional `initialEntries` are also taken into account. + */ +export function wrapCreateMemoryRouterV7< + TState extends RouterState = RouterState, + TRouter extends Router = Router, +>(createMemoryRouterFunction: CreateRouterFunction): CreateRouterFunction { + return createV6CompatibleWrapCreateMemoryRouter(createMemoryRouterFunction, '7'); +} + /** * A wrapper function that adds Sentry routing instrumentation to a React Router v7 useRoutes hook. * This is used to automatically capture route changes as transactions when using the useRoutes hook. diff --git a/packages/react/test/errorboundary.test.tsx b/packages/react/test/errorboundary.test.tsx index e0a7328995e0..81f276255a96 100644 --- a/packages/react/test/errorboundary.test.tsx +++ b/packages/react/test/errorboundary.test.tsx @@ -4,7 +4,7 @@ import { fireEvent, render, screen } from '@testing-library/react'; import * as React from 'react'; import { useState } from 'react'; -import type { ErrorBoundaryProps } from '../src/errorboundary'; +import type { ErrorBoundaryProps, FallbackRender } from '../src/errorboundary'; import { ErrorBoundary, UNKNOWN_COMPONENT, withErrorBoundary } from '../src/errorboundary'; const mockCaptureException = jest.fn(); @@ -537,47 +537,47 @@ describe('ErrorBoundary', () => { expect(mockOnReset).toHaveBeenCalledTimes(1); expect(mockOnReset).toHaveBeenCalledWith(expect.any(Error), expect.any(String), expect.any(String)); }); + it.each` + fallback | handled | expected + ${true} | ${undefined} | ${true} + ${false} | ${undefined} | ${false} + ${true} | ${false} | ${false} + ${true} | ${true} | ${true} + ${false} | ${true} | ${true} + ${false} | ${false} | ${false} + `( + 'sets `handled: $expected` when `handled` is $handled and `fallback` is $fallback', + async ({ + fallback, + handled, + expected, + }: { + fallback: boolean; + handled: boolean | undefined; + expected: boolean; + }) => { + const fallbackComponent: FallbackRender | undefined = fallback + ? ({ resetError }) =>