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..dcaf1b4cf902 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 }}) @@ -713,7 +715,7 @@ jobs: strategy: fail-fast: false matrix: - node: [14, 16, 18, 20, 22] + node: [14, 16, '18.20.5', 20, 22] typescript: - false include: @@ -736,8 +738,8 @@ jobs: node_version: ${{ matrix.node == 14 && '14' || '' }} - name: Overwrite typescript version - if: matrix.typescript - run: node ./scripts/use-ts-version.js ${{ matrix.typescript }} + if: matrix.typescript == '3.8' + run: node ./scripts/use-ts-3_8.js working-directory: dev-packages/node-integration-tests - name: Run integration tests @@ -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..1da6d6b602be 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -47,14 +47,14 @@ 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', path: 'packages/browser/build/npm/esm/index.js', import: createImport('init', 'browserTracingIntegration', 'replayIntegration'), gzip: true, - limit: '68 KB', + limit: '68.5 KB', modifyWebpackConfig: function (config) { const webpack = require('webpack'); const TerserPlugin = require('terser-webpack-plugin'); diff --git a/CHANGELOG.md b/CHANGELOG.md index dd6332f4c125..7e248963972f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,192 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 8.54.0 + +- feat(v8/deps): Upgrade all OpenTelemetry dependencies ([#15098](https://github.com/getsentry/sentry-javascript/pull/15098)) +- fix(node/v8): Add compatibility layer for Prisma v5 ([#15210](https://github.com/getsentry/sentry-javascript/pull/15210)) + +Work in this release was contributed by @nwalters512. Thank you for your contribution! + +## 8.53.0 + +- feat(v8/nuxt): Add `url` to `SourcemapsUploadOptions` (#15202) +- fix(v8/react): `fromLocation` can be undefined in Tanstack Router Instrumentation (#15237) + +Work in this release was contributed by @tannerlinsley. Thank you for your contribution! + +## 8.52.1 + +- fix(v8/nextjs): Fix nextjs build warning (#15226) +- ref(v8/browser): Add protocol attributes to resource spans #15224 +- ref(v8/core): Don't set `this.name` to `new.target.prototype.constructor.name` (#15222) + +Work in this release was contributed by @Zen-cronic. Thank you for your contribution! + +## 8.52.0 + +### Important Changes + +- **feat(solidstart): Add `withSentry` wrapper for SolidStart config ([#15135](https://github.com/getsentry/sentry-javascript/pull/15135))** + +To enable the SolidStart SDK, wrap your SolidStart Config with `withSentry`. The `sentrySolidStartVite` plugin is now automatically +added by `withSentry` and you can pass the Sentry build-time options like this: + +```js +import { defineConfig } from '@solidjs/start/config'; +import { withSentry } from '@sentry/solidstart'; + +export default defineConfig( + withSentry( + { + /* Your SolidStart config options... */ + }, + { + // Options for setting up source maps + org: process.env.SENTRY_ORG, + project: process.env.SENTRY_PROJECT, + authToken: process.env.SENTRY_AUTH_TOKEN, + }, + ), +); +``` + +With the `withSentry` wrapper, the Sentry server config should not be added to the `public` directory anymore. +Add the Sentry server config in `src/instrument.server.ts`. Then, the server config will be placed inside the server build output as `instrument.server.mjs`. + +Now, there are two options to set up the SDK: + +1. **(recommended)** Provide an `--import` CLI flag to the start command like this (path depends on your server setup): + `node --import ./.output/server/instrument.server.mjs .output/server/index.mjs` +2. Add `autoInjectServerSentry: 'top-level-import'` and the Sentry config will be imported at the top of the server entry (comes with tracing limitations) + ```js + withSentry( + { + /* Your SolidStart config options... */ + }, + { + // Optional: Install Sentry with a top-level import + autoInjectServerSentry: 'top-level-import', + }, + ); + ``` + +### Other Changes + +- feat(v8/core): Add client outcomes for breadcrumbs buffer ([#15149](https://github.com/getsentry/sentry-javascript/pull/15149)) +- feat(v8/core): Improve error formatting in ZodErrors integration ([#15155](https://github.com/getsentry/sentry-javascript/pull/15155)) +- fix(v8/bun): Ensure instrumentation of `Bun.serve` survives a server reload ([#15157](https://github.com/getsentry/sentry-javascript/pull/15157)) +- fix(v8/core): Pass `module` into `loadModule` ([#15139](https://github.com/getsentry/sentry-javascript/pull/15139)) (#15166) + +Work in this release was contributed by @jahands, @jrandolf, and @nathankleyn. Thank you for your contributions! + +## 8.51.0 + +### Important Changes + +- **feat(v8/node): Add `prismaInstrumentation` option to Prisma integration as escape hatch for all Prisma versions ([#15128](https://github.com/getsentry/sentry-javascript/pull/15128))** + + This release adds a compatibility API to add support for Prisma version 6. + To capture performance data for Prisma version 6: + + 1. Install the `@prisma/instrumentation` package on version 6. + 1. Pass a `new PrismaInstrumentation()` instance as exported from `@prisma/instrumentation` to the `prismaInstrumentation` option: + + ```js + import { PrismaInstrumentation } from '@prisma/instrumentation'; + + Sentry.init({ + integrations: [ + prismaIntegration({ + // Override the default instrumentation that Sentry uses + prismaInstrumentation: new PrismaInstrumentation(), + }), + ], + }); + ``` + + The passed instrumentation instance will override the default instrumentation instance the integration would use, while the `prismaIntegration` will still ensure data compatibility for the various Prisma versions. + + 1. Remove the `previewFeatures = ["tracing"]` option from the client generator block of your Prisma schema. + +### Other Changes + +- feat(v8/browser): Add `multiplexedtransport.js` CDN bundle ([#15046](https://github.com/getsentry/sentry-javascript/pull/15046)) +- feat(v8/browser): Add Unleash integration ([#14948](https://github.com/getsentry/sentry-javascript/pull/14948)) +- feat(v8/deno): Deprecate Deno SDK as published on deno.land ([#15121](https://github.com/getsentry/sentry-javascript/pull/15121)) +- feat(v8/sveltekit): Deprecate `fetchProxyScriptNonce` option ([#15011](https://github.com/getsentry/sentry-javascript/pull/15011)) +- fix(v8/aws-lambda): Avoid overwriting root span name ([#15054](https://github.com/getsentry/sentry-javascript/pull/15054)) +- fix(v8/core): `fatal` events should set session as crashed ([#15073](https://github.com/getsentry/sentry-javascript/pull/15073)) +- fix(v8/node/nestjs): Use method on current fastify request ([#15104](https://github.com/getsentry/sentry-javascript/pull/15104)) + +Work in this release was contributed by @tjhiggins, and @nwalters512. Thank you for your contributions! + +## 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 contributions! + +## 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..5dadcfff7c6f 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.54.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.54.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..1e0303b9c356 --- /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({ featureFlagClientClass: 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/badSignatureDeprecated/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/badSignatureDeprecated/init.js new file mode 100644 index 000000000000..dc92fbc296a4 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/badSignatureDeprecated/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/badSignatureDeprecated/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/badSignatureDeprecated/test.ts new file mode 100644 index 000000000000..9b95d4d51c81 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/badSignatureDeprecated/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/basicDeprecated/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/basicDeprecated/init.js new file mode 100644 index 000000000000..9f1f28730cf7 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/basicDeprecated/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/basicDeprecated/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/basicDeprecated/test.ts new file mode 100644 index 000000000000..5bb72caddd24 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/basicDeprecated/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..ddc74b6427b4 --- /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({ featureFlagClientClass: 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/integrations/featureFlags/unleash/withScopeDeprecated/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/withScopeDeprecated/init.js new file mode 100644 index 000000000000..9f1f28730cf7 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/withScopeDeprecated/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/withScopeDeprecated/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/withScopeDeprecated/test.ts new file mode 100644 index 000000000000..2d821bf6c81d --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/withScopeDeprecated/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 { +sentryTest('should add resource spans to pageload transaction', async ({ getLocalTestUrl, page, browserName }) => { if (shouldSkipTracingTest()) { sentryTest.skip(); } + const isWebkitRun = browserName === 'webkit'; + // Intercepting asset requests to avoid network-related flakiness and random retries (on Firefox). await page.route('https://example.com/path/to/image.svg', (route: Route) => - route.fulfill({ path: `${__dirname}/assets/image.svg` }), + route.fulfill({ + path: `${__dirname}/assets/image.svg`, + headers: { + 'Timing-Allow-Origin': '*', + 'Content-Type': 'image/svg+xml', + }, + }), ); await page.route('https://example.com/path/to/script.js', (route: Route) => - route.fulfill({ path: `${__dirname}/assets/script.js` }), + route.fulfill({ + path: `${__dirname}/assets/script.js`, + headers: { + 'Timing-Allow-Origin': '*', + 'Content-Type': 'application/javascript', + }, + }), ); await page.route('https://example.com/path/to/style.css', (route: Route) => - route.fulfill({ path: `${__dirname}/assets/style.css` }), + route.fulfill({ + path: `${__dirname}/assets/style.css`, + headers: { + 'Timing-Allow-Origin': '*', + 'Content-Type': 'text/css', + }, + }), ); const url = await getLocalTestUrl({ testDir: __dirname }); @@ -27,11 +47,14 @@ sentryTest('should add resource spans to pageload transaction', async ({ getLoca const resourceSpans = eventData.spans?.filter(({ op }) => op?.startsWith('resource')); const scriptSpans = resourceSpans?.filter(({ op }) => op === 'resource.script'); - const linkSpans = resourceSpans?.filter(({ op }) => op === 'resource.link'); - const imgSpans = resourceSpans?.filter(({ op }) => op === 'resource.img'); + const linkSpan = resourceSpans?.filter(({ op }) => op === 'resource.link')[0]; + const imgSpan = resourceSpans?.filter(({ op }) => op === 'resource.img')[0]; + + const spanId = eventData.contexts?.trace?.span_id; + const traceId = eventData.contexts?.trace?.trace_id; - expect(imgSpans).toHaveLength(1); - expect(linkSpans).toHaveLength(1); + expect(spanId).toBeDefined(); + expect(traceId).toBeDefined(); const hasCdnBundle = (process.env.PW_BUNDLE || '').startsWith('bundle'); @@ -41,11 +64,90 @@ sentryTest('should add resource spans to pageload transaction', async ({ getLoca } expect(scriptSpans?.map(({ description }) => description).sort()).toEqual(expectedScripts); + expect(scriptSpans?.map(({ parent_span_id }) => parent_span_id)).toEqual(expectedScripts.map(() => spanId)); - const spanId = eventData.contexts?.trace?.span_id; + const customScriptSpan = scriptSpans?.find( + ({ description }) => description === 'https://example.com/path/to/script.js', + ); - expect(spanId).toBeDefined(); - expect(imgSpans?.[0].parent_span_id).toBe(spanId); - expect(linkSpans?.[0].parent_span_id).toBe(spanId); - expect(scriptSpans?.map(({ parent_span_id }) => parent_span_id)).toEqual(expectedScripts.map(() => spanId)); + expect(imgSpan).toEqual({ + data: { + 'http.decoded_response_content_length': expect.any(Number), + 'http.response_content_length': expect.any(Number), + 'http.response_transfer_size': expect.any(Number), + 'network.protocol.name': '', + 'network.protocol.version': 'unknown', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'resource.img', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics', + 'server.address': 'example.com', + 'url.same_origin': false, + 'url.scheme': 'https', + ...(!isWebkitRun && { + 'resource.render_blocking_status': 'non-blocking', + 'http.response_delivery_type': '', + }), + }, + description: 'https://example.com/path/to/image.svg', + op: 'resource.img', + origin: 'auto.resource.browser.metrics', + parent_span_id: spanId, + span_id: expect.stringMatching(/^[a-f0-9]{16}$/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: traceId, + }); + + expect(linkSpan).toEqual({ + data: { + 'http.decoded_response_content_length': expect.any(Number), + 'http.response_content_length': expect.any(Number), + 'http.response_transfer_size': expect.any(Number), + 'network.protocol.name': '', + 'network.protocol.version': 'unknown', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'resource.link', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics', + 'server.address': 'example.com', + 'url.same_origin': false, + 'url.scheme': 'https', + ...(!isWebkitRun && { + 'resource.render_blocking_status': 'non-blocking', + 'http.response_delivery_type': '', + }), + }, + description: 'https://example.com/path/to/style.css', + op: 'resource.link', + origin: 'auto.resource.browser.metrics', + parent_span_id: spanId, + span_id: expect.stringMatching(/^[a-f0-9]{16}$/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: traceId, + }); + + expect(customScriptSpan).toEqual({ + data: { + 'http.decoded_response_content_length': expect.any(Number), + 'http.response_content_length': expect.any(Number), + 'http.response_transfer_size': expect.any(Number), + 'network.protocol.name': '', + 'network.protocol.version': 'unknown', + 'sentry.op': 'resource.script', + 'sentry.origin': 'auto.resource.browser.metrics', + 'server.address': 'example.com', + 'url.same_origin': false, + 'url.scheme': 'https', + ...(!isWebkitRun && { + 'resource.render_blocking_status': 'non-blocking', + 'http.response_delivery_type': '', + }), + }, + description: 'https://example.com/path/to/script.js', + op: 'resource.script', + origin: 'auto.resource.browser.metrics', + parent_span_id: spanId, + span_id: expect.stringMatching(/^[a-f0-9]{16}$/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: traceId, + }); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/fetch/test.ts index bcbfa1890cdd..e1aca233ecf2 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/fetch/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch/test.ts @@ -1,8 +1,10 @@ import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; - import { sentryTest } from '../../../../utils/fixtures'; -import { getMultipleSentryEnvelopeRequests, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { + envelopeRequestParser, + shouldSkipTracingTest, + waitForTransactionRequestOnUrl, +} from '../../../../utils/helpers'; sentryTest('should create spans for fetch requests', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { @@ -11,14 +13,8 @@ sentryTest('should create spans for fetch requests', async ({ getLocalTestUrl, p const url = await getLocalTestUrl({ testDir: __dirname }); - // Because we fetch from http://example.com, fetch will throw a CORS error in firefox and webkit. - // Chromium does not throw for cors errors. - // This means that we will intercept a dynamic amount of envelopes here. - - // We will wait 500ms for all envelopes to be sent. Generally, in all browsers, the last sent - // envelope contains tracing data. - const envelopes = await getMultipleSentryEnvelopeRequests(page, 4, { url, timeout: 10000 }); - const tracingEvent = envelopes.find(event => event.type === 'transaction')!; // last envelope contains tracing data on all browsers + const req = await waitForTransactionRequestOnUrl(page, url); + const tracingEvent = envelopeRequestParser(req); const requestSpans = tracingEvent.spans?.filter(({ op }) => op === 'http.client'); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/init.js index 7cd076a052e5..092c43f75eac 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/request/init.js @@ -7,4 +7,5 @@ Sentry.init({ integrations: [Sentry.browserTracingIntegration()], tracePropagationTargets: ['http://example.com'], tracesSampleRate: 1, + autoSessionTracking: false, }); diff --git a/dev-packages/browser-integration-tests/suites/transport/multiplexed/init.js b/dev-packages/browser-integration-tests/suites/transport/multiplexed/init.js new file mode 100644 index 000000000000..9247e1d8bcc2 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/transport/multiplexed/init.js @@ -0,0 +1,20 @@ +import * as Sentry from '@sentry/browser'; + +import { makeMultiplexedTransport } from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + transport: makeMultiplexedTransport(Sentry.makeFetchTransport, ({ getEvent }) => { + const event = getEvent('event'); + + if (event.tags.to === 'a') { + return ['https://public@dsn.ingest.sentry.io/1337']; + } else if (event.tags.to === 'b') { + return ['https://public@dsn.ingest.sentry.io/1337']; + } else { + throw new Error('Unknown destination'); + } + }), +}); diff --git a/dev-packages/browser-integration-tests/suites/transport/multiplexed/subject.js b/dev-packages/browser-integration-tests/suites/transport/multiplexed/subject.js new file mode 100644 index 000000000000..89bb4b22eca1 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/transport/multiplexed/subject.js @@ -0,0 +1,10 @@ +setTimeout(() => { + Sentry.withScope(scope => { + scope.setTag('to', 'a'); + Sentry.captureException(new Error('Error a')); + }); + Sentry.withScope(scope => { + scope.setTag('to', 'b'); + Sentry.captureException(new Error('Error b')); + }); +}, 0); diff --git a/dev-packages/browser-integration-tests/suites/transport/multiplexed/test.ts b/dev-packages/browser-integration-tests/suites/transport/multiplexed/test.ts new file mode 100644 index 000000000000..0bf274291df4 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/transport/multiplexed/test.ts @@ -0,0 +1,20 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/core'; + +import { sentryTest } from '../../../utils/fixtures'; +import { getMultipleSentryEnvelopeRequests } from '../../../utils/helpers'; + +sentryTest('sends event to DSNs specified in makeMultiplexedTransport', async ({ getLocalTestUrl, page }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); + const errorEvents = await getMultipleSentryEnvelopeRequests(page, 2, { envelopeType: 'event', url }); + + expect(errorEvents).toHaveLength(2); + + const [evt1, evt2] = errorEvents; + + const errorA = evt1?.tags?.to === 'a' ? evt1 : evt2; + const errorB = evt1?.tags?.to === 'b' ? evt1 : evt2; + + expect(errorA.tags?.to).toBe('a'); + expect(errorB.tags?.to).toBe('b'); +}); diff --git a/dev-packages/browser-integration-tests/utils/generatePlugin.ts b/dev-packages/browser-integration-tests/utils/generatePlugin.ts index b9b4dcb4c1f3..7ca1250296b9 100644 --- a/dev-packages/browser-integration-tests/utils/generatePlugin.ts +++ b/dev-packages/browser-integration-tests/utils/generatePlugin.ts @@ -1,6 +1,6 @@ import fs from 'fs'; import path from 'path'; -import type { Package } from '@sentry/core'; +import { type Package } from '@sentry/core'; import HtmlWebpackPlugin, { createHtmlTagObject } from 'html-webpack-plugin'; import type { Compiler } from 'webpack'; @@ -38,6 +38,8 @@ const IMPORTED_INTEGRATION_CDN_BUNDLE_PATHS: Record = { sessionTimingIntegration: 'sessiontiming', feedbackIntegration: 'feedback', moduleMetadataIntegration: 'modulemetadata', + // technically, this is not an integration, but let's add it anyway for simplicity + makeMultiplexedTransport: 'multiplexedtransport', }; const BUNDLE_PATHS: Record> = { diff --git a/dev-packages/bundle-analyzer-scenarios/package.json b/dev-packages/bundle-analyzer-scenarios/package.json index c5146697c4d4..48c1948947b3 100644 --- a/dev-packages/bundle-analyzer-scenarios/package.json +++ b/dev-packages/bundle-analyzer-scenarios/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/bundle-analyzer-scenarios", - "version": "8.45.0", + "version": "8.54.0", "description": "Scenarios to test bundle analysis with", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/dev-packages/bundle-analyzer-scenarios", diff --git a/dev-packages/clear-cache-gh-action/package.json b/dev-packages/clear-cache-gh-action/package.json index acf92547dca5..1b55f71e82b3 100644 --- a/dev-packages/clear-cache-gh-action/package.json +++ b/dev-packages/clear-cache-gh-action/package.json @@ -1,7 +1,7 @@ { "name": "@sentry-internal/clear-cache-gh-action", "description": "An internal Github Action to clear GitHub caches.", - "version": "8.45.0", + "version": "8.54.0", "license": "MIT", "engines": { "node": ">=18" diff --git a/dev-packages/e2e-tests/package.json b/dev-packages/e2e-tests/package.json index 6452d7752eba..e33e4ae03cd9 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.54.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/create-remix-app-express-vite-dev/package.json b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/package.json index aeee72f96477..b37c7a8c0705 100644 --- a/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/package.json +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express-vite-dev/package.json @@ -44,7 +44,7 @@ "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.6.0", "typescript": "^5.1.6", - "vite": "^5.4.10", + "vite": "^5.4.11", "vite-tsconfig-paths": "^4.2.1" }, "volta": { diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/app.controller.ts b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/app.controller.ts index 33a6b1957d99..ca29bb9fb9ae 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/app.controller.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/app.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Param, ParseIntPipe, UseFilters, UseGuards, UseInterceptors } from '@nestjs/common'; +import { All, Controller, Get, Param, ParseIntPipe, UseFilters, UseGuards, UseInterceptors } from '@nestjs/common'; import { flush } from '@sentry/nestjs'; import { AppService } from './app.service'; import { AsyncInterceptor } from './async-example.interceptor'; @@ -121,4 +121,9 @@ export class AppController { testFunctionName() { return this.appService.getFunctionName(); } + + @All('test-all') + testAll() { + return {}; + } } diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/transactions.test.ts index 609e01709650..96b60e5d976f 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/transactions.test.ts @@ -808,3 +808,8 @@ test('Calling canActivate method on service with Injectable decorator returns 20 const response = await fetch(`${baseURL}/test-service-canActivate`); expect(response.status).toBe(200); }); + +test('Calling @All method on service with Injectable decorator returns 200', async ({ baseURL }) => { + const response = await fetch(`${baseURL}/test-all`); + expect(response.status).toBe(200); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/app/request-instrumentation/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-14/app/request-instrumentation/page.tsx index c11efda8adc9..b06521f2cb10 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-14/app/request-instrumentation/page.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-14/app/request-instrumentation/page.tsx @@ -3,9 +3,9 @@ import http from 'http'; export const dynamic = 'force-dynamic'; export default async function Page() { - await fetch('http://example.com/', { cache: 'no-cache' }).then(res => res.text()); + await fetch('http://github.com/', { cache: 'no-cache' }).then(res => res.text()); await new Promise(resolve => { - http.get('http://example.com/', res => { + http.get('http://github.com/', res => { res.on('data', () => { // Noop consuming some data so that request can close :) }); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/tests/request-instrumentation.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-14/tests/request-instrumentation.test.ts index 65b9c4312d91..fc2feb101760 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-14/tests/request-instrumentation.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-14/tests/request-instrumentation.test.ts @@ -19,7 +19,7 @@ test('Should send a transaction with a fetch span', async ({ page }) => { 'sentry.op': 'http.client', 'sentry.origin': 'auto.http.otel.node_fetch', }), - description: 'GET http://example.com/', + description: 'GET http://github.com/', }), ); @@ -30,7 +30,7 @@ test('Should send a transaction with a fetch span', async ({ page }) => { 'sentry.op': 'http.client', 'sentry.origin': 'auto.http.otel.http', }), - description: 'GET http://example.com/', + description: 'GET http://github.com/', }), ); }); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/assert-build.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/assert-build.ts index 955988101724..70564e0c12bb 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/assert-build.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/assert-build.ts @@ -10,7 +10,7 @@ const buildStderr = fs.readFileSync('.tmp_build_stderr', 'utf-8'); // Assert that there was no funky build time warning when we are on a stable (pinned) version if (nextjsVersion !== 'latest' && !nextjsVersion.includes('-canary') && !nextjsVersion.includes('-rc')) { - assert.doesNotMatch(buildStderr, /Import trace for requested module/); // This is Next.js/Webpack speech for "something is off" + assert.doesNotMatch(buildStderr, /Import trace for requested module/, `Build warning in output:\n${buildStderr}`); // This is Next.js/Webpack speech for "something is off" } // Assert that all static components stay static and all dynamic components stay dynamic 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/react-send-to-sentry/tests/send-to-sentry.test.ts b/dev-packages/e2e-tests/test-applications/react-send-to-sentry/tests/send-to-sentry.test.ts index d9c3e09f2ad2..dc33d271bc18 100644 --- a/dev-packages/e2e-tests/test-applications/react-send-to-sentry/tests/send-to-sentry.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-send-to-sentry/tests/send-to-sentry.test.ts @@ -190,7 +190,7 @@ test('Sends a Replay recording to Sentry', async ({ browser }) => { if (response.ok) { const data = await response.json(); - return data[0]; + return { data: data[0], length: data[0].length }; } return response.status; @@ -199,5 +199,6 @@ test('Sends a Replay recording to Sentry', async ({ browser }) => { timeout: EVENT_POLLING_TIMEOUT, }, ) - .toEqual(ReplayRecordingData); + // Check that that all expected data is present but relax the order to avoid flakes + .toEqual({ data: expect.arrayContaining(ReplayRecordingData), length: ReplayRecordingData.length }); }); diff --git a/dev-packages/e2e-tests/test-applications/solid-solidrouter/package.json b/dev-packages/e2e-tests/test-applications/solid-solidrouter/package.json index cbb7afd9d09c..0c727d46de50 100644 --- a/dev-packages/e2e-tests/test-applications/solid-solidrouter/package.json +++ b/dev-packages/e2e-tests/test-applications/solid-solidrouter/package.json @@ -21,7 +21,7 @@ "postcss": "^8.4.33", "solid-devtools": "^0.29.2", "tailwindcss": "^3.4.1", - "vite": "^5.4.10", + "vite": "^5.4.11", "vite-plugin-solid": "^2.8.2" }, "dependencies": { diff --git a/dev-packages/e2e-tests/test-applications/solid/package.json b/dev-packages/e2e-tests/test-applications/solid/package.json index bb37aa10f263..d61ac0a0a322 100644 --- a/dev-packages/e2e-tests/test-applications/solid/package.json +++ b/dev-packages/e2e-tests/test-applications/solid/package.json @@ -21,7 +21,7 @@ "postcss": "^8.4.33", "solid-devtools": "^0.29.2", "tailwindcss": "^3.4.1", - "vite": "^5.4.10", + "vite": "^5.4.11", "vite-plugin-solid": "^2.8.2" }, "dependencies": { diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/.gitignore b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/.gitignore new file mode 100644 index 000000000000..a51ed3c20c8d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/.gitignore @@ -0,0 +1,46 @@ + +dist +.solid +.output +.vercel +.netlify +.vinxi + +# Environment +.env +.env*.local + +# dependencies +/node_modules +/.pnp +.pnp.js + +# IDEs and editors +/.idea +.project +.classpath +*.launch +.settings/ + +# Temp +gitignore + +# testing +/coverage + +# 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/solidstart-dynamic-import/.npmrc b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/.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/solidstart-dynamic-import/README.md b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/README.md new file mode 100644 index 000000000000..9a141e9c2f0d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/README.md @@ -0,0 +1,45 @@ +# SolidStart + +Everything you need to build a Solid project, powered by [`solid-start`](https://start.solidjs.com); + +## Creating a project + +```bash +# create a new project in the current directory +npm init solid@latest + +# create a new project in my-app +npm init solid@latest my-app +``` + +## Developing + +Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a +development server: + +```bash +npm run dev + +# or start the server and open the app in a new browser tab +npm run dev -- --open +``` + +## Building + +Solid apps are built with _presets_, which optimise your project for deployment to different environments. + +By default, `npm run build` will generate a Node app that you can run with `npm start`. To use a different preset, add +it to the `devDependencies` in `package.json` and specify in your `app.config.js`. + +## Testing + +Tests are written with `vitest`, `@solidjs/testing-library` and `@testing-library/jest-dom` to extend expect with some +helpful custom matchers. + +To run them, simply start: + +```sh +npm test +``` + +## This project was created with the [Solid CLI](https://solid-cli.netlify.app) diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/app.config.ts b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/app.config.ts new file mode 100644 index 000000000000..f41b1cb186ef --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/app.config.ts @@ -0,0 +1,11 @@ +import { withSentry } from '@sentry/solidstart'; +import { defineConfig } from '@solidjs/start/config'; + +export default defineConfig( + withSentry( + {}, + { + autoInjectServerSentry: 'experimental_dynamic-import', + }, + ), +); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/package.json b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/package.json new file mode 100644 index 000000000000..62393e038dce --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/package.json @@ -0,0 +1,37 @@ +{ + "name": "solidstart-dynamic-import-e2e-testapp", + "version": "0.0.0", + "scripts": { + "clean": "pnpx rimraf node_modules pnpm-lock.yaml .vinxi .output", + "dev": "vinxi dev", + "build": "vinxi build && sh ./post_build.sh", + "preview": "HOST=localhost PORT=3030 vinxi start", + "test:prod": "TEST_ENV=production playwright test", + "test:build": "pnpm install && pnpm build", + "test:assert": "pnpm test:prod" + }, + "type": "module", + "dependencies": { + "@sentry/solidstart": "latest || *" + }, + "devDependencies": { + "@playwright/test": "^1.44.1", + "@solidjs/meta": "^0.29.4", + "@solidjs/router": "^0.13.4", + "@solidjs/start": "^1.0.2", + "@solidjs/testing-library": "^0.8.7", + "@testing-library/jest-dom": "^6.4.2", + "@testing-library/user-event": "^14.5.2", + "@vitest/ui": "^1.5.0", + "jsdom": "^24.0.0", + "solid-js": "1.8.17", + "typescript": "^5.4.5", + "vinxi": "^0.4.0", + "vite": "^5.4.10", + "vite-plugin-solid": "^2.10.2", + "vitest": "^1.5.0" + }, + "overrides": { + "@vercel/nft": "0.27.4" + } +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/playwright.config.mjs new file mode 100644 index 000000000000..395acfc282f9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/playwright.config.mjs @@ -0,0 +1,8 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; + +const config = getPlaywrightConfig({ + startCommand: 'pnpm preview', + port: 3030, +}); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/post_build.sh b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/post_build.sh new file mode 100644 index 000000000000..6ed67c9afb8a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/post_build.sh @@ -0,0 +1,8 @@ +# TODO: Investigate the need for this script periodically and remove once these modules are correctly resolved. + +# This script copies `import-in-the-middle` and `@sentry/solidstart` from the E2E test project root `node_modules` +# to the nitro server build output `node_modules` as these are not properly resolved in our yarn workspace/pnpm +# e2e structure. Some files like `hook.mjs` and `@sentry/solidstart/solidrouter.server.js` are missing. This is +# not reproducible in an external project (when pinning `@vercel/nft` to `v0.27.0` and higher). +cp -r node_modules/.pnpm/import-in-the-middle@1.*/node_modules/import-in-the-middle .output/server/node_modules +cp -rL node_modules/@sentry/solidstart .output/server/node_modules/@sentry diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/public/favicon.ico b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/public/favicon.ico new file mode 100644 index 000000000000..fb282da0719e Binary files /dev/null and b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/public/favicon.ico differ diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/app.tsx b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/app.tsx new file mode 100644 index 000000000000..3eb85218b575 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/app.tsx @@ -0,0 +1,22 @@ +import { withSentryRouterRouting } from '@sentry/solidstart/solidrouter'; +import { MetaProvider, Title } from '@solidjs/meta'; +import { Router } from '@solidjs/router'; +import { FileRoutes } from '@solidjs/start/router'; +import { Suspense } from 'solid-js'; + +const SentryRouter = withSentryRouterRouting(Router); + +export default function App() { + return ( + ( + + SolidStart - with Vitest + {props.children} + + )} + > + + + ); +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/entry-client.tsx b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/entry-client.tsx new file mode 100644 index 000000000000..11087fbb5918 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/entry-client.tsx @@ -0,0 +1,18 @@ +// @refresh reload +import * as Sentry from '@sentry/solidstart'; +import { solidRouterBrowserTracingIntegration } from '@sentry/solidstart/solidrouter'; +import { StartClient, mount } from '@solidjs/start/client'; + +Sentry.init({ + // We can't use env variables here, seems like they are stripped + // out in production builds. + dsn: 'https://public@dsn.ingest.sentry.io/1337', + environment: 'qa', // dynamic sampling bias to keep transactions + integrations: [solidRouterBrowserTracingIntegration()], + tunnel: 'http://localhost:3031/', // proxy server + // Performance Monitoring + tracesSampleRate: 1.0, // Capture 100% of the transactions + debug: !!import.meta.env.DEBUG, +}); + +mount(() => , document.getElementById('app')!); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/entry-server.tsx b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/entry-server.tsx new file mode 100644 index 000000000000..276935366318 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/entry-server.tsx @@ -0,0 +1,21 @@ +// @refresh reload +import { StartServer, createHandler } from '@solidjs/start/server'; + +export default createHandler(() => ( + ( + + + + + + {assets} + + +
{children}
+ {scripts} + + + )} + /> +)); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/instrument.server.ts b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/instrument.server.ts new file mode 100644 index 000000000000..3dd5d8933b7b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/instrument.server.ts @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/solidstart'; + +Sentry.init({ + dsn: process.env.E2E_TEST_DSN, + environment: 'qa', // dynamic sampling bias to keep transactions + tracesSampleRate: 1.0, // Capture 100% of the transactions + tunnel: 'http://localhost:3031/', // proxy server + debug: !!process.env.DEBUG, +}); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/back-navigation.tsx b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/back-navigation.tsx new file mode 100644 index 000000000000..ddd970944bf3 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/back-navigation.tsx @@ -0,0 +1,9 @@ +import { A } from '@solidjs/router'; + +export default function BackNavigation() { + return ( + + User 6 + + ); +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/client-error.tsx b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/client-error.tsx new file mode 100644 index 000000000000..5e405e8c4e40 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/client-error.tsx @@ -0,0 +1,15 @@ +export default function ClientErrorPage() { + return ( +
+ +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/error-boundary.tsx b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/error-boundary.tsx new file mode 100644 index 000000000000..b22607667e7e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/error-boundary.tsx @@ -0,0 +1,64 @@ +import * as Sentry from '@sentry/solidstart'; +import type { ParentProps } from 'solid-js'; +import { ErrorBoundary, createSignal, onMount } from 'solid-js'; + +const SentryErrorBoundary = Sentry.withSentryErrorBoundary(ErrorBoundary); + +const [count, setCount] = createSignal(1); +const [caughtError, setCaughtError] = createSignal(false); + +export default function ErrorBoundaryTestPage() { + return ( + + {caughtError() && ( + + )} +
+
+ +
+
+
+ ); +} + +function Throw(props: { error: string }) { + onMount(() => { + throw new Error(props.error); + }); + return null; +} + +function SampleErrorBoundary(props: ParentProps) { + return ( + ( +
+

Error Boundary Fallback

+
+ {error.message} +
+ +
+ )} + > + {props.children} +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/index.tsx b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/index.tsx new file mode 100644 index 000000000000..9a0b22cc38c6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/index.tsx @@ -0,0 +1,31 @@ +import { A } from '@solidjs/router'; + +export default function Home() { + return ( + <> +

Welcome to Solid Start

+

+ Visit docs.solidjs.com/solid-start to read the documentation +

+ + + ); +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/server-error.tsx b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/server-error.tsx new file mode 100644 index 000000000000..05dce5e10a56 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/server-error.tsx @@ -0,0 +1,17 @@ +import { withServerActionInstrumentation } from '@sentry/solidstart'; +import { createAsync } from '@solidjs/router'; + +const getPrefecture = async () => { + 'use server'; + return await withServerActionInstrumentation('getPrefecture', () => { + throw new Error('Error thrown from Solid Start E2E test app server route'); + + return { prefecture: 'Kanagawa' }; + }); +}; + +export default function ServerErrorPage() { + const data = createAsync(() => getPrefecture()); + + return
Prefecture: {data()?.prefecture}
; +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/users/[id].tsx b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/users/[id].tsx new file mode 100644 index 000000000000..22abd3ba8803 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/users/[id].tsx @@ -0,0 +1,21 @@ +import { withServerActionInstrumentation } from '@sentry/solidstart'; +import { createAsync, useParams } from '@solidjs/router'; + +const getPrefecture = async () => { + 'use server'; + return await withServerActionInstrumentation('getPrefecture', () => { + return { prefecture: 'Ehime' }; + }); +}; +export default function User() { + const params = useParams(); + const userData = createAsync(() => getPrefecture()); + + return ( +
+ User ID: {params.id} +
+ Prefecture: {userData()?.prefecture} +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/start-event-proxy.mjs new file mode 100644 index 000000000000..343e434e030b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'solidstart-dynamic-import', +}); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/errorboundary.test.ts b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/errorboundary.test.ts new file mode 100644 index 000000000000..599b5c121455 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/errorboundary.test.ts @@ -0,0 +1,92 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test('captures an exception', async ({ page }) => { + const errorEventPromise = waitForError('solidstart-dynamic-import', errorEvent => { + return ( + !errorEvent.type && + errorEvent.exception?.values?.[0]?.value === + 'Error 1 thrown from Sentry ErrorBoundary in Solid Start E2E test app' + ); + }); + + await page.goto('/error-boundary'); + // The first page load causes a hydration error on the dev server sometimes - a reload works around this + await page.reload(); + await page.locator('#caughtErrorBtn').click(); + const errorEvent = await errorEventPromise; + + expect(errorEvent).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Error 1 thrown from Sentry ErrorBoundary in Solid Start E2E test app', + mechanism: { + type: 'generic', + handled: true, + }, + }, + ], + }, + transaction: '/error-boundary', + }); +}); + +test('captures a second exception after resetting the boundary', async ({ page }) => { + const firstErrorEventPromise = waitForError('solidstart-dynamic-import', errorEvent => { + return ( + !errorEvent.type && + errorEvent.exception?.values?.[0]?.value === + 'Error 1 thrown from Sentry ErrorBoundary in Solid Start E2E test app' + ); + }); + + await page.goto('/error-boundary'); + await page.locator('#caughtErrorBtn').click(); + const firstErrorEvent = await firstErrorEventPromise; + + expect(firstErrorEvent).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Error 1 thrown from Sentry ErrorBoundary in Solid Start E2E test app', + mechanism: { + type: 'generic', + handled: true, + }, + }, + ], + }, + transaction: '/error-boundary', + }); + + const secondErrorEventPromise = waitForError('solidstart-dynamic-import', errorEvent => { + return ( + !errorEvent.type && + errorEvent.exception?.values?.[0]?.value === + 'Error 2 thrown from Sentry ErrorBoundary in Solid Start E2E test app' + ); + }); + + await page.locator('#errorBoundaryResetBtn').click(); + await page.locator('#caughtErrorBtn').click(); + const secondErrorEvent = await secondErrorEventPromise; + + expect(secondErrorEvent).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Error 2 thrown from Sentry ErrorBoundary in Solid Start E2E test app', + mechanism: { + type: 'generic', + handled: true, + }, + }, + ], + }, + transaction: '/error-boundary', + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/errors.client.test.ts b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/errors.client.test.ts new file mode 100644 index 000000000000..3a1b3ad4b812 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/errors.client.test.ts @@ -0,0 +1,30 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test.describe('client-side errors', () => { + test('captures error thrown on click', async ({ page }) => { + const errorPromise = waitForError('solidstart-dynamic-import', async errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Uncaught error thrown from Solid Start E2E test app'; + }); + + await page.goto(`/client-error`); + await page.locator('#errorBtn').click(); + const error = await errorPromise; + + expect(error).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Uncaught error thrown from Solid Start E2E test app', + mechanism: { + handled: false, + }, + }, + ], + }, + transaction: '/client-error', + }); + expect(error.transaction).toEqual('/client-error'); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/errors.server.test.ts new file mode 100644 index 000000000000..7ef5cd0e07de --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/errors.server.test.ts @@ -0,0 +1,30 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test.describe('server-side errors', () => { + test('captures server action error', async ({ page }) => { + const errorEventPromise = waitForError('solidstart-dynamic-import', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Error thrown from Solid Start E2E test app server route'; + }); + + await page.goto(`/server-error`); + + const error = await errorEventPromise; + + expect(error).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Error thrown from Solid Start E2E test app server route', + mechanism: { + type: 'solidstart', + handled: false, + }, + }, + ], + }, + transaction: 'GET /server-error', + }); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/performance.client.test.ts b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/performance.client.test.ts new file mode 100644 index 000000000000..63f97d519cf8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/performance.client.test.ts @@ -0,0 +1,95 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('sends a pageload transaction', async ({ page }) => { + const transactionPromise = waitForTransaction('solidstart-dynamic-import', async transactionEvent => { + return transactionEvent?.transaction === '/' && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + await page.goto('/'); + const pageloadTransaction = await transactionPromise; + + expect(pageloadTransaction).toMatchObject({ + contexts: { + trace: { + op: 'pageload', + origin: 'auto.pageload.browser', + }, + }, + transaction: '/', + transaction_info: { + source: 'url', + }, + }); +}); + +test('sends a navigation transaction', async ({ page }) => { + const transactionPromise = waitForTransaction('solidstart-dynamic-import', async transactionEvent => { + return transactionEvent?.transaction === '/users/5' && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/`); + await page.locator('#navLink').click(); + const navigationTransaction = await transactionPromise; + + expect(navigationTransaction).toMatchObject({ + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.solidstart.solidrouter', + }, + }, + transaction: '/users/5', + transaction_info: { + source: 'url', + }, + }); +}); + +test('updates the transaction when using the back button', async ({ page }) => { + // Solid Router sends a `-1` navigation when using the back button. + // The sentry solidRouterBrowserTracingIntegration tries to update such + // transactions with the proper name once the `useLocation` hook triggers. + const navigationTxnPromise = waitForTransaction('solidstart-dynamic-import', async transactionEvent => { + return transactionEvent?.transaction === '/users/6' && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/back-navigation`); + await page.locator('#navLink').click(); + const navigationTxn = await navigationTxnPromise; + + expect(navigationTxn).toMatchObject({ + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.solidstart.solidrouter', + }, + }, + transaction: '/users/6', + transaction_info: { + source: 'url', + }, + }); + + const backNavigationTxnPromise = waitForTransaction('solidstart-dynamic-import', async transactionEvent => { + return ( + transactionEvent?.transaction === '/back-navigation' && transactionEvent.contexts?.trace?.op === 'navigation' + ); + }); + + await page.goBack(); + const backNavigationTxn = await backNavigationTxnPromise; + + expect(backNavigationTxn).toMatchObject({ + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.solidstart.solidrouter', + }, + }, + transaction: '/back-navigation', + transaction_info: { + source: 'url', + }, + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/performance.server.test.ts b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/performance.server.test.ts new file mode 100644 index 000000000000..c300014bf012 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/performance.server.test.ts @@ -0,0 +1,55 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, +} from '@sentry/core'; + +test('sends a server action transaction on pageload', async ({ page }) => { + const transactionPromise = waitForTransaction('solidstart-dynamic-import', transactionEvent => { + return transactionEvent?.transaction === 'GET /users/6'; + }); + + await page.goto('/users/6'); + + const transaction = await transactionPromise; + + expect(transaction.spans).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + description: 'getPrefecture', + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function.server_action', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.solidstart', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, + }), + ]), + ); +}); + +test('sends a server action transaction on client navigation', async ({ page }) => { + const transactionPromise = waitForTransaction('solidstart-dynamic-import', transactionEvent => { + return transactionEvent?.transaction === 'POST getPrefecture'; + }); + + await page.goto('/'); + await page.locator('#navLink').click(); + await page.waitForURL('/users/5'); + + const transaction = await transactionPromise; + + expect(transaction.spans).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + description: 'getPrefecture', + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function.server_action', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.solidstart', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, + }), + ]), + ); +}); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tsconfig.json b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tsconfig.json new file mode 100644 index 000000000000..6f11292cc5d8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "allowJs": true, + "strict": true, + "noEmit": true, + "types": ["vinxi/types/client", "vitest/globals", "@testing-library/jest-dom"], + "isolatedModules": true, + "paths": { + "~/*": ["./src/*"] + } + } +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/vitest.config.ts b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/vitest.config.ts new file mode 100644 index 000000000000..6c2b639dc300 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/vitest.config.ts @@ -0,0 +1,10 @@ +import solid from 'vite-plugin-solid'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [solid()], + resolve: { + conditions: ['development', 'browser'], + }, + envPrefix: 'PUBLIC_', +}); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-spa/package.json b/dev-packages/e2e-tests/test-applications/solidstart-spa/package.json index f4ff0802e159..9f8d07823804 100644 --- a/dev-packages/e2e-tests/test-applications/solidstart-spa/package.json +++ b/dev-packages/e2e-tests/test-applications/solidstart-spa/package.json @@ -27,7 +27,7 @@ "solid-js": "1.8.17", "typescript": "^5.4.5", "vinxi": "^0.4.0", - "vite": "^5.2.8", + "vite": "^5.4.11", "vite-plugin-solid": "^2.10.2", "vitest": "^1.5.0" }, diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/.gitignore b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/.gitignore new file mode 100644 index 000000000000..a51ed3c20c8d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/.gitignore @@ -0,0 +1,46 @@ + +dist +.solid +.output +.vercel +.netlify +.vinxi + +# Environment +.env +.env*.local + +# dependencies +/node_modules +/.pnp +.pnp.js + +# IDEs and editors +/.idea +.project +.classpath +*.launch +.settings/ + +# Temp +gitignore + +# testing +/coverage + +# 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/solidstart-top-level-import/.npmrc b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/.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/solidstart-top-level-import/README.md b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/README.md new file mode 100644 index 000000000000..9a141e9c2f0d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/README.md @@ -0,0 +1,45 @@ +# SolidStart + +Everything you need to build a Solid project, powered by [`solid-start`](https://start.solidjs.com); + +## Creating a project + +```bash +# create a new project in the current directory +npm init solid@latest + +# create a new project in my-app +npm init solid@latest my-app +``` + +## Developing + +Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a +development server: + +```bash +npm run dev + +# or start the server and open the app in a new browser tab +npm run dev -- --open +``` + +## Building + +Solid apps are built with _presets_, which optimise your project for deployment to different environments. + +By default, `npm run build` will generate a Node app that you can run with `npm start`. To use a different preset, add +it to the `devDependencies` in `package.json` and specify in your `app.config.js`. + +## Testing + +Tests are written with `vitest`, `@solidjs/testing-library` and `@testing-library/jest-dom` to extend expect with some +helpful custom matchers. + +To run them, simply start: + +```sh +npm test +``` + +## This project was created with the [Solid CLI](https://solid-cli.netlify.app) diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/app.config.ts b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/app.config.ts new file mode 100644 index 000000000000..e4e73e9fc570 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/app.config.ts @@ -0,0 +1,11 @@ +import { withSentry } from '@sentry/solidstart'; +import { defineConfig } from '@solidjs/start/config'; + +export default defineConfig( + withSentry( + {}, + { + autoInjectServerSentry: 'top-level-import', + }, + ), +); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/package.json b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/package.json new file mode 100644 index 000000000000..3df1995d6354 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/package.json @@ -0,0 +1,37 @@ +{ + "name": "solidstart-top-level-import-e2e-testapp", + "version": "0.0.0", + "scripts": { + "clean": "pnpx rimraf node_modules pnpm-lock.yaml .vinxi .output", + "dev": "vinxi dev", + "build": "vinxi build && sh ./post_build.sh", + "preview": "HOST=localhost PORT=3030 vinxi start", + "test:prod": "TEST_ENV=production playwright test", + "test:build": "pnpm install && pnpm build", + "test:assert": "pnpm test:prod" + }, + "type": "module", + "dependencies": { + "@sentry/solidstart": "latest || *" + }, + "devDependencies": { + "@playwright/test": "^1.44.1", + "@solidjs/meta": "^0.29.4", + "@solidjs/router": "^0.13.4", + "@solidjs/start": "^1.0.2", + "@solidjs/testing-library": "^0.8.7", + "@testing-library/jest-dom": "^6.4.2", + "@testing-library/user-event": "^14.5.2", + "@vitest/ui": "^1.5.0", + "jsdom": "^24.0.0", + "solid-js": "1.8.17", + "typescript": "^5.4.5", + "vinxi": "^0.4.0", + "vite": "^5.4.10", + "vite-plugin-solid": "^2.10.2", + "vitest": "^1.5.0" + }, + "overrides": { + "@vercel/nft": "0.27.4" + } +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/playwright.config.mjs new file mode 100644 index 000000000000..395acfc282f9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/playwright.config.mjs @@ -0,0 +1,8 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; + +const config = getPlaywrightConfig({ + startCommand: 'pnpm preview', + port: 3030, +}); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/post_build.sh b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/post_build.sh new file mode 100644 index 000000000000..6ed67c9afb8a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/post_build.sh @@ -0,0 +1,8 @@ +# TODO: Investigate the need for this script periodically and remove once these modules are correctly resolved. + +# This script copies `import-in-the-middle` and `@sentry/solidstart` from the E2E test project root `node_modules` +# to the nitro server build output `node_modules` as these are not properly resolved in our yarn workspace/pnpm +# e2e structure. Some files like `hook.mjs` and `@sentry/solidstart/solidrouter.server.js` are missing. This is +# not reproducible in an external project (when pinning `@vercel/nft` to `v0.27.0` and higher). +cp -r node_modules/.pnpm/import-in-the-middle@1.*/node_modules/import-in-the-middle .output/server/node_modules +cp -rL node_modules/@sentry/solidstart .output/server/node_modules/@sentry diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/public/favicon.ico b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/public/favicon.ico new file mode 100644 index 000000000000..fb282da0719e Binary files /dev/null and b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/public/favicon.ico differ diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/app.tsx b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/app.tsx new file mode 100644 index 000000000000..3eb85218b575 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/app.tsx @@ -0,0 +1,22 @@ +import { withSentryRouterRouting } from '@sentry/solidstart/solidrouter'; +import { MetaProvider, Title } from '@solidjs/meta'; +import { Router } from '@solidjs/router'; +import { FileRoutes } from '@solidjs/start/router'; +import { Suspense } from 'solid-js'; + +const SentryRouter = withSentryRouterRouting(Router); + +export default function App() { + return ( + ( + + SolidStart - with Vitest + {props.children} + + )} + > + + + ); +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/entry-client.tsx b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/entry-client.tsx new file mode 100644 index 000000000000..11087fbb5918 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/entry-client.tsx @@ -0,0 +1,18 @@ +// @refresh reload +import * as Sentry from '@sentry/solidstart'; +import { solidRouterBrowserTracingIntegration } from '@sentry/solidstart/solidrouter'; +import { StartClient, mount } from '@solidjs/start/client'; + +Sentry.init({ + // We can't use env variables here, seems like they are stripped + // out in production builds. + dsn: 'https://public@dsn.ingest.sentry.io/1337', + environment: 'qa', // dynamic sampling bias to keep transactions + integrations: [solidRouterBrowserTracingIntegration()], + tunnel: 'http://localhost:3031/', // proxy server + // Performance Monitoring + tracesSampleRate: 1.0, // Capture 100% of the transactions + debug: !!import.meta.env.DEBUG, +}); + +mount(() => , document.getElementById('app')!); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/entry-server.tsx b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/entry-server.tsx new file mode 100644 index 000000000000..276935366318 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/entry-server.tsx @@ -0,0 +1,21 @@ +// @refresh reload +import { StartServer, createHandler } from '@solidjs/start/server'; + +export default createHandler(() => ( + ( + + + + + + {assets} + + +
{children}
+ {scripts} + + + )} + /> +)); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/instrument.server.ts b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/instrument.server.ts new file mode 100644 index 000000000000..3dd5d8933b7b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/instrument.server.ts @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/solidstart'; + +Sentry.init({ + dsn: process.env.E2E_TEST_DSN, + environment: 'qa', // dynamic sampling bias to keep transactions + tracesSampleRate: 1.0, // Capture 100% of the transactions + tunnel: 'http://localhost:3031/', // proxy server + debug: !!process.env.DEBUG, +}); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/back-navigation.tsx b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/back-navigation.tsx new file mode 100644 index 000000000000..ddd970944bf3 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/back-navigation.tsx @@ -0,0 +1,9 @@ +import { A } from '@solidjs/router'; + +export default function BackNavigation() { + return ( + + User 6 + + ); +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/client-error.tsx b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/client-error.tsx new file mode 100644 index 000000000000..5e405e8c4e40 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/client-error.tsx @@ -0,0 +1,15 @@ +export default function ClientErrorPage() { + return ( +
+ +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/error-boundary.tsx b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/error-boundary.tsx new file mode 100644 index 000000000000..b22607667e7e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/error-boundary.tsx @@ -0,0 +1,64 @@ +import * as Sentry from '@sentry/solidstart'; +import type { ParentProps } from 'solid-js'; +import { ErrorBoundary, createSignal, onMount } from 'solid-js'; + +const SentryErrorBoundary = Sentry.withSentryErrorBoundary(ErrorBoundary); + +const [count, setCount] = createSignal(1); +const [caughtError, setCaughtError] = createSignal(false); + +export default function ErrorBoundaryTestPage() { + return ( + + {caughtError() && ( + + )} +
+
+ +
+
+
+ ); +} + +function Throw(props: { error: string }) { + onMount(() => { + throw new Error(props.error); + }); + return null; +} + +function SampleErrorBoundary(props: ParentProps) { + return ( + ( +
+

Error Boundary Fallback

+
+ {error.message} +
+ +
+ )} + > + {props.children} +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/index.tsx b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/index.tsx new file mode 100644 index 000000000000..9a0b22cc38c6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/index.tsx @@ -0,0 +1,31 @@ +import { A } from '@solidjs/router'; + +export default function Home() { + return ( + <> +

Welcome to Solid Start

+

+ Visit docs.solidjs.com/solid-start to read the documentation +

+ + + ); +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/server-error.tsx b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/server-error.tsx new file mode 100644 index 000000000000..05dce5e10a56 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/server-error.tsx @@ -0,0 +1,17 @@ +import { withServerActionInstrumentation } from '@sentry/solidstart'; +import { createAsync } from '@solidjs/router'; + +const getPrefecture = async () => { + 'use server'; + return await withServerActionInstrumentation('getPrefecture', () => { + throw new Error('Error thrown from Solid Start E2E test app server route'); + + return { prefecture: 'Kanagawa' }; + }); +}; + +export default function ServerErrorPage() { + const data = createAsync(() => getPrefecture()); + + return
Prefecture: {data()?.prefecture}
; +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/users/[id].tsx b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/users/[id].tsx new file mode 100644 index 000000000000..22abd3ba8803 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/users/[id].tsx @@ -0,0 +1,21 @@ +import { withServerActionInstrumentation } from '@sentry/solidstart'; +import { createAsync, useParams } from '@solidjs/router'; + +const getPrefecture = async () => { + 'use server'; + return await withServerActionInstrumentation('getPrefecture', () => { + return { prefecture: 'Ehime' }; + }); +}; +export default function User() { + const params = useParams(); + const userData = createAsync(() => getPrefecture()); + + return ( +
+ User ID: {params.id} +
+ Prefecture: {userData()?.prefecture} +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/start-event-proxy.mjs new file mode 100644 index 000000000000..46cc8824da18 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'solidstart-top-level-import', +}); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/errorboundary.test.ts b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/errorboundary.test.ts new file mode 100644 index 000000000000..49f50f882b50 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/errorboundary.test.ts @@ -0,0 +1,90 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test('captures an exception', async ({ page }) => { + const errorEventPromise = waitForError('solidstart-top-level-import', errorEvent => { + return ( + !errorEvent.type && + errorEvent.exception?.values?.[0]?.value === + 'Error 1 thrown from Sentry ErrorBoundary in Solid Start E2E test app' + ); + }); + + await page.goto('/error-boundary'); + await page.locator('#caughtErrorBtn').click(); + const errorEvent = await errorEventPromise; + + expect(errorEvent).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Error 1 thrown from Sentry ErrorBoundary in Solid Start E2E test app', + mechanism: { + type: 'generic', + handled: true, + }, + }, + ], + }, + transaction: '/error-boundary', + }); +}); + +test('captures a second exception after resetting the boundary', async ({ page }) => { + const firstErrorEventPromise = waitForError('solidstart-top-level-import', errorEvent => { + return ( + !errorEvent.type && + errorEvent.exception?.values?.[0]?.value === + 'Error 1 thrown from Sentry ErrorBoundary in Solid Start E2E test app' + ); + }); + + await page.goto('/error-boundary'); + await page.locator('#caughtErrorBtn').click(); + const firstErrorEvent = await firstErrorEventPromise; + + expect(firstErrorEvent).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Error 1 thrown from Sentry ErrorBoundary in Solid Start E2E test app', + mechanism: { + type: 'generic', + handled: true, + }, + }, + ], + }, + transaction: '/error-boundary', + }); + + const secondErrorEventPromise = waitForError('solidstart-top-level-import', errorEvent => { + return ( + !errorEvent.type && + errorEvent.exception?.values?.[0]?.value === + 'Error 2 thrown from Sentry ErrorBoundary in Solid Start E2E test app' + ); + }); + + await page.locator('#errorBoundaryResetBtn').click(); + await page.locator('#caughtErrorBtn').click(); + const secondErrorEvent = await secondErrorEventPromise; + + expect(secondErrorEvent).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Error 2 thrown from Sentry ErrorBoundary in Solid Start E2E test app', + mechanism: { + type: 'generic', + handled: true, + }, + }, + ], + }, + transaction: '/error-boundary', + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/errors.client.test.ts b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/errors.client.test.ts new file mode 100644 index 000000000000..9e4a0269eee4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/errors.client.test.ts @@ -0,0 +1,30 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test.describe('client-side errors', () => { + test('captures error thrown on click', async ({ page }) => { + const errorPromise = waitForError('solidstart-top-level-import', async errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Uncaught error thrown from Solid Start E2E test app'; + }); + + await page.goto(`/client-error`); + await page.locator('#errorBtn').click(); + const error = await errorPromise; + + expect(error).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Uncaught error thrown from Solid Start E2E test app', + mechanism: { + handled: false, + }, + }, + ], + }, + transaction: '/client-error', + }); + expect(error.transaction).toEqual('/client-error'); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/errors.server.test.ts new file mode 100644 index 000000000000..682dd34e10f9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/errors.server.test.ts @@ -0,0 +1,30 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test.describe('server-side errors', () => { + test('captures server action error', async ({ page }) => { + const errorEventPromise = waitForError('solidstart-top-level-import', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Error thrown from Solid Start E2E test app server route'; + }); + + await page.goto(`/server-error`); + + const error = await errorEventPromise; + + expect(error).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Error thrown from Solid Start E2E test app server route', + mechanism: { + type: 'solidstart', + handled: false, + }, + }, + ], + }, + // transaction: 'GET /server-error', --> only possible with `--import` CLI flag + }); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/performance.client.test.ts b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/performance.client.test.ts new file mode 100644 index 000000000000..bd5dece39b33 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/performance.client.test.ts @@ -0,0 +1,95 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('sends a pageload transaction', async ({ page }) => { + const transactionPromise = waitForTransaction('solidstart-top-level-import', async transactionEvent => { + return transactionEvent?.transaction === '/' && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + await page.goto('/'); + const pageloadTransaction = await transactionPromise; + + expect(pageloadTransaction).toMatchObject({ + contexts: { + trace: { + op: 'pageload', + origin: 'auto.pageload.browser', + }, + }, + transaction: '/', + transaction_info: { + source: 'url', + }, + }); +}); + +test('sends a navigation transaction', async ({ page }) => { + const transactionPromise = waitForTransaction('solidstart-top-level-import', async transactionEvent => { + return transactionEvent?.transaction === '/users/5' && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/`); + await page.locator('#navLink').click(); + const navigationTransaction = await transactionPromise; + + expect(navigationTransaction).toMatchObject({ + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.solidstart.solidrouter', + }, + }, + transaction: '/users/5', + transaction_info: { + source: 'url', + }, + }); +}); + +test('updates the transaction when using the back button', async ({ page }) => { + // Solid Router sends a `-1` navigation when using the back button. + // The sentry solidRouterBrowserTracingIntegration tries to update such + // transactions with the proper name once the `useLocation` hook triggers. + const navigationTxnPromise = waitForTransaction('solidstart-top-level-import', async transactionEvent => { + return transactionEvent?.transaction === '/users/6' && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/back-navigation`); + await page.locator('#navLink').click(); + const navigationTxn = await navigationTxnPromise; + + expect(navigationTxn).toMatchObject({ + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.solidstart.solidrouter', + }, + }, + transaction: '/users/6', + transaction_info: { + source: 'url', + }, + }); + + const backNavigationTxnPromise = waitForTransaction('solidstart-top-level-import', async transactionEvent => { + return ( + transactionEvent?.transaction === '/back-navigation' && transactionEvent.contexts?.trace?.op === 'navigation' + ); + }); + + await page.goBack(); + const backNavigationTxn = await backNavigationTxnPromise; + + expect(backNavigationTxn).toMatchObject({ + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.solidstart.solidrouter', + }, + }, + transaction: '/back-navigation', + transaction_info: { + source: 'url', + }, + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/performance.server.test.ts b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/performance.server.test.ts new file mode 100644 index 000000000000..8072a7e75181 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/performance.server.test.ts @@ -0,0 +1,55 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, +} from '@sentry/core'; + +test('sends a server action transaction on pageload', async ({ page }) => { + const transactionPromise = waitForTransaction('solidstart-top-level-import', transactionEvent => { + return transactionEvent?.transaction === 'GET /users/6'; + }); + + await page.goto('/users/6'); + + const transaction = await transactionPromise; + + expect(transaction.spans).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + description: 'getPrefecture', + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function.server_action', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.solidstart', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, + }), + ]), + ); +}); + +test('sends a server action transaction on client navigation', async ({ page }) => { + const transactionPromise = waitForTransaction('solidstart-top-level-import', transactionEvent => { + return transactionEvent?.transaction === 'POST getPrefecture'; + }); + + await page.goto('/'); + await page.locator('#navLink').click(); + await page.waitForURL('/users/5'); + + const transaction = await transactionPromise; + + expect(transaction.spans).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + description: 'getPrefecture', + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function.server_action', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.solidstart', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, + }), + ]), + ); +}); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tsconfig.json b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tsconfig.json new file mode 100644 index 000000000000..6f11292cc5d8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "allowJs": true, + "strict": true, + "noEmit": true, + "types": ["vinxi/types/client", "vitest/globals", "@testing-library/jest-dom"], + "isolatedModules": true, + "paths": { + "~/*": ["./src/*"] + } + } +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/vitest.config.ts b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/vitest.config.ts new file mode 100644 index 000000000000..6c2b639dc300 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/vitest.config.ts @@ -0,0 +1,10 @@ +import solid from 'vite-plugin-solid'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [solid()], + resolve: { + conditions: ['development', 'browser'], + }, + envPrefix: 'PUBLIC_', +}); diff --git a/dev-packages/e2e-tests/test-applications/solidstart/package.json b/dev-packages/e2e-tests/test-applications/solidstart/package.json index 032a4af9058a..7002a99f69f9 100644 --- a/dev-packages/e2e-tests/test-applications/solidstart/package.json +++ b/dev-packages/e2e-tests/test-applications/solidstart/package.json @@ -27,7 +27,7 @@ "solid-js": "1.8.17", "typescript": "^5.4.5", "vinxi": "^0.4.0", - "vite": "^5.4.10", + "vite": "^5.4.11", "vite-plugin-solid": "^2.10.2", "vitest": "^1.5.0" }, diff --git a/dev-packages/e2e-tests/test-applications/svelte-5/package.json b/dev-packages/e2e-tests/test-applications/svelte-5/package.json index 1022247cc6ea..ed6cf3ada0d7 100644 --- a/dev-packages/e2e-tests/test-applications/svelte-5/package.json +++ b/dev-packages/e2e-tests/test-applications/svelte-5/package.json @@ -22,7 +22,7 @@ "svelte-check": "^3.6.7", "tslib": "^2.6.2", "typescript": "^5.2.2", - "vite": "^5.4.10" + "vite": "^5.4.11" }, "dependencies": { "@sentry/svelte": "latest || *" diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/package.json index 1ce9273bba52..88d9a37ab98c 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/package.json +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/package.json @@ -29,7 +29,7 @@ "svelte-check": "^3.6.0", "tslib": "^2.4.1", "typescript": "^5.0.0", - "vite": "^5.4.10" + "vite": "^5.4.11" }, "type": "module" } diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/package.json index 0c531cd72357..5a2d9ce7b4d5 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/package.json +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-twp/package.json @@ -28,7 +28,7 @@ "svelte-check": "^3.6.0", "tslib": "^2.4.1", "typescript": "^5.0.0", - "vite": "^5.4.10" + "vite": "^5.4.11" }, "type": "module" } diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json index 39f47c873a5f..3f2f87500e25 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json @@ -28,7 +28,7 @@ "svelte": "^4.2.8", "svelte-check": "^3.6.0", "typescript": "^5.0.0", - "vite": "^5.4.10" + "vite": "^5.4.11" }, "type": "module" } diff --git a/dev-packages/e2e-tests/test-applications/tanstack-router/package.json b/dev-packages/e2e-tests/test-applications/tanstack-router/package.json index 54387ae46cde..a2715d739999 100644 --- a/dev-packages/e2e-tests/test-applications/tanstack-router/package.json +++ b/dev-packages/e2e-tests/test-applications/tanstack-router/package.json @@ -24,7 +24,7 @@ "@typescript-eslint/parser": "^7.2.0", "@vitejs/plugin-react-swc": "^3.5.0", "typescript": "^5.2.2", - "vite": "^5.4.10", + "vite": "^5.4.11", "@playwright/test": "^1.44.1", "@sentry-internal/test-utils": "link:../../../test-utils" }, diff --git a/dev-packages/e2e-tests/test-applications/vue-3/package.json b/dev-packages/e2e-tests/test-applications/vue-3/package.json index f34bdf6d6c0e..0cfe8f31a0eb 100644 --- a/dev-packages/e2e-tests/test-applications/vue-3/package.json +++ b/dev-packages/e2e-tests/test-applications/vue-3/package.json @@ -32,7 +32,7 @@ "http-server": "^14.1.1", "npm-run-all2": "^6.2.0", "typescript": "~5.3.0", - "vite": "^5.4.10", + "vite": "^5.4.11", "vue-tsc": "^1.8.27" }, "volta": { 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..9c11e947160e 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.54.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..7545243c8652 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.54.0", "license": "MIT", "engines": { "node": ">=14.18" @@ -16,11 +16,12 @@ "build:types": "tsc -p tsconfig.types.json", "clean": "rimraf -g **/node_modules && run-p clean:script", "clean:script": "node scripts/clean.js", - "prisma:init": "(cd suites/tracing/prisma-orm && ts-node ./setup.ts)", + "prisma-v5:init": "cd suites/tracing/prisma-orm-v5 && ts-node ./setup.ts", + "prisma-v6:init": "cd suites/tracing/prisma-orm-v6 && ts-node ./setup.ts", "lint": "eslint . --format stylish", "fix": "eslint . --format stylish --fix", "type-check": "tsc", - "pretest": "run-s --silent prisma:init", + "pretest": "run-s --silent prisma-v5:init prisma-v6:init", "test": "jest --config ./jest.config.js", "test:watch": "yarn test --watch" }, @@ -30,10 +31,9 @@ "@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", + "@sentry/aws-serverless": "8.54.0", + "@sentry/core": "8.54.0", + "@sentry/node": "8.54.0", "@types/mongodb": "^3.6.20", "@types/mysql": "^2.15.21", "@types/pg": "^8.6.5", diff --git a/dev-packages/node-integration-tests/scripts/use-ts-3_8.js b/dev-packages/node-integration-tests/scripts/use-ts-3_8.js new file mode 100644 index 000000000000..d759179f8e06 --- /dev/null +++ b/dev-packages/node-integration-tests/scripts/use-ts-3_8.js @@ -0,0 +1,39 @@ +/* eslint-disable no-console */ +const { execSync } = require('child_process'); +const { join } = require('path'); +const { readFileSync, writeFileSync } = require('fs'); + +const cwd = join(__dirname, '../../..'); + +// Newer versions of the Express types use syntax that isn't supported by TypeScript 3.8. +// We'll pin to the last version of those types that are compatible. +console.log('Pinning Express types to old versions...'); + +const packageJsonPath = join(cwd, 'package.json'); +const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')); + +if (!packageJson.resolutions) packageJson.resolutions = {}; +packageJson.resolutions['@types/express'] = '4.17.13'; +packageJson.resolutions['@types/express-serve-static-core'] = '4.17.30'; + +writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); + +const tsVersion = '3.8'; + +console.log(`Installing typescript@${tsVersion}, and @types/node@14...`); + +execSync(`yarn add --dev --ignore-workspace-root-check typescript@${tsVersion} @types/node@^14`, { + stdio: 'inherit', + cwd, +}); + +console.log('Removing unsupported tsconfig options...'); + +const baseTscConfigPath = join(cwd, 'packages/typescript/tsconfig.json'); + +const tsConfig = require(baseTscConfigPath); + +// TS 3.8 fails build when it encounters a config option it does not understand, so we remove it :( +delete tsConfig.compilerOptions.noUncheckedIndexedAccess; + +writeFileSync(baseTscConfigPath, JSON.stringify(tsConfig, null, 2)); diff --git a/dev-packages/node-integration-tests/scripts/use-ts-version.js b/dev-packages/node-integration-tests/scripts/use-ts-version.js deleted file mode 100644 index 0b64d735436c..000000000000 --- a/dev-packages/node-integration-tests/scripts/use-ts-version.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable no-console */ -const { execSync } = require('child_process'); -const { join } = require('path'); -const { writeFileSync } = require('fs'); - -const cwd = join(__dirname, '../../..'); - -const tsVersion = process.argv[2] || '3.8'; - -console.log(`Installing typescript@${tsVersion}...`); - -execSync(`yarn add --dev --ignore-workspace-root-check typescript@${tsVersion}`, { stdio: 'inherit', cwd }); - -console.log('Removing unsupported tsconfig options...'); - -const baseTscConfigPath = join(cwd, 'packages/typescript/tsconfig.json'); - -const tsConfig = require(baseTscConfigPath); - -// TS 3.8 fails build when it encounteres a config option it does not understand, so we remove it :( -delete tsConfig.compilerOptions.noUncheckedIndexedAccess; - -writeFileSync(baseTscConfigPath, JSON.stringify(tsConfig, null, 2)); 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/docker-compose.yml b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/docker-compose.yml similarity index 81% rename from dev-packages/node-integration-tests/suites/tracing/prisma-orm/docker-compose.yml rename to dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/docker-compose.yml index 45caa4bb3179..37d45547b537 100644 --- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/docker-compose.yml +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/docker-compose.yml @@ -4,7 +4,7 @@ services: db: image: postgres:13 restart: always - container_name: integration-tests-prisma + container_name: integration-tests-prisma-v5 ports: - '5433:5432' environment: diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/package.json b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/package.json similarity index 80% rename from dev-packages/node-integration-tests/suites/tracing/prisma-orm/package.json rename to dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/package.json index b40c92b4356e..3ccf81ee2f71 100644 --- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/package.json +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/package.json @@ -3,9 +3,6 @@ "version": "1.0.0", "description": "", "main": "index.js", - "engines": { - "node": ">=16" - }, "scripts": { "db-up": "docker compose up -d", "generate": "prisma generate", @@ -16,7 +13,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/prisma/migrations/migration_lock.toml b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/prisma/migrations/migration_lock.toml similarity index 100% rename from dev-packages/node-integration-tests/suites/tracing/prisma-orm/prisma/migrations/migration_lock.toml rename to dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/prisma/migrations/migration_lock.toml diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/prisma/migrations/sentry_test/migration.sql b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/prisma/migrations/sentry_test/migration.sql similarity index 100% rename from dev-packages/node-integration-tests/suites/tracing/prisma-orm/prisma/migrations/sentry_test/migration.sql rename to dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/prisma/migrations/sentry_test/migration.sql diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/prisma/schema.prisma b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/prisma/schema.prisma similarity index 100% rename from dev-packages/node-integration-tests/suites/tracing/prisma-orm/prisma/schema.prisma rename to dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/prisma/schema.prisma diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/scenario.js b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/scenario.js new file mode 100644 index 000000000000..767a6f27bdaa --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/scenario.js @@ -0,0 +1,52 @@ +const Sentry = require('@sentry/node'); +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, + integrations: [Sentry.prismaIntegration()], +}); + +const { randomBytes } = require('crypto'); +const { PrismaClient } = require('@prisma/client'); + +// Stop the process from exiting before the transaction is sent +setInterval(() => {}, 1000); + +async function run() { + const client = new PrismaClient(); + + await Sentry.startSpanManual( + { + name: 'Test Transaction', + op: 'transaction', + }, + async span => { + await client.user.create({ + data: { + name: 'Tilda', + email: `tilda_${randomBytes(4).toString('hex')}@sentry.io`, + }, + }); + + await client.user.findMany(); + + await client.user.deleteMany({ + where: { + email: { + contains: 'sentry.io', + }, + }, + }); + + setTimeout(async () => { + span.end(); + await client.$disconnect(); + }, 500); + }, + ); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/setup.ts b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/setup.ts similarity index 100% rename from dev-packages/node-integration-tests/suites/tracing/prisma-orm/setup.ts rename to dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/setup.ts diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/test.ts b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/test.ts new file mode 100644 index 000000000000..4cc1757c0d19 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/test.ts @@ -0,0 +1,139 @@ +import { conditionalTest } from '../../../utils'; +import { createRunner } from '../../../utils/runner'; + +conditionalTest({ min: 16 })('Prisma ORM Tests', () => { + test('CJS - should instrument PostgreSQL queries from Prisma ORM', done => { + createRunner(__dirname, 'scenario.js') + .expect({ + transaction: transaction => { + expect(transaction.transaction).toBe('Test Transaction'); + + 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-v5/yarn.lock b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/yarn.lock new file mode 100644 index 000000000000..860aa032d6cc --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/yarn.lock @@ -0,0 +1,58 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@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.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.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.22.0" + +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.22.0" + optionalDependencies: + fsevents "2.3.3" diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/docker-compose.yml b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/docker-compose.yml new file mode 100644 index 000000000000..ddab7cb9c563 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/docker-compose.yml @@ -0,0 +1,13 @@ +version: '3.9' + +services: + db: + image: postgres:13 + restart: always + container_name: integration-tests-prisma-v6 + ports: + - '5434:5432' + environment: + POSTGRES_USER: prisma + POSTGRES_PASSWORD: prisma + POSTGRES_DB: tests diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/package.json b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/package.json new file mode 100644 index 000000000000..3ccf81ee2f71 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/package.json @@ -0,0 +1,19 @@ +{ + "name": "sentry-prisma-test", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "db-up": "docker compose up -d", + "generate": "prisma generate", + "migrate": "prisma migrate dev -n sentry-test", + "setup": "run-s --silent db-up generate migrate" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@prisma/client": "5.22.0", + "prisma": "5.22.0" + } +} diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/prisma/migrations/migration_lock.toml b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/prisma/migrations/migration_lock.toml new file mode 100644 index 000000000000..fbffa92c2bb7 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/prisma/migrations/sentry_test/migration.sql b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/prisma/migrations/sentry_test/migration.sql new file mode 100644 index 000000000000..8619aaceb2b0 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/prisma/migrations/sentry_test/migration.sql @@ -0,0 +1,12 @@ +-- CreateTable +CREATE TABLE "User" ( + "id" SERIAL NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "email" TEXT NOT NULL, + "name" TEXT, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/prisma/schema.prisma b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/prisma/schema.prisma new file mode 100644 index 000000000000..71a4923afb8c --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/prisma/schema.prisma @@ -0,0 +1,15 @@ +datasource db { + url = "postgresql://prisma:prisma@localhost:5434/tests" + provider = "postgresql" +} + +generator client { + provider = "prisma-client-js" +} + +model User { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + email String @unique + name String? +} diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/scenario.js b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/scenario.js similarity index 100% rename from dev-packages/node-integration-tests/suites/tracing/prisma-orm/scenario.js rename to dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/scenario.js diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/setup.ts b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/setup.ts new file mode 100755 index 000000000000..a0052511b380 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/setup.ts @@ -0,0 +1,18 @@ +import { execSync } from 'child_process'; +import { parseSemver } from '@sentry/core'; + +const NODE_VERSION = parseSemver(process.versions.node); + +// Prisma v5 requires Node.js v16+ +// https://www.prisma.io/docs/orm/more/upgrade-guides/upgrading-versions/upgrading-to-prisma-5#nodejs-minimum-version-change +if (NODE_VERSION.major && NODE_VERSION.major < 16) { + // eslint-disable-next-line no-console + console.warn(`Skipping Prisma tests on Node: ${NODE_VERSION.major}`); + process.exit(0); +} + +try { + execSync('yarn && yarn setup'); +} catch (_) { + process.exit(1); +} diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/test.ts b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/test.ts new file mode 100644 index 000000000000..52633f0e176b --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/test.ts @@ -0,0 +1,17 @@ +import { conditionalTest } from '../../../utils'; +import { createRunner } from '../../../utils/runner'; + +conditionalTest({ min: 16 })('Prisma ORM Tests', () => { + test('CJS - should instrument PostgreSQL queries from Prisma ORM', done => { + createRunner(__dirname, 'scenario.js') + .expect({ + transaction: transaction => { + expect(transaction.transaction).toBe('Test Transaction'); + + const spans = transaction.spans || []; + expect(spans).toHaveLength(0); + }, + }) + .start(done); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/yarn.lock b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/yarn.lock new file mode 100644 index 000000000000..860aa032d6cc --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/yarn.lock @@ -0,0 +1,58 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@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.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.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.22.0" + +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.22.0" + optionalDependencies: + fsevents "2.3.3" 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 deleted file mode 100644 index dd92de5d0292..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/test.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { conditionalTest } from '../../../utils'; -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: EXPECTED_TRANSACTION }).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 deleted file mode 100644 index 9c0fc47be4be..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/yarn.lock +++ /dev/null @@ -1,51 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# 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== - 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== - 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== - dependencies: - "@prisma/debug" "5.9.1" - -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== - dependencies: - "@prisma/engines" "5.9.1" 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..0158d94974c4 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.54.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..5c6107f725c4 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.54.0", "license": "MIT", "engines": { "node": ">=18" diff --git a/dev-packages/test-utils/package.json b/dev-packages/test-utils/package.json index 09ad4cf5a55d..a1bf82222bd6 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.54.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.54.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..e2fe0e986c20 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.54.0", "npmClient": "yarn" } diff --git a/package.json b/package.json index e948ae773c72..b8a10c1cb6c7 100644 --- a/package.json +++ b/package.json @@ -113,7 +113,7 @@ "@types/jest": "^27.4.1", "@types/jsdom": "^21.1.6", "@types/node": "^14.18.0", - "@vitest/coverage-v8": "^1.6.0", + "@vitest/coverage-v8": "^2.1.8", "deepmerge": "^4.2.2", "downlevel-dts": "~0.11.0", "eslint": "7.32.0", @@ -134,7 +134,7 @@ "ts-jest": "^27.1.4", "ts-node": "10.9.1", "typescript": "4.9.5", - "vitest": "^1.6.0", + "vitest": "^2.1.8", "yalc": "^1.0.0-pre.53" }, "//_resolutions_comment": [ diff --git a/packages/angular/package.json b/packages/angular/package.json index 06bb0492c2f7..0715eae655e7 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/angular", - "version": "8.45.0", + "version": "8.54.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.54.0", + "@sentry/core": "8.54.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/angular/vitest.config.ts b/packages/angular/vitest.config.ts index 9f09af3b153e..82015893133b 100644 --- a/packages/angular/vitest.config.ts +++ b/packages/angular/vitest.config.ts @@ -1,10 +1,9 @@ -import type { UserConfig } from 'vitest'; import { defineConfig } from 'vitest/config'; import baseConfig from '../../vite/vite.config'; export default defineConfig({ test: { - ...(baseConfig as UserConfig & { test: any }).test, + ...baseConfig.test, coverage: {}, globals: true, setupFiles: ['./setup-test.ts'], diff --git a/packages/astro/package.json b/packages/astro/package.json index 43c374a766cc..b8461f46233c 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/astro", - "version": "8.45.0", + "version": "8.54.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,14 +56,14 @@ "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.54.0", + "@sentry/core": "8.54.0", + "@sentry/node": "8.54.0", "@sentry/vite-plugin": "^2.22.6" }, "devDependencies": { "astro": "^3.5.0", - "vite": "^5.4.10" + "vite": "^5.4.11" }, "scripts": { "build": "run-p build:transpile build:types", 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 { - event.transaction = context.functionName; - return event; - }); + scope.setTransactionName(context.functionName); scope.setTag('server_name', process.env._AWS_XRAY_DAEMON_ADDRESS || process.env.SENTRY_NAME || hostname()); scope.setTag('url', `awslambda:///${context.functionName}`); } diff --git a/packages/aws-serverless/test/sdk.test.ts b/packages/aws-serverless/test/sdk.test.ts index 7ab59670cdf2..28b58a830e61 100644 --- a/packages/aws-serverless/test/sdk.test.ts +++ b/packages/aws-serverless/test/sdk.test.ts @@ -18,6 +18,7 @@ const mockScope = { setTag: jest.fn(), setContext: jest.fn(), addEventProcessor: jest.fn(), + setTransactionName: jest.fn(), }; jest.mock('@sentry/node', () => { @@ -81,12 +82,8 @@ const fakeCallback: Callback = (err, result) => { }; function expectScopeSettings() { - expect(mockScope.addEventProcessor).toBeCalledTimes(1); - // Test than an event processor to add `transaction` is registered for the scope - const eventProcessor = mockScope.addEventProcessor.mock.calls[0][0]; - const event: Event = {}; - eventProcessor(event); - expect(event).toEqual({ transaction: 'functionName' }); + expect(mockScope.setTransactionName).toBeCalledTimes(1); + expect(mockScope.setTransactionName).toBeCalledWith('functionName'); expect(mockScope.setTag).toBeCalledWith('server_name', expect.anything()); diff --git a/packages/browser-utils/package.json b/packages/browser-utils/package.json index 15d5bde00065..b401a2ef538d 100644 --- a/packages/browser-utils/package.json +++ b/packages/browser-utils/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/browser-utils", - "version": "8.45.0", + "version": "8.54.0", "description": "Browser Utilities for all Sentry JavaScript SDKs", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/browser-utils", @@ -39,7 +39,7 @@ "access": "public" }, "dependencies": { - "@sentry/core": "8.45.0" + "@sentry/core": "8.54.0" }, "scripts": { "build": "run-p build:transpile build:types", diff --git a/packages/browser-utils/src/index.ts b/packages/browser-utils/src/index.ts index c71b2d70e31d..30bc3a29888e 100644 --- a/packages/browser-utils/src/index.ts +++ b/packages/browser-utils/src/index.ts @@ -17,6 +17,8 @@ export { registerInpInteractionListener, } from './metrics/browserMetrics'; +export { extractNetworkProtocol } from './metrics/utils'; + export { addClickKeypressInstrumentationHandler } from './instrument/dom'; export { addHistoryInstrumentationHandler } from './instrument/history'; diff --git a/packages/browser-utils/src/metrics/browserMetrics.ts b/packages/browser-utils/src/metrics/browserMetrics.ts index aadde247642c..685fba295357 100644 --- a/packages/browser-utils/src/metrics/browserMetrics.ts +++ b/packages/browser-utils/src/metrics/browserMetrics.ts @@ -20,7 +20,13 @@ import { addPerformanceInstrumentationHandler, addTtfbInstrumentationHandler, } from './instrument'; -import { getBrowserPerformanceAPI, isMeasurementValue, msToSec, startAndEndSpan } from './utils'; +import { + extractNetworkProtocol, + getBrowserPerformanceAPI, + isMeasurementValue, + msToSec, + startAndEndSpan, +} from './utils'; import { getActivationStart } from './web-vitals/lib/getActivationStart'; import { getNavigationEntry } from './web-vitals/lib/getNavigationEntry'; import { getVisibilityWatcher } from './web-vitals/lib/getVisibilityWatcher'; @@ -596,6 +602,10 @@ export function _addResourceSpans( attributes['url.same_origin'] = resourceUrl.includes(WINDOW.location.origin); + const { name, version } = extractNetworkProtocol(entry.nextHopProtocol); + attributes['network.protocol.name'] = name; + attributes['network.protocol.version'] = version; + const startTimestamp = timeOrigin + startTime; const endTimestamp = startTimestamp + duration; diff --git a/packages/browser-utils/src/metrics/utils.ts b/packages/browser-utils/src/metrics/utils.ts index b6bc9fc54f2f..bf1517b5a715 100644 --- a/packages/browser-utils/src/metrics/utils.ts +++ b/packages/browser-utils/src/metrics/utils.ts @@ -134,3 +134,34 @@ export function getBrowserPerformanceAPI(): Performance | undefined { export function msToSec(time: number): number { return time / 1000; } + +/** + * Converts ALPN protocol ids to name and version. + * + * (https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids) + * @param nextHopProtocol PerformanceResourceTiming.nextHopProtocol + */ +export function extractNetworkProtocol(nextHopProtocol: string): { name: string; version: string } { + let name = 'unknown'; + let version = 'unknown'; + let _name = ''; + for (const char of nextHopProtocol) { + // http/1.1 etc. + if (char === '/') { + [name, version] = nextHopProtocol.split('/') as [string, string]; + break; + } + // h2, h3 etc. + if (!isNaN(Number(char))) { + name = _name === 'h' ? 'http' : _name; + version = nextHopProtocol.split(_name)[1] as string; + break; + } + _name += char; + } + if (_name === nextHopProtocol) { + // webrtc, ftp, etc. + name = _name; + } + return { name, version }; +} diff --git a/packages/browser-utils/test/browser/browserMetrics.test.ts b/packages/browser-utils/test/browser/browserMetrics.test.ts index 2ff1c2df209a..98a3bb375c00 100644 --- a/packages/browser-utils/test/browser/browserMetrics.test.ts +++ b/packages/browser-utils/test/browser/browserMetrics.test.ts @@ -131,6 +131,7 @@ describe('_addResourceSpans', () => { encodedBodySize: 256, decodedBodySize: 256, renderBlockingStatus: 'non-blocking', + nextHopProtocol: 'http/1.1', }); _addResourceSpans(span, entry, resourceEntryName, 123, 456, 100); @@ -150,6 +151,7 @@ describe('_addResourceSpans', () => { encodedBodySize: 256, decodedBodySize: 256, renderBlockingStatus: 'non-blocking', + nextHopProtocol: 'http/1.1', }); _addResourceSpans(span, entry, 'https://example.com/assets/to/me', 123, 456, 100); @@ -169,6 +171,7 @@ describe('_addResourceSpans', () => { encodedBodySize: 456, decodedBodySize: 593, renderBlockingStatus: 'non-blocking', + nextHopProtocol: 'http/1.1', }); const timeOrigin = 100; @@ -195,6 +198,8 @@ describe('_addResourceSpans', () => { ['url.scheme']: 'https', ['server.address']: 'example.com', ['url.same_origin']: true, + ['network.protocol.name']: 'http', + ['network.protocol.version']: '1.1', }, }), ); @@ -233,6 +238,7 @@ describe('_addResourceSpans', () => { const { initiatorType, op } = table[i]!; const entry = mockPerformanceResourceTiming({ initiatorType, + nextHopProtocol: 'http/1.1', }); _addResourceSpans(span, entry, 'https://example.com/assets/to/me', 123, 234, 465); @@ -254,6 +260,7 @@ describe('_addResourceSpans', () => { encodedBodySize: 0, decodedBodySize: 0, renderBlockingStatus: 'non-blocking', + nextHopProtocol: 'h2', }); _addResourceSpans(span, entry, resourceEntryName, 100, 23, 345); @@ -271,6 +278,8 @@ describe('_addResourceSpans', () => { ['url.scheme']: 'https', ['server.address']: 'example.com', ['url.same_origin']: true, + ['network.protocol.name']: 'http', + ['network.protocol.version']: '2', }, }), ); @@ -288,6 +297,7 @@ describe('_addResourceSpans', () => { transferSize: 2147483647, encodedBodySize: 2147483647, decodedBodySize: 2147483647, + nextHopProtocol: 'h3', }); _addResourceSpans(span, entry, resourceEntryName, 100, 23, 345); @@ -301,6 +311,8 @@ describe('_addResourceSpans', () => { 'server.address': 'example.com', 'url.same_origin': true, 'url.scheme': 'https', + ['network.protocol.name']: 'http', + ['network.protocol.version']: '3', }, description: '/assets/to/css', timestamp: 468, @@ -325,6 +337,7 @@ describe('_addResourceSpans', () => { transferSize: null, encodedBodySize: null, decodedBodySize: null, + nextHopProtocol: 'h3', } as unknown as PerformanceResourceTiming; _addResourceSpans(span, entry, resourceEntryName, 100, 23, 345); @@ -338,6 +351,8 @@ describe('_addResourceSpans', () => { 'server.address': 'example.com', 'url.same_origin': true, 'url.scheme': 'https', + ['network.protocol.name']: 'http', + ['network.protocol.version']: '3', }, description: '/assets/to/css', timestamp: 468, @@ -365,6 +380,7 @@ describe('_addResourceSpans', () => { encodedBodySize: 0, decodedBodySize: 0, deliveryType, + nextHopProtocol: 'h3', }); _addResourceSpans(span, entry, resourceEntryName, 100, 23, 345); diff --git a/packages/browser-utils/test/browser/utils.test.ts b/packages/browser-utils/test/browser/utils.test.ts index bb7a757e4b6a..01fb5da605c4 100644 --- a/packages/browser-utils/test/browser/utils.test.ts +++ b/packages/browser-utils/test/browser/utils.test.ts @@ -1,5 +1,5 @@ import { SentrySpan, getCurrentScope, getIsolationScope, setCurrentClient, spanToJSON } from '@sentry/core'; -import { startAndEndSpan } from '../../src/metrics/utils'; +import { extractNetworkProtocol, startAndEndSpan } from '../../src/metrics/utils'; import { TestClient, getDefaultClientOptions } from '../utils/TestClient'; describe('startAndEndSpan()', () => { @@ -54,3 +54,44 @@ describe('startAndEndSpan()', () => { expect(spanToJSON(parentSpan).start_timestamp).toEqual(123); }); }); + +describe('HTTPTimings', () => { + test.each([ + ['http/0.9', { name: 'http', version: '0.9' }], + ['http/1.0', { name: 'http', version: '1.0' }], + ['http/1.1', { name: 'http', version: '1.1' }], + ['spdy/1', { name: 'spdy', version: '1' }], + ['spdy/2', { name: 'spdy', version: '2' }], + ['spdy/3', { name: 'spdy', version: '3' }], + ['stun.turn', { name: 'stun.turn', version: 'unknown' }], + ['stun.nat-discovery', { name: 'stun.nat-discovery', version: 'unknown' }], + ['h2', { name: 'http', version: '2' }], + ['h2c', { name: 'http', version: '2c' }], + ['webrtc', { name: 'webrtc', version: 'unknown' }], + ['c-webrtc', { name: 'c-webrtc', version: 'unknown' }], + ['ftp', { name: 'ftp', version: 'unknown' }], + ['imap', { name: 'imap', version: 'unknown' }], + ['pop3', { name: 'pop', version: '3' }], + ['managesieve', { name: 'managesieve', version: 'unknown' }], + ['coap', { name: 'coap', version: 'unknown' }], + ['xmpp-client', { name: 'xmpp-client', version: 'unknown' }], + ['xmpp-server', { name: 'xmpp-server', version: 'unknown' }], + ['acme-tls/1', { name: 'acme-tls', version: '1' }], + ['mqtt', { name: 'mqtt', version: 'unknown' }], + ['dot', { name: 'dot', version: 'unknown' }], + ['ntske/1', { name: 'ntske', version: '1' }], + ['sunrpc', { name: 'sunrpc', version: 'unknown' }], + ['h3', { name: 'http', version: '3' }], + ['smb', { name: 'smb', version: 'unknown' }], + ['irc', { name: 'irc', version: 'unknown' }], + ['nntp', { name: 'nntp', version: 'unknown' }], + ['nnsp', { name: 'nnsp', version: 'unknown' }], + ['doq', { name: 'doq', version: 'unknown' }], + ['sip/2', { name: 'sip', version: '2' }], + ['tds/8.0', { name: 'tds', version: '8.0' }], + ['dicom', { name: 'dicom', version: 'unknown' }], + ['', { name: '', version: 'unknown' }], + ])('Extracting version from ALPN protocol %s', (protocol, expected) => { + expect(extractNetworkProtocol(protocol)).toMatchObject(expected); + }); +}); diff --git a/packages/browser/package.json b/packages/browser/package.json index f588f2801eb0..0716fe0904d1 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/browser", - "version": "8.45.0", + "version": "8.54.0", "description": "Official Sentry SDK for browsers", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/browser", @@ -39,14 +39,14 @@ "access": "public" }, "dependencies": { - "@sentry-internal/browser-utils": "8.45.0", - "@sentry-internal/feedback": "8.45.0", - "@sentry-internal/replay": "8.45.0", - "@sentry-internal/replay-canvas": "8.45.0", - "@sentry/core": "8.45.0" + "@sentry-internal/browser-utils": "8.54.0", + "@sentry-internal/feedback": "8.54.0", + "@sentry-internal/replay": "8.54.0", + "@sentry-internal/replay-canvas": "8.54.0", + "@sentry/core": "8.54.0" }, "devDependencies": { - "@sentry-internal/integration-shims": "8.45.0", + "@sentry-internal/integration-shims": "8.54.0", "fake-indexeddb": "^4.0.1" }, "scripts": { diff --git a/packages/browser/rollup.bundle.config.mjs b/packages/browser/rollup.bundle.config.mjs index f65c27aad6e9..eaf1e1b54a8e 100644 --- a/packages/browser/rollup.bundle.config.mjs +++ b/packages/browser/rollup.bundle.config.mjs @@ -37,6 +37,19 @@ reexportedPluggableIntegrationFiles.forEach(integrationName => { builds.push(...makeBundleConfigVariants(integrationsBundleConfig)); }); +// Bundle config for additional exports we don't want to include in the main SDK bundle +// if we need more of these, we can generalize the config as for pluggable integrations +builds.push( + ...makeBundleConfigVariants( + makeBaseBundleConfig({ + bundleType: 'addon', + entrypoints: ['src/pluggable-exports-bundle/index.multiplexedtransport.ts'], + licenseTitle: '@sentry/browser - multiplexedtransport', + outputFileBase: () => 'bundles/multiplexedtransport', + }), + ), +); + const baseBundleConfig = makeBaseBundleConfig({ bundleType: 'standalone', entrypoints: ['src/index.bundle.ts'], diff --git a/packages/browser/src/exports.ts b/packages/browser/src/exports.ts index 492f9da23b38..295e6daa36cc 100644 --- a/packages/browser/src/exports.ts +++ b/packages/browser/src/exports.ts @@ -62,6 +62,7 @@ export { spanToJSON, spanToTraceHeader, spanToBaggageHeader, + updateSpanName, } from '@sentry/core'; export { diff --git a/packages/browser/src/feedbackSync.ts b/packages/browser/src/feedbackSync.ts index b99c9a4b752f..ede41fefb221 100644 --- a/packages/browser/src/feedbackSync.ts +++ b/packages/browser/src/feedbackSync.ts @@ -3,11 +3,9 @@ import { feedbackModalIntegration, feedbackScreenshotIntegration, } from '@sentry-internal/feedback'; -import { lazyLoadIntegration } from './utils/lazyLoadIntegration'; /** Add a widget to capture user feedback to your application. */ export const feedbackSyncIntegration = buildFeedbackIntegration({ - lazyLoadIntegration, getModalIntegration: () => 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..54c5abf159d6 --- /dev/null +++ b/packages/browser/src/integrations/featureFlags/unleash/integration.ts @@ -0,0 +1,87 @@ +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'; + +type UnleashIntegrationOptions = { + featureFlagClientClass?: UnleashClientClass; + + /** + * @deprecated Use `featureFlagClientClass` instead. + */ + unleashClientClass?: UnleashClientClass; +}; + +/** + * 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'; + * + * Sentry.init({ + * dsn: '___PUBLIC_DSN___', + * integrations: [Sentry.unleashIntegration({featureFlagClientClass: UnleashClient})], + * }); + * + * const unleash = new UnleashClient(...); + * unleash.start(); + * + * unleash.isEnabled('my-feature'); + * Sentry.captureException(new Error('something went wrong')); + * ``` + */ +export const unleashIntegration = defineIntegration( + // eslint-disable-next-line deprecation/deprecation + ({ featureFlagClientClass, unleashClientClass }: UnleashIntegrationOptions) => { + const _unleashClientClass = featureFlagClientClass ?? unleashClientClass; + if (!_unleashClientClass) { + throw new Error('featureFlagClientClass option is required'); + } + + 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..c87798859911 --- /dev/null +++ b/packages/browser/src/integrations/featureFlags/unleash/types.ts @@ -0,0 +1,23 @@ +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 interface IConfig { + [key: string]: unknown; + appName: string; + clientKey: string; + url: URL | string; +} + +export type UnleashClientClass = new (config: IConfig) => UnleashClient; diff --git a/packages/browser/src/pluggable-exports-bundle/index.multiplexedtransport.ts b/packages/browser/src/pluggable-exports-bundle/index.multiplexedtransport.ts new file mode 100644 index 000000000000..a7d637d9e62f --- /dev/null +++ b/packages/browser/src/pluggable-exports-bundle/index.multiplexedtransport.ts @@ -0,0 +1 @@ +export { makeMultiplexedTransport } from '@sentry/core'; 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/tracing/request.ts b/packages/browser/src/tracing/request.ts index 5f32b227fa85..5418d5b9f154 100644 --- a/packages/browser/src/tracing/request.ts +++ b/packages/browser/src/tracing/request.ts @@ -2,6 +2,7 @@ import { SENTRY_XHR_DATA_KEY, addPerformanceInstrumentationHandler, addXhrInstrumentationHandler, + extractNetworkProtocol, } from '@sentry-internal/browser-utils'; import type { Client, HandlerDataXhr, SentryWrappedXMLHttpRequest, Span } from '@sentry/core'; import { @@ -227,37 +228,6 @@ function addHTTPTimings(span: Span): void { }); } -/** - * Converts ALPN protocol ids to name and version. - * - * (https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids) - * @param nextHopProtocol PerformanceResourceTiming.nextHopProtocol - */ -export function extractNetworkProtocol(nextHopProtocol: string): { name: string; version: string } { - let name = 'unknown'; - let version = 'unknown'; - let _name = ''; - for (const char of nextHopProtocol) { - // http/1.1 etc. - if (char === '/') { - [name, version] = nextHopProtocol.split('/') as [string, string]; - break; - } - // h2, h3 etc. - if (!isNaN(Number(char))) { - name = _name === 'h' ? 'http' : _name; - version = nextHopProtocol.split(_name)[1] as string; - break; - } - _name += char; - } - if (_name === nextHopProtocol) { - // webrtc, ftp, etc. - name = _name; - } - return { name, version }; -} - function getAbsoluteTime(time: number = 0): number { return ((browserPerformanceTimeOrigin || performance.timeOrigin) + time) / 1000; } 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/integrations/featureFlags/unleash.test.ts b/packages/browser/test/integrations/featureFlags/unleash.test.ts new file mode 100644 index 000000000000..659d90fd9b03 --- /dev/null +++ b/packages/browser/test/integrations/featureFlags/unleash.test.ts @@ -0,0 +1,7 @@ +import { unleashIntegration } from '../../../src'; + +describe('Unleash', () => { + it('Throws error if given empty options', () => { + expect(() => unleashIntegration({})).toThrow('featureFlagClientClass option is required'); + }); +}); diff --git a/packages/browser/test/tracing/request.test.ts b/packages/browser/test/tracing/request.test.ts index 337d08caff24..655d07e53e17 100644 --- a/packages/browser/test/tracing/request.test.ts +++ b/packages/browser/test/tracing/request.test.ts @@ -5,7 +5,7 @@ import * as utils from '@sentry/core'; import type { Client } from '@sentry/core'; import { WINDOW } from '../../src/helpers'; -import { extractNetworkProtocol, instrumentOutgoingRequests, shouldAttachHeaders } from '../../src/tracing/request'; +import { instrumentOutgoingRequests, shouldAttachHeaders } from '../../src/tracing/request'; beforeAll(() => { // @ts-expect-error need to override global Request because it's not in the vi environment (even with an @@ -64,57 +64,6 @@ describe('instrumentOutgoingRequests', () => { }); }); -interface ProtocolInfo { - name: string; - version: string; -} - -describe('HTTPTimings', () => { - test('Extracting version from ALPN protocol', () => { - const nextHopToNetworkVersion: Record = { - 'http/0.9': { name: 'http', version: '0.9' }, - 'http/1.0': { name: 'http', version: '1.0' }, - 'http/1.1': { name: 'http', version: '1.1' }, - 'spdy/1': { name: 'spdy', version: '1' }, - 'spdy/2': { name: 'spdy', version: '2' }, - 'spdy/3': { name: 'spdy', version: '3' }, - 'stun.turn': { name: 'stun.turn', version: 'unknown' }, - 'stun.nat-discovery': { name: 'stun.nat-discovery', version: 'unknown' }, - h2: { name: 'http', version: '2' }, - h2c: { name: 'http', version: '2c' }, - webrtc: { name: 'webrtc', version: 'unknown' }, - 'c-webrtc': { name: 'c-webrtc', version: 'unknown' }, - ftp: { name: 'ftp', version: 'unknown' }, - imap: { name: 'imap', version: 'unknown' }, - pop3: { name: 'pop', version: '3' }, - managesieve: { name: 'managesieve', version: 'unknown' }, - coap: { name: 'coap', version: 'unknown' }, - 'xmpp-client': { name: 'xmpp-client', version: 'unknown' }, - 'xmpp-server': { name: 'xmpp-server', version: 'unknown' }, - 'acme-tls/1': { name: 'acme-tls', version: '1' }, - mqtt: { name: 'mqtt', version: 'unknown' }, - dot: { name: 'dot', version: 'unknown' }, - 'ntske/1': { name: 'ntske', version: '1' }, - sunrpc: { name: 'sunrpc', version: 'unknown' }, - h3: { name: 'http', version: '3' }, - smb: { name: 'smb', version: 'unknown' }, - irc: { name: 'irc', version: 'unknown' }, - nntp: { name: 'nntp', version: 'unknown' }, - nnsp: { name: 'nnsp', version: 'unknown' }, - doq: { name: 'doq', version: 'unknown' }, - 'sip/2': { name: 'sip', version: '2' }, - 'tds/8.0': { name: 'tds', version: '8.0' }, - dicom: { name: 'dicom', version: 'unknown' }, - }; - - const protocols = Object.keys(nextHopToNetworkVersion); - for (const protocol of protocols) { - const expected = nextHopToNetworkVersion[protocol]!; - expect(extractNetworkProtocol(protocol)).toMatchObject(expected); - } - }); -}); - describe('shouldAttachHeaders', () => { describe('should prefer `tracePropagationTargets` over defaults', () => { it('should return `true` if the url matches the new tracePropagationTargets', () => { 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..00d7eb8b750a 100644 --- a/packages/bun/package.json +++ b/packages/bun/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/bun", - "version": "8.45.0", + "version": "8.54.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.54.0", + "@sentry/node": "8.54.0", + "@sentry/opentelemetry": "8.54.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/bun/src/integrations/bunserver.ts b/packages/bun/src/integrations/bunserver.ts index 862d5bd87212..7abffa058157 100644 --- a/packages/bun/src/integrations/bunserver.ts +++ b/packages/bun/src/integrations/bunserver.ts @@ -47,7 +47,18 @@ export function instrumentBunServe(): void { Bun.serve = new Proxy(Bun.serve, { apply(serveTarget, serveThisArg, serveArgs: Parameters) { instrumentBunServeOptions(serveArgs[0]); - return serveTarget.apply(serveThisArg, serveArgs); + const server: ReturnType = serveTarget.apply(serveThisArg, serveArgs); + + // A Bun server can be reloaded, re-wrap any fetch function passed to it + // We can't use a Proxy for this as Bun does `instanceof` checks internally that fail if we + // wrap the Server instance. + const originalReload: typeof server.reload = server.reload.bind(server); + server.reload = (serveOptions: Parameters[0]) => { + instrumentBunServeOptions(serveOptions); + return originalReload(serveOptions); + }; + + return server; }, }); } diff --git a/packages/bun/test/integrations/bunserver.test.ts b/packages/bun/test/integrations/bunserver.test.ts index b1dc17381ccb..dd1f738a334b 100644 --- a/packages/bun/test/integrations/bunserver.test.ts +++ b/packages/bun/test/integrations/bunserver.test.ts @@ -1,67 +1,87 @@ -import { beforeAll, beforeEach, describe, expect, test } from 'bun:test'; +import { afterEach, beforeAll, beforeEach, describe, expect, test } from 'bun:test'; +import type { Span } from '@sentry/core'; import { getDynamicSamplingContextFromSpan, setCurrentClient, spanIsSampled, spanToJSON } from '@sentry/core'; import { BunClient } from '../../src/client'; import { instrumentBunServe } from '../../src/integrations/bunserver'; import { getDefaultBunClientOptions } from '../helpers'; -// Fun fact: Bun = 2 21 14 :) -const DEFAULT_PORT = 22114; - describe('Bun Serve Integration', () => { let client: BunClient; + // Fun fact: Bun = 2 21 14 :) + let port: number = 22114; beforeAll(() => { instrumentBunServe(); }); beforeEach(() => { - const options = getDefaultBunClientOptions({ tracesSampleRate: 1, debug: true }); + const options = getDefaultBunClientOptions({ tracesSampleRate: 1 }); client = new BunClient(options); setCurrentClient(client); client.init(); }); + afterEach(() => { + // Don't reuse the port; Bun server stops lazily so tests may accidentally hit a server still closing from a + // previous test + port += 1; + }); + test('generates a transaction around a request', async () => { + let generatedSpan: Span | undefined; + client.on('spanEnd', span => { - expect(spanToJSON(span).status).toBe('ok'); - expect(spanToJSON(span).data?.['http.response.status_code']).toEqual(200); - expect(spanToJSON(span).op).toEqual('http.server'); - expect(spanToJSON(span).description).toEqual('GET /'); + generatedSpan = span; }); const server = Bun.serve({ async fetch(_req) { return new Response('Bun!'); }, - port: DEFAULT_PORT, + port, }); + await fetch(`http://localhost:${port}/`); + server.stop(); - await fetch('http://localhost:22114/'); + if (!generatedSpan) { + throw 'No span was generated in the test'; + } - server.stop(); + expect(spanToJSON(generatedSpan).status).toBe('ok'); + expect(spanToJSON(generatedSpan).data?.['http.response.status_code']).toEqual(200); + expect(spanToJSON(generatedSpan).op).toEqual('http.server'); + expect(spanToJSON(generatedSpan).description).toEqual('GET /'); }); test('generates a post transaction', async () => { + let generatedSpan: Span | undefined; + client.on('spanEnd', span => { - expect(spanToJSON(span).status).toBe('ok'); - expect(spanToJSON(span).data?.['http.response.status_code']).toEqual(200); - expect(spanToJSON(span).op).toEqual('http.server'); - expect(spanToJSON(span).description).toEqual('POST /'); + generatedSpan = span; }); const server = Bun.serve({ async fetch(_req) { return new Response('Bun!'); }, - port: DEFAULT_PORT, + port, }); - await fetch('http://localhost:22114/', { + await fetch(`http://localhost:${port}/`, { method: 'POST', }); server.stop(); + + if (!generatedSpan) { + throw 'No span was generated in the test'; + } + + expect(spanToJSON(generatedSpan).status).toBe('ok'); + expect(spanToJSON(generatedSpan).data?.['http.response.status_code']).toEqual(200); + expect(spanToJSON(generatedSpan).op).toEqual('http.server'); + expect(spanToJSON(generatedSpan).description).toEqual('POST /'); }); test('continues a trace', async () => { @@ -70,55 +90,93 @@ describe('Bun Serve Integration', () => { const PARENT_SAMPLED = '1'; const SENTRY_TRACE_HEADER = `${TRACE_ID}-${PARENT_SPAN_ID}-${PARENT_SAMPLED}`; - const SENTRY_BAGGAGE_HEADER = 'sentry-version=1.0,sentry-environment=production'; + const SENTRY_BAGGAGE_HEADER = 'sentry-version=1.0,sentry-sample_rand=0.42,sentry-environment=production'; - client.on('spanEnd', span => { - expect(span.spanContext().traceId).toBe(TRACE_ID); - expect(spanToJSON(span).parent_span_id).toBe(PARENT_SPAN_ID); - expect(spanIsSampled(span)).toBe(true); - expect(span.isRecording()).toBe(false); + let generatedSpan: Span | undefined; - expect(getDynamicSamplingContextFromSpan(span)).toStrictEqual({ - version: '1.0', - environment: 'production', - }); + client.on('spanEnd', span => { + generatedSpan = span; }); const server = Bun.serve({ async fetch(_req) { return new Response('Bun!'); }, - port: DEFAULT_PORT, + port, }); - await fetch('http://localhost:22114/', { + await fetch(`http://localhost:${port}/`, { headers: { 'sentry-trace': SENTRY_TRACE_HEADER, baggage: SENTRY_BAGGAGE_HEADER }, }); server.stop(); + + if (!generatedSpan) { + throw 'No span was generated in the test'; + } + + expect(generatedSpan.spanContext().traceId).toBe(TRACE_ID); + expect(spanToJSON(generatedSpan).parent_span_id).toBe(PARENT_SPAN_ID); + expect(spanIsSampled(generatedSpan)).toBe(true); + expect(generatedSpan.isRecording()).toBe(false); + + expect(getDynamicSamplingContextFromSpan(generatedSpan)).toStrictEqual({ + version: '1.0', + sample_rand: '0.42', + environment: 'production', + }); }); test('does not create transactions for OPTIONS or HEAD requests', async () => { - client.on('spanEnd', () => { - // This will never run, but we want to make sure it doesn't run. - expect(false).toEqual(true); + let generatedSpan: Span | undefined; + + client.on('spanEnd', span => { + generatedSpan = span; }); const server = Bun.serve({ async fetch(_req) { return new Response('Bun!'); }, - port: DEFAULT_PORT, + port, }); - await fetch('http://localhost:22114/', { + await fetch(`http://localhost:${port}/`, { method: 'OPTIONS', }); - await fetch('http://localhost:22114/', { + await fetch(`http://localhost:${port}/`, { method: 'HEAD', }); server.stop(); + + expect(generatedSpan).toBeUndefined(); + }); + + test('intruments the server again if it is reloaded', async () => { + let serverWasInstrumented = false; + client.on('spanEnd', () => { + serverWasInstrumented = true; + }); + + const server = Bun.serve({ + async fetch(_req) { + return new Response('Bun!'); + }, + port, + }); + + server.reload({ + async fetch(_req) { + return new Response('Reloaded Bun!'); + }, + }); + + await fetch(`http://localhost:${port}/`); + + server.stop(); + + expect(serverWasInstrumented).toBeTrue(); }); }); diff --git a/packages/bun/test/sdk.test.ts b/packages/bun/test/sdk.test.ts index a548cc2614c7..11870f30c101 100644 --- a/packages/bun/test/sdk.test.ts +++ b/packages/bun/test/sdk.test.ts @@ -1,14 +1,20 @@ -import { expect, test } from 'bun:test'; +import { describe, expect, test } from 'bun:test'; import { init } from '../src/index'; -test("calling init shouldn't fail", () => { - init({ +describe('Bun SDK', () => { + const initOptions = { dsn: 'https://00000000000000000000000000000000@o000000.ingest.sentry.io/0000000', + tracesSampleRate: 1, + }; + + test("calling init shouldn't fail", () => { + expect(() => { + init(initOptions); + }).not.toThrow(); }); - expect(true).toBe(true); -}); -test('should return client from init', () => { - expect(init({})).not.toBeUndefined(); + test('should return client from init', () => { + expect(init(initOptions)).not.toBeUndefined(); + }); }); diff --git a/packages/cloudflare/package.json b/packages/cloudflare/package.json index efec51c5c0f5..b1ff74862744 100644 --- a/packages/cloudflare/package.json +++ b/packages/cloudflare/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/cloudflare", - "version": "8.45.0", + "version": "8.54.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.54.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..887649741bec 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/core", - "version": "8.45.0", + "version": "8.54.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", @@ -41,7 +41,8 @@ "TODO(v9):": "Remove these dependencies", "devDependencies": { "@types/array.prototype.flat": "^1.2.1", - "array.prototype.flat": "^1.3.0" + "array.prototype.flat": "^1.3.0", + "zod": "^3.24.1" }, "scripts": { "build": "run-p build:transpile build:types", 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/baseclient.ts b/packages/core/src/baseclient.ts index c394a0d77a95..c2dff2122734 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -587,7 +587,7 @@ export abstract class BaseClient implements Client { /** Updates existing session based on the provided event */ protected _updateSessionFromEvent(session: Session, event: Event): void { - let crashed = false; + let crashed = event.level === 'fatal'; let errored = false; const exceptions = event.exception && event.exception.values; @@ -721,11 +721,10 @@ export abstract class BaseClient implements Client { if (DEBUG_BUILD) { // If something's gone wrong, log the error as a warning. If it's just us having used a `SentryError` for // control flow, log just the message (no stack) as a log-level log. - const sentryError = reason as SentryError; - if (sentryError.logLevel === 'log') { - logger.log(sentryError.message); + if (reason instanceof SentryError && reason.logLevel === 'log') { + logger.log(reason.message); } else { - logger.warn(sentryError); + logger.warn(reason); } } return undefined; 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/integrations/zoderrors.ts b/packages/core/src/integrations/zoderrors.ts index fc36925eb0ea..f059a1dc4ced 100644 --- a/packages/core/src/integrations/zoderrors.ts +++ b/packages/core/src/integrations/zoderrors.ts @@ -5,33 +5,45 @@ import { truncate } from '../utils-hoist/string'; interface ZodErrorsOptions { key?: string; + /** + * Limits the number of Zod errors inlined in each Sentry event. + * + * @default 10 + */ limit?: number; + /** + * Save full list of Zod issues as an attachment in Sentry + * + * @default false + */ + saveZodIssuesAsAttachment?: boolean; } const DEFAULT_LIMIT = 10; const INTEGRATION_NAME = 'ZodErrors'; -// Simplified ZodIssue type definition +/** + * Simplified ZodIssue type definition + */ interface ZodIssue { path: (string | number)[]; message?: string; - expected?: string | number; - received?: string | number; + expected?: unknown; + received?: unknown; unionErrors?: unknown[]; keys?: unknown[]; + invalid_literal?: unknown; } interface ZodError extends Error { issues: ZodIssue[]; - - get errors(): ZodError['issues']; } function originalExceptionIsZodError(originalException: unknown): originalException is ZodError { return ( isError(originalException) && originalException.name === 'ZodError' && - Array.isArray((originalException as ZodError).errors) + Array.isArray((originalException as ZodError).issues) ); } @@ -45,9 +57,18 @@ type SingleLevelZodIssue = { /** * Formats child objects or arrays to a string - * That is preserved when sent to Sentry + * that is preserved when sent to Sentry. + * + * Without this, we end up with something like this in Sentry: + * + * [ + * [Object], + * [Object], + * [Object], + * [Object] + * ] */ -function formatIssueTitle(issue: ZodIssue): SingleLevelZodIssue { +export function flattenIssue(issue: ZodIssue): SingleLevelZodIssue { return { ...issue, path: 'path' in issue && Array.isArray(issue.path) ? issue.path.join('.') : undefined, @@ -56,26 +77,70 @@ function formatIssueTitle(issue: ZodIssue): SingleLevelZodIssue { }; } +/** + * Takes ZodError issue path array and returns a flattened version as a string. + * This makes it easier to display paths within a Sentry error message. + * + * Array indexes are normalized to reduce duplicate entries + * + * @param path ZodError issue path + * @returns flattened path + * + * @example + * flattenIssuePath([0, 'foo', 1, 'bar']) // -> '.foo..bar' + */ +export function flattenIssuePath(path: Array): string { + return path + .map(p => { + if (typeof p === 'number') { + return ''; + } else { + return p; + } + }) + .join('.'); +} + /** * Zod error message is a stringified version of ZodError.issues * This doesn't display well in the Sentry UI. Replace it with something shorter. */ -function formatIssueMessage(zodError: ZodError): string { +export function formatIssueMessage(zodError: ZodError): string { const errorKeyMap = new Set(); for (const iss of zodError.issues) { - if (iss.path && iss.path[0]) { - errorKeyMap.add(iss.path[0]); + const issuePath = flattenIssuePath(iss.path); + if (issuePath.length > 0) { + errorKeyMap.add(issuePath); } } - const errorKeys = Array.from(errorKeyMap); + const errorKeys = Array.from(errorKeyMap); + if (errorKeys.length === 0) { + // If there are no keys, then we're likely validating the root + // variable rather than a key within an object. This attempts + // to extract what type it was that failed to validate. + // For example, z.string().parse(123) would return "string" here. + let rootExpectedType = 'variable'; + if (zodError.issues.length > 0) { + const iss = zodError.issues[0]; + if (iss !== undefined && 'expected' in iss && typeof iss.expected === 'string') { + rootExpectedType = iss.expected; + } + } + return `Failed to validate ${rootExpectedType}`; + } return `Failed to validate keys: ${truncate(errorKeys.join(', '), 100)}`; } /** - * Applies ZodError issues to an event extras and replaces the error message + * Applies ZodError issues to an event extra and replaces the error message */ -export function applyZodErrorsToEvent(limit: number, event: Event, hint?: EventHint): Event { +export function applyZodErrorsToEvent( + limit: number, + saveZodIssuesAsAttachment: boolean = false, + event: Event, + hint: EventHint, +): Event { if ( !event.exception || !event.exception.values || @@ -87,35 +152,72 @@ export function applyZodErrorsToEvent(limit: number, event: Event, hint?: EventH return event; } - return { - ...event, - exception: { - ...event.exception, - values: [ - { - ...event.exception.values[0], - value: formatIssueMessage(hint.originalException), + try { + const issuesToFlatten = saveZodIssuesAsAttachment + ? hint.originalException.issues + : hint.originalException.issues.slice(0, limit); + const flattenedIssues = issuesToFlatten.map(flattenIssue); + + if (saveZodIssuesAsAttachment) { + // Sometimes having the full error details can be helpful. + // Attachments have much higher limits, so we can include the full list of issues. + if (!Array.isArray(hint.attachments)) { + hint.attachments = []; + } + hint.attachments.push({ + filename: 'zod_issues.json', + data: JSON.stringify({ + issues: flattenedIssues, + }), + }); + } + + return { + ...event, + exception: { + ...event.exception, + values: [ + { + ...event.exception.values[0], + value: formatIssueMessage(hint.originalException), + }, + ...event.exception.values.slice(1), + ], + }, + extra: { + ...event.extra, + 'zoderror.issues': flattenedIssues.slice(0, limit), + }, + }; + } catch (e) { + // Hopefully we never throw errors here, but record it + // with the event just in case. + return { + ...event, + extra: { + ...event.extra, + 'zoderrors sentry integration parse error': { + message: 'an exception was thrown while processing ZodError within applyZodErrorsToEvent()', + error: e instanceof Error ? `${e.name}: ${e.message}\n${e.stack}` : 'unknown', }, - ...event.exception.values.slice(1), - ], - }, - extra: { - ...event.extra, - 'zoderror.issues': hint.originalException.errors.slice(0, limit).map(formatIssueTitle), - }, - }; + }, + }; + } } const _zodErrorsIntegration = ((options: ZodErrorsOptions = {}) => { - const limit = options.limit || DEFAULT_LIMIT; + const limit = typeof options.limit === 'undefined' ? DEFAULT_LIMIT : options.limit; return { name: INTEGRATION_NAME, - processEvent(originalEvent, hint) { - const processedEvent = applyZodErrorsToEvent(limit, originalEvent, hint); + processEvent(originalEvent, hint): Event { + const processedEvent = applyZodErrorsToEvent(limit, options.saveZodIssuesAsAttachment, originalEvent, hint); return processedEvent; }, }; }) satisfies IntegrationFn; +/** + * Sentry integration to process Zod errors, making them easier to work with in Sentry. + */ export const zodErrorsIntegration = defineIntegration(_zodErrorsIntegration); diff --git a/packages/core/src/scope.ts b/packages/core/src/scope.ts index 5bba8615e876..dae18881f483 100644 --- a/packages/core/src/scope.ts +++ b/packages/core/src/scope.ts @@ -306,7 +306,15 @@ class ScopeClass implements ScopeInterface { } /** - * @inheritDoc + * Sets the transaction name on the scope so that the name of e.g. taken server route or + * the page location is attached to future events. + * + * IMPORTANT: Calling this function does NOT change the name of the currently active + * root span. If you want to change the name of the active root span, use + * `Sentry.updateSpanName(rootSpan, 'new name')` instead. + * + * By default, the SDK updates the scope's transaction name automatically on sensible + * occasions, such as a page navigation or when handling a new request on the server. */ public setTransactionName(name?: string): this { this._transactionName = name; @@ -435,9 +443,13 @@ class ScopeClass implements ScopeInterface { ...breadcrumb, }; - const breadcrumbs = this._breadcrumbs; - breadcrumbs.push(mergedBreadcrumb); - this._breadcrumbs = breadcrumbs.length > maxCrumbs ? breadcrumbs.slice(-maxCrumbs) : breadcrumbs; + this._breadcrumbs.push(mergedBreadcrumb); + if (this._breadcrumbs.length > maxCrumbs) { + this._breadcrumbs = this._breadcrumbs.slice(-maxCrumbs); + if (this._client) { + this._client.recordDroppedEvent('buffer_overflow', 'log_item'); + } + } this._notifyScopeListeners(); 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/clientreport.ts b/packages/core/src/types-hoist/clientreport.ts index b6ab1766e68c..069adec43c62 100644 --- a/packages/core/src/types-hoist/clientreport.ts +++ b/packages/core/src/types-hoist/clientreport.ts @@ -8,7 +8,8 @@ export type EventDropReason = | 'ratelimit_backoff' | 'sample_rate' | 'send_error' - | 'internal_sdk_error'; + | 'internal_sdk_error' + | 'buffer_overflow'; export type Outcome = { reason: EventDropReason; 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/datacategory.ts b/packages/core/src/types-hoist/datacategory.ts index bd1c0b693e4d..98c4657cf011 100644 --- a/packages/core/src/types-hoist/datacategory.ts +++ b/packages/core/src/types-hoist/datacategory.ts @@ -14,7 +14,7 @@ export type DataCategory = | 'replay' // Events with `event_type` csp, hpkp, expectct, expectstaple | 'security' - // Attachment bytes stored (unused for rate limiting + // Attachment bytes stored (unused for rate limiting) | 'attachment' // Session update events | 'session' @@ -30,5 +30,9 @@ export type DataCategory = | 'metric_bucket' // Span | 'span' + // Log event + | 'log_item' + // Log bytes stored (unused for rate limiting) + | 'log_byte' // Unknown data category | 'unknown'; 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/error.ts b/packages/core/src/utils-hoist/error.ts index 622aaff9cf80..5ae28093a8bf 100644 --- a/packages/core/src/utils-hoist/error.ts +++ b/packages/core/src/utils-hoist/error.ts @@ -2,9 +2,6 @@ import type { ConsoleLevel } from '../types-hoist'; /** An error emitted by Sentry SDKs and related utilities. */ export class SentryError extends Error { - /** Display name of this error instance. */ - public name: string; - public logLevel: ConsoleLevel; public constructor( @@ -13,11 +10,6 @@ export class SentryError extends Error { ) { super(message); - this.name = new.target.prototype.constructor.name; - // This sets the prototype to be `Error`, not `SentryError`. It's unclear why we do this, but commenting this line - // out causes various (seemingly totally unrelated) playwright tests consistently time out. FYI, this makes - // instances of `SentryError` fail `obj instanceof SentryError` checks. - Object.setPrototypeOf(this, new.target.prototype); this.logLevel = logLevel; } } 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/node.ts b/packages/core/src/utils-hoist/node.ts index 3805248bdedd..489a5c2cb57d 100644 --- a/packages/core/src/utils-hoist/node.ts +++ b/packages/core/src/utils-hoist/node.ts @@ -42,14 +42,16 @@ export function dynamicRequire(mod: any, request: string): any { * That is to mimic the behavior of `require.resolve` exactly. * * @param moduleName module name to require + * @param existingModule module to use for requiring * @returns possibly required module */ -export function loadModule(moduleName: string): T | undefined { +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function loadModule(moduleName: string, existingModule: any = module): T | undefined { let mod: T | undefined; try { // eslint-disable-next-line deprecation/deprecation - mod = dynamicRequire(module, moduleName); + mod = dynamicRequire(existingModule, moduleName); } catch (e) { // no-empty } @@ -57,9 +59,9 @@ export function loadModule(moduleName: string): T | undefined { if (!mod) { try { // eslint-disable-next-line deprecation/deprecation - const { cwd } = dynamicRequire(module, 'process'); + const { cwd } = dynamicRequire(existingModule, 'process'); // eslint-disable-next-line deprecation/deprecation - mod = dynamicRequire(module, `${cwd()}/node_modules/${moduleName}`) as T; + mod = dynamicRequire(existingModule, `${cwd()}/node_modules/${moduleName}`) as T; } catch (e) { // no-empty } 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/baseclient.test.ts b/packages/core/test/lib/baseclient.test.ts index ce480879bb27..338e88ee72e8 100644 --- a/packages/core/test/lib/baseclient.test.ts +++ b/packages/core/test/lib/baseclient.test.ts @@ -201,6 +201,22 @@ describe('BaseClient', () => { expect(isolationScopeBreadcrumbs).toEqual([{ message: 'hello3', timestamp: expect.any(Number) }]); }); + test('it records `buffer_overflow` client discard reason when buffer overflows', () => { + const options = getDefaultTestClientOptions({ maxBreadcrumbs: 1 }); + const client = new TestClient(options); + const recordLostEventSpy = jest.spyOn(client, 'recordDroppedEvent'); + setCurrentClient(client); + getIsolationScope().setClient(client); + client.init(); + + addBreadcrumb({ message: 'hello1' }); + addBreadcrumb({ message: 'hello2' }); + addBreadcrumb({ message: 'hello3' }); + + expect(recordLostEventSpy).toHaveBeenCalledTimes(2); + expect(recordLostEventSpy).toHaveBeenLastCalledWith('buffer_overflow', 'log_item'); + }); + test('calls `beforeBreadcrumb` and adds the breadcrumb without any changes', () => { const beforeBreadcrumb = jest.fn(breadcrumb => breadcrumb); const options = getDefaultTestClientOptions({ beforeBreadcrumb }); diff --git a/packages/core/test/lib/integrations/zoderrrors.test.ts b/packages/core/test/lib/integrations/zoderrrors.test.ts index d5583fb57380..d7119995ae27 100644 --- a/packages/core/test/lib/integrations/zoderrrors.test.ts +++ b/packages/core/test/lib/integrations/zoderrrors.test.ts @@ -1,6 +1,12 @@ +import { z } from 'zod'; import type { Event, EventHint } from '../../../src/types-hoist'; -import { applyZodErrorsToEvent } from '../../../src/integrations/zoderrors'; +import { + applyZodErrorsToEvent, + flattenIssue, + flattenIssuePath, + formatIssueMessage, +} from '../../../src/integrations/zoderrors'; // Simplified type definition interface ZodIssue { @@ -44,13 +50,13 @@ describe('applyZodErrorsToEvent()', () => { test('should not do anything if exception is not a ZodError', () => { const event: Event = {}; const eventHint: EventHint = { originalException: new Error() }; - applyZodErrorsToEvent(100, event, eventHint); + applyZodErrorsToEvent(100, false, event, eventHint); // no changes expect(event).toStrictEqual({}); }); - test('should add ZodError issues to extras and format message', () => { + test('should add ZodError issues to extra and format message', () => { const issues = [ { code: 'invalid_type', @@ -75,13 +81,13 @@ describe('applyZodErrorsToEvent()', () => { }; const eventHint: EventHint = { originalException }; - const processedEvent = applyZodErrorsToEvent(100, event, eventHint); + const processedEvent = applyZodErrorsToEvent(100, false, event, eventHint); expect(processedEvent.exception).toStrictEqual({ values: [ { type: 'Error', - value: 'Failed to validate keys: names', + value: 'Failed to validate keys: names.', }, ], }); @@ -96,5 +102,421 @@ describe('applyZodErrorsToEvent()', () => { }, ], }); + + // No attachments added + expect(eventHint.attachments).toBe(undefined); + }); + + test('should add all ZodError issues as attachment', () => { + const issues = [ + { + code: 'invalid_type', + expected: 'string', + received: 'number', + path: ['names', 1], + keys: ['extra'], + message: 'Invalid input: expected string, received number', + }, + { + code: 'invalid_type', + expected: 'string', + received: 'number', + path: ['foo', 1], + keys: ['extra2'], + message: 'Invalid input: expected string, received number', + }, + ] satisfies ZodIssue[]; + const originalException = ZodError.create(issues); + + const event: Event = { + exception: { + values: [ + { + type: 'Error', + value: originalException.message, + }, + ], + }, + }; + + const eventHint: EventHint = { originalException }; + const processedEvent = applyZodErrorsToEvent(1, true, event, eventHint); + + expect(processedEvent.exception).toStrictEqual({ + values: [ + { + type: 'Error', + value: 'Failed to validate keys: names., foo.', + }, + ], + }); + + // Only adds the first issue to extra due to the limit + expect(processedEvent.extra).toStrictEqual({ + 'zoderror.issues': [ + { + ...issues[0], + path: issues[0]?.path.join('.'), + keys: JSON.stringify(issues[0]?.keys), + unionErrors: undefined, + }, + ], + }); + + // hint attachments contains the full issue list + expect(Array.isArray(eventHint.attachments)).toBe(true); + expect(eventHint.attachments?.length).toBe(1); + const attachment = eventHint.attachments?.[0]; + if (attachment === undefined) { + throw new Error('attachment is undefined'); + } + expect(attachment.filename).toBe('zod_issues.json'); + expect(JSON.parse(attachment.data.toString())).toMatchInlineSnapshot(` +Object { + "issues": Array [ + Object { + "code": "invalid_type", + "expected": "string", + "keys": "[\\"extra\\"]", + "message": "Invalid input: expected string, received number", + "path": "names.1", + "received": "number", + }, + Object { + "code": "invalid_type", + "expected": "string", + "keys": "[\\"extra2\\"]", + "message": "Invalid input: expected string, received number", + "path": "foo.1", + "received": "number", + }, + ], +} +`); + }); +}); + +describe('flattenIssue()', () => { + it('flattens path field', () => { + const zodError = z + .object({ + foo: z.string().min(1), + nested: z.object({ + bar: z.literal('baz'), + }), + }) + .safeParse({ + foo: '', + nested: { + bar: 'not-baz', + }, + }).error; + if (zodError === undefined) { + throw new Error('zodError is undefined'); + } + + // Original zod error + expect(zodError.issues).toMatchInlineSnapshot(` +Array [ + Object { + "code": "too_small", + "exact": false, + "inclusive": true, + "message": "String must contain at least 1 character(s)", + "minimum": 1, + "path": Array [ + "foo", + ], + "type": "string", + }, + Object { + "code": "invalid_literal", + "expected": "baz", + "message": "Invalid literal value, expected \\"baz\\"", + "path": Array [ + "nested", + "bar", + ], + "received": "not-baz", + }, +] +`); + + const issues = zodError.issues; + expect(issues.length).toBe(2); + + // Format it for use in Sentry + expect(issues.map(flattenIssue)).toMatchInlineSnapshot(` +Array [ + Object { + "code": "too_small", + "exact": false, + "inclusive": true, + "keys": undefined, + "message": "String must contain at least 1 character(s)", + "minimum": 1, + "path": "foo", + "type": "string", + "unionErrors": undefined, + }, + Object { + "code": "invalid_literal", + "expected": "baz", + "keys": undefined, + "message": "Invalid literal value, expected \\"baz\\"", + "path": "nested.bar", + "received": "not-baz", + "unionErrors": undefined, + }, +] +`); + + expect(zodError.flatten(flattenIssue)).toMatchInlineSnapshot(` +Object { + "fieldErrors": Object { + "foo": Array [ + Object { + "code": "too_small", + "exact": false, + "inclusive": true, + "keys": undefined, + "message": "String must contain at least 1 character(s)", + "minimum": 1, + "path": "foo", + "type": "string", + "unionErrors": undefined, + }, + ], + "nested": Array [ + Object { + "code": "invalid_literal", + "expected": "baz", + "keys": undefined, + "message": "Invalid literal value, expected \\"baz\\"", + "path": "nested.bar", + "received": "not-baz", + "unionErrors": undefined, + }, + ], + }, + "formErrors": Array [], +} +`); + }); + + it('flattens keys field to string', () => { + const zodError = z + .object({ + foo: z.string().min(1), + }) + .strict() + .safeParse({ + foo: 'bar', + extra_key_abc: 'hello', + extra_key_def: 'world', + }).error; + if (zodError === undefined) { + throw new Error('zodError is undefined'); + } + + // Original zod error + expect(zodError.issues).toMatchInlineSnapshot(` +Array [ + Object { + "code": "unrecognized_keys", + "keys": Array [ + "extra_key_abc", + "extra_key_def", + ], + "message": "Unrecognized key(s) in object: 'extra_key_abc', 'extra_key_def'", + "path": Array [], + }, +] +`); + + const issues = zodError.issues; + expect(issues.length).toBe(1); + + // Format it for use in Sentry + const iss = issues[0]; + if (iss === undefined) { + throw new Error('iss is undefined'); + } + const formattedIssue = flattenIssue(iss); + + // keys is now a string rather than array. + // Note: path is an empty string because the issue is at the root. + // TODO: Maybe somehow make it clearer that this is at the root? + expect(formattedIssue).toMatchInlineSnapshot(` +Object { + "code": "unrecognized_keys", + "keys": "[\\"extra_key_abc\\",\\"extra_key_def\\"]", + "message": "Unrecognized key(s) in object: 'extra_key_abc', 'extra_key_def'", + "path": "", + "unionErrors": undefined, +} +`); + expect(typeof formattedIssue.keys === 'string').toBe(true); + }); +}); + +describe('flattenIssuePath()', () => { + it('returns single path', () => { + expect(flattenIssuePath(['foo'])).toBe('foo'); + }); + + it('flattens nested string paths', () => { + expect(flattenIssuePath(['foo', 'bar'])).toBe('foo.bar'); + }); + + it('uses placeholder for path index within array', () => { + expect(flattenIssuePath([0, 'foo', 1, 'bar', 'baz'])).toBe('.foo..bar.baz'); + }); +}); + +describe('formatIssueMessage()', () => { + it('adds invalid keys to message', () => { + const zodError = z + .object({ + foo: z.string().min(1), + nested: z.object({ + bar: z.literal('baz'), + }), + }) + .safeParse({ + foo: '', + nested: { + bar: 'not-baz', + }, + }).error; + if (zodError === undefined) { + throw new Error('zodError is undefined'); + } + + const message = formatIssueMessage(zodError); + expect(message).toMatchInlineSnapshot('"Failed to validate keys: foo, nested.bar"'); + }); + + describe('adds expected type if root variable is invalid', () => { + test('object', () => { + const zodError = z + .object({ + foo: z.string().min(1), + }) + .safeParse(123).error; + if (zodError === undefined) { + throw new Error('zodError is undefined'); + } + + // Original zod error + expect(zodError.issues).toMatchInlineSnapshot(` +Array [ + Object { + "code": "invalid_type", + "expected": "object", + "message": "Expected object, received number", + "path": Array [], + "received": "number", + }, +] +`); + + const message = formatIssueMessage(zodError); + expect(message).toMatchInlineSnapshot('"Failed to validate object"'); + }); + + test('number', () => { + const zodError = z.number().safeParse('123').error; + if (zodError === undefined) { + throw new Error('zodError is undefined'); + } + + // Original zod error + expect(zodError.issues).toMatchInlineSnapshot(` +Array [ + Object { + "code": "invalid_type", + "expected": "number", + "message": "Expected number, received string", + "path": Array [], + "received": "string", + }, +] +`); + + const message = formatIssueMessage(zodError); + expect(message).toMatchInlineSnapshot('"Failed to validate number"'); + }); + + test('string', () => { + const zodError = z.string().safeParse(123).error; + if (zodError === undefined) { + throw new Error('zodError is undefined'); + } + + // Original zod error + expect(zodError.issues).toMatchInlineSnapshot(` +Array [ + Object { + "code": "invalid_type", + "expected": "string", + "message": "Expected string, received number", + "path": Array [], + "received": "number", + }, +] +`); + + const message = formatIssueMessage(zodError); + expect(message).toMatchInlineSnapshot('"Failed to validate string"'); + }); + + test('array', () => { + const zodError = z.string().array().safeParse('123').error; + if (zodError === undefined) { + throw new Error('zodError is undefined'); + } + + // Original zod error + expect(zodError.issues).toMatchInlineSnapshot(` +Array [ + Object { + "code": "invalid_type", + "expected": "array", + "message": "Expected array, received string", + "path": Array [], + "received": "string", + }, +] +`); + + const message = formatIssueMessage(zodError); + expect(message).toMatchInlineSnapshot('"Failed to validate array"'); + }); + + test('wrong type in array', () => { + const zodError = z.string().array().safeParse([123]).error; + if (zodError === undefined) { + throw new Error('zodError is undefined'); + } + + // Original zod error + expect(zodError.issues).toMatchInlineSnapshot(` +Array [ + Object { + "code": "invalid_type", + "expected": "string", + "message": "Expected string, received number", + "path": Array [ + 0, + ], + "received": "number", + }, +] +`); + + const message = formatIssueMessage(zodError); + expect(message).toMatchInlineSnapshot('"Failed to validate keys: "'); + }); }); }); 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/README.md b/packages/deno/README.md index 502778cf8abb..e6c8159cb76b 100644 --- a/packages/deno/README.md +++ b/packages/deno/README.md @@ -12,7 +12,6 @@ ## Links -- [SDK on Deno registry](https://deno.land/x/sentry) - [Official SDK Docs](https://docs.sentry.io/quickstart/) - [TypeDoc](http://getsentry.github.io/sentry-javascript/) @@ -21,14 +20,13 @@ The Sentry Deno SDK is in beta. Please help us improve the SDK by ## Usage +> DEPRECATION NOTICE: The Sentry Deno SDK as published on the Deno registry (deno.land) is deprecated. +> Import the package from the npm registry instead. + To use this SDK, call `Sentry.init(options)` as early as possible in the main entry module. This will initialize the SDK and hook into the environment. Note that you can turn off almost all side effects using the respective options. ```javascript -// Import from the Deno registry -import * as Sentry from 'https://deno.land/x/sentry/index.mjs'; - -// or import from npm registry import * as Sentry from 'npm:@sentry/deno'; Sentry.init({ diff --git a/packages/deno/package.json b/packages/deno/package.json index b9f7dbdc8ea8..271620f7cff2 100644 --- a/packages/deno/package.json +++ b/packages/deno/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/deno", - "version": "8.45.0", + "version": "8.54.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.54.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..c0fc6d4d8273 100644 --- a/packages/ember/package.json +++ b/packages/ember/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/ember", - "version": "8.45.0", + "version": "8.54.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.54.0", + "@sentry/core": "8.54.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..f283623524ef 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.54.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.54.0", + "@sentry-internal/typescript": "8.54.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..a640a5537b3c 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.54.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..3be95be4aa00 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.54.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.54.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..fd916898588b 100644 --- a/packages/gatsby/package.json +++ b/packages/gatsby/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/gatsby", - "version": "8.45.0", + "version": "8.54.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.54.0", + "@sentry/react": "8.54.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..2f6562b3e924 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.54.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.54.0", + "@sentry/node": "8.54.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..8522e54ca34e 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.54.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.54.0" }, "engines": { "node": ">=14.18" diff --git a/packages/nestjs/package.json b/packages/nestjs/package.json index 1bef4cc56c2f..7036be81047b 100644 --- a/packages/nestjs/package.json +++ b/packages/nestjs/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/nestjs", - "version": "8.45.0", + "version": "8.54.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.54.0", + "@sentry/node": "8.54.0" }, "devDependencies": { "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", diff --git a/packages/nestjs/src/setup.ts b/packages/nestjs/src/setup.ts index e75054d3391d..a32751c22ae4 100644 --- a/packages/nestjs/src/setup.ts +++ b/packages/nestjs/src/setup.ts @@ -27,9 +27,9 @@ import { isExpectedError } from './helpers'; // https://github.com/fastify/fastify/blob/87f9f20687c938828f1138f91682d568d2a31e53/types/request.d.ts#L41 interface FastifyRequest { routeOptions?: { - method?: string; url?: string; }; + method?: string; } // Partial extract of ExpressRequest interface @@ -72,9 +72,7 @@ class SentryTracingInterceptor implements NestInterceptor { const req = context.switchToHttp().getRequest() as FastifyRequest | ExpressRequest; if ('routeOptions' in req && req.routeOptions && req.routeOptions.url) { // fastify case - getIsolationScope().setTransactionName( - `${(req.routeOptions.method || 'GET').toUpperCase()} ${req.routeOptions.url}`, - ); + getIsolationScope().setTransactionName(`${(req.method || 'GET').toUpperCase()} ${req.routeOptions.url}`); } else if ('route' in req && req.route && req.route.path) { // express case getIsolationScope().setTransactionName(`${(req.method || 'GET').toUpperCase()} ${req.route.path}`); diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index baafbe149a1d..a749a17ec43e 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/nextjs", - "version": "8.45.0", + "version": "8.54.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.54.0", + "@sentry/core": "8.54.0", + "@sentry/node": "8.54.0", + "@sentry/opentelemetry": "8.54.0", + "@sentry/react": "8.54.0", + "@sentry/vercel-edge": "8.54.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..432459004b68 100644 --- a/packages/nextjs/src/config/webpack.ts +++ b/packages/nextjs/src/config/webpack.ts @@ -332,7 +332,7 @@ export function constructWebpackConfigFunction( // Symbolication for dev-mode errors is done elsewhere. if (!isDev) { // eslint-disable-next-line @typescript-eslint/no-explicit-any - const { sentryWebpackPlugin } = loadModule<{ sentryWebpackPlugin: any }>('@sentry/webpack-plugin') ?? {}; + const { sentryWebpackPlugin } = loadModule<{ sentryWebpackPlugin: any }>('@sentry/webpack-plugin', module) ?? {}; if (sentryWebpackPlugin) { if (!userSentryOptions.sourcemaps?.disable) { @@ -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()`.", ); } @@ -716,6 +716,7 @@ function addOtelWarningIgnoreRule(newConfig: WebpackConfigObjectWithModuleRules) // We provide these objects in addition to the hook above to provide redundancy in case the hook fails. { module: /@opentelemetry\/instrumentation/, message: /Critical dependency/ }, { module: /@prisma\/instrumentation/, message: /Critical dependency/ }, + { module: /require-in-the-middle/, message: /Critical dependency/ }, ] satisfies IgnoreWarningsOption; if (newConfig.ignoreWarnings === undefined) { 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..541fda472b7a 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.54.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.54.0" }, "devDependencies": { "rollup": "^4.24.4" diff --git a/packages/node/package.json b/packages/node/package.json index 618b9aa89725..f4a0c2f4bc2f 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/node", - "version": "8.45.0", + "version": "8.54.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", @@ -66,39 +66,39 @@ }, "dependencies": { "@opentelemetry/api": "^1.9.0", - "@opentelemetry/context-async-hooks": "^1.29.0", - "@opentelemetry/core": "^1.29.0", - "@opentelemetry/instrumentation": "^0.56.0", - "@opentelemetry/instrumentation-amqplib": "^0.45.0", - "@opentelemetry/instrumentation-connect": "0.42.0", - "@opentelemetry/instrumentation-dataloader": "0.15.0", - "@opentelemetry/instrumentation-express": "0.46.0", - "@opentelemetry/instrumentation-fastify": "0.43.0", - "@opentelemetry/instrumentation-fs": "0.18.0", - "@opentelemetry/instrumentation-generic-pool": "0.42.0", - "@opentelemetry/instrumentation-graphql": "0.46.0", - "@opentelemetry/instrumentation-hapi": "0.44.0", - "@opentelemetry/instrumentation-http": "0.56.0", - "@opentelemetry/instrumentation-ioredis": "0.46.0", - "@opentelemetry/instrumentation-kafkajs": "0.6.0", - "@opentelemetry/instrumentation-knex": "0.43.0", - "@opentelemetry/instrumentation-koa": "0.46.0", - "@opentelemetry/instrumentation-lru-memoizer": "0.43.0", - "@opentelemetry/instrumentation-mongodb": "0.50.0", - "@opentelemetry/instrumentation-mongoose": "0.45.0", - "@opentelemetry/instrumentation-mysql": "0.44.0", - "@opentelemetry/instrumentation-mysql2": "0.44.0", - "@opentelemetry/instrumentation-nestjs-core": "0.43.0", - "@opentelemetry/instrumentation-pg": "0.49.0", - "@opentelemetry/instrumentation-redis-4": "0.45.0", - "@opentelemetry/instrumentation-tedious": "0.17.0", - "@opentelemetry/instrumentation-undici": "0.9.0", - "@opentelemetry/resources": "^1.29.0", - "@opentelemetry/sdk-trace-base": "^1.29.0", + "@opentelemetry/context-async-hooks": "^1.30.1", + "@opentelemetry/core": "^1.30.1", + "@opentelemetry/instrumentation": "^0.57.1", + "@opentelemetry/instrumentation-amqplib": "^0.46.0", + "@opentelemetry/instrumentation-connect": "0.43.0", + "@opentelemetry/instrumentation-dataloader": "0.16.0", + "@opentelemetry/instrumentation-express": "0.47.0", + "@opentelemetry/instrumentation-fastify": "0.44.1", + "@opentelemetry/instrumentation-fs": "0.19.0", + "@opentelemetry/instrumentation-generic-pool": "0.43.0", + "@opentelemetry/instrumentation-graphql": "0.47.0", + "@opentelemetry/instrumentation-hapi": "0.45.1", + "@opentelemetry/instrumentation-http": "0.57.1", + "@opentelemetry/instrumentation-ioredis": "0.47.0", + "@opentelemetry/instrumentation-kafkajs": "0.7.0", + "@opentelemetry/instrumentation-knex": "0.44.0", + "@opentelemetry/instrumentation-koa": "0.47.0", + "@opentelemetry/instrumentation-lru-memoizer": "0.44.0", + "@opentelemetry/instrumentation-mongodb": "0.51.0", + "@opentelemetry/instrumentation-mongoose": "0.46.0", + "@opentelemetry/instrumentation-mysql": "0.45.0", + "@opentelemetry/instrumentation-mysql2": "0.45.0", + "@opentelemetry/instrumentation-nestjs-core": "0.44.0", + "@opentelemetry/instrumentation-pg": "0.50.0", + "@opentelemetry/instrumentation-redis-4": "0.46.0", + "@opentelemetry/instrumentation-tedious": "0.18.0", + "@opentelemetry/instrumentation-undici": "0.10.0", + "@opentelemetry/resources": "^1.30.1", + "@opentelemetry/sdk-trace-base": "^1.30.1", "@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.54.0", + "@sentry/opentelemetry": "8.54.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/fastify.ts b/packages/node/src/integrations/tracing/fastify.ts index a87980045b54..92936b5d6ee8 100644 --- a/packages/node/src/integrations/tracing/fastify.ts +++ b/packages/node/src/integrations/tracing/fastify.ts @@ -25,10 +25,10 @@ interface Fastify { * Works for Fastify 3, 4 and presumably 5. */ interface FastifyRequestRouteInfo { + method?: string; // since fastify@4.10.0 routeOptions?: { url?: string; - method?: string; }; routerPath?: string; } @@ -107,7 +107,7 @@ export function setupFastifyErrorHandler(fastify: Fastify): void { // Taken from Otel Fastify instrumentation: // https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/plugins/node/opentelemetry-instrumentation-fastify/src/instrumentation.ts#L94-L96 const routeName = reqWithRouteInfo.routeOptions?.url || reqWithRouteInfo.routerPath; - const method = reqWithRouteInfo.routeOptions?.method || 'GET'; + const method = reqWithRouteInfo.method || 'GET'; getIsolationScope().setTransactionName(`${method} ${routeName}`); }); diff --git a/packages/node/src/integrations/tracing/nest/nest.ts b/packages/node/src/integrations/tracing/nest/nest.ts index b5c9ea4bb61f..0b5be4cc116a 100644 --- a/packages/node/src/integrations/tracing/nest/nest.ts +++ b/packages/node/src/integrations/tracing/nest/nest.ts @@ -91,9 +91,7 @@ export function setupNestErrorHandler(app: MinimalNestJsApp, baseFilter: NestJsE const req = context.switchToHttp().getRequest(); if ('routeOptions' in req && req.routeOptions && req.routeOptions.url) { // fastify case - getIsolationScope().setTransactionName( - `${req.routeOptions.method?.toUpperCase() || 'GET'} ${req.routeOptions.url}`, - ); + getIsolationScope().setTransactionName(`${req.method?.toUpperCase() || 'GET'} ${req.routeOptions.url}`); } else if ('route' in req && req.route && req.route.path) { // express case getIsolationScope().setTransactionName(`${req.method?.toUpperCase() || 'GET'} ${req.route.path}`); diff --git a/packages/node/src/integrations/tracing/nest/types.ts b/packages/node/src/integrations/tracing/nest/types.ts index a983832ac8c6..8283e652edfb 100644 --- a/packages/node/src/integrations/tracing/nest/types.ts +++ b/packages/node/src/integrations/tracing/nest/types.ts @@ -4,9 +4,9 @@ // https://github.com/fastify/fastify/blob/87f9f20687c938828f1138f91682d568d2a31e53/types/request.d.ts#L41 interface FastifyRequest { routeOptions?: { - method?: string; url?: string; }; + method?: string; } // Partial extract of ExpressRequest interface diff --git a/packages/node/src/integrations/tracing/prisma.ts b/packages/node/src/integrations/tracing/prisma.ts index a42d41a6b5ec..930d34d602b9 100644 --- a/packages/node/src/integrations/tracing/prisma.ts +++ b/packages/node/src/integrations/tracing/prisma.ts @@ -1,51 +1,78 @@ +import type { Instrumentation } from '@opentelemetry/instrumentation'; // When importing CJS modules into an ESM module, we cannot import the named exports directly. import * as prismaInstrumentation from '@prisma/instrumentation'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, defineIntegration, spanToJSON } from '@sentry/core'; -import type { IntegrationFn } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, consoleSandbox, defineIntegration, spanToJSON } from '@sentry/core'; import { generateInstrumentOnce } from '../../otel/instrument'; +import type { PrismaV5TracingHelper } from './prisma/vendor/v5-tracing-helper'; +import type { PrismaV6TracingHelper } from './prisma/vendor/v6-tracing-helper'; const INTEGRATION_NAME = 'Prisma'; -export const instrumentPrisma = generateInstrumentOnce(INTEGRATION_NAME, () => { - const EsmInteropPrismaInstrumentation: typeof prismaInstrumentation.PrismaInstrumentation = - // @ts-expect-error We need to do the following for interop reasons - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - prismaInstrumentation.default?.PrismaInstrumentation || prismaInstrumentation.PrismaInstrumentation; +const EsmInteropPrismaInstrumentation: typeof prismaInstrumentation.PrismaInstrumentation = + // @ts-expect-error We need to do the following for interop reasons + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + prismaInstrumentation.default?.PrismaInstrumentation || prismaInstrumentation.PrismaInstrumentation; - return new EsmInteropPrismaInstrumentation({}); -}); +type CompatibilityLayerTraceHelper = PrismaV5TracingHelper & PrismaV6TracingHelper; -const _prismaIntegration = (() => { - return { - name: INTEGRATION_NAME, - setupOnce() { - instrumentPrisma(); - }, +function isPrismaV5TracingHelper(helper: unknown): helper is PrismaV5TracingHelper { + return !!helper && typeof helper === 'object' && 'createEngineSpan' in helper; +} - setup(client) { - client.on('spanStart', span => { - const spanJSON = spanToJSON(span); - if (spanJSON.description?.startsWith('prisma:')) { - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.db.otel.prisma'); - } +class SentryPrismaInteropInstrumentation extends EsmInteropPrismaInstrumentation { + public constructor() { + super(); + } - if (spanJSON.description === 'prisma:engine:db_query') { - span.setAttribute('db.system', 'prisma'); - } - }); - }, - }; -}) satisfies IntegrationFn; + public enable(): void { + super.enable(); + + // The PrismaIntegration (super class) defines a global variable `global["PRISMA_INSTRUMENTATION"]` when `enable()` is called. This global variable holds a "TracingHelper" which Prisma uses internally to create tracing data. It's their way of not depending on OTEL with their main package. The sucky thing is, prisma broke the interface of the tracing helper with the v6 major update. This means that if you use Prisma 6 with the v5 instrumentation (or vice versa) Prisma just blows up, because tries to call methods on the helper that no longer exist. + // Because we actually want to use the v6 instrumentation and not blow up in Prisma 5 user's faces, what we're doing here is backfilling the v5 method (`createEngineSpan`) with a noop so that no longer crashes when it attempts to call that function. + // We still won't fully emit all the spans, but this could potentially be implemented in the future. + const prismaInstrumentationObject = (globalThis as Record).PRISMA_INSTRUMENTATION; + const prismaTracingHelper = + prismaInstrumentationObject && + typeof prismaInstrumentationObject === 'object' && + 'helper' in prismaInstrumentationObject + ? prismaInstrumentationObject.helper + : undefined; + + let emittedWarning = false; + + if (isPrismaV5TracingHelper(prismaTracingHelper)) { + (prismaTracingHelper as CompatibilityLayerTraceHelper).dispatchEngineSpans = () => { + consoleSandbox(() => { + if (!emittedWarning) { + emittedWarning = true; + // eslint-disable-next-line no-console + console.warn( + '[Sentry] This version (v8) of the Sentry SDK does not support tracing with Prisma version 6 out of the box. To trace Prisma version 6, pass a `prismaInstrumentation` for version 6 to the Sentry `prismaIntegration`. Read more: https://docs.sentry.io/platforms/javascript/guides/node/configuration/integrations/prisma/', + ); + } + }); + }; + } + } +} + +export const instrumentPrisma = generateInstrumentOnce<{ prismaInstrumentation?: Instrumentation }>( + INTEGRATION_NAME, + options => { + // Use a passed instrumentation instance to support older Prisma versions + if (options?.prismaInstrumentation) { + return options.prismaInstrumentation; + } + + return new SentryPrismaInteropInstrumentation(); + }, +); /** - * Adds Sentry tracing instrumentation for the [prisma](https://www.npmjs.com/package/prisma) library. - * + * Adds Sentry tracing instrumentation for the [Prisma](https://www.npmjs.com/package/prisma) ORM. * For more information, see the [`prismaIntegration` documentation](https://docs.sentry.io/platforms/javascript/guides/node/configuration/integrations/prisma/). * - * @example - * - * Make sure `previewFeatures = ["tracing"]` is set in the prisma client generator block. See the - * [prisma docs](https://www.prisma.io/docs/concepts/components/prisma-client/opentelemetry-tracing) for more details. + * Make sure `previewFeatures = ["tracing"]` is added to the generator block in your Prisma schema. * * ```prisma * generator client { @@ -54,14 +81,63 @@ const _prismaIntegration = (() => { * } * ``` * - * Then you can use the integration like this: + * NOTE: By default, this integration works with Prisma version 5. + * To get performance instrumentation for other Prisma versions, + * 1. Install the `@prisma/instrumentation` package with the desired version. + * 1. Pass a `new PrismaInstrumentation()` instance as exported from `@prisma/instrumentation` to the `prismaInstrumentation` option of this integration: * - * ```javascript - * const Sentry = require('@sentry/node'); + * ```js + * import { PrismaInstrumentation } from '@prisma/instrumentation' * - * Sentry.init({ - * integrations: [Sentry.prismaIntegration()], - * }); - * ``` + * Sentry.init({ + * integrations: [ + * prismaIntegration({ + * // Override the default instrumentation that Sentry uses + * prismaInstrumentation: new PrismaInstrumentation() + * }) + * ] + * }) + * ``` + * + * The passed instrumentation instance will override the default instrumentation instance the integration would use, while the `prismaIntegration` will still ensure data compatibility for the various Prisma versions. */ -export const prismaIntegration = defineIntegration(_prismaIntegration); +export const prismaIntegration = defineIntegration( + ({ + prismaInstrumentation, + }: { + /** + * Overrides the instrumentation used by the Sentry SDK with the passed in instrumentation instance. + * + * NOTE: By default, the Sentry SDK uses the Prisma v5 instrumentation. Use this option if you need performance instrumentation different Prisma versions. + * + * For more information refer to the documentation of `prismaIntegration()` or see https://docs.sentry.io/platforms/javascript/guides/node/configuration/integrations/prisma/ + */ + prismaInstrumentation?: Instrumentation; + } = {}) => { + return { + name: INTEGRATION_NAME, + setupOnce() { + instrumentPrisma({ prismaInstrumentation }); + }, + setup(client) { + client.on('spanStart', span => { + const spanJSON = spanToJSON(span); + if (spanJSON.description?.startsWith('prisma:')) { + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.db.otel.prisma'); + } + + // Make sure we use the query text as the span name, for ex. SELECT * FROM "User" WHERE "id" = $1 + if (spanJSON.description === 'prisma:engine:db_query' && spanJSON.data?.['db.query.text']) { + span.updateName(spanJSON.data['db.query.text'] as string); + } + + // 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/integrations/tracing/prisma/vendor/v5-tracing-helper.ts b/packages/node/src/integrations/tracing/prisma/vendor/v5-tracing-helper.ts new file mode 100644 index 000000000000..8823a8ca7728 --- /dev/null +++ b/packages/node/src/integrations/tracing/prisma/vendor/v5-tracing-helper.ts @@ -0,0 +1,41 @@ +// Vendored from https://github.com/prisma/prisma/blob/718358aa37975c18e5ea62f5b659fb47630b7609/packages/internals/src/tracing/types.ts#L1 + +import type { Context, Span, SpanOptions } from '@opentelemetry/api'; + +type V5SpanCallback = (span?: Span, context?: Context) => R; + +type V5ExtendedSpanOptions = SpanOptions & { + name: string; + internal?: boolean; + middleware?: boolean; + active?: boolean; + context?: Context; +}; + +type EngineSpanEvent = { + span: boolean; + spans: V5EngineSpan[]; +}; + +type V5EngineSpanKind = 'client' | 'internal'; + +type V5EngineSpan = { + span: boolean; + name: string; + trace_id: string; + span_id: string; + parent_span_id: string; + start_time: [number, number]; + end_time: [number, number]; + attributes?: Record; + links?: { trace_id: string; span_id: string }[]; + kind: V5EngineSpanKind; +}; + +export interface PrismaV5TracingHelper { + isEnabled(): boolean; + getTraceParent(context?: Context): string; + createEngineSpan(engineSpanEvent: EngineSpanEvent): void; + getActiveContext(): Context | undefined; + runInChildSpan(nameOrOptions: string | V5ExtendedSpanOptions, callback: V5SpanCallback): R; +} diff --git a/packages/node/src/integrations/tracing/prisma/vendor/v6-tracing-helper.ts b/packages/node/src/integrations/tracing/prisma/vendor/v6-tracing-helper.ts new file mode 100644 index 000000000000..2ad1482a2e1a --- /dev/null +++ b/packages/node/src/integrations/tracing/prisma/vendor/v6-tracing-helper.ts @@ -0,0 +1,38 @@ +// https://github.com/prisma/prisma/blob/d45607dfa10c4ef08cb8f79f18fa84ef33910150/packages/internals/src/tracing/types.ts#L1 + +import type { Context, Span, SpanOptions } from '@opentelemetry/api'; + +type V6SpanCallback = (span?: Span, context?: Context) => R; + +type V6ExtendedSpanOptions = SpanOptions & { + name: string; + internal?: boolean; + middleware?: boolean; + active?: boolean; + context?: Context; +}; + +type V6EngineSpanId = string; + +type V6HrTime = [number, number]; + +type EngineSpanKind = 'client' | 'internal'; + +type PrismaV6EngineSpan = { + id: V6EngineSpanId; + parentId: string | null; + name: string; + startTime: V6HrTime; + endTime: V6HrTime; + kind: EngineSpanKind; + attributes?: Record; + links?: V6EngineSpanId[]; +}; + +export interface PrismaV6TracingHelper { + isEnabled(): boolean; + getTraceParent(context?: Context): string; + dispatchEngineSpans(spans: PrismaV6EngineSpan[]): void; + getActiveContext(): Context | undefined; + runInChildSpan(nameOrOptions: string | V6ExtendedSpanOptions, callback: V6SpanCallback): R; +} 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..7f59381d2778 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/nuxt", - "version": "8.45.0", + "version": "8.54.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.54.0", + "@sentry/core": "8.54.0", + "@sentry/node": "8.54.0", + "@sentry/opentelemetry": "8.54.0", "@sentry/rollup-plugin": "2.22.7", "@sentry/vite-plugin": "2.22.6", - "@sentry/vue": "8.45.0" + "@sentry/vue": "8.54.0" }, "devDependencies": { "@nuxt/module-builder": "^0.8.4", diff --git a/packages/nuxt/src/common/types.ts b/packages/nuxt/src/common/types.ts index 93ca94016924..8a9a453ff7db 100644 --- a/packages/nuxt/src/common/types.ts +++ b/packages/nuxt/src/common/types.ts @@ -32,6 +32,13 @@ type SourceMapsOptions = { */ org?: string; + /** + * The URL of your Sentry instance if you're using self-hosted Sentry. + * + * @default https://sentry.io by default the plugin will point towards the Sentry SaaS URL + */ + url?: string; + /** * The project slug of your Sentry project. * Instead of specifying this option, you can also set the `SENTRY_PROJECT` environment variable. 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/src/runtime/plugins/sentry.server.ts b/packages/nuxt/src/runtime/plugins/sentry.server.ts index b748115f5c81..15992be5d0b8 100644 --- a/packages/nuxt/src/runtime/plugins/sentry.server.ts +++ b/packages/nuxt/src/runtime/plugins/sentry.server.ts @@ -52,7 +52,11 @@ export default defineNitroPlugin(nitroApp => { }); async function flushIfServerless(): Promise { - const isServerless = !!process.env.LAMBDA_TASK_ROOT || !!process.env.VERCEL || !!process.env.NETLIFY; + const isServerless = + !!process.env.FUNCTIONS_WORKER_RUNTIME || // Azure Functions + !!process.env.LAMBDA_TASK_ROOT || // AWS Lambda + !!process.env.VERCEL || + !!process.env.NETLIFY; // @ts-expect-error This is not typed if (GLOBAL_OBJ[Symbol.for('@vercel/request-context')]) { diff --git a/packages/nuxt/src/vite/sourceMaps.ts b/packages/nuxt/src/vite/sourceMaps.ts index 2f90094e6138..0b264e822bcc 100644 --- a/packages/nuxt/src/vite/sourceMaps.ts +++ b/packages/nuxt/src/vite/sourceMaps.ts @@ -91,6 +91,7 @@ export function getPluginOptions( project: sourceMapsUploadOptions.project ?? process.env.SENTRY_PROJECT, authToken: sourceMapsUploadOptions.authToken ?? process.env.SENTRY_AUTH_TOKEN, telemetry: sourceMapsUploadOptions.telemetry ?? true, + url: sourceMapsUploadOptions.url ?? process.env.SENTRY_URL, debug: moduleOptions.debug ?? false, _metaOptions: { telemetry: { diff --git a/packages/nuxt/test/vite/sourceMaps.test.ts b/packages/nuxt/test/vite/sourceMaps.test.ts index 0c90429fa8d5..b33d314f5166 100644 --- a/packages/nuxt/test/vite/sourceMaps.test.ts +++ b/packages/nuxt/test/vite/sourceMaps.test.ts @@ -20,6 +20,7 @@ describe('getPluginOptions', () => { SENTRY_ORG: 'default-org', SENTRY_PROJECT: 'default-project', SENTRY_AUTH_TOKEN: 'default-token', + SENTRY_URL: 'https://santry.io', }; process.env = { ...defaultEnv }; @@ -31,6 +32,7 @@ describe('getPluginOptions', () => { org: 'default-org', project: 'default-project', authToken: 'default-token', + url: 'https://santry.io', telemetry: true, sourcemaps: expect.objectContaining({ rewriteSources: expect.any(Function), @@ -114,6 +116,7 @@ describe('getPluginOptions', () => { assets: ['custom-assets/**/*'], filesToDeleteAfterUpload: ['delete-this.js'], }, + url: 'https://santry.io', }, debug: true, unstable_sentryBundlerPluginOptions: { @@ -124,6 +127,7 @@ describe('getPluginOptions', () => { release: { name: 'test-release', }, + url: 'https://suntry.io', }, }; const options = getPluginOptions(customOptions, false); @@ -140,6 +144,7 @@ describe('getPluginOptions', () => { release: expect.objectContaining({ name: 'test-release', }), + url: 'https://suntry.io', }), ); }); 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..db8e16c2223c 100644 --- a/packages/opentelemetry/package.json +++ b/packages/opentelemetry/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/opentelemetry", - "version": "8.45.0", + "version": "8.54.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,20 +39,22 @@ "access": "public" }, "dependencies": { - "@sentry/core": "8.45.0" + "@sentry/core": "8.54.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", - "@opentelemetry/core": "^1.29.0", - "@opentelemetry/instrumentation": "^0.56.0", - "@opentelemetry/sdk-trace-base": "^1.29.0", + "@opentelemetry/context-async-hooks": "^1.30.1", + "@opentelemetry/core": "^1.30.1", + "@opentelemetry/instrumentation": "^0.57.1", + "@opentelemetry/sdk-trace-base": "^1.30.1", "@opentelemetry/semantic-conventions": "^1.28.0" }, "devDependencies": { "@opentelemetry/api": "^1.9.0", - "@opentelemetry/context-async-hooks": "^1.29.0", - "@opentelemetry/core": "^1.29.0", - "@opentelemetry/sdk-trace-base": "^1.29.0", + "@opentelemetry/context-async-hooks": "^1.30.1", + "@opentelemetry/core": "^1.30.1", + "@opentelemetry/instrumentation": "^0.57.1", + "@opentelemetry/sdk-trace-base": "^1.30.1", "@opentelemetry/semantic-conventions": "^1.28.0" }, "scripts": { 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..448b2d4d94d8 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.54.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.54.0", + "@sentry/node": "8.54.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..0e7ebf5c837e 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/react", - "version": "8.45.0", + "version": "8.54.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.54.0", + "@sentry/core": "8.54.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/src/tanstackrouter.ts b/packages/react/src/tanstackrouter.ts index 2f5467ee1640..2fb55afc0c4e 100644 --- a/packages/react/src/tanstackrouter.ts +++ b/packages/react/src/tanstackrouter.ts @@ -64,8 +64,9 @@ export function tanstackRouterBrowserTracingIntegration( if (instrumentNavigation) { // The onBeforeNavigate hook is called at the very beginning of a navigation and is only called once per navigation, even when the user is redirected castRouterInstance.subscribe('onBeforeNavigate', onBeforeNavigateArgs => { + const fromLocationState = onBeforeNavigateArgs.fromLocation && onBeforeNavigateArgs.fromLocation.state; // onBeforeNavigate is called during pageloads. We can avoid creating navigation spans by comparing the states of the to and from arguments. - if (onBeforeNavigateArgs.toLocation.state === onBeforeNavigateArgs.fromLocation.state) { + if (onBeforeNavigateArgs.toLocation.state === fromLocationState) { return; } diff --git a/packages/react/src/vendor/tanstackrouter-types.ts b/packages/react/src/vendor/tanstackrouter-types.ts index e5eeba71aa87..417d2b1447b1 100644 --- a/packages/react/src/vendor/tanstackrouter-types.ts +++ b/packages/react/src/vendor/tanstackrouter-types.ts @@ -46,7 +46,7 @@ export interface VendoredTanstackRouter { eventType: 'onResolved' | 'onBeforeNavigate', callback: (stateUpdate: { toLocation: VendoredTanstackRouterLocation; - fromLocation: VendoredTanstackRouterLocation; + fromLocation?: VendoredTanstackRouterLocation; }) => void, ): () => void; } 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 }) =>