diff --git a/.craft.yml b/.craft.yml index 2ee55c49d35d..4bbb53e1e1f4 100644 --- a/.craft.yml +++ b/.craft.yml @@ -105,6 +105,9 @@ targets: - name: npm id: '@sentry/nextjs' includeNames: /^sentry-nextjs-\d.*\.tgz$/ + - name: npm + id: '@sentry/nuxt' + includeNames: /^sentry-nuxt-\d.*\.tgz$/ - name: npm id: '@sentry/remix' includeNames: /^sentry-remix-\d.*\.tgz$/ diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 779e221b093c..fdaa607f0411 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -913,8 +913,7 @@ jobs: # - AND if the profiling node bindings were either successful or skipped if: | always() && needs.job_build.result == 'success' && - (needs.job_compile_bindings_profiling_node.result == 'success' || needs.job_compile_bindings_profiling_node.result == 'skipped') && - (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) + (needs.job_compile_bindings_profiling_node.result == 'success' || needs.job_compile_bindings_profiling_node.result == 'skipped') needs: [job_get_metadata, job_build, job_compile_bindings_profiling_node] runs-on: ubuntu-20.04-large-js timeout-minutes: 15 @@ -996,7 +995,6 @@ jobs: 'angular-17', 'angular-18', 'aws-lambda-layer-cjs', - 'cloudflare-astro', 'node-express', 'create-react-app', 'create-next-app', @@ -1017,6 +1015,7 @@ jobs: 'nextjs-app-dir', 'nextjs-14', 'nextjs-15', + 'react-17', 'react-19', 'react-create-hash-router', 'react-router-6-use-routes', @@ -1113,16 +1112,6 @@ jobs: timeout-minutes: 5 run: pnpm test:assert - - name: Deploy Astro to Cloudflare - uses: cloudflare/pages-action@v1 - if: matrix.test-application == 'cloudflare-astro' - with: - apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} - accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - projectName: ${{ secrets.CLOUDFLARE_PROJECT_NAME }} - directory: dist - workingDirectory: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }} - job_optional_e2e_tests: name: E2E ${{ matrix.label || matrix.test-application }} Test # We only run E2E tests for non-fork PRs because the E2E tests require secrets to work and they can't be accessed from forks @@ -1130,7 +1119,8 @@ jobs: # See: https://github.com/actions/runner/issues/2205 if: always() && needs.job_e2e_prepare.result == 'success' && - (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) + (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && + github.actor != 'dependabot[bot]' needs: [job_get_metadata, job_build, job_e2e_prepare] runs-on: ubuntu-20.04 timeout-minutes: 10 @@ -1148,6 +1138,7 @@ jobs: matrix: test-application: [ + 'cloudflare-astro', 'react-send-to-sentry', 'node-express-send-to-sentry', 'debug-id-sourcemaps', @@ -1214,6 +1205,16 @@ jobs: timeout-minutes: 5 run: pnpm ${{ matrix.assert-command || 'test:assert' }} + - name: Deploy Astro to Cloudflare + uses: cloudflare/pages-action@v1 + if: matrix.test-application == 'cloudflare-astro' + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + projectName: ${{ secrets.CLOUDFLARE_PROJECT_NAME }} + directory: dist + workingDirectory: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }} + job_profiling_e2e_tests: name: E2E ${{ matrix.label || matrix.test-application }} Test # We only run E2E tests for non-fork PRs because the E2E tests require secrets to work and they can't be accessed from forks @@ -1252,7 +1253,7 @@ jobs: ref: ${{ env.HEAD_COMMIT }} - uses: pnpm/action-setup@v4 with: - version: 8.3.1 + version: 9.4.0 - name: Set up Node uses: actions/setup-node@v4 with: diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index 65e691e2ff0b..9dcbd43e5547 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -103,7 +103,7 @@ jobs: ref: ${{ env.HEAD_COMMIT }} - uses: pnpm/action-setup@v4 with: - version: 8.3.1 + version: 9.4.0 - name: Set up Node uses: actions/setup-node@v4 diff --git a/.github/workflows/external-contributors.yml b/.github/workflows/external-contributors.yml index a797d93732eb..a0869cc3d2d4 100644 --- a/.github/workflows/external-contributors.yml +++ b/.github/workflows/external-contributors.yml @@ -9,16 +9,18 @@ on: jobs: external_contributor: name: External Contributors + permissions: + pull-requests: write + contents: write runs-on: ubuntu-20.04 if: | - github.event.pull_request.author_association != 'COLLABORATOR' + github.event.pull_request.merged == true + && github.event.pull_request.author_association != 'COLLABORATOR' && github.event.pull_request.author_association != 'MEMBER' && github.event.pull_request.author_association != 'OWNER' && github.actor != 'dependabot[bot]' steps: - uses: actions/checkout@v4 - with: - ref: ${{ github.head_ref }} - name: Set up Node uses: actions/setup-node@v4 with: @@ -32,6 +34,8 @@ jobs: uses: ./dev-packages/external-contributor-gh-action with: name: ${{ github.event.pull_request.user.login }} + author_association: ${{ github.event.pull_request.author_association }} + - name: Create PR with changes uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c with: @@ -40,5 +44,5 @@ jobs: branch: 'external-contributor/patch-${{ github.event.pull_request.user.login }}' base: 'develop' delete-branch: true - body: This PR adds the external contributor to the CHANGELOG.md file, so that they are credited for their contribution. + body: "This PR adds the external contributor to the CHANGELOG.md file, so that they are credited for their contribution. See #${{ github.event.pull_request.number }}" diff --git a/CHANGELOG.md b/CHANGELOG.md index 469cb8258063..4504804bc586 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,83 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 8.16.0 + +### Important Changes + +- **feat(nextjs): Use spans generated by Next.js for App Router (#12729)** + +Previously, the `@sentry/nextjs` SDK automatically recorded spans in the form of transactions for each of your top-level +server components (pages, layouts, ...). This approach had a few drawbacks, the main ones being that traces didn't have +a root span, and more importantly, if you had data stream to the client, its duration was not captured because the +server component spans had finished before the data could finish streaming. + +With this release, we will capture the duration of App Router requests in their entirety as a single transaction with +server component spans being descendants of that transaction. This means you will get more data that is also more +accurate. Note that this does not apply to the Edge runtime. For the Edge runtime, the SDK will emit transactions as it +has before. + +Generally speaking, this change means that you will see less _transactions_ and more _spans_ in Sentry. You will no +longer receive server component transactions like `Page Server Component (/path/to/route)` (unless using the Edge +runtime), and you will instead receive transactions for your App Router SSR requests that look like +`GET /path/to/route`. + +If you are on Sentry SaaS, this may have an effect on your quota consumption: Less transactions, more spans. + +- **- feat(nestjs): Add nest cron monitoring support (#12781)** + +The `@sentry/nestjs` SDK now includes a `@SentryCron` decorator that can be used to augment the native NestJS `@Cron` +decorator to send check-ins to Sentry before and after each cron job run: + +```typescript +import { Cron } from '@nestjs/schedule'; +import { SentryCron, MonitorConfig } from '@sentry/nestjs'; +import type { MonitorConfig } from '@sentry/types'; + +const monitorConfig: MonitorConfig = { + schedule: { + type: 'crontab', + value: '* * * * *', + }, + checkinMargin: 2, // In minutes. Optional. + maxRuntime: 10, // In minutes. Optional. + timezone: 'America/Los_Angeles', // Optional. +}; + +export class MyCronService { + @Cron('* * * * *') + @SentryCron('my-monitor-slug', monitorConfig) + handleCron() { + // Your cron job logic here + } +} +``` + +### Other Changes + +- feat(node): Allow to pass instrumentation config to `httpIntegration` (#12761) +- feat(nuxt): Add server error hook (#12796) +- feat(nuxt): Inject sentry config with Nuxt `addPluginTemplate` (#12760) +- fix: Apply stack frame metadata before event processors (#12799) +- fix(feedback): Add missing `h` import in `ScreenshotEditor` (#12784) +- fix(node): Ensure `autoSessionTracking` is enabled by default (#12790) +- ref(feedback): Let CropCorner inherit the existing h prop (#12814) +- ref(otel): Ensure we never swallow args for ContextManager (#12798) + +## 8.15.0 + +- feat(core): allow unregistering callback through `on` (#11710) +- feat(nestjs): Add function-level span decorator to nestjs (#12721) +- feat(otel): Export & use `spanTimeInputToSeconds` for otel span exporter (#12699) +- fix(core): Pass origin as referrer for `lazyLoadIntegration` (#12766) +- fix(deno): Publish from build directory (#12773) +- fix(hapi): Specify error channel to filter boom errors (#12725) +- fix(react): Revert back to `jsxRuntime: 'classic'` to prevent breaking react 17 (#12775) +- fix(tracing): Report dropped spans for transactions (#12751) +- ref(scope): Delete unused public `getStack()` (#12737) + +Work in this release was contributed by @arturovt and @jaulz. Thank you for your contributions! + ## 8.14.0 ### Important Changes diff --git a/dev-packages/browser-integration-tests/fixtures/loader.js b/dev-packages/browser-integration-tests/fixtures/loader.js index 17a9a1d652a8..8e30865d35ad 100644 --- a/dev-packages/browser-integration-tests/fixtures/loader.js +++ b/dev-packages/browser-integration-tests/fixtures/loader.js @@ -1,139 +1,5 @@ -!(function (n, e, r, t, i, o, a, c, s) { - for (var u = s, f = 0; f < document.scripts.length; f++) - if (document.scripts[f].src.indexOf(o) > -1) { - u && 'no' === document.scripts[f].getAttribute('data-lazy') && (u = !1); - break; - } - var p = []; - function l(n) { - return 'e' in n; - } - function d(n) { - return 'p' in n; - } - function _(n) { - return 'f' in n; - } - var v = []; - function y(n) { - u && - (l(n) || d(n) || (_(n) && n.f.indexOf('capture') > -1) || (_(n) && n.f.indexOf('showReportDialog') > -1)) && - m(), - v.push(n); - } - function g() { - y({ e: [].slice.call(arguments) }); - } - function h(n) { - y({ p: n }); - } - function E() { - try { - n.SENTRY_SDK_SOURCE = 'loader'; - var e = n[i], - o = e.init; - (e.init = function (i) { - n.removeEventListener(r, g), n.removeEventListener(t, h); - var a = c; - for (var s in i) Object.prototype.hasOwnProperty.call(i, s) && (a[s] = i[s]); - !(function (n, e) { - var r = n.integrations || []; - if (!Array.isArray(r)) return; - var t = r.map(function (n) { - return n.name; - }); - n.tracesSampleRate && - -1 === t.indexOf('BrowserTracing') && - (e.BrowserTracing - ? r.push(new e.BrowserTracing()) - : e.browserTracingIntegration && r.push(e.browserTracingIntegration())); - (n.replaysSessionSampleRate || n.replaysOnErrorSampleRate) && - -1 === t.indexOf('Replay') && - (e.Replay ? r.push(new e.Replay()) : e.replayIntegration && r.push(e.replayIntegration())); - n.integrations = r; - })(a, e), - o(a); - }), - setTimeout(function () { - return (function (e) { - try { - 'function' == typeof n.sentryOnLoad && (n.sentryOnLoad(), (n.sentryOnLoad = void 0)); - for (var r = 0; r < p.length; r++) 'function' == typeof p[r] && p[r](); - p.splice(0); - for (r = 0; r < v.length; r++) { - _((o = v[r])) && 'init' === o.f && e.init.apply(e, o.a); - } - L() || e.init(); - var t = n.onerror, - i = n.onunhandledrejection; - for (r = 0; r < v.length; r++) { - var o; - if (_((o = v[r]))) { - if ('init' === o.f) continue; - e[o.f].apply(e, o.a); - } else l(o) && t ? t.apply(n, o.e) : d(o) && i && i.apply(n, [o.p]); - } - } catch (n) { - console.error(n); - } - })(e); - }); - } catch (n) { - console.error(n); - } - } - var O = !1; - function m() { - if (!O) { - O = !0; - var n = e.scripts[0], - r = e.createElement('script'); - (r.src = a), - (r.crossOrigin = 'anonymous'), - r.addEventListener('load', E, { once: !0, passive: !0 }), - n.parentNode.insertBefore(r, n); - } - } - function L() { - var e = n.__SENTRY__; - - // TODO: This is a temporary hack to make the loader script compatible with the versioned - // carrier. This needs still needs to be added to the actual loader script before we - // release the loader for v8! - var v = e && e.version && e[e.version]; - - return !(void 0 === e || !e.hub || !e.hub.getClient()) || !!v; - } - (n[i] = n[i] || {}), - (n[i].onLoad = function (n) { - L() ? n() : p.push(n); - }), - (n[i].forceLoad = function () { - setTimeout(function () { - m(); - }); - }), - [ - 'init', - 'addBreadcrumb', - 'captureMessage', - 'captureException', - 'captureEvent', - 'configureScope', - 'withScope', - 'showReportDialog', - ].forEach(function (e) { - n[i][e] = function () { - y({ f: e, a: arguments }); - }; - }), - n.addEventListener(r, g), - n.addEventListener(t, h), - u || - setTimeout(function () { - m(); - }); -})( +!function(n,e,r,t,i,o,a,c,s){for(var u=s,f=0;f-1){u&&"no"===document.scripts[f].getAttribute("data-lazy")&&(u=!1);break}var p=[];function l(n){return"e"in n}function d(n){return"p"in n}function _(n){return"f"in n}var v=[];function y(n){u&&(l(n)||d(n)||_(n)&&n.f.indexOf("capture")>-1||_(n)&&n.f.indexOf("showReportDialog")>-1)&&m(),v.push(n)}function g(){y({e:[].slice.call(arguments)})}function h(n){y({p:n})}function E(){try{n.SENTRY_SDK_SOURCE="loader";var e=n[i],o=e.init;e.init=function(i){n.removeEventListener(r,g),n.removeEventListener(t,h);var a=c;for(var s in i)Object.prototype.hasOwnProperty.call(i,s)&&(a[s]=i[s]);!function(n,e){var r=n.integrations||[];if(!Array.isArray(r))return;var t=r.map((function(n){return n.name}));n.tracesSampleRate&&-1===t.indexOf("BrowserTracing")&&(e.browserTracingIntegration?r.push(e.browserTracingIntegration({enableInp:!0})):e.BrowserTracing&&r.push(new e.BrowserTracing));(n.replaysSessionSampleRate||n.replaysOnErrorSampleRate)&&-1===t.indexOf("Replay")&&(e.replayIntegration?r.push(e.replayIntegration()):e.Replay&&r.push(new e.Replay));n.integrations=r}(a,e),o(a)},setTimeout((function(){return function(e){try{"function"==typeof n.sentryOnLoad&&(n.sentryOnLoad(),n.sentryOnLoad=void 0);for(var r=0;r { + value.stacktrace.frames.forEach(frame => { + moduleMetadataEntries.push(frame.module_metadata); + }); + }); + } catch (e) { + // noop + } + } + + event.extra = { + ...event.extra, + module_metadata_entries: moduleMetadataEntries, + }; + + return event; + }, +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadata/subject.js b/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadata/subject.js new file mode 100644 index 000000000000..a9adc99a645d --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadata/subject.js @@ -0,0 +1,22 @@ +var _sentryModuleMetadataGlobal = + typeof window !== 'undefined' + ? window + : typeof global !== 'undefined' + ? global + : typeof self !== 'undefined' + ? self + : {}; + +_sentryModuleMetadataGlobal._sentryModuleMetadata = _sentryModuleMetadataGlobal._sentryModuleMetadata || {}; + +_sentryModuleMetadataGlobal._sentryModuleMetadata[new Error().stack] = Object.assign( + {}, + _sentryModuleMetadataGlobal._sentryModuleMetadata[new Error().stack], + { + foo: 'bar', + }, +); + +setTimeout(() => { + throw new Error('I am a module metadata Error'); +}, 0); diff --git a/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadata/test.ts b/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadata/test.ts new file mode 100644 index 000000000000..d6414e154a23 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadata/test.ts @@ -0,0 +1,17 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; + +sentryTest('should provide module_metadata on stack frames in beforeSend', async ({ getLocalTestPath, page }) => { + // moduleMetadataIntegration is not included in any CDN bundles + if (process.env.PW_BUNDLE?.startsWith('bundle')) { + sentryTest.skip(); + } + + const url = await getLocalTestPath({ testDir: __dirname }); + + const errorEvent = await getFirstSentryEnvelopeRequest(page, url); + expect(errorEvent.extra?.['module_metadata_entries']).toEqual([{ foo: 'bar' }]); +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadataWithRewriteFrames/init.js b/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadataWithRewriteFrames/init.js new file mode 100644 index 000000000000..885b1d2da2c1 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadataWithRewriteFrames/init.js @@ -0,0 +1,38 @@ +import * as Sentry from '@sentry/browser'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [ + Sentry.moduleMetadataIntegration(), + Sentry.rewriteFramesIntegration({ + iteratee: frame => { + return { + ...frame, + filename: 'bloop', // something that should completely mess with module metadata association + }; + }, + }), + ], + beforeSend(event) { + const moduleMetadataEntries = []; + + if (event.type === undefined) { + try { + event.exception.values.forEach(value => { + value.stacktrace.frames.forEach(frame => { + moduleMetadataEntries.push(frame.module_metadata); + }); + }); + } catch (e) { + // noop + } + } + + event.extra = { + ...event.extra, + module_metadata_entries: moduleMetadataEntries, + }; + + return event; + }, +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadataWithRewriteFrames/subject.js b/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadataWithRewriteFrames/subject.js new file mode 100644 index 000000000000..1294723753fc --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadataWithRewriteFrames/subject.js @@ -0,0 +1,22 @@ +var _sentryModuleMetadataGlobal = + typeof window !== 'undefined' + ? window + : typeof global !== 'undefined' + ? global + : typeof self !== 'undefined' + ? self + : {}; + +_sentryModuleMetadataGlobal._sentryModuleMetadata = _sentryModuleMetadataGlobal._sentryModuleMetadata || {}; + +_sentryModuleMetadataGlobal._sentryModuleMetadata[new Error().stack] = Object.assign( + {}, + _sentryModuleMetadataGlobal._sentryModuleMetadata[new Error().stack], + { + foo: 'baz', + }, +); + +setTimeout(() => { + throw new Error('I am a module metadata Error'); +}, 0); diff --git a/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadataWithRewriteFrames/test.ts b/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadataWithRewriteFrames/test.ts new file mode 100644 index 000000000000..6996ef1ded6d --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadataWithRewriteFrames/test.ts @@ -0,0 +1,20 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; + +sentryTest( + 'should provide module_metadata on stack frames in beforeSend even though an event processor (rewriteFramesIntegration) modified the filename', + async ({ getLocalTestPath, page }) => { + // moduleMetadataIntegration is not included in any CDN bundles + if (process.env.PW_BUNDLE?.startsWith('bundle')) { + sentryTest.skip(); + } + + const url = await getLocalTestPath({ testDir: __dirname }); + + const errorEvent = await getFirstSentryEnvelopeRequest(page, url); + expect(errorEvent?.extra?.['module_metadata_entries']).toEqual([{ foo: 'baz' }]); + }, +); diff --git a/dev-packages/bundle-analyzer-scenarios/package.json b/dev-packages/bundle-analyzer-scenarios/package.json index 861664c14289..1f43a05dc920 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.14.0", + "version": "8.16.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/e2e-tests/package.json b/dev-packages/e2e-tests/package.json index 88195e9e9341..f7963fb099e3 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.14.0", + "version": "8.16.0", "license": "MIT", "private": true, "scripts": { diff --git a/dev-packages/e2e-tests/test-applications/create-next-app/package.json b/dev-packages/e2e-tests/test-applications/create-next-app/package.json index 2c0945051ee5..9c240942b3b7 100644 --- a/dev-packages/e2e-tests/test-applications/create-next-app/package.json +++ b/dev-packages/e2e-tests/test-applications/create-next-app/package.json @@ -12,7 +12,6 @@ "test:assert": "pnpm test:prod && pnpm test:dev" }, "dependencies": { - "@next/font": "13.0.7", "@sentry/nextjs": "latest || *", "@types/node": "18.11.17", "@types/react": "18.0.26", diff --git a/dev-packages/e2e-tests/test-applications/nestjs/package.json b/dev-packages/e2e-tests/test-applications/nestjs/package.json index 6ad2576fc3cc..94c4e445bfe0 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs/package.json +++ b/dev-packages/e2e-tests/test-applications/nestjs/package.json @@ -17,6 +17,7 @@ "dependencies": { "@nestjs/common": "^10.0.0", "@nestjs/core": "^10.0.0", + "@nestjs/schedule": "^4.1.0", "@nestjs/platform-express": "^10.0.0", "@sentry/nestjs": "latest || *", "@sentry/types": "latest || *", diff --git a/dev-packages/e2e-tests/test-applications/nestjs/src/app.controller.ts b/dev-packages/e2e-tests/test-applications/nestjs/src/app.controller.ts index 154f62ada912..7fda9eef768e 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs/src/app.controller.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs/src/app.controller.ts @@ -69,6 +69,21 @@ export class AppController1 { async testOutgoingHttpExternalDisallowed() { return this.appService.testOutgoingHttpExternalDisallowed(); } + + @Get('test-span-decorator-async') + async testSpanDecoratorAsync() { + return { result: await this.appService.testSpanDecoratorAsync() }; + } + + @Get('test-span-decorator-sync') + async testSpanDecoratorSync() { + return { result: await this.appService.testSpanDecoratorSync() }; + } + + @Get('kill-test-cron') + async killTestCron() { + this.appService.killTestCron(); + } } @Controller() diff --git a/dev-packages/e2e-tests/test-applications/nestjs/src/app.module.ts b/dev-packages/e2e-tests/test-applications/nestjs/src/app.module.ts index 5fda2f1e209f..b4f9d5588dda 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs/src/app.module.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs/src/app.module.ts @@ -1,9 +1,10 @@ import { Module } from '@nestjs/common'; +import { ScheduleModule } from '@nestjs/schedule'; import { AppController1, AppController2 } from './app.controller'; import { AppService1, AppService2 } from './app.service'; @Module({ - imports: [], + imports: [ScheduleModule.forRoot()], controllers: [AppController1], providers: [AppService1], }) diff --git a/dev-packages/e2e-tests/test-applications/nestjs/src/app.service.ts b/dev-packages/e2e-tests/test-applications/nestjs/src/app.service.ts index 1103c65941a1..b6fd70769e1f 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs/src/app.service.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs/src/app.service.ts @@ -1,9 +1,21 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { Cron, SchedulerRegistry } from '@nestjs/schedule'; import * as Sentry from '@sentry/nestjs'; +import { SentryCron, SentryTraced } from '@sentry/nestjs'; +import type { MonitorConfig } from '@sentry/types'; import { makeHttpRequest } from './utils'; +const monitorConfig: MonitorConfig = { + schedule: { + type: 'crontab', + value: '* * * * *', + }, +}; + @Injectable() export class AppService1 { + constructor(private schedulerRegistry: SchedulerRegistry) {} + testSuccess() { return { version: 'v1' }; } @@ -75,6 +87,40 @@ export class AppService1 { async testOutgoingHttpExternalDisallowed() { return makeHttpRequest('http://localhost:3040/external-disallowed'); } + + @SentryTraced('wait and return a string') + async wait() { + await new Promise(resolve => setTimeout(resolve, 500)); + return 'test'; + } + + async testSpanDecoratorAsync() { + return await this.wait(); + } + + @SentryTraced('return a string') + getString(): string { + return 'test'; + } + + async testSpanDecoratorSync() { + return this.getString(); + } + + /* + Actual cron schedule differs from schedule defined in config because Sentry + only supports minute granularity, but we don't want to wait (worst case) a + full minute for the tests to finish. + */ + @Cron('*/5 * * * * *', { name: 'test-cron-job' }) + @SentryCron('test-cron-slug', monitorConfig) + async testCron() { + console.log('Test cron!'); + } + + async killTestCron() { + this.schedulerRegistry.deleteCronJob('test-cron-job'); + } } @Injectable() diff --git a/dev-packages/e2e-tests/test-applications/nestjs/tests/cron-decorator.test.ts b/dev-packages/e2e-tests/test-applications/nestjs/tests/cron-decorator.test.ts new file mode 100644 index 000000000000..c13623337343 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs/tests/cron-decorator.test.ts @@ -0,0 +1,34 @@ +import { expect, test } from '@playwright/test'; +import { waitForEnvelopeItem } from '@sentry-internal/test-utils'; + +test('Cron job triggers send of in_progress envelope', async ({ baseURL }) => { + const inProgressEnvelopePromise = waitForEnvelopeItem('nestjs', envelope => { + return envelope[0].type === 'check_in'; + }); + + const inProgressEnvelope = await inProgressEnvelopePromise; + + expect(inProgressEnvelope[1]).toEqual( + expect.objectContaining({ + check_in_id: expect.any(String), + monitor_slug: 'test-cron-slug', + status: 'in_progress', + environment: 'qa', + monitor_config: { + schedule: { + type: 'crontab', + value: '* * * * *', + }, + }, + contexts: { + trace: { + span_id: expect.any(String), + trace_id: expect.any(String), + }, + }, + }), + ); + + // kill cron so tests don't get stuck + await fetch(`${baseURL}/kill-test-cron`); +}); diff --git a/dev-packages/e2e-tests/test-applications/nestjs/tests/span-decorator.test.ts b/dev-packages/e2e-tests/test-applications/nestjs/tests/span-decorator.test.ts new file mode 100644 index 000000000000..3efdfb979d73 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs/tests/span-decorator.test.ts @@ -0,0 +1,74 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('Transaction includes span and correct value for decorated async function', async ({ baseURL }) => { + const transactionEventPromise = waitForTransaction('nestjs', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /test-span-decorator-async' + ); + }); + + const response = await fetch(`${baseURL}/test-span-decorator-async`); + const body = await response.json(); + + expect(body.result).toEqual('test'); + + const transactionEvent = await transactionEventPromise; + + expect(transactionEvent.spans).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + span_id: expect.any(String), + trace_id: expect.any(String), + data: { + 'sentry.origin': 'manual', + 'sentry.op': 'wait and return a string', + 'otel.kind': 'INTERNAL', + }, + description: 'wait', + parent_span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + op: 'wait and return a string', + origin: 'manual', + }), + ]), + ); +}); + +test('Transaction includes span and correct value for decorated sync function', async ({ baseURL }) => { + const transactionEventPromise = waitForTransaction('nestjs', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /test-span-decorator-sync' + ); + }); + + const response = await fetch(`${baseURL}/test-span-decorator-sync`); + const body = await response.json(); + + expect(body.result).toEqual('test'); + + const transactionEvent = await transactionEventPromise; + + expect(transactionEvent.spans).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + span_id: expect.any(String), + trace_id: expect.any(String), + data: { + 'sentry.origin': 'manual', + 'sentry.op': 'return a string', + 'otel.kind': 'INTERNAL', + }, + description: 'getString', + parent_span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + op: 'return a string', + origin: 'manual', + }), + ]), + ); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/app/propagation/test-outgoing-fetch-external-disallowed/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-14/app/propagation/test-outgoing-fetch-external-disallowed/route.ts index b57d873f3ce7..be38866a9e94 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-14/app/propagation/test-outgoing-fetch-external-disallowed/route.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-14/app/propagation/test-outgoing-fetch-external-disallowed/route.ts @@ -3,8 +3,8 @@ import { NextResponse } from 'next/server'; export const dynamic = 'force-dynamic'; export async function GET() { - const data = await fetch(`http://localhost:3030/propagation/test-outgoing-fetch-external-disallowed/check`).then( - res => res.json(), - ); + const data = await fetch(`http://localhost:3030/propagation/test-outgoing-fetch-external-disallowed/check`, { + cache: 'no-store', + }).then(res => res.json()); return NextResponse.json(data); } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/app/propagation/test-outgoing-fetch/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-14/app/propagation/test-outgoing-fetch/route.ts index df9f2e772931..4ee7fac97f83 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-14/app/propagation/test-outgoing-fetch/route.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-14/app/propagation/test-outgoing-fetch/route.ts @@ -3,6 +3,8 @@ import { NextResponse } from 'next/server'; export const dynamic = 'force-dynamic'; export async function GET() { - const data = await fetch(`http://localhost:3030/propagation/test-outgoing-fetch/check`).then(res => res.json()); + const data = await fetch(`http://localhost:3030/propagation/test-outgoing-fetch/check`, { cache: 'no-store' }).then( + res => res.json(), + ); return NextResponse.json(data); } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/playwright.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-14/playwright.config.ts index c675d003853a..8448829443d6 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-14/playwright.config.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-14/playwright.config.ts @@ -5,9 +5,15 @@ if (!testEnv) { throw new Error('No test env defined'); } -const config = getPlaywrightConfig({ - startCommand: testEnv === 'development' ? 'pnpm next dev -p 3030' : 'pnpm next start -p 3030', - port: 3030, -}); +const config = getPlaywrightConfig( + { + startCommand: testEnv === 'development' ? 'pnpm next dev -p 3030' : 'pnpm next start -p 3030', + port: 3030, + }, + { + // This comes with the risk of tests leaking into each other but the tests run quite slow so we should parallelize + workers: '100%', + }, +); export default config; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/tests/generation-functions.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-14/tests/generation-functions.test.ts index 7739e9ac17de..303582ec1b24 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-14/tests/generation-functions.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-14/tests/generation-functions.test.ts @@ -1,19 +1,29 @@ import { expect, test } from '@playwright/test'; import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; -test('Should send a transaction event for a generateMetadata() function invokation', async ({ page }) => { - const testTitle = 'foobarasdf'; +test('Should emit a span for a generateMetadata() function invokation', async ({ page }) => { + const testTitle = 'should-emit-span'; const transactionPromise = waitForTransaction('nextjs-14', async transactionEvent => { return ( - transactionEvent?.transaction === 'Page.generateMetadata (/generation-functions)' && - (transactionEvent.extra?.route_data as any)?.searchParams?.metadataTitle === testTitle + transactionEvent.contexts?.trace?.data?.['http.target'] === `/generation-functions?metadataTitle=${testTitle}` ); }); await page.goto(`/generation-functions?metadataTitle=${testTitle}`); - expect(await transactionPromise).toBeDefined(); + const transaction = await transactionPromise; + + expect(transaction.spans).toContainEqual( + expect.objectContaining({ + description: 'generateMetadata /generation-functions/page', + origin: 'manual', + parent_span_id: expect.any(String), + span_id: expect.any(String), + status: 'ok', + trace_id: expect.any(String), + }), + ); const pageTitle = await page.title(); expect(pageTitle).toBe(testTitle); @@ -22,12 +32,12 @@ test('Should send a transaction event for a generateMetadata() function invokati test('Should send a transaction and an error event for a faulty generateMetadata() function invokation', async ({ page, }) => { - const testTitle = 'foobarbaz'; + const testTitle = 'should-emit-error'; const transactionPromise = waitForTransaction('nextjs-14', async transactionEvent => { return ( - transactionEvent.transaction === 'Page.generateMetadata (/generation-functions)' && - (transactionEvent.extra?.route_data as any)?.searchParams?.metadataTitle === testTitle + transactionEvent.contexts?.trace?.data?.['http.target'] === + `/generation-functions?metadataTitle=${testTitle}&shouldThrowInGenerateMetadata=1` ); }); @@ -54,14 +64,23 @@ test('Should send a transaction event for a generateViewport() function invokati const transactionPromise = waitForTransaction('nextjs-14', async transactionEvent => { return ( - transactionEvent?.transaction === 'Page.generateViewport (/generation-functions)' && - (transactionEvent.extra?.route_data as any)?.searchParams?.viewportThemeColor === testTitle + transactionEvent.contexts?.trace?.data?.['http.target'] === + `/generation-functions?viewportThemeColor=${testTitle}` ); }); await page.goto(`/generation-functions?viewportThemeColor=${testTitle}`); - expect(await transactionPromise).toBeDefined(); + expect((await transactionPromise).spans).toContainEqual( + expect.objectContaining({ + description: 'generateViewport /generation-functions/page', + origin: 'manual', + parent_span_id: expect.any(String), + span_id: expect.any(String), + status: 'ok', + trace_id: expect.any(String), + }), + ); }); test('Should send a transaction and an error event for a faulty generateViewport() function invokation', async ({ @@ -71,8 +90,8 @@ test('Should send a transaction and an error event for a faulty generateViewport const transactionPromise = waitForTransaction('nextjs-14', async transactionEvent => { return ( - transactionEvent?.transaction === 'Page.generateViewport (/generation-functions)' && - (transactionEvent.extra?.route_data as any)?.searchParams?.viewportThemeColor === testTitle + transactionEvent.contexts?.trace?.data?.['http.target'] === + `/generation-functions?viewportThemeColor=${testTitle}&shouldThrowInGenerateViewport=1` ); }); @@ -97,8 +116,8 @@ test('Should send a transaction event with correct status for a generateMetadata const transactionPromise = waitForTransaction('nextjs-14', async transactionEvent => { return ( - transactionEvent?.transaction === 'Page.generateMetadata (/generation-functions/with-redirect)' && - (transactionEvent.extra?.route_data as any)?.searchParams?.metadataTitle === testTitle + transactionEvent.contexts?.trace?.data?.['http.target'] === + `/generation-functions/with-redirect?metadataTitle=${testTitle}` ); }); @@ -114,8 +133,8 @@ test('Should send a transaction event with correct status for a generateMetadata const transactionPromise = waitForTransaction('nextjs-14', async transactionEvent => { return ( - transactionEvent?.transaction === 'Page.generateMetadata (/generation-functions/with-notfound)' && - (transactionEvent.extra?.route_data as any)?.searchParams?.metadataTitle === testTitle + transactionEvent.contexts?.trace?.data?.['http.target'] === + `/generation-functions/with-notfound?metadataTitle=${testTitle}` ); }); 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 be6bfab11b84..061c9d3cc5d6 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 @@ -3,7 +3,7 @@ import { waitForTransaction } from '@sentry-internal/test-utils'; test('Should send a transaction with a fetch span', async ({ page }) => { const transactionPromise = waitForTransaction('nextjs-14', async transactionEvent => { - return transactionEvent?.transaction === 'Page Server Component (/request-instrumentation)'; + return transactionEvent?.transaction === 'GET /request-instrumentation'; }); await page.goto(`/request-instrumentation`); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/app/pageload-tracing/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-15/app/pageload-tracing/page.tsx index 4d2763b992b5..41552d578fd4 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15/app/pageload-tracing/page.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/app/pageload-tracing/page.tsx @@ -6,7 +6,7 @@ export default async function Page() { } export async function generateMetadata() { - (await fetch('http://example.com/')).text(); + (await fetch('http://example.com/', { cache: 'no-store' })).text(); return { title: 'my title', diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/nextjs-15/playwright.config.mjs index c675d003853a..8448829443d6 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15/playwright.config.mjs +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/playwright.config.mjs @@ -5,9 +5,15 @@ if (!testEnv) { throw new Error('No test env defined'); } -const config = getPlaywrightConfig({ - startCommand: testEnv === 'development' ? 'pnpm next dev -p 3030' : 'pnpm next start -p 3030', - port: 3030, -}); +const config = getPlaywrightConfig( + { + startCommand: testEnv === 'development' ? 'pnpm next dev -p 3030' : 'pnpm next start -p 3030', + port: 3030, + }, + { + // This comes with the risk of tests leaking into each other but the tests run quite slow so we should parallelize + workers: '100%', + }, +); export default config; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/tests/pageload-tracing.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/pageload-tracing.test.ts index 38325fa6a0e0..3e41c04e2644 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15/tests/pageload-tracing.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/pageload-tracing.test.ts @@ -1,17 +1,9 @@ import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; -test('all server component transactions should be attached to the pageload request span', async ({ page }) => { - const pageServerComponentTransactionPromise = waitForTransaction('nextjs-15', async transactionEvent => { - return transactionEvent?.transaction === 'Page Server Component (/pageload-tracing)'; - }); - - const layoutServerComponentTransactionPromise = waitForTransaction('nextjs-15', async transactionEvent => { - return transactionEvent?.transaction === 'Layout Server Component (/pageload-tracing)'; - }); - - const metadataTransactionPromise = waitForTransaction('nextjs-15', async transactionEvent => { - return transactionEvent?.transaction === 'Page.generateMetadata (/pageload-tracing)'; +test('App router transactions should be attached to the pageload request span', async ({ page }) => { + const serverTransactionPromise = waitForTransaction('nextjs-15', async transactionEvent => { + return transactionEvent?.transaction === 'GET /pageload-tracing'; }); const pageloadTransactionPromise = waitForTransaction('nextjs-15', async transactionEvent => { @@ -20,18 +12,13 @@ test('all server component transactions should be attached to the pageload reque await page.goto(`/pageload-tracing`); - const [pageServerComponentTransaction, layoutServerComponentTransaction, metadataTransaction, pageloadTransaction] = - await Promise.all([ - pageServerComponentTransactionPromise, - layoutServerComponentTransactionPromise, - metadataTransactionPromise, - pageloadTransactionPromise, - ]); + const [serverTransaction, pageloadTransaction] = await Promise.all([ + serverTransactionPromise, + pageloadTransactionPromise, + ]); const pageloadTraceId = pageloadTransaction.contexts?.trace?.trace_id; expect(pageloadTraceId).toBeTruthy(); - expect(pageServerComponentTransaction.contexts?.trace?.trace_id).toBe(pageloadTraceId); - expect(layoutServerComponentTransaction.contexts?.trace?.trace_id).toBe(pageloadTraceId); - expect(metadataTransaction.contexts?.trace?.trace_id).toBe(pageloadTraceId); + expect(serverTransaction.contexts?.trace?.trace_id).toBe(pageloadTraceId); }); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/tests/ppr-error.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/ppr-error.test.ts index 6fc1a6716127..7c7c0b91eed2 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15/tests/ppr-error.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/ppr-error.test.ts @@ -3,12 +3,12 @@ import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; test('should not capture React-internal errors for PPR rendering', async ({ page }) => { const pageServerComponentTransactionPromise = waitForTransaction('nextjs-15', async transactionEvent => { - return transactionEvent?.transaction === 'Page Server Component (/ppr-error/[param])'; + return transactionEvent?.transaction === 'GET /ppr-error/[param]'; }); let errorEventReceived = false; - waitForError('nextjs-15', async transactionEvent => { - return transactionEvent?.transaction === 'Page Server Component (/ppr-error/[param])'; + waitForError('nextjs-15', async errorEvent => { + return errorEvent?.transaction === 'Page Server Component (/ppr-error/[param])'; }).then(() => { errorEventReceived = true; }); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/tests/suspense-error.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/suspense-error.test.ts index 6c9a58dab4f3..e158e87ae19f 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15/tests/suspense-error.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/suspense-error.test.ts @@ -3,18 +3,19 @@ import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; test('should not capture serverside suspense errors', async ({ page }) => { const pageServerComponentTransactionPromise = waitForTransaction('nextjs-15', async transactionEvent => { - return transactionEvent?.transaction === 'Page Server Component (/suspense-error)'; + return transactionEvent?.transaction === 'GET /suspense-error'; }); let errorEvent; - waitForError('nextjs-15', async transactionEvent => { - return transactionEvent?.transaction === 'Page Server Component (/suspense-error)'; + waitForError('nextjs-15', async errorEvent => { + return errorEvent?.transaction === 'Page Server Component (/suspense-error)'; }).then(event => { errorEvent = event; }); await page.goto(`/suspense-error`); + // Just to be a little bit more sure await page.waitForTimeout(5000); const pageServerComponentTransaction = await pageServerComponentTransactionPromise; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/package.json b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/package.json index 4e47e84efc8b..8ccad25e6ab4 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/package.json @@ -15,7 +15,6 @@ "test:assert": "pnpm test:test-build && pnpm test:prod && pnpm test:dev" }, "dependencies": { - "@next/font": "13.0.7", "@sentry/nextjs": "latest || *", "@types/node": "18.11.17", "@types/react": "18.0.26", diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/playwright.config.mjs index c675d003853a..8448829443d6 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/playwright.config.mjs +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/playwright.config.mjs @@ -5,9 +5,15 @@ if (!testEnv) { throw new Error('No test env defined'); } -const config = getPlaywrightConfig({ - startCommand: testEnv === 'development' ? 'pnpm next dev -p 3030' : 'pnpm next start -p 3030', - port: 3030, -}); +const config = getPlaywrightConfig( + { + startCommand: testEnv === 'development' ? 'pnpm next dev -p 3030' : 'pnpm next start -p 3030', + port: 3030, + }, + { + // This comes with the risk of tests leaking into each other but the tests run quite slow so we should parallelize + workers: '100%', + }, +); export default config; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/client-app-routing-instrumentation.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/client-app-routing-instrumentation.test.ts index 41e63f910f79..8645d36c4c8a 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/client-app-routing-instrumentation.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/client-app-routing-instrumentation.test.ts @@ -28,7 +28,7 @@ test('Creates a navigation transaction for app router routes', async ({ page }) await page.goto(`/server-component/parameter/${randomRoute}`); await clientPageloadTransactionPromise; - await page.getByText('Page (/server-component/parameter/[parameter])').isVisible(); + await page.getByText('Page (/server-component/[parameter])').isVisible(); const clientNavigationTransactionPromise = waitForTransaction('nextjs-app-dir', transactionEvent => { return ( @@ -39,7 +39,7 @@ test('Creates a navigation transaction for app router routes', async ({ page }) const serverComponentTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => { return ( - transactionEvent?.transaction === 'Page Server Component (/server-component/parameter/[...parameters])' && + transactionEvent?.transaction === 'GET /server-component/parameter/foo/bar/baz' && (await clientNavigationTransactionPromise).contexts?.trace?.trace_id === transactionEvent.contexts?.trace?.trace_id ); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/client-errors.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/client-errors.test.ts index b45f61e274a2..d1ea09ac2c76 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/client-errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/client-errors.test.ts @@ -1,7 +1,13 @@ import { expect, test } from '@playwright/test'; import { waitForError } from '@sentry-internal/test-utils'; +const packageJson = require('../package.json'); + test('Sends a client-side exception to Sentry', async ({ page }) => { + const nextjsVersion = packageJson.dependencies.next; + const nextjsMajor = Number(nextjsVersion.split('.')[0]); + const isDevMode = process.env.TEST_ENV === 'development'; + await page.goto('/'); const errorEventPromise = waitForError('nextjs-app-dir', errorEvent => { @@ -23,6 +29,9 @@ test('Sends a client-side exception to Sentry', async ({ page }) => { expect(errorEvent.transaction).toEqual('/'); expect(errorEvent.contexts?.trace).toEqual({ + // Next.js >= 15 propagates a trace ID to the client via a meta tag. Also, only dev mode emits a meta tag because + // the requested page is static and only in dev mode SSR is kicked off. + parent_span_id: nextjsMajor >= 15 && isDevMode ? expect.any(String) : undefined, trace_id: expect.any(String), span_id: expect.any(String), }); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/connected-servercomponent-trace.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/connected-servercomponent-trace.test.ts index 287c1e8f8633..3d2f29358d54 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/connected-servercomponent-trace.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/connected-servercomponent-trace.test.ts @@ -1,50 +1,21 @@ import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; -test('Will capture a connected trace for all server components and generation functions when visiting a page', async ({ +test('Will create a transaction with spans for every server component and metadata generation functions when visiting a page', async ({ page, }) => { - const someConnectedEvent = waitForTransaction('nextjs-app-dir', async transactionEvent => { - return ( - transactionEvent?.transaction === 'Layout Server Component (/(nested-layout)/nested-layout)' || - transactionEvent?.transaction === 'Layout Server Component (/(nested-layout))' || - transactionEvent?.transaction === 'Page Server Component (/(nested-layout)/nested-layout)' || - transactionEvent?.transaction === 'Page.generateMetadata (/(nested-layout)/nested-layout)' - ); + const serverTransactionEventPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => { + return transactionEvent?.transaction === 'GET /nested-layout'; }); - const layout1Transaction = waitForTransaction('nextjs-app-dir', async transactionEvent => { - return ( - transactionEvent?.transaction === 'Layout Server Component (/(nested-layout)/nested-layout)' && - (await someConnectedEvent).contexts?.trace?.trace_id === transactionEvent.contexts?.trace?.trace_id - ); - }); - - const layout2Transaction = waitForTransaction('nextjs-app-dir', async transactionEvent => { - return ( - transactionEvent?.transaction === 'Layout Server Component (/(nested-layout))' && - (await someConnectedEvent).contexts?.trace?.trace_id === transactionEvent.contexts?.trace?.trace_id - ); - }); - - const pageTransaction = waitForTransaction('nextjs-app-dir', async transactionEvent => { - return ( - transactionEvent?.transaction === 'Page Server Component (/(nested-layout)/nested-layout)' && - (await someConnectedEvent).contexts?.trace?.trace_id === transactionEvent.contexts?.trace?.trace_id - ); - }); + await page.goto('/nested-layout'); - const generateMetadataTransaction = waitForTransaction('nextjs-app-dir', async transactionEvent => { - return ( - transactionEvent?.transaction === 'Page.generateMetadata (/(nested-layout)/nested-layout)' && - (await someConnectedEvent).contexts?.trace?.trace_id === transactionEvent.contexts?.trace?.trace_id - ); + const spanDescriptions = (await serverTransactionEventPromise).spans?.map(span => { + return span.description; }); - await page.goto('/nested-layout'); - - expect(await layout1Transaction).toBeDefined(); - expect(await layout2Transaction).toBeDefined(); - expect(await pageTransaction).toBeDefined(); - expect(await generateMetadataTransaction).toBeDefined(); + expect(spanDescriptions).toContainEqual('Layout Server Component (/(nested-layout)/nested-layout)'); + expect(spanDescriptions).toContainEqual('Layout Server Component (/(nested-layout))'); + expect(spanDescriptions).toContainEqual('Page Server Component (/(nested-layout)/nested-layout)'); + expect(spanDescriptions).toContainEqual('Page.generateMetadata (/(nested-layout)/nested-layout)'); }); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/server-components.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/server-components.test.ts index 6f0413d0cc61..ba232ad558b0 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/server-components.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/server-components.test.ts @@ -1,15 +1,9 @@ import { expect, test } from '@playwright/test'; import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; -test('Sends a transaction for a server component', async ({ page }) => { - // TODO: Fix that this is flakey on dev server - might be an SDK bug - test.skip(process.env.TEST_ENV === 'production', 'Flakey on dev-server'); - +test('Sends a transaction for a request to app router', async ({ page }) => { const serverComponentTransactionPromise = waitForTransaction('nextjs-app-dir', transactionEvent => { - return ( - transactionEvent?.contexts?.trace?.op === 'function.nextjs' && - transactionEvent?.transaction === 'Page Server Component (/server-component/parameter/[...parameters])' - ); + return transactionEvent?.transaction === 'GET /server-component/parameter/[...parameters]'; }); await page.goto('/server-component/parameter/1337/42'); @@ -18,13 +12,19 @@ test('Sends a transaction for a server component', async ({ page }) => { expect(transactionEvent.contexts?.trace).toEqual({ data: expect.objectContaining({ - 'sentry.op': 'function.nextjs', - 'sentry.origin': 'auto.function.nextjs', + 'sentry.op': 'http.server', + 'sentry.origin': 'auto.http.otel.http', 'sentry.sample_rate': 1, - 'sentry.source': 'component', + 'sentry.source': 'route', + 'http.method': 'GET', + 'http.response.status_code': 200, + 'http.route': '/server-component/parameter/[...parameters]', + 'http.status_code': 200, + 'http.target': '/server-component/parameter/1337/42', + 'otel.kind': 'SERVER', }), - op: 'function.nextjs', - origin: 'auto.function.nextjs', + op: 'http.server', + origin: 'auto.http.otel.http', span_id: expect.any(String), status: 'ok', trace_id: expect.any(String), @@ -37,22 +37,23 @@ test('Sends a transaction for a server component', async ({ page }) => { headers: expect.any(Object), url: expect.any(String), }, - transaction: 'Page Server Component (/server-component/parameter/[...parameters])', - type: 'transaction', - transaction_info: { - source: 'component', - }, - spans: [], }), ); -}); -test('Should not set an error status on a server component transaction when it redirects', async ({ page }) => { - // TODO: Fix that this is flakey on dev server - might be an SDK bug - test.skip(process.env.TEST_ENV === 'production', 'Flakey on dev-server'); + expect(Object.keys(transactionEvent.request?.headers!).length).toBeGreaterThan(0); + + // The transaction should not contain any spans with the same name as the transaction + // e.g. "GET /server-component/parameter/[...parameters]" + expect( + transactionEvent.spans?.filter(span => { + return span.description === transactionEvent.transaction; + }), + ).toHaveLength(0); +}); +test('Should not set an error status on an app router transaction when it redirects', async ({ page }) => { const serverComponentTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => { - return transactionEvent?.transaction === 'Page Server Component (/server-component/redirect)'; + return transactionEvent?.transaction === 'GET /server-component/redirect'; }); await page.goto('/server-component/redirect'); @@ -62,26 +63,31 @@ test('Should not set an error status on a server component transaction when it r expect(transactionEvent.contexts?.trace?.status).not.toBe('internal_error'); }); -test('Should set a "not_found" status on a server component transaction when notFound() is called', async ({ +test('Should set a "not_found" status on a server component span when notFound() is called and the request span should have status ok', async ({ page, }) => { - // TODO: Fix that this is flakey on dev server - might be an SDK bug - test.skip(process.env.TEST_ENV === 'production', 'Flakey on dev-server'); - const serverComponentTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => { - return transactionEvent?.transaction === 'Page Server Component (/server-component/not-found)'; + return transactionEvent?.transaction === 'GET /server-component/not-found'; }); await page.goto('/server-component/not-found'); const transactionEvent = await serverComponentTransactionPromise; - expect(transactionEvent.contexts?.trace?.status).toBe('not_found'); + // Transaction should have status ok, because the http status is ok, but the server component span should be not_found + expect(transactionEvent.contexts?.trace?.status).toBe('ok'); + expect(transactionEvent.spans).toContainEqual( + expect.objectContaining({ + description: 'Page Server Component (/server-component/not-found)', + op: 'function.nextjs', + status: 'not_found', + }), + ); }); -test('Should capture an error and transaction with correct status for a faulty server component', async ({ page }) => { +test('Should capture an error and transaction for a app router page', async ({ page }) => { const transactionEventPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => { - return transactionEvent?.transaction === 'Page Server Component (/server-component/faulty)'; + return transactionEvent?.transaction === 'GET /server-component/faulty'; }); const errorEventPromise = waitForError('nextjs-app-dir', errorEvent => { @@ -93,10 +99,19 @@ test('Should capture an error and transaction with correct status for a faulty s const transactionEvent = await transactionEventPromise; const errorEvent = await errorEventPromise; - expect(transactionEvent.contexts?.trace?.status).toBe('internal_error'); - + // Error event should have the right transaction name expect(errorEvent.transaction).toBe(`Page Server Component (/server-component/faulty)`); + // Transaction should have status ok, because the http status is ok, but the server component span should be internal_error + expect(transactionEvent.contexts?.trace?.status).toBe('ok'); + expect(transactionEvent.spans).toContainEqual( + expect.objectContaining({ + description: 'Page Server Component (/server-component/faulty)', + op: 'function.nextjs', + status: 'internal_error', + }), + ); + expect(errorEvent.tags?.['my-isolated-tag']).toBe(true); expect(errorEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined(); expect(transactionEvent.tags?.['my-isolated-tag']).toBe(true); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts index 94af99d6c386..42fee84295b8 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts @@ -4,6 +4,10 @@ import { waitForTransaction } from '@sentry-internal/test-utils'; const packageJson = require('../package.json'); test('Sends a pageload transaction', async ({ page }) => { + const nextjsVersion = packageJson.dependencies.next; + const nextjsMajor = Number(nextjsVersion.split('.')[0]); + const isDevMode = process.env.TEST_ENV === 'development'; + const pageloadTransactionEventPromise = waitForTransaction('nextjs-app-dir', transactionEvent => { return transactionEvent?.contexts?.trace?.op === 'pageload' && transactionEvent?.transaction === '/'; }); @@ -23,6 +27,9 @@ test('Sends a pageload transaction', async ({ page }) => { version: expect.any(String), }, trace: { + // Next.js >= 15 propagates a trace ID to the client via a meta tag. Also, only dev mode emits a meta tag because + // the requested page is static and only in dev mode SSR is kicked off. + parent_span_id: nextjsMajor >= 15 && isDevMode ? expect.any(String) : undefined, span_id: expect.any(String), trace_id: expect.any(String), op: 'pageload', diff --git a/dev-packages/e2e-tests/test-applications/react-17/.gitignore b/dev-packages/e2e-tests/test-applications/react-17/.gitignore new file mode 100644 index 000000000000..84634c973eeb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-17/.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-17/.npmrc b/dev-packages/e2e-tests/test-applications/react-17/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-17/.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-17/package.json b/dev-packages/e2e-tests/test-applications/react-17/package.json new file mode 100644 index 000000000000..db60c16938dc --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-17/package.json @@ -0,0 +1,52 @@ +{ + "name": "react-17", + "version": "0.1.0", + "private": true, + "dependencies": { + "@sentry/react": "latest || *", + "@types/react": "17.0.2", + "@types/react-dom": "17.0.2", + "react": "17.0.2", + "react-dom": "17.0.2", + "react-router-dom": "~6.3.0", + "react-scripts": "5.0.1", + "typescript": "4.9.5" + }, + "scripts": { + "build": "react-scripts build", + "dev": "react-scripts start", + "start": "serve -s build", + "test": "playwright test", + "clean": "npx rimraf node_modules pnpm-lock.yaml", + "test:build": "pnpm install && npx playwright install && pnpm build", + "test:build-ts3.8": "pnpm install && pnpm add typescript@3.8 && npx playwright install && pnpm build", + "test:build-canary": "pnpm install && pnpm add react@canary react-dom@canary && npx playwright install && 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-17/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/react-17/playwright.config.mjs new file mode 100644 index 000000000000..31f2b913b58b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-17/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-17/public/index.html b/dev-packages/e2e-tests/test-applications/react-17/public/index.html new file mode 100644 index 000000000000..39da76522bea --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-17/public/index.html @@ -0,0 +1,24 @@ + + + + + + + + React App + + + +
+ + + diff --git a/dev-packages/e2e-tests/test-applications/react-17/src/index.tsx b/dev-packages/e2e-tests/test-applications/react-17/src/index.tsx new file mode 100644 index 000000000000..49609a988202 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-17/src/index.tsx @@ -0,0 +1,58 @@ +import * as Sentry from '@sentry/react'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import { + BrowserRouter, + Route, + Routes, + 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', + + // Always capture replays, so we can test this properly + replaysSessionSampleRate: 1.0, + replaysOnErrorSampleRate: 0.0, + + tunnel: 'http://localhost:3031', +}); + +const SentryRoutes = Sentry.withSentryReactRouterV6Routing(Routes); + +function App() { + return ( + + + + } /> + } /> + + + + ); +} + +ReactDOM.render(, document.getElementById('root')); diff --git a/dev-packages/e2e-tests/test-applications/react-17/src/pages/Index.tsx b/dev-packages/e2e-tests/test-applications/react-17/src/pages/Index.tsx new file mode 100644 index 000000000000..c7aa909c3c6e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-17/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 to user + + + ); +}; + +export default Index; diff --git a/dev-packages/e2e-tests/test-applications/react-17/src/pages/User.tsx b/dev-packages/e2e-tests/test-applications/react-17/src/pages/User.tsx new file mode 100644 index 000000000000..62f0c2d17533 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-17/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-17/src/react-app-env.d.ts b/dev-packages/e2e-tests/test-applications/react-17/src/react-app-env.d.ts new file mode 100644 index 000000000000..6431bc5fc6b2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-17/src/react-app-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/dev-packages/e2e-tests/test-applications/react-17/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/react-17/start-event-proxy.mjs new file mode 100644 index 000000000000..6b825e527516 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-17/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'react-17', +}); diff --git a/dev-packages/e2e-tests/test-applications/react-17/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/react-17/tests/errors.test.ts new file mode 100644 index 000000000000..444e30fc0067 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-17/tests/errors.test.ts @@ -0,0 +1,59 @@ +import { expect, test } from '@playwright/test'; +import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; + +test('Sends correct error event', async ({ page }) => { + const errorEventPromise = waitForError('react-17', 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.any(String), + span_id: expect.any(String), + }); +}); + +test('Sets correct transactionName', async ({ page }) => { + const transactionPromise = waitForTransaction('react-17', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + const errorEventPromise = waitForError('react-17', event => { + return !event.type && event.exception?.values?.[0]?.value === 'I am an error!'; + }); + + await page.goto('/'); + const transactionEvent = await transactionPromise; + + // Only capture error once transaction was sent + 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.transaction).toEqual('/'); + + expect(errorEvent.contexts?.trace).toEqual({ + trace_id: transactionEvent.contexts?.trace?.trace_id, + span_id: expect.not.stringContaining(transactionEvent.contexts?.trace?.span_id || ''), + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/react-17/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/react-17/tests/transactions.test.ts new file mode 100644 index 000000000000..665b5c02aafe --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-17/tests/transactions.test.ts @@ -0,0 +1,98 @@ +import { expect, test } from '@playwright/test'; +import { waitForEnvelopeItem, waitForTransaction } from '@sentry-internal/test-utils'; + +test('sends a pageload transaction with a parameterized URL', async ({ page }) => { + const transactionPromise = waitForTransaction('react-17', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + await page.goto(`/`); + + const rootSpan = await transactionPromise; + + expect(rootSpan).toMatchObject({ + contexts: { + trace: { + op: 'pageload', + origin: 'auto.pageload.react.reactrouter_v6', + }, + }, + transaction: '/', + transaction_info: { + source: 'route', + }, + }); +}); + +test('sends a navigation transaction with a parameterized URL', async ({ page }) => { + page.on('console', msg => console.log(msg.text())); + const pageloadTxnPromise = waitForTransaction('react-17', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + const navigationTxnPromise = waitForTransaction('react-17', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/`); + await pageloadTxnPromise; + + const linkElement = page.locator('id=navigation'); + + const [_, navigationTxn] = await Promise.all([linkElement.click(), navigationTxnPromise]); + + expect(navigationTxn).toMatchObject({ + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.react.reactrouter_v6', + }, + }, + transaction: '/user/:id', + transaction_info: { + source: 'route', + }, + }); +}); + +test('sends an INP span', async ({ page }) => { + const inpSpanPromise = waitForEnvelopeItem('react-17', item => { + return item[0].type === 'span'; + }); + + await page.goto(`/`); + + await page.click('#exception-button'); + + await page.waitForTimeout(500); + + // Page hide to trigger INP + await page.evaluate(() => { + window.dispatchEvent(new Event('pagehide')); + }); + + const inpSpan = await inpSpanPromise; + + expect(inpSpan[1]).toEqual({ + data: { + 'sentry.origin': 'auto.http.browser.inp', + 'sentry.op': 'ui.interaction.click', + release: 'e2e-test', + environment: 'qa', + transaction: '/', + 'sentry.exclusive_time': expect.any(Number), + replay_id: expect.any(String), + }, + description: 'body > div#root > input#exception-button[type="button"]', + op: 'ui.interaction.click', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: expect.any(String), + origin: 'auto.http.browser.inp', + exclusive_time: expect.any(Number), + measurements: { inp: { unit: 'millisecond', value: expect.any(Number) } }, + segment_id: expect.any(String), + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/react-17/tsconfig.json b/dev-packages/e2e-tests/test-applications/react-17/tsconfig.json new file mode 100644 index 000000000000..76ffed0e7ed2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-17/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-jsx" + }, + "include": ["src", "tests"] +} diff --git a/dev-packages/e2e-tests/test-applications/solid/src/index.tsx b/dev-packages/e2e-tests/test-applications/solid/src/index.tsx index df121347daeb..882bb32d853a 100644 --- a/dev-packages/e2e-tests/test-applications/solid/src/index.tsx +++ b/dev-packages/e2e-tests/test-applications/solid/src/index.tsx @@ -5,9 +5,7 @@ import App from './app'; import './index.css'; Sentry.init({ - dsn: - import.meta.env.PUBLIC_E2E_TEST_DSN || - 'https://3b6c388182fb435097f41d181be2b2ba@o4504321058471936.ingest.sentry.io/4504321066008576', + dsn: import.meta.env.PUBLIC_E2E_TEST_DSN, debug: true, environment: 'qa', // dynamic sampling bias to keep transactions integrations: [Sentry.browserTracingIntegration()], diff --git a/dev-packages/external-contributor-gh-action/action.yml b/dev-packages/external-contributor-gh-action/action.yml index 8c6fb9c04944..913a87e79ea8 100644 --- a/dev-packages/external-contributor-gh-action/action.yml +++ b/dev-packages/external-contributor-gh-action/action.yml @@ -4,6 +4,9 @@ inputs: name: required: true description: 'The name of the external contributor' + author_association: + required: false + description: 'The association of the author' runs: using: 'node20' main: 'index.mjs' diff --git a/dev-packages/external-contributor-gh-action/package.json b/dev-packages/external-contributor-gh-action/package.json index 8b092abfdd20..67f9ab9259d7 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.14.0", + "version": "8.16.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 6bb7db47474e..3c5869491951 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.14.0", + "version": "8.16.0", "license": "MIT", "engines": { "node": ">=14.18" @@ -31,8 +31,8 @@ "@nestjs/core": "^10.3.3", "@nestjs/platform-express": "^10.3.3", "@prisma/client": "5.9.1", - "@sentry/node": "8.14.0", - "@sentry/types": "8.14.0", + "@sentry/node": "8.16.0", + "@sentry/types": "8.16.0", "@types/mongodb": "^3.6.20", "@types/mysql": "^2.15.21", "@types/pg": "^8.6.5", diff --git a/dev-packages/node-integration-tests/suites/anr/test.ts b/dev-packages/node-integration-tests/suites/anr/test.ts index fcd9f121ed34..c3cb935532b1 100644 --- a/dev-packages/node-integration-tests/suites/anr/test.ts +++ b/dev-packages/node-integration-tests/suites/anr/test.ts @@ -110,6 +110,7 @@ conditionalTest({ min: 16 })('should report ANR when event loop blocked', () => test('With session', done => { createRunner(__dirname, 'basic-session.js') .withMockSentryServer() + .unignore('session') .expect({ session: { status: 'abnormal', diff --git a/dev-packages/node-integration-tests/suites/esm/warn-esm/test.ts b/dev-packages/node-integration-tests/suites/esm/warn-esm/test.ts index e52f8b915f86..25f90460e2b9 100644 --- a/dev-packages/node-integration-tests/suites/esm/warn-esm/test.ts +++ b/dev-packages/node-integration-tests/suites/esm/warn-esm/test.ts @@ -13,7 +13,7 @@ test("warns if using ESM on Node.js versions that don't support `register()`", a return; } - const runner = createRunner(__dirname, 'server.mjs').ignore('session', 'sessions', 'event').start(); + const runner = createRunner(__dirname, 'server.mjs').ignore('event').start(); await runner.makeRequest('get', '/test/success'); @@ -26,7 +26,7 @@ test('does not warn if using ESM on Node.js versions that support `register()`', return; } - const runner = createRunner(__dirname, 'server.mjs').ignore('session', 'sessions', 'event').start(); + const runner = createRunner(__dirname, 'server.mjs').ignore('event').start(); await runner.makeRequest('get', '/test/success'); @@ -34,7 +34,7 @@ test('does not warn if using ESM on Node.js versions that support `register()`', }); test('does not warn if using CJS', async () => { - const runner = createRunner(__dirname, 'server.js').ignore('session', 'sessions', 'event').start(); + const runner = createRunner(__dirname, 'server.js').ignore('event').start(); await runner.makeRequest('get', '/test/success'); diff --git a/dev-packages/node-integration-tests/suites/express/handle-error-scope-data-loss/test.ts b/dev-packages/node-integration-tests/suites/express/handle-error-scope-data-loss/test.ts index f1bb9a1229b1..ecf69671b9f4 100644 --- a/dev-packages/node-integration-tests/suites/express/handle-error-scope-data-loss/test.ts +++ b/dev-packages/node-integration-tests/suites/express/handle-error-scope-data-loss/test.ts @@ -15,7 +15,6 @@ afterAll(() => { */ test('withScope scope is NOT applied to thrown error caught by global handler', done => { const runner = createRunner(__dirname, 'server.ts') - .ignore('session', 'sessions') .expect({ event: { exception: { @@ -53,7 +52,6 @@ test('withScope scope is NOT applied to thrown error caught by global handler', */ test('isolation scope is applied to thrown error caught by global handler', done => { const runner = createRunner(__dirname, 'server.ts') - .ignore('session', 'sessions') .expect({ event: { exception: { diff --git a/dev-packages/node-integration-tests/suites/express/handle-error-tracesSampleRate-0/test.ts b/dev-packages/node-integration-tests/suites/express/handle-error-tracesSampleRate-0/test.ts index f76edf06bedb..955d725ae0c5 100644 --- a/dev-packages/node-integration-tests/suites/express/handle-error-tracesSampleRate-0/test.ts +++ b/dev-packages/node-integration-tests/suites/express/handle-error-tracesSampleRate-0/test.ts @@ -6,7 +6,7 @@ afterAll(() => { test('should capture and send Express controller error with txn name if tracesSampleRate is 0', done => { const runner = createRunner(__dirname, 'server.ts') - .ignore('session', 'sessions', 'transaction') + .ignore('transaction') .expect({ event: { exception: { diff --git a/dev-packages/node-integration-tests/suites/express/handle-error-tracesSampleRate-unset/test.ts b/dev-packages/node-integration-tests/suites/express/handle-error-tracesSampleRate-unset/test.ts index c48a3b1f9444..cb43073fa994 100644 --- a/dev-packages/node-integration-tests/suites/express/handle-error-tracesSampleRate-unset/test.ts +++ b/dev-packages/node-integration-tests/suites/express/handle-error-tracesSampleRate-unset/test.ts @@ -6,7 +6,7 @@ afterAll(() => { test('should capture and send Express controller error if tracesSampleRate is not set.', done => { const runner = createRunner(__dirname, 'server.ts') - .ignore('session', 'sessions', 'transaction') + .ignore('transaction') .expect({ event: { exception: { diff --git a/dev-packages/node-integration-tests/suites/express/multiple-init/test.ts b/dev-packages/node-integration-tests/suites/express/multiple-init/test.ts index 76f630128aa5..f654fc361442 100644 --- a/dev-packages/node-integration-tests/suites/express/multiple-init/test.ts +++ b/dev-packages/node-integration-tests/suites/express/multiple-init/test.ts @@ -6,7 +6,6 @@ afterAll(() => { test('allows to call init multiple times', done => { const runner = createRunner(__dirname, 'server.ts') - .ignore('session', 'sessions') .expect({ event: { exception: { diff --git a/dev-packages/node-integration-tests/suites/express/multiple-routers/common-infix-parameterized/test.ts b/dev-packages/node-integration-tests/suites/express/multiple-routers/common-infix-parameterized/test.ts index f7dc3a74cf2d..e7b9edabbfc8 100644 --- a/dev-packages/node-integration-tests/suites/express/multiple-routers/common-infix-parameterized/test.ts +++ b/dev-packages/node-integration-tests/suites/express/multiple-routers/common-infix-parameterized/test.ts @@ -6,7 +6,7 @@ afterAll(() => { test('should construct correct url with common infixes with multiple parameterized routers.', done => { createRunner(__dirname, 'server.ts') - .ignore('transaction', 'session', 'sessions') + .ignore('transaction') .expect({ event: { message: 'Custom Message', transaction: 'GET /api/v1/user/:userId' } }) .start(done) .makeRequest('get', '/api/v1/user/3212'); diff --git a/dev-packages/node-integration-tests/suites/express/multiple-routers/common-infix/test.ts b/dev-packages/node-integration-tests/suites/express/multiple-routers/common-infix/test.ts index 2a38c4c8838a..52d6b631bea2 100644 --- a/dev-packages/node-integration-tests/suites/express/multiple-routers/common-infix/test.ts +++ b/dev-packages/node-integration-tests/suites/express/multiple-routers/common-infix/test.ts @@ -6,7 +6,7 @@ afterAll(() => { test('should construct correct url with common infixes with multiple routers.', done => { createRunner(__dirname, 'server.ts') - .ignore('transaction', 'session', 'sessions') + .ignore('transaction') .expect({ event: { message: 'Custom Message', transaction: 'GET /api2/v1/test' } }) .start(done) .makeRequest('get', '/api2/v1/test'); diff --git a/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix-parameterized-reverse/test.ts b/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix-parameterized-reverse/test.ts index d5a839d739b0..5fabe5b92df6 100644 --- a/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix-parameterized-reverse/test.ts +++ b/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix-parameterized-reverse/test.ts @@ -6,7 +6,7 @@ afterAll(() => { test('should construct correct urls with multiple parameterized routers (use order reversed).', done => { createRunner(__dirname, 'server.ts') - .ignore('transaction', 'session', 'sessions') + .ignore('transaction') .expect({ event: { message: 'Custom Message', transaction: 'GET /api/v1/user/:userId' } }) .start(done) .makeRequest('get', '/api/v1/user/1234/'); diff --git a/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix-parameterized/test.ts b/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix-parameterized/test.ts index fbe805e39bc1..bab934f54522 100644 --- a/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix-parameterized/test.ts +++ b/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix-parameterized/test.ts @@ -6,7 +6,7 @@ afterAll(() => { test('should construct correct urls with multiple parameterized routers.', done => { createRunner(__dirname, 'server.ts') - .ignore('transaction', 'session', 'sessions') + .ignore('transaction') .expect({ event: { message: 'Custom Message', transaction: 'GET /api/v1/user/:userId' } }) .start(done) .makeRequest('get', '/api/v1/user/1234/'); diff --git a/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix-same-length-parameterized copy/test.ts b/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix-same-length-parameterized copy/test.ts index e97ff2edeced..94d363f4faa4 100644 --- a/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix-same-length-parameterized copy/test.ts +++ b/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix-same-length-parameterized copy/test.ts @@ -6,7 +6,7 @@ afterAll(() => { test('should construct correct url with multiple parameterized routers of the same length (use order reversed).', done => { createRunner(__dirname, 'server.ts') - .ignore('transaction', 'session', 'sessions') + .ignore('transaction') .expect({ event: { message: 'Custom Message', transaction: 'GET /api/v1/:userId' } }) .start(done) .makeRequest('get', '/api/v1/1234/'); diff --git a/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix-same-length-parameterized/test.ts b/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix-same-length-parameterized/test.ts index e1bc0c9ffd3b..373b2c102c4c 100644 --- a/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix-same-length-parameterized/test.ts +++ b/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix-same-length-parameterized/test.ts @@ -6,7 +6,7 @@ afterAll(() => { test('should construct correct url with multiple parameterized routers of the same length.', done => { createRunner(__dirname, 'server.ts') - .ignore('transaction', 'session', 'sessions') + .ignore('transaction') .expect({ event: { message: 'Custom Message', transaction: 'GET /api/v1/:userId' } }) .start(done) .makeRequest('get', '/api/v1/1234/'); diff --git a/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix/test.ts b/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix/test.ts index 6ae3ddd6e0d5..ea217bf6bc05 100644 --- a/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix/test.ts +++ b/dev-packages/node-integration-tests/suites/express/multiple-routers/common-prefix/test.ts @@ -6,7 +6,7 @@ afterAll(() => { test('should construct correct urls with multiple routers.', done => { createRunner(__dirname, 'server.ts') - .ignore('transaction', 'session', 'sessions') + .ignore('transaction') .expect({ event: { message: 'Custom Message', transaction: 'GET /api/v1/test' } }) .start(done) .makeRequest('get', '/api/v1/test'); diff --git a/dev-packages/node-integration-tests/suites/express/multiple-routers/complex-router/test.ts b/dev-packages/node-integration-tests/suites/express/multiple-routers/complex-router/test.ts index a544095982c7..606120248e35 100644 --- a/dev-packages/node-integration-tests/suites/express/multiple-routers/complex-router/test.ts +++ b/dev-packages/node-integration-tests/suites/express/multiple-routers/complex-router/test.ts @@ -28,7 +28,7 @@ conditionalTest({ min: 16 })('complex-router', () => { }; createRunner(__dirname, 'server.ts') - .ignore('event', 'session', 'sessions') + .ignore('event') .expect({ transaction: EXPECTED_TRANSACTION as any }) .start(done) .makeRequest('get', '/api/api/v1/sub-router/users/123/posts/456'); @@ -54,7 +54,7 @@ conditionalTest({ min: 16 })('complex-router', () => { }; createRunner(__dirname, 'server.ts') - .ignore('event', 'session', 'sessions') + .ignore('event') .expect({ transaction: EXPECTED_TRANSACTION as any }) .start(done) .makeRequest('get', '/api/api/v1/sub-router/users/123/posts/456?param=1'); @@ -80,7 +80,7 @@ conditionalTest({ min: 16 })('complex-router', () => { }; createRunner(__dirname, 'server.ts') - .ignore('event', 'session', 'sessions') + .ignore('event') .expect({ transaction: EXPECTED_TRANSACTION as any }) .start(done) .makeRequest('get', '/api/api/v1/sub-router/users/123/posts/456/?param=1'); diff --git a/dev-packages/node-integration-tests/suites/express/multiple-routers/middle-layer-parameterized/test.ts b/dev-packages/node-integration-tests/suites/express/multiple-routers/middle-layer-parameterized/test.ts index f6548ed161b9..4a6ae304af14 100644 --- a/dev-packages/node-integration-tests/suites/express/multiple-routers/middle-layer-parameterized/test.ts +++ b/dev-packages/node-integration-tests/suites/express/multiple-routers/middle-layer-parameterized/test.ts @@ -27,7 +27,7 @@ conditionalTest({ min: 16 })('middle-layer-parameterized', () => { }; createRunner(__dirname, 'server.ts') - .ignore('event', 'session', 'sessions') + .ignore('event') .expect({ transaction: EXPECTED_TRANSACTION as any }) .start(done) .makeRequest('get', '/api/v1/users/123/posts/456'); diff --git a/dev-packages/node-integration-tests/suites/express/span-isolationScope/test.ts b/dev-packages/node-integration-tests/suites/express/span-isolationScope/test.ts index b6954e426a1d..2e2b6945526e 100644 --- a/dev-packages/node-integration-tests/suites/express/span-isolationScope/test.ts +++ b/dev-packages/node-integration-tests/suites/express/span-isolationScope/test.ts @@ -6,7 +6,6 @@ afterAll(() => { test('correctly applies isolation scope to span', done => { createRunner(__dirname, 'server.ts') - .ignore('session', 'sessions') .expect({ transaction: { transaction: 'GET /test/isolationScope', diff --git a/dev-packages/node-integration-tests/suites/express/tracing/test.ts b/dev-packages/node-integration-tests/suites/express/tracing/test.ts index c05849f443ce..44852233ed67 100644 --- a/dev-packages/node-integration-tests/suites/express/tracing/test.ts +++ b/dev-packages/node-integration-tests/suites/express/tracing/test.ts @@ -1,6 +1,6 @@ import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; -describe('express tracing experimental', () => { +describe('express tracing', () => { afterAll(() => { cleanupChildProcesses(); }); @@ -8,7 +8,6 @@ describe('express tracing experimental', () => { describe('CJS', () => { test('should create and send transactions for Express routes and spans for middlewares.', done => { createRunner(__dirname, 'server.js') - .ignore('session', 'sessions') .expect({ transaction: { contexts: { @@ -51,7 +50,6 @@ describe('express tracing experimental', () => { test('should set a correct transaction name for routes specified in RegEx', done => { createRunner(__dirname, 'server.js') - .ignore('session', 'sessions') .expect({ transaction: { transaction: 'GET /\\/test\\/regex/', @@ -80,7 +78,6 @@ describe('express tracing experimental', () => { 'should set a correct transaction name for routes consisting of arrays of routes for %p', ((segment: string, done: () => void) => { createRunner(__dirname, 'server.js') - .ignore('session', 'sessions') .expect({ transaction: { transaction: 'GET /test/array1,/\\/test\\/array[2-9]/', @@ -117,7 +114,6 @@ describe('express tracing experimental', () => { ['arr/requiredPath/optionalPath/lastParam'], ])('should handle more complex regexes in route arrays correctly for %p', ((segment: string, done: () => void) => { createRunner(__dirname, 'server.js') - .ignore('session', 'sessions') .expect({ transaction: { transaction: 'GET /test/arr/:id,/\\/test\\/arr[0-9]*\\/required(path)?(\\/optionalPath)?\\/(lastParam)?/', diff --git a/dev-packages/node-integration-tests/suites/express/tracing/withError/test.ts b/dev-packages/node-integration-tests/suites/express/tracing/withError/test.ts index f164bdd0caab..4dd004ad2239 100644 --- a/dev-packages/node-integration-tests/suites/express/tracing/withError/test.ts +++ b/dev-packages/node-integration-tests/suites/express/tracing/withError/test.ts @@ -8,7 +8,7 @@ describe('express tracing experimental', () => { describe('CJS', () => { test('should apply the scope transactionName to error events', done => { createRunner(__dirname, 'server.js') - .ignore('session', 'sessions', 'transaction') + .ignore('transaction') .expect({ event: { exception: { diff --git a/dev-packages/node-integration-tests/suites/express/without-tracing/test.ts b/dev-packages/node-integration-tests/suites/express/without-tracing/test.ts index 73d6a322ed28..236c978dcd9a 100644 --- a/dev-packages/node-integration-tests/suites/express/without-tracing/test.ts +++ b/dev-packages/node-integration-tests/suites/express/without-tracing/test.ts @@ -6,7 +6,6 @@ afterAll(() => { test('correctly applies isolation scope even without tracing', done => { const runner = createRunner(__dirname, 'server.ts') - .ignore('session', 'sessions') .expect({ event: { transaction: 'GET /test/isolationScope/1', diff --git a/dev-packages/node-integration-tests/suites/proxy/test.ts b/dev-packages/node-integration-tests/suites/proxy/test.ts index dc709f5251c6..cc2fc0b83404 100644 --- a/dev-packages/node-integration-tests/suites/proxy/test.ts +++ b/dev-packages/node-integration-tests/suites/proxy/test.ts @@ -7,7 +7,6 @@ afterAll(() => { test('proxies sentry requests', done => { createRunner(__dirname, 'basic.js') .withMockSentryServer() - .ignore('session') .expect({ event: { message: 'Hello, via proxy!', diff --git a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/test.ts b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/test.ts index c63fe2daf5df..7076388b9d29 100644 --- a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/test.ts @@ -41,7 +41,6 @@ conditionalTest({ min: 18 })('LocalVariables integration', () => { test('Should not include local variables by default', done => { createRunner(__dirname, 'no-local-variables.js') - .ignore('session') .expect({ event: event => { for (const frame of event.exception?.values?.[0]?.stacktrace?.frames || []) { @@ -53,10 +52,7 @@ conditionalTest({ min: 18 })('LocalVariables integration', () => { }); test('Should include local variables when enabled', done => { - createRunner(__dirname, 'local-variables.js') - .ignore('session') - .expect({ event: EXPECTED_LOCAL_VARIABLES_EVENT }) - .start(done); + createRunner(__dirname, 'local-variables.js').expect({ event: EXPECTED_LOCAL_VARIABLES_EVENT }).start(done); }); test('Should include local variables when instrumenting via --require', done => { @@ -64,29 +60,22 @@ conditionalTest({ min: 18 })('LocalVariables integration', () => { createRunner(__dirname, 'local-variables-no-sentry.js') .withFlags(`--require=${requirePath}`) - .ignore('session') .expect({ event: EXPECTED_LOCAL_VARIABLES_EVENT }) .start(done); }); test('Should include local variables with ESM', done => { - createRunner(__dirname, 'local-variables-caught.mjs') - .ignore('session') - .expect({ event: EXPECTED_LOCAL_VARIABLES_EVENT }) - .start(done); + createRunner(__dirname, 'local-variables-caught.mjs').expect({ event: EXPECTED_LOCAL_VARIABLES_EVENT }).start(done); }); conditionalTest({ min: 19 })('Node v19+', () => { test('Should not import inspector when not in use', done => { - createRunner(__dirname, 'deny-inspector.mjs').ensureNoErrorOutput().ignore('session').start(done); + createRunner(__dirname, 'deny-inspector.mjs').ensureNoErrorOutput().start(done); }); }); test('Includes local variables for caught exceptions when enabled', done => { - createRunner(__dirname, 'local-variables-caught.js') - .ignore('session') - .expect({ event: EXPECTED_LOCAL_VARIABLES_EVENT }) - .start(done); + createRunner(__dirname, 'local-variables-caught.js').expect({ event: EXPECTED_LOCAL_VARIABLES_EVENT }).start(done); }); test('Should not leak memory', done => { diff --git a/dev-packages/node-integration-tests/suites/sessions/crashed-session-aggregate/test.ts b/dev-packages/node-integration-tests/suites/sessions/crashed-session-aggregate/test.ts index a94d7c46dba0..6e8a86e627d9 100644 --- a/dev-packages/node-integration-tests/suites/sessions/crashed-session-aggregate/test.ts +++ b/dev-packages/node-integration-tests/suites/sessions/crashed-session-aggregate/test.ts @@ -11,7 +11,8 @@ test('should aggregate successful and crashed sessions', async () => { }); const runner = createRunner(__dirname, 'server.ts') - .ignore('transaction', 'event', 'session') + .ignore('transaction', 'event') + .unignore('sessions') .expectError() .expect({ sessions: { diff --git a/dev-packages/node-integration-tests/suites/sessions/errored-session-aggregate/test.ts b/dev-packages/node-integration-tests/suites/sessions/errored-session-aggregate/test.ts index da9733690507..383bfca96062 100644 --- a/dev-packages/node-integration-tests/suites/sessions/errored-session-aggregate/test.ts +++ b/dev-packages/node-integration-tests/suites/sessions/errored-session-aggregate/test.ts @@ -11,7 +11,8 @@ test('should aggregate successful, crashed and erroneous sessions', async () => }); const runner = createRunner(__dirname, 'server.ts') - .ignore('transaction', 'event', 'session') + .ignore('transaction', 'event') + .unignore('sessions') .expectError() .expect({ sessions: { diff --git a/dev-packages/node-integration-tests/suites/sessions/exited-session-aggregate/test.ts b/dev-packages/node-integration-tests/suites/sessions/exited-session-aggregate/test.ts index ba42536a3bbf..23b80109fa43 100644 --- a/dev-packages/node-integration-tests/suites/sessions/exited-session-aggregate/test.ts +++ b/dev-packages/node-integration-tests/suites/sessions/exited-session-aggregate/test.ts @@ -11,7 +11,8 @@ test('should aggregate successful sessions', async () => { }); const runner = createRunner(__dirname, 'server.ts') - .ignore('transaction', 'event', 'session') + .ignore('transaction', 'event') + .unignore('sessions') .expectError() .expect({ sessions: { diff --git a/dev-packages/node-integration-tests/suites/tracing/connect/test.ts b/dev-packages/node-integration-tests/suites/tracing/connect/test.ts index 08e96e691c6f..e343e6edf6fd 100644 --- a/dev-packages/node-integration-tests/suites/tracing/connect/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/connect/test.ts @@ -47,7 +47,7 @@ describe('connect auto-instrumentation', () => { test('CJS - should capture errors in `connect` middleware.', done => { createRunner(__dirname, 'scenario.js') - .ignore('transaction', 'session', 'sessions') + .ignore('transaction') .expectError() .expect({ event: EXPECTED_EVENT }) .start(done) @@ -56,7 +56,7 @@ describe('connect auto-instrumentation', () => { test('CJS - should report errored transactions.', done => { createRunner(__dirname, 'scenario.js') - .ignore('event', 'session', 'sessions') + .ignore('event') .expect({ transaction: { transaction: 'GET /error' } }) .expectError() .start(done) diff --git a/dev-packages/node-integration-tests/suites/tracing/envelope-header/error-active-span-unsampled/test.ts b/dev-packages/node-integration-tests/suites/tracing/envelope-header/error-active-span-unsampled/test.ts index b4d971654ea0..c962bc36800d 100644 --- a/dev-packages/node-integration-tests/suites/tracing/envelope-header/error-active-span-unsampled/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/envelope-header/error-active-span-unsampled/test.ts @@ -2,7 +2,7 @@ import { createRunner } from '../../../../utils/runner'; test('envelope header for error event during active unsampled span is correct', done => { createRunner(__dirname, 'scenario.ts') - .ignore('session', 'sessions', 'transaction') + .ignore('transaction') .expectHeader({ event: { trace: { diff --git a/dev-packages/node-integration-tests/suites/tracing/envelope-header/error-active-span/test.ts b/dev-packages/node-integration-tests/suites/tracing/envelope-header/error-active-span/test.ts index 0e425ac58d55..f81364dec824 100644 --- a/dev-packages/node-integration-tests/suites/tracing/envelope-header/error-active-span/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/envelope-header/error-active-span/test.ts @@ -2,7 +2,7 @@ import { createRunner } from '../../../../utils/runner'; test('envelope header for error event during active span is correct', done => { createRunner(__dirname, 'scenario.ts') - .ignore('session', 'sessions', 'transaction') + .ignore('transaction') .expectHeader({ event: { trace: { diff --git a/dev-packages/node-integration-tests/suites/tracing/envelope-header/error/test.ts b/dev-packages/node-integration-tests/suites/tracing/envelope-header/error/test.ts index e45e18baa29a..87229650bfb4 100644 --- a/dev-packages/node-integration-tests/suites/tracing/envelope-header/error/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/envelope-header/error/test.ts @@ -2,7 +2,6 @@ import { createRunner } from '../../../../utils/runner'; test('envelope header for error events is correct', done => { createRunner(__dirname, 'scenario.ts') - .ignore('session', 'sessions') .expectHeader({ event: { trace: { diff --git a/dev-packages/node-integration-tests/suites/tracing/envelope-header/transaction-route/test.ts b/dev-packages/node-integration-tests/suites/tracing/envelope-header/transaction-route/test.ts index b64ef4ff55ed..a9d7a3ac04e8 100644 --- a/dev-packages/node-integration-tests/suites/tracing/envelope-header/transaction-route/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/envelope-header/transaction-route/test.ts @@ -2,7 +2,6 @@ import { createRunner } from '../../../../utils/runner'; test('envelope header for transaction event of route correct', done => { createRunner(__dirname, 'scenario.ts') - .ignore('session', 'sessions') .expectHeader({ transaction: { trace: { diff --git a/dev-packages/node-integration-tests/suites/tracing/envelope-header/transaction-url/test.ts b/dev-packages/node-integration-tests/suites/tracing/envelope-header/transaction-url/test.ts index 80f187165614..2656835cd98d 100644 --- a/dev-packages/node-integration-tests/suites/tracing/envelope-header/transaction-url/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/envelope-header/transaction-url/test.ts @@ -2,7 +2,6 @@ import { createRunner } from '../../../../utils/runner'; test('envelope header for transaction event with source=url correct', done => { createRunner(__dirname, 'scenario.ts') - .ignore('session', 'sessions') .expectHeader({ transaction: { trace: { diff --git a/dev-packages/node-integration-tests/suites/tracing/envelope-header/transaction/test.ts b/dev-packages/node-integration-tests/suites/tracing/envelope-header/transaction/test.ts index 10ef348dfaf0..436c7d7adf2e 100644 --- a/dev-packages/node-integration-tests/suites/tracing/envelope-header/transaction/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/envelope-header/transaction/test.ts @@ -2,7 +2,6 @@ import { createRunner } from '../../../../utils/runner'; test('envelope header for transaction event is correct', done => { createRunner(__dirname, 'scenario.ts') - .ignore('session', 'sessions') .expectHeader({ transaction: { trace: { diff --git a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-experimental.js b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-experimental.js new file mode 100644 index 000000000000..9b4e62766f4e --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-experimental.js @@ -0,0 +1,38 @@ +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, + + integrations: [ + Sentry.httpIntegration({ + instrumentation: { + _experimentalConfig: { + serverName: 'sentry-test-server-name', + }, + }, + }), + ], +}); + +// 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', (_req, res) => { + res.send({ response: 'response 1' }); +}); + +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server.js b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server.js new file mode 100644 index 000000000000..d10c24db500d --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/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, + + integrations: [ + Sentry.httpIntegration({ + instrumentation: { + requestHook: (span, req) => { + span.setAttribute('attr1', 'yes'); + Sentry.setExtra('requestHookCalled', { + url: req.url, + method: req.method, + }); + }, + responseHook: (span, res) => { + span.setAttribute('attr2', 'yes'); + Sentry.setExtra('responseHookCalled', { + url: res.req.url, + method: res.req.method, + }); + }, + applyCustomAttributesOnSpan: (span, req, res) => { + span.setAttribute('attr3', 'yes'); + Sentry.setExtra('applyCustomAttributesOnSpanCalled', { + reqUrl: req.url, + reqMethod: req.method, + resUrl: res.req.url, + resMethod: res.req.method, + }); + }, + }, + }), + ], +}); + +// 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', (_req, res) => { + res.send({ response: 'response 1' }); +}); + +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/test.ts b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/test.ts new file mode 100644 index 000000000000..6be5d36e2ee3 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/test.ts @@ -0,0 +1,78 @@ +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +describe('httpIntegration', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + test('allows to pass instrumentation options to integration', done => { + // response shape seems different on Node 14, so we skip this there + const nodeMajorVersion = Number(process.versions.node.split('.')[0]); + if (nodeMajorVersion <= 14) { + done(); + return; + } + + createRunner(__dirname, 'server.js') + .expect({ + transaction: { + contexts: { + trace: { + span_id: expect.any(String), + trace_id: expect.any(String), + data: { + url: expect.stringMatching(/\/test$/), + 'http.response.status_code': 200, + attr1: 'yes', + attr2: 'yes', + attr3: 'yes', + }, + op: 'http.server', + status: 'ok', + }, + }, + extra: { + requestHookCalled: { + url: expect.stringMatching(/\/test$/), + method: 'GET', + }, + responseHookCalled: { + url: expect.stringMatching(/\/test$/), + method: 'GET', + }, + applyCustomAttributesOnSpanCalled: { + reqUrl: expect.stringMatching(/\/test$/), + reqMethod: 'GET', + resUrl: expect.stringMatching(/\/test$/), + resMethod: 'GET', + }, + }, + }, + }) + .start(done) + .makeRequest('get', '/test'); + }); + + test('allows to pass experimental config through to integration', done => { + createRunner(__dirname, 'server-experimental.js') + .expect({ + transaction: { + contexts: { + trace: { + span_id: expect.any(String), + trace_id: expect.any(String), + data: { + url: expect.stringMatching(/\/test$/), + 'http.response.status_code': 200, + 'http.server_name': 'sentry-test-server-name', + }, + op: 'http.server', + status: 'ok', + }, + }, + }, + }) + .start(done) + .makeRequest('get', '/test'); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/maxSpans/test.ts b/dev-packages/node-integration-tests/suites/tracing/maxSpans/test.ts index 9bc623b12200..2554a9b790b6 100644 --- a/dev-packages/node-integration-tests/suites/tracing/maxSpans/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/maxSpans/test.ts @@ -8,7 +8,6 @@ test('it limits spans to 1000', done => { } createRunner(__dirname, 'scenario.ts') - .ignore('session', 'sessions') .expect({ transaction: { transaction: 'parent', diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/fetch-breadcrumbs/test.ts b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-breadcrumbs/test.ts index f410f8209110..2c238c9ecd15 100644 --- a/dev-packages/node-integration-tests/suites/tracing/requests/fetch-breadcrumbs/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-breadcrumbs/test.ts @@ -10,7 +10,6 @@ conditionalTest({ min: 18 })('outgoing fetch', () => { createRunner(__dirname, 'scenario.ts') .withEnv({ SERVER_URL }) .ensureNoErrorOutput() - .ignore('session', 'sessions') .expect({ event: { breadcrumbs: [ diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/fetch-noSampleRate/test.ts b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-noSampleRate/test.ts index 2de3dcfea66b..f69f4f54c56d 100644 --- a/dev-packages/node-integration-tests/suites/tracing/requests/fetch-noSampleRate/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-noSampleRate/test.ts @@ -34,7 +34,6 @@ conditionalTest({ min: 18 })('outgoing fetch', () => { createRunner(__dirname, 'scenario.ts') .withEnv({ SERVER_URL }) .ensureNoErrorOutput() - .ignore('session', 'sessions') .expect({ event: { exception: { diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/fetch-sampled-no-active-span/test.ts b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-sampled-no-active-span/test.ts index 40e14fe648f8..8b9ff1486565 100644 --- a/dev-packages/node-integration-tests/suites/tracing/requests/fetch-sampled-no-active-span/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-sampled-no-active-span/test.ts @@ -29,7 +29,6 @@ conditionalTest({ min: 18 })('outgoing fetch', () => { .then(SERVER_URL => { createRunner(__dirname, 'scenario.ts') .withEnv({ SERVER_URL }) - .ignore('session', 'sessions') .expect({ event: { exception: { diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/fetch-unsampled/test.ts b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-unsampled/test.ts index 0b0ceeaa499c..016881f47399 100644 --- a/dev-packages/node-integration-tests/suites/tracing/requests/fetch-unsampled/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-unsampled/test.ts @@ -29,7 +29,6 @@ conditionalTest({ min: 18 })('outgoing fetch', () => { .then(SERVER_URL => { createRunner(__dirname, 'scenario.ts') .withEnv({ SERVER_URL }) - .ignore('session', 'sessions') .expect({ event: { exception: { diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/http-breadcrumbs/test.ts b/dev-packages/node-integration-tests/suites/tracing/requests/http-breadcrumbs/test.ts index e6f44c7953f8..aab8338d0a35 100644 --- a/dev-packages/node-integration-tests/suites/tracing/requests/http-breadcrumbs/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/requests/http-breadcrumbs/test.ts @@ -8,7 +8,6 @@ test('outgoing http requests create breadcrumbs', done => { createRunner(__dirname, 'scenario.ts') .withEnv({ SERVER_URL }) .ensureNoErrorOutput() - .ignore('session', 'sessions') .expect({ event: { breadcrumbs: [ diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/http-noSampleRate/test.ts b/dev-packages/node-integration-tests/suites/tracing/requests/http-noSampleRate/test.ts index 570d9f4865a6..b6766442683e 100644 --- a/dev-packages/node-integration-tests/suites/tracing/requests/http-noSampleRate/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/requests/http-noSampleRate/test.ts @@ -32,7 +32,6 @@ test('outgoing http requests are correctly instrumented without tracesSampleRate createRunner(__dirname, 'scenario.ts') .withEnv({ SERVER_URL }) .ensureNoErrorOutput() - .ignore('session', 'sessions') .expect({ event: { exception: { diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/http-sampled-no-active-span/test.ts b/dev-packages/node-integration-tests/suites/tracing/requests/http-sampled-no-active-span/test.ts index ee0151cd6297..9f18f050b929 100644 --- a/dev-packages/node-integration-tests/suites/tracing/requests/http-sampled-no-active-span/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/requests/http-sampled-no-active-span/test.ts @@ -31,7 +31,6 @@ test('outgoing sampled http requests without active span are correctly instrumen .then(SERVER_URL => { createRunner(__dirname, 'scenario.ts') .withEnv({ SERVER_URL }) - .ignore('session', 'sessions') .expect({ event: { exception: { diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/http-unsampled/test.ts b/dev-packages/node-integration-tests/suites/tracing/requests/http-unsampled/test.ts index c860958622fa..be4a2f542875 100644 --- a/dev-packages/node-integration-tests/suites/tracing/requests/http-unsampled/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/requests/http-unsampled/test.ts @@ -31,7 +31,6 @@ test('outgoing http requests are correctly instrumented when not sampled', done .then(SERVER_URL => { createRunner(__dirname, 'scenario.ts') .withEnv({ SERVER_URL }) - .ignore('session', 'sessions') .expect({ event: { exception: { diff --git a/dev-packages/node-integration-tests/utils/runner.ts b/dev-packages/node-integration-tests/utils/runner.ts index 9d0eb8a64b25..6e663cd13d75 100644 --- a/dev-packages/node-integration-tests/utils/runner.ts +++ b/dev-packages/node-integration-tests/utils/runner.ts @@ -164,7 +164,8 @@ export function createRunner(...paths: string[]) { const expectedEnvelopes: Expected[] = []; let expectedEnvelopeHeaders: ExpectedEnvelopeHeader[] | undefined = undefined; const flags: string[] = []; - const ignored: EnvelopeItemType[] = []; + // By default, we ignore session & sessions + const ignored: EnvelopeItemType[] = ['session', 'sessions']; let withEnv: Record = {}; let withSentryServer = false; let dockerOptions: DockerOptions | undefined; @@ -209,6 +210,15 @@ export function createRunner(...paths: string[]) { ignored.push(...types); return this; }, + unignore: function (...types: EnvelopeItemType[]) { + for (const t of types) { + const pos = ignored.indexOf(t); + if (pos > -1) { + ignored.splice(pos, 1); + } + } + return this; + }, withDockerCompose: function (options: DockerOptions) { dockerOptions = options; return this; diff --git a/dev-packages/overhead-metrics/package.json b/dev-packages/overhead-metrics/package.json index 706fdc8bd117..b3075ef1ee99 100644 --- a/dev-packages/overhead-metrics/package.json +++ b/dev-packages/overhead-metrics/package.json @@ -1,6 +1,6 @@ { "private": true, - "version": "8.14.0", + "version": "8.16.0", "name": "@sentry-internal/overhead-metrics", "main": "index.js", "author": "Sentry", diff --git a/dev-packages/rollup-utils/npmHelpers.mjs b/dev-packages/rollup-utils/npmHelpers.mjs index e95e63700c09..410d0847d928 100644 --- a/dev-packages/rollup-utils/npmHelpers.mjs +++ b/dev-packages/rollup-utils/npmHelpers.mjs @@ -22,7 +22,7 @@ import { makeSetSDKSourcePlugin, makeSucrasePlugin, } from './plugins/index.mjs'; -import { makePackageNodeEsm, makeReactEsmJsxRuntimePlugin } from './plugins/make-esm-plugin.mjs'; +import { makePackageNodeEsm } from './plugins/make-esm-plugin.mjs'; import { mergePlugins } from './utils.mjs'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -143,7 +143,7 @@ export function makeNPMConfigVariants(baseConfig, options = {}) { output: { format: 'esm', dir: path.join(baseConfig.output.dir, 'esm'), - plugins: [makePackageNodeEsm(), makeReactEsmJsxRuntimePlugin()], + plugins: [makePackageNodeEsm()], }, }); } diff --git a/dev-packages/rollup-utils/package.json b/dev-packages/rollup-utils/package.json index 49cc6f2cd4a2..8a6b26a3c078 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.14.0", + "version": "8.16.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/rollup-utils/plugins/make-esm-plugin.mjs b/dev-packages/rollup-utils/plugins/make-esm-plugin.mjs index 91aba689f888..04dd68beaa1c 100644 --- a/dev-packages/rollup-utils/plugins/make-esm-plugin.mjs +++ b/dev-packages/rollup-utils/plugins/make-esm-plugin.mjs @@ -1,5 +1,4 @@ import fs from 'node:fs'; -import replacePlugin from '@rollup/plugin-replace'; /** * Outputs a package.json file with {type: module} in the root of the output directory so that Node @@ -30,17 +29,3 @@ export function makePackageNodeEsm() { }, }; } - -/** - * Makes sure that whenever we add an `react/jsx-runtime` import, we add a `.js` to make the import esm compatible. - */ -export function makeReactEsmJsxRuntimePlugin() { - return replacePlugin({ - preventAssignment: false, - sourceMap: true, - values: { - "'react/jsx-runtime'": "'react/jsx-runtime.js'", - '"react/jsx-runtime"': '"react/jsx-runtime.js"', - }, - }); -} diff --git a/dev-packages/size-limit-gh-action/package.json b/dev-packages/size-limit-gh-action/package.json index c9f4239ccdcf..d754e1916ca3 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.14.0", + "version": "8.16.0", "license": "MIT", "engines": { "node": ">=18" diff --git a/dev-packages/test-utils/package.json b/dev-packages/test-utils/package.json index 36458ac47acc..d8452ee6782f 100644 --- a/dev-packages/test-utils/package.json +++ b/dev-packages/test-utils/package.json @@ -1,6 +1,6 @@ { "private": true, - "version": "8.14.0", + "version": "8.16.0", "name": "@sentry-internal/test-utils", "author": "Sentry", "license": "MIT", @@ -45,8 +45,8 @@ }, "devDependencies": { "@playwright/test": "^1.44.1", - "@sentry/types": "8.14.0", - "@sentry/utils": "8.14.0" + "@sentry/types": "8.16.0", + "@sentry/utils": "8.16.0" }, "volta": { "extends": "../../package.json" diff --git a/dev-packages/test-utils/src/playwright-config.ts b/dev-packages/test-utils/src/playwright-config.ts index 4f2ea54bc3b4..33de29f5a7fc 100644 --- a/dev-packages/test-utils/src/playwright-config.ts +++ b/dev-packages/test-utils/src/playwright-config.ts @@ -37,7 +37,7 @@ export function getPlaywrightConfig( /* In dev mode some apps are flaky, so we allow retry there... */ retries: testEnv === 'development' ? 3 : 0, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: 'list', + reporter: process.env.CI ? 'line' : 'list', /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ diff --git a/lerna.json b/lerna.json index c041189cbd91..d3087938610c 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "8.14.0", + "version": "8.16.0", "npmClient": "yarn" } diff --git a/packages/angular/package.json b/packages/angular/package.json index f845f6a418af..fc5d94ea5cda 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/angular", - "version": "8.14.0", + "version": "8.16.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,10 +21,10 @@ "rxjs": "^6.5.5 || ^7.x" }, "dependencies": { - "@sentry/browser": "8.14.0", - "@sentry/core": "8.14.0", - "@sentry/types": "8.14.0", - "@sentry/utils": "8.14.0", + "@sentry/browser": "8.16.0", + "@sentry/core": "8.16.0", + "@sentry/types": "8.16.0", + "@sentry/utils": "8.16.0", "tslib": "^2.4.1" }, "devDependencies": { diff --git a/packages/astro/package.json b/packages/astro/package.json index 68d91b53a569..b0d4a8c37385 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/astro", - "version": "8.14.0", + "version": "8.16.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,11 +56,11 @@ "astro": ">=3.x || >=4.0.0-beta" }, "dependencies": { - "@sentry/browser": "8.14.0", - "@sentry/core": "8.14.0", - "@sentry/node": "8.14.0", - "@sentry/types": "8.14.0", - "@sentry/utils": "8.14.0", + "@sentry/browser": "8.16.0", + "@sentry/core": "8.16.0", + "@sentry/node": "8.16.0", + "@sentry/types": "8.16.0", + "@sentry/utils": "8.16.0", "@sentry/vite-plugin": "^2.20.1" }, "devDependencies": { diff --git a/packages/aws-serverless/package.json b/packages/aws-serverless/package.json index 7da5dc2d8dcd..d8561ff41b9f 100644 --- a/packages/aws-serverless/package.json +++ b/packages/aws-serverless/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/aws-serverless", - "version": "8.14.0", + "version": "8.16.0", "description": "Official Sentry SDK for AWS Lambda and AWS Serverless Environments", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/serverless", @@ -63,10 +63,10 @@ "dependencies": { "@opentelemetry/instrumentation-aws-lambda": "0.42.0", "@opentelemetry/instrumentation-aws-sdk": "0.42.0", - "@sentry/core": "8.14.0", - "@sentry/node": "8.14.0", - "@sentry/types": "8.14.0", - "@sentry/utils": "8.14.0", + "@sentry/core": "8.16.0", + "@sentry/node": "8.16.0", + "@sentry/types": "8.16.0", + "@sentry/utils": "8.16.0", "@types/aws-lambda": "^8.10.62" }, "devDependencies": { diff --git a/packages/browser-utils/package.json b/packages/browser-utils/package.json index f3e632f1066f..584c84e92da1 100644 --- a/packages/browser-utils/package.json +++ b/packages/browser-utils/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/browser-utils", - "version": "8.14.0", + "version": "8.16.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,9 +39,9 @@ "access": "public" }, "dependencies": { - "@sentry/core": "8.14.0", - "@sentry/types": "8.14.0", - "@sentry/utils": "8.14.0" + "@sentry/core": "8.16.0", + "@sentry/types": "8.16.0", + "@sentry/utils": "8.16.0" }, "scripts": { "build": "run-p build:transpile build:types", diff --git a/packages/browser/package.json b/packages/browser/package.json index e322a88fb122..870b06e752fe 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/browser", - "version": "8.14.0", + "version": "8.16.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,16 +39,16 @@ "access": "public" }, "dependencies": { - "@sentry-internal/browser-utils": "8.14.0", - "@sentry-internal/feedback": "8.14.0", - "@sentry-internal/replay": "8.14.0", - "@sentry-internal/replay-canvas": "8.14.0", - "@sentry/core": "8.14.0", - "@sentry/types": "8.14.0", - "@sentry/utils": "8.14.0" + "@sentry-internal/browser-utils": "8.16.0", + "@sentry-internal/feedback": "8.16.0", + "@sentry-internal/replay": "8.16.0", + "@sentry-internal/replay-canvas": "8.16.0", + "@sentry/core": "8.16.0", + "@sentry/types": "8.16.0", + "@sentry/utils": "8.16.0" }, "devDependencies": { - "@sentry-internal/integration-shims": "8.14.0", + "@sentry-internal/integration-shims": "8.16.0", "fake-indexeddb": "^4.0.1", "webpack": "^4.47.0" }, diff --git a/packages/browser/src/utils/lazyLoadIntegration.ts b/packages/browser/src/utils/lazyLoadIntegration.ts index c09bdfa45eae..4479c2e69590 100644 --- a/packages/browser/src/utils/lazyLoadIntegration.ts +++ b/packages/browser/src/utils/lazyLoadIntegration.ts @@ -51,6 +51,7 @@ export async function lazyLoadIntegration(name: keyof typeof LazyLoadableIntegra const script = WINDOW.document.createElement('script'); script.src = url; script.crossOrigin = 'anonymous'; + script.referrerPolicy = 'origin'; const waitForLoad = new Promise((resolve, reject) => { script.addEventListener('load', () => resolve()); diff --git a/packages/bun/package.json b/packages/bun/package.json index 7b0198a978ff..33e41b0e8df6 100644 --- a/packages/bun/package.json +++ b/packages/bun/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/bun", - "version": "8.14.0", + "version": "8.16.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,11 +39,11 @@ "access": "public" }, "dependencies": { - "@sentry/core": "8.14.0", - "@sentry/node": "8.14.0", - "@sentry/opentelemetry": "8.14.0", - "@sentry/types": "8.14.0", - "@sentry/utils": "8.14.0" + "@sentry/core": "8.16.0", + "@sentry/node": "8.16.0", + "@sentry/opentelemetry": "8.16.0", + "@sentry/types": "8.16.0", + "@sentry/utils": "8.16.0" }, "devDependencies": { "bun-types": "latest" diff --git a/packages/core/package.json b/packages/core/package.json index 951075192dae..0b0da42d2894 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/core", - "version": "8.14.0", + "version": "8.16.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", @@ -39,8 +39,8 @@ "access": "public" }, "dependencies": { - "@sentry/types": "8.14.0", - "@sentry/utils": "8.14.0" + "@sentry/types": "8.16.0", + "@sentry/utils": "8.16.0" }, "scripts": { "build": "run-p build:transpile build:types", diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index 150f92388875..1ad35e291013 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -455,6 +455,8 @@ export abstract class BaseClient implements Client { public on(hook: 'close', callback: () => void): () => void; + public on(hook: 'applyFrameMetadata', callback: (event: Event) => void): () => void; + /** @inheritdoc */ public on(hook: string, callback: unknown): () => void { // Note that the code below, with nullish coalescing assignment, @@ -541,6 +543,9 @@ export abstract class BaseClient implements Client { /** @inheritdoc */ public emit(hook: 'close'): void; + /** @inheritdoc */ + public emit(hook: 'applyFrameMetadata', event: Event): void; + /** @inheritdoc */ public emit(hook: string, ...rest: unknown[]): void { const callbacks = this._hooks[hook]; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 8ea4b032d44c..1971bb8c94bd 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -78,6 +78,7 @@ export { getRootSpan, getActiveSpan, addChildSpanToSpan, + spanTimeInputToSeconds, } from './utils/spanUtils'; export { parseSampleRate } from './utils/parseSampleRate'; export { applySdkMetadata } from './utils/sdkMetadata'; diff --git a/packages/core/src/integrations/metadata.ts b/packages/core/src/integrations/metadata.ts index 917d8a12beb8..48fa23d6b6ab 100644 --- a/packages/core/src/integrations/metadata.ts +++ b/packages/core/src/integrations/metadata.ts @@ -1,14 +1,21 @@ -import type { EventItem, IntegrationFn } from '@sentry/types'; +import type { EventItem } from '@sentry/types'; import { forEachEnvelopeItem } from '@sentry/utils'; import { defineIntegration } from '../integration'; import { addMetadataToStackFrames, stripMetadataFromStackFrames } from '../metadata'; -const INTEGRATION_NAME = 'ModuleMetadata'; - -const _moduleMetadataIntegration = (() => { +/** + * Adds module metadata to stack frames. + * + * Metadata can be injected by the Sentry bundler plugins using the `moduleMetadata` config option. + * + * When this integration is added, the metadata passed to the bundler plugin is added to the stack frames of all events + * under the `module_metadata` property. This can be used to help in tagging or routing of events from different teams + * our sources + */ +export const moduleMetadataIntegration = defineIntegration(() => { return { - name: INTEGRATION_NAME, + name: 'ModuleMetadata', setup(client) { // We need to strip metadata from stack frames before sending them to Sentry since these are client side only. client.on('beforeEnvelope', envelope => { @@ -23,23 +30,16 @@ const _moduleMetadataIntegration = (() => { } }); }); - }, - processEvent(event, _hint, client) { - const stackParser = client.getOptions().stackParser; - addMetadataToStackFrames(stackParser, event); - return event; + client.on('applyFrameMetadata', event => { + // Only apply stack frame metadata to error events + if (event.type !== undefined) { + return; + } + + const stackParser = client.getOptions().stackParser; + addMetadataToStackFrames(stackParser, event); + }); }, }; -}) satisfies IntegrationFn; - -/** - * Adds module metadata to stack frames. - * - * Metadata can be injected by the Sentry bundler plugins using the `_experiments.moduleMetadata` config option. - * - * When this integration is added, the metadata passed to the bundler plugin is added to the stack frames of all events - * under the `module_metadata` property. This can be used to help in tagging or routing of events from different teams - * our sources - */ -export const moduleMetadataIntegration = defineIntegration(_moduleMetadataIntegration); +}); diff --git a/packages/core/src/integrations/third-party-errors-filter.ts b/packages/core/src/integrations/third-party-errors-filter.ts index 70e7317f58c3..1f1887604866 100644 --- a/packages/core/src/integrations/third-party-errors-filter.ts +++ b/packages/core/src/integrations/third-party-errors-filter.ts @@ -53,11 +53,19 @@ export const thirdPartyErrorFilterIntegration = defineIntegration((options: Opti } }); }); + + client.on('applyFrameMetadata', event => { + // Only apply stack frame metadata to error events + if (event.type !== undefined) { + return; + } + + const stackParser = client.getOptions().stackParser; + addMetadataToStackFrames(stackParser, event); + }); }, - processEvent(event, _hint, client) { - const stackParser = client.getOptions().stackParser; - addMetadataToStackFrames(stackParser, event); + processEvent(event) { const frameKeys = getBundleKeysForAllFramesWithFilenames(event); if (frameKeys) { diff --git a/packages/core/src/utils/prepareEvent.ts b/packages/core/src/utils/prepareEvent.ts index 71fef195dd31..f7f209a49089 100644 --- a/packages/core/src/utils/prepareEvent.ts +++ b/packages/core/src/utils/prepareEvent.ts @@ -60,6 +60,10 @@ export function prepareEvent( applyClientOptions(prepared, options); applyIntegrationsMetadata(prepared, integrations); + if (client) { + client.emit('applyFrameMetadata', event); + } + // Only put debug IDs onto frames for error events. if (event.type === undefined) { applyDebugIds(prepared, options.stackParser); diff --git a/packages/core/src/utils/spanUtils.ts b/packages/core/src/utils/spanUtils.ts index 0878f0b383b3..266a0035b382 100644 --- a/packages/core/src/utils/spanUtils.ts +++ b/packages/core/src/utils/spanUtils.ts @@ -69,7 +69,7 @@ export function spanToTraceHeader(span: Span): string { } /** - * Convert a span time input intp a timestamp in seconds. + * Convert a span time input into a timestamp in seconds. */ export function spanTimeInputToSeconds(input: SpanTimeInput | undefined): number { if (typeof input === 'number') { diff --git a/packages/core/test/lib/integrations/third-party-errors-filter.test.ts b/packages/core/test/lib/integrations/third-party-errors-filter.test.ts index d0fd02045080..b11b93498b02 100644 --- a/packages/core/test/lib/integrations/third-party-errors-filter.test.ts +++ b/packages/core/test/lib/integrations/third-party-errors-filter.test.ts @@ -1,12 +1,14 @@ import type { Client, Event } from '@sentry/types'; import { GLOBAL_OBJ, createStackParser, nodeStackLineParser } from '@sentry/utils'; import { thirdPartyErrorFilterIntegration } from '../../../src/integrations/third-party-errors-filter'; +import { addMetadataToStackFrames } from '../../../src/metadata'; function clone(data: T): T { return JSON.parse(JSON.stringify(data)); } const stack = new Error().stack || ''; +const stackParser = createStackParser(nodeStackLineParser()); const eventWithThirdAndFirstPartyFrames: Event = { exception: { @@ -90,11 +92,7 @@ const eventWithOnlyThirdPartyFrames: Event = { }; // This only needs the stackParser -const MOCK_CLIENT = { - getOptions: () => ({ - stackParser: createStackParser(nodeStackLineParser()), - }), -} as unknown as Client; +const MOCK_CLIENT = {} as unknown as Client; describe('ThirdPartyErrorFilter', () => { beforeEach(() => { @@ -103,6 +101,10 @@ describe('ThirdPartyErrorFilter', () => { '_sentryBundlerPluginAppKey:some-key': true, '_sentryBundlerPluginAppKey:some-other-key': true, }; + + addMetadataToStackFrames(stackParser, eventWithThirdAndFirstPartyFrames); + addMetadataToStackFrames(stackParser, eventWithOnlyFirstPartyFrames); + addMetadataToStackFrames(stackParser, eventWithOnlyThirdPartyFrames); }); describe('drop-error-if-contains-third-party-frames', () => { diff --git a/packages/core/test/lib/prepareEvent.test.ts b/packages/core/test/lib/prepareEvent.test.ts index 11c1ceca991c..0698aa15e447 100644 --- a/packages/core/test/lib/prepareEvent.test.ts +++ b/packages/core/test/lib/prepareEvent.test.ts @@ -205,10 +205,13 @@ describe('prepareEvent', () => { const options = {} as ClientOptions; const client = { + emit() { + // noop + }, getEventProcessors() { return [eventProcessor]; }, - } as Client; + } as unknown as Client; const processedEvent = await prepareEvent( options, event, @@ -393,10 +396,13 @@ describe('prepareEvent', () => { const options = {} as ClientOptions; const client = { + emit() { + // noop + }, getEventProcessors() { return [] as EventProcessor[]; }, - } as Client; + } as unknown as Client; const processedEvent = await prepareEvent( options, @@ -430,10 +436,13 @@ describe('prepareEvent', () => { const options = {} as ClientOptions; const client = { + emit() { + // noop + }, getEventProcessors() { return [] as EventProcessor[]; }, - } as Client; + } as unknown as Client; const captureContext = new Scope(); captureContext.setTags({ foo: 'bar' }); @@ -470,10 +479,13 @@ describe('prepareEvent', () => { const options = {} as ClientOptions; const client = { + emit() { + // noop + }, getEventProcessors() { return [] as EventProcessor[]; }, - } as Client; + } as unknown as Client; const captureContextScope = new Scope(); captureContextScope.setTags({ foo: 'bar' }); diff --git a/packages/deno/package.json b/packages/deno/package.json index 4451a454cea2..9fe4bd555c50 100644 --- a/packages/deno/package.json +++ b/packages/deno/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/deno", - "version": "8.14.0", + "version": "8.16.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,9 +24,9 @@ "/build" ], "dependencies": { - "@sentry/core": "8.14.0", - "@sentry/types": "8.14.0", - "@sentry/utils": "8.14.0" + "@sentry/core": "8.16.0", + "@sentry/types": "8.16.0", + "@sentry/utils": "8.16.0" }, "devDependencies": { "@rollup/plugin-typescript": "^11.1.5", @@ -41,7 +41,7 @@ "build:types": "run-s deno-types build:types:tsc build:types:bundle", "build:types:tsc": "tsc -p tsconfig.types.json", "build:types:bundle": "rollup -c rollup.types.config.mjs", - "build:tarball": "node ./scripts/prepack.js && npm pack", + "build:tarball": "node ./scripts/prepack.js && npm pack ./build", "circularDepCheck": "madge --circular src/index.ts", "clean": "rimraf build build-types build-test coverage sentry-deno-*.tgz", "prefix": "yarn deno-types", @@ -55,7 +55,7 @@ "test:types": "deno check ./build/index.mjs", "test:unit": "deno test --allow-read --allow-run", "test:unit:update": "deno test --allow-read --allow-write --allow-run -- --update", - "yalc:publish": "node ./scripts/prepack.js && yalc publish --push --sig" + "yalc:publish": "node ./scripts/prepack.js && yalc publish build --push --sig" }, "volta": { "extends": "../../package.json" diff --git a/packages/deno/scripts/prepack.js b/packages/deno/scripts/prepack.js index 19422f912715..6c7db2bc9878 100644 --- a/packages/deno/scripts/prepack.js +++ b/packages/deno/scripts/prepack.js @@ -18,6 +18,8 @@ const ENTRY_POINTS = ['main', 'module', 'types', 'browser']; const EXPORT_MAP_ENTRY_POINT = 'exports'; const TYPES_VERSIONS_ENTRY_POINT = 'typesVersions'; +const ASSETS = ['README.md', 'LICENSE', 'package.json', '.npmignore']; + const PACKAGE_JSON = 'package.json'; /** @@ -52,11 +54,25 @@ if (!fs.existsSync(path.resolve(BUILD_DIR))) { process.exit(1); } +const buildDirContents = fs.readdirSync(path.resolve(BUILD_DIR)); + +// copy non-code assets to build dir +ASSETS.forEach(asset => { + const assetPath = path.resolve(asset); + if (fs.existsSync(assetPath)) { + const destinationPath = path.resolve(BUILD_DIR, path.basename(asset)); + console.log(`Copying ${path.basename(asset)} to ${path.relative('../..', destinationPath)}.`); + fs.copyFileSync(assetPath, destinationPath); + } +}); + // package.json modifications +const newPackageJsonPath = path.resolve(BUILD_DIR, PACKAGE_JSON); + /** * @type {PackageJson} */ -const newPkgJson = { ...pkgJson }; +const newPkgJson = require(newPackageJsonPath); // modify entry points to point to correct paths (i.e. strip out the build directory) ENTRY_POINTS.filter(entryPoint => newPkgJson[entryPoint]).forEach(entryPoint => { @@ -100,7 +116,7 @@ if (newPkgJson[TYPES_VERSIONS_ENTRY_POINT]) { }); } -const newPackageJsonPath = path.resolve(BUILD_DIR, PACKAGE_JSON); +newPkgJson.files = buildDirContents; // write modified package.json to file (pretty-printed with 2 spaces) try { diff --git a/packages/ember/package.json b/packages/ember/package.json index 8c24ee2c5cb4..a431721bc8ea 100644 --- a/packages/ember/package.json +++ b/packages/ember/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/ember", - "version": "8.14.0", + "version": "8.16.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", @@ -33,10 +33,10 @@ "dependencies": { "@babel/core": "^7.24.4", "@embroider/macros": "^1.16.0", - "@sentry/browser": "8.14.0", - "@sentry/core": "8.14.0", - "@sentry/types": "8.14.0", - "@sentry/utils": "8.14.0", + "@sentry/browser": "8.16.0", + "@sentry/core": "8.16.0", + "@sentry/types": "8.16.0", + "@sentry/utils": "8.16.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 1139de035a0d..10094be82f49 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.14.0", + "version": "8.16.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.14.0", - "@sentry-internal/typescript": "8.14.0", + "@sentry-internal/eslint-plugin-sdk": "8.16.0", + "@sentry-internal/typescript": "8.16.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 a2e5a6ff8806..199386d18539 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.14.0", + "version": "8.16.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 3445f2e888e6..f435ca3bab81 100644 --- a/packages/feedback/package.json +++ b/packages/feedback/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/feedback", - "version": "8.14.0", + "version": "8.16.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,9 +39,9 @@ "access": "public" }, "dependencies": { - "@sentry/core": "8.14.0", - "@sentry/types": "8.14.0", - "@sentry/utils": "8.14.0" + "@sentry/core": "8.16.0", + "@sentry/types": "8.16.0", + "@sentry/utils": "8.16.0" }, "devDependencies": { "preact": "^10.19.4" diff --git a/packages/feedback/src/screenshot/components/CropCorner.tsx b/packages/feedback/src/screenshot/components/CropCorner.tsx new file mode 100644 index 000000000000..de3b6e506e71 --- /dev/null +++ b/packages/feedback/src/screenshot/components/CropCorner.tsx @@ -0,0 +1,38 @@ +import type { VNode, h as hType } from 'preact'; + +interface FactoryParams { + h: typeof hType; +} + +export default function CropCornerFactory({ + h, // eslint-disable-line @typescript-eslint/no-unused-vars +}: FactoryParams) { + return function CropCorner({ + top, + left, + corner, + onGrabButton, + }: { + top: number; + left: number; + corner: string; + onGrabButton: (e: Event, corner: string) => void; + }): VNode { + return ( + + ); + }; +} diff --git a/packages/feedback/src/screenshot/components/ScreenshotEditor.tsx b/packages/feedback/src/screenshot/components/ScreenshotEditor.tsx index 84528d2908a1..55f8f99997c8 100644 --- a/packages/feedback/src/screenshot/components/ScreenshotEditor.tsx +++ b/packages/feedback/src/screenshot/components/ScreenshotEditor.tsx @@ -1,8 +1,11 @@ /* eslint-disable max-lines */ import type { FeedbackInternalOptions, FeedbackModalIntegration } from '@sentry/types'; import type { ComponentType, VNode, h as hType } from 'preact'; +// biome-ignore lint/nursery/noUnusedImports: reason +import { h } from 'preact'; // eslint-disable-line @typescript-eslint/no-unused-vars import type * as Hooks from 'preact/hooks'; import { DOCUMENT, WINDOW } from '../../constants'; +import CropCornerFactory from './CropCorner'; import { createScreenshotInputStyles } from './ScreenshotInput.css'; import { useTakeScreenshotFactory } from './useTakeScreenshot'; @@ -62,7 +65,7 @@ const getContainedSize = (img: HTMLCanvasElement): Box => { }; export function ScreenshotEditorFactory({ - h, // eslint-disable-line @typescript-eslint/no-unused-vars + h, hooks, imageBuffer, dialog, @@ -72,6 +75,7 @@ export function ScreenshotEditorFactory({ return function ScreenshotEditor({ onError }: Props): VNode { const styles = hooks.useMemo(() => ({ __html: createScreenshotInputStyles().innerText }), []); + const CropCorner = CropCornerFactory({ h }); const canvasContainerRef = hooks.useRef(null); const cropContainerRef = hooks.useRef(null); @@ -326,32 +330,3 @@ export function ScreenshotEditorFactory({ ); }; } - -function CropCorner({ - top, - left, - corner, - onGrabButton, -}: { - top: number; - left: number; - corner: string; - onGrabButton: (e: Event, corner: string) => void; -}): VNode { - return ( - - ); -} diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json index 9891e5cf6cf3..8e9b144ff189 100644 --- a/packages/gatsby/package.json +++ b/packages/gatsby/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/gatsby", - "version": "8.14.0", + "version": "8.16.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,10 +45,10 @@ "access": "public" }, "dependencies": { - "@sentry/core": "8.14.0", - "@sentry/react": "8.14.0", - "@sentry/types": "8.14.0", - "@sentry/utils": "8.14.0", + "@sentry/core": "8.16.0", + "@sentry/react": "8.16.0", + "@sentry/types": "8.16.0", + "@sentry/utils": "8.16.0", "@sentry/webpack-plugin": "2.16.0" }, "peerDependencies": { diff --git a/packages/google-cloud-serverless/package.json b/packages/google-cloud-serverless/package.json index baa9cd1475f1..f45cc8772da3 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.14.0", + "version": "8.16.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", @@ -48,10 +48,10 @@ "access": "public" }, "dependencies": { - "@sentry/core": "8.14.0", - "@sentry/node": "8.14.0", - "@sentry/types": "8.14.0", - "@sentry/utils": "8.14.0", + "@sentry/core": "8.16.0", + "@sentry/node": "8.16.0", + "@sentry/types": "8.16.0", + "@sentry/utils": "8.16.0", "@types/express": "^4.17.14" }, "devDependencies": { diff --git a/packages/integration-shims/package.json b/packages/integration-shims/package.json index 070a91ef6e5e..2ad4da98a39e 100644 --- a/packages/integration-shims/package.json +++ b/packages/integration-shims/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/integration-shims", - "version": "8.14.0", + "version": "8.16.0", "description": "Shims for integrations in Sentry SDK.", "main": "build/cjs/index.js", "module": "build/esm/index.js", @@ -55,9 +55,9 @@ "url": "https://github.com/getsentry/sentry-javascript/issues" }, "dependencies": { - "@sentry/core": "8.14.0", - "@sentry/types": "8.14.0", - "@sentry/utils": "8.14.0" + "@sentry/core": "8.16.0", + "@sentry/types": "8.16.0", + "@sentry/utils": "8.16.0" }, "engines": { "node": ">=14.18" diff --git a/packages/nestjs/README.md b/packages/nestjs/README.md index 58ab6bc95372..8928327b1470 100644 --- a/packages/nestjs/README.md +++ b/packages/nestjs/README.md @@ -38,6 +38,24 @@ Sentry.init({ Note that it is necessary to initialize Sentry **before you import any package that may be instrumented by us**. +## Span Decorator + +Use the @SentryTraced() decorator to gain additional performance insights for any function within your NestJS +application. + +```js +import { Injectable } from '@nestjs/common'; +import { SentryTraced } from '@sentry/nestjs'; + +@Injectable() +export class ExampleService { + @SentryTraced('example function') + async performTask() { + // Your business logic here + } +} +``` + ## Links - [Official SDK Docs](https://docs.sentry.io/platforms/javascript/guides/nestjs/) diff --git a/packages/nestjs/package.json b/packages/nestjs/package.json index 8a71fbfc34e3..fd72f89569ba 100644 --- a/packages/nestjs/package.json +++ b/packages/nestjs/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/nestjs", - "version": "8.14.0", + "version": "8.16.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", @@ -39,8 +39,8 @@ "access": "public" }, "dependencies": { - "@sentry/core": "8.14.0", - "@sentry/node": "8.14.0" + "@sentry/core": "8.16.0", + "@sentry/node": "8.16.0" }, "scripts": { "build": "run-p build:transpile build:types", diff --git a/packages/nestjs/src/cron-decorator.ts b/packages/nestjs/src/cron-decorator.ts new file mode 100644 index 000000000000..8cb86c6d66cc --- /dev/null +++ b/packages/nestjs/src/cron-decorator.ts @@ -0,0 +1,24 @@ +import * as Sentry from '@sentry/node'; +import type { MonitorConfig } from '@sentry/types'; + +/** + * A decorator wrapping the native nest Cron decorator, sending check-ins to Sentry. + */ +export const SentryCron = (monitorSlug: string, monitorConfig?: MonitorConfig): MethodDecorator => { + return (target: unknown, propertyKey, descriptor: PropertyDescriptor) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const originalMethod = descriptor.value as (...args: any[]) => Promise; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + descriptor.value = function (...args: any[]) { + return Sentry.withMonitor( + monitorSlug, + () => { + return originalMethod.apply(this, args); + }, + monitorConfig, + ); + }; + return descriptor; + }; +}; diff --git a/packages/nestjs/src/index.ts b/packages/nestjs/src/index.ts index 6ac8d97b4241..00519cf49b9e 100644 --- a/packages/nestjs/src/index.ts +++ b/packages/nestjs/src/index.ts @@ -1,3 +1,6 @@ export * from '@sentry/node'; export { init } from './sdk'; + +export { SentryTraced } from './span-decorator'; +export { SentryCron } from './cron-decorator'; diff --git a/packages/nestjs/src/span-decorator.ts b/packages/nestjs/src/span-decorator.ts new file mode 100644 index 000000000000..c56056a26621 --- /dev/null +++ b/packages/nestjs/src/span-decorator.ts @@ -0,0 +1,25 @@ +import { startSpan } from '@sentry/node'; + +/** + * A decorator usable to wrap arbitrary functions with spans. + */ +export function SentryTraced(op: string = 'function') { + return function (target: unknown, propertyKey: string, descriptor: PropertyDescriptor) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const originalMethod = descriptor.value as (...args: any[]) => Promise; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + descriptor.value = function (...args: any[]) { + return startSpan( + { + op: op, + name: propertyKey, + }, + async () => { + return originalMethod.apply(this, args); + }, + ); + }; + return descriptor; + }; +} diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index ba6baffaa678..f39e917a052f 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/nextjs", - "version": "8.14.0", + "version": "8.16.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", @@ -69,14 +69,15 @@ }, "dependencies": { "@opentelemetry/instrumentation-http": "0.52.1", + "@opentelemetry/semantic-conventions": "^1.25.1", "@rollup/plugin-commonjs": "26.0.1", - "@sentry/core": "8.14.0", - "@sentry/node": "8.14.0", - "@sentry/opentelemetry": "8.14.0", - "@sentry/react": "8.14.0", - "@sentry/types": "8.14.0", - "@sentry/utils": "8.14.0", - "@sentry/vercel-edge": "8.14.0", + "@sentry/core": "8.16.0", + "@sentry/node": "8.16.0", + "@sentry/opentelemetry": "8.16.0", + "@sentry/react": "8.16.0", + "@sentry/types": "8.16.0", + "@sentry/utils": "8.16.0", + "@sentry/vercel-edge": "8.16.0", "@sentry/webpack-plugin": "2.20.1", "chalk": "3.0.0", "resolve": "1.22.8", diff --git a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts index 0c76821afa28..5944b520f6ea 100644 --- a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts +++ b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts @@ -2,10 +2,14 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, SPAN_STATUS_ERROR, SPAN_STATUS_OK, + Scope, captureException, getActiveSpan, + getCapturedScopesOnSpan, getClient, + getRootSpan, handleCallbackErrors, + setCapturedScopesOnSpan, startSpanManual, withIsolationScope, withScope, @@ -16,11 +20,7 @@ import { propagationContextFromHeaders, uuid4, winterCGHeadersToDict } from '@se import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import type { GenerationFunctionContext } from '../common/types'; import { isNotFoundNavigationError, isRedirectNavigationError } from './nextNavigationErrorUtils'; -import { - commonObjectToIsolationScope, - commonObjectToPropagationContext, - escapeNextjsTracing, -} from './utils/tracingUtils'; +import { commonObjectToIsolationScope, commonObjectToPropagationContext } from './utils/tracingUtils'; /** * Wraps a generation function (e.g. generateMetadata) with Sentry error and performance instrumentation. @@ -34,87 +34,98 @@ export function wrapGenerationFunctionWithSentry a return new Proxy(generationFunction, { apply: (originalFunction, thisArg, args) => { const requestTraceId = getActiveSpan()?.spanContext().traceId; - return escapeNextjsTracing(() => { - let headers: WebFetchHeaders | undefined = undefined; - // We try-catch here just in case anything goes wrong with the async storage here goes wrong since it is Next.js internal API - try { - headers = requestAsyncStorage?.getStore()?.headers; - } catch (e) { - /** empty */ - } + let headers: WebFetchHeaders | undefined = undefined; + // We try-catch here just in case anything goes wrong with the async storage here goes wrong since it is Next.js internal API + try { + headers = requestAsyncStorage?.getStore()?.headers; + } catch (e) { + /** empty */ + } - let data: Record | undefined = undefined; - if (getClient()?.getOptions().sendDefaultPii) { - const props: unknown = args[0]; - const params = props && typeof props === 'object' && 'params' in props ? props.params : undefined; - const searchParams = - props && typeof props === 'object' && 'searchParams' in props ? props.searchParams : undefined; - data = { params, searchParams }; - } + const isolationScope = commonObjectToIsolationScope(headers); - const headersDict = headers ? winterCGHeadersToDict(headers) : undefined; + const activeSpan = getActiveSpan(); + if (activeSpan) { + const rootSpan = getRootSpan(activeSpan); + const { scope } = getCapturedScopesOnSpan(rootSpan); + setCapturedScopesOnSpan(rootSpan, scope ?? new Scope(), isolationScope); - const isolationScope = commonObjectToIsolationScope(headers); + // We mark the root span as an app router span so we can allow-list it in our span processor that would normally filter out all Next.js transactions/spans + rootSpan.setAttribute('sentry.rsc', true); + } - return withIsolationScope(isolationScope, () => { - return withScope(scope => { - scope.setTransactionName(`${componentType}.${generationFunctionIdentifier} (${componentRoute})`); + let data: Record | undefined = undefined; + if (getClient()?.getOptions().sendDefaultPii) { + const props: unknown = args[0]; + const params = props && typeof props === 'object' && 'params' in props ? props.params : undefined; + const searchParams = + props && typeof props === 'object' && 'searchParams' in props ? props.searchParams : undefined; + data = { params, searchParams }; + } - isolationScope.setSDKProcessingMetadata({ - request: { - headers: headersDict, - }, - }); + const headersDict = headers ? winterCGHeadersToDict(headers) : undefined; - const propagationContext = commonObjectToPropagationContext( - headers, - headersDict?.['sentry-trace'] - ? propagationContextFromHeaders(headersDict['sentry-trace'], headersDict['baggage']) - : { - traceId: requestTraceId || uuid4(), - spanId: uuid4().substring(16), - }, - ); + return withIsolationScope(isolationScope, () => { + return withScope(scope => { + scope.setTransactionName(`${componentType}.${generationFunctionIdentifier} (${componentRoute})`); - scope.setExtra('route_data', data); - scope.setPropagationContext(propagationContext); + isolationScope.setSDKProcessingMetadata({ + request: { + headers: headersDict, + }, + }); - return startSpanManual( - { - op: 'function.nextjs', - name: `${componentType}.${generationFunctionIdentifier} (${componentRoute})`, - forceTransaction: true, - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs', + const propagationContext = commonObjectToPropagationContext( + headers, + headersDict?.['sentry-trace'] + ? propagationContextFromHeaders(headersDict['sentry-trace'], headersDict['baggage']) + : { + traceId: requestTraceId || uuid4(), + spanId: uuid4().substring(16), }, + ); + scope.setPropagationContext(propagationContext); + + scope.setExtra('route_data', data); + + return startSpanManual( + { + op: 'function.nextjs', + name: `${componentType}.${generationFunctionIdentifier} (${componentRoute})`, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs', }, - span => { - return handleCallbackErrors( - () => originalFunction.apply(thisArg, args), - err => { - if (isNotFoundNavigationError(err)) { - // We don't want to report "not-found"s - span.setStatus({ code: SPAN_STATUS_ERROR, message: 'not_found' }); - } else if (isRedirectNavigationError(err)) { - // We don't want to report redirects - span.setStatus({ code: SPAN_STATUS_OK }); - } else { - span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); - captureException(err, { - mechanism: { - handled: false, - }, - }); - } - }, - () => { - span.end(); - }, - ); - }, - ); - }); + }, + span => { + return handleCallbackErrors( + () => originalFunction.apply(thisArg, args), + err => { + // When you read this code you might think: "Wait a minute, shouldn't we set the status on the root span too?" + // The answer is: "No." - The status of the root span is determined by whatever status code Next.js decides to put on the response. + if (isNotFoundNavigationError(err)) { + // We don't want to report "not-found"s + span.setStatus({ code: SPAN_STATUS_ERROR, message: 'not_found' }); + getRootSpan(span).setStatus({ code: SPAN_STATUS_ERROR, message: 'not_found' }); + } else if (isRedirectNavigationError(err)) { + // We don't want to report redirects + span.setStatus({ code: SPAN_STATUS_OK }); + } else { + span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); + getRootSpan(span).setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); + captureException(err, { + mechanism: { + handled: false, + }, + }); + } + }, + () => { + span.end(); + }, + ); + }, + ); }); }); }, diff --git a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts index 0d1e224bdf47..e8d734a90ff3 100644 --- a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts +++ b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts @@ -2,9 +2,13 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, SPAN_STATUS_ERROR, SPAN_STATUS_OK, + Scope, captureException, getActiveSpan, + getCapturedScopesOnSpan, + getRootSpan, handleCallbackErrors, + setCapturedScopesOnSpan, startSpanManual, withIsolationScope, withScope, @@ -15,11 +19,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { isNotFoundNavigationError, isRedirectNavigationError } from '../common/nextNavigationErrorUtils'; import type { ServerComponentContext } from '../common/types'; import { flushSafelyWithTimeout } from './utils/responseEnd'; -import { - commonObjectToIsolationScope, - commonObjectToPropagationContext, - escapeNextjsTracing, -} from './utils/tracingUtils'; +import { commonObjectToIsolationScope, commonObjectToPropagationContext } from './utils/tracingUtils'; import { vercelWaitUntil } from './utils/vercelWaitUntil'; /** @@ -37,21 +37,31 @@ export function wrapServerComponentWithSentry any> return new Proxy(appDirComponent, { apply: (originalFunction, thisArg, args) => { const requestTraceId = getActiveSpan()?.spanContext().traceId; - return escapeNextjsTracing(() => { - const isolationScope = commonObjectToIsolationScope(context.headers); + const isolationScope = commonObjectToIsolationScope(context.headers); - const headersDict = context.headers ? winterCGHeadersToDict(context.headers) : undefined; + const activeSpan = getActiveSpan(); + if (activeSpan) { + const rootSpan = getRootSpan(activeSpan); + const { scope } = getCapturedScopesOnSpan(rootSpan); + setCapturedScopesOnSpan(rootSpan, scope ?? new Scope(), isolationScope); - isolationScope.setSDKProcessingMetadata({ - request: { - headers: headersDict, - }, - }); + // We mark the root span as an app router span so we can allow-list it in our span processor that would normally filter out all Next.js transactions/spans + rootSpan.setAttribute('sentry.rsc', true); + } - return withIsolationScope(isolationScope, () => { - return withScope(scope => { - scope.setTransactionName(`${componentType} Server Component (${componentRoute})`); + const headersDict = context.headers ? winterCGHeadersToDict(context.headers) : undefined; + + isolationScope.setSDKProcessingMetadata({ + request: { + headers: headersDict, + }, + }); + return withIsolationScope(isolationScope, () => { + return withScope(scope => { + scope.setTransactionName(`${componentType} Server Component (${componentRoute})`); + + if (process.env.NEXT_RUNTIME === 'edge') { const propagationContext = commonObjectToPropagationContext( context.headers, headersDict?.['sentry-trace'] @@ -63,43 +73,45 @@ export function wrapServerComponentWithSentry any> ); scope.setPropagationContext(propagationContext); - return startSpanManual( - { - op: 'function.nextjs', - name: `${componentType} Server Component (${componentRoute})`, - forceTransaction: true, - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs', - }, - }, - span => { - return handleCallbackErrors( - () => originalFunction.apply(thisArg, args), - error => { - if (isNotFoundNavigationError(error)) { - // We don't want to report "not-found"s - span.setStatus({ code: SPAN_STATUS_ERROR, message: 'not_found' }); - } else if (isRedirectNavigationError(error)) { - // We don't want to report redirects - span.setStatus({ code: SPAN_STATUS_OK }); - } else { - span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); - captureException(error, { - mechanism: { - handled: false, - }, - }); - } - }, - () => { - span.end(); - vercelWaitUntil(flushSafelyWithTimeout()); - }, - ); + } + + return startSpanManual( + { + op: 'function.nextjs', + name: `${componentType} Server Component (${componentRoute})`, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs', }, - ); - }); + }, + span => { + return handleCallbackErrors( + () => originalFunction.apply(thisArg, args), + error => { + // When you read this code you might think: "Wait a minute, shouldn't we set the status on the root span too?" + // The answer is: "No." - The status of the root span is determined by whatever status code Next.js decides to put on the response. + if (isNotFoundNavigationError(error)) { + // We don't want to report "not-found"s + span.setStatus({ code: SPAN_STATUS_ERROR, message: 'not_found' }); + } else if (isRedirectNavigationError(error)) { + // We don't want to report redirects + span.setStatus({ code: SPAN_STATUS_OK }); + } else { + span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); + captureException(error, { + mechanism: { + handled: false, + }, + }); + } + }, + () => { + span.end(); + vercelWaitUntil(flushSafelyWithTimeout()); + }, + ); + }, + ); }); }); }, diff --git a/packages/nextjs/src/server/httpIntegration.ts b/packages/nextjs/src/server/httpIntegration.ts deleted file mode 100644 index 4fdc615deb92..000000000000 --- a/packages/nextjs/src/server/httpIntegration.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; -import { httpIntegration as originalHttpIntegration } from '@sentry/node'; -import type { IntegrationFn } from '@sentry/types'; - -/** - * Next.js handles incoming requests itself, - * but it does not handle outgoing requests. - * Today, it is not possible to use the HttpInstrumentation for only outgoing requests - - * until https://github.com/open-telemetry/opentelemetry-js/pull/4643 is merged & released. - * So in the meanwhile, we extend the base HttpInstrumentation to not wrap incoming requests. - */ -class CustomNextjsHttpIntegration extends HttpInstrumentation { - // Instead of the default behavior, we just don't do any wrapping for incoming requests - protected _getPatchIncomingRequestFunction(_component: 'http' | 'https') { - return ( - original: (event: string, ...args: unknown[]) => boolean, - ): ((this: unknown, event: string, ...args: unknown[]) => boolean) => { - return function incomingRequest(this: unknown, event: string, ...args: unknown[]): boolean { - return original.apply(this, [event, ...args]); - }; - }; - } -} - -interface HttpOptions { - /** - * Whether breadcrumbs should be recorded for requests. - * Defaults to true - */ - breadcrumbs?: boolean; - - /** - * Do not capture spans or breadcrumbs for outgoing HTTP requests to URLs where the given callback returns `true`. - * This controls both span & breadcrumb creation - spans will be non recording if tracing is disabled. - */ - ignoreOutgoingRequests?: (url: string) => boolean; -} - -/** - * The http integration instruments Node's internal http and https modules. - * It creates breadcrumbs and spans for outgoing HTTP requests which will be attached to the currently active span. - */ -export const httpIntegration = ((options: HttpOptions = {}) => { - return originalHttpIntegration({ - ...options, - _instrumentation: CustomNextjsHttpIntegration, - }); -}) satisfies IntegrationFn; diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index 741c4092c61b..1132a6e1eed2 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -1,8 +1,17 @@ -import { applySdkMetadata, getClient, getGlobalScope } from '@sentry/core'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + applySdkMetadata, + getClient, + getGlobalScope, + getRootSpan, + spanToJSON, +} from '@sentry/core'; import { getDefaultIntegrations, init as nodeInit } from '@sentry/node'; import type { NodeClient, NodeOptions } from '@sentry/node'; import { GLOBAL_OBJ, logger } from '@sentry/utils'; +import { SEMATTRS_HTTP_METHOD, SEMATTRS_HTTP_ROUTE, SEMATTRS_HTTP_TARGET } from '@opentelemetry/semantic-conventions'; +import type { EventProcessor } from '@sentry/types'; import { DEBUG_BUILD } from '../common/debug-build'; import { devErrorSymbolicationEventProcessor } from '../common/devErrorSymbolicationEventProcessor'; import { getVercelEnv } from '../common/getVercelEnv'; @@ -10,10 +19,6 @@ import { isBuild } from '../common/utils/isBuild'; import { distDirRewriteFramesIntegration } from './distDirRewriteFramesIntegration'; export * from '@sentry/node'; -import type { EventProcessor } from '@sentry/types'; -import { httpIntegration } from './httpIntegration'; - -export { httpIntegration }; export { captureUnderscoreErrorException } from '../common/_error'; @@ -86,14 +91,7 @@ export function init(options: NodeOptions): NodeClient | undefined { return; } - const customDefaultIntegrations = [ - ...getDefaultIntegrations(options).filter( - integration => - // Next.js comes with its own Http instrumentation for OTel which would lead to double spans for route handler requests - integration.name !== 'Http', - ), - httpIntegration(), - ]; + const customDefaultIntegrations = getDefaultIntegrations(options); // Turn off Next.js' own fetch instrumentation // https://github.com/lforst/nextjs-fork/blob/1994fd186defda77ad971c36dc3163db263c993f/packages/next/src/server/lib/patch-fetch.ts#L245 @@ -128,8 +126,14 @@ export function init(options: NodeOptions): NodeClient | undefined { applySdkMetadata(opts, 'nextjs', ['nextjs', 'node']); const client = nodeInit(opts); - client?.on('beforeSampling', ({ spanAttributes, spanName, parentSampled, parentContext }, samplingDecision) => { + // We allowlist the "BaseServer.handleRequest" span, since that one is responsible for App Router requests, which are actually useful for us. + // HOWEVER, that span is not only responsible for App Router requests, which is why we additionally filter for certain transactions in an + // event processor further below. + if (spanAttributes['next.span_type'] === 'BaseServer.handleRequest') { + return; + } + // If we encounter a span emitted by Next.js, we do not want to sample it // The reason for this is that the data quality of the spans varies, it is different per version of Next, // and we need to keep our manual instrumentation around for the edge runtime anyhow. @@ -140,6 +144,46 @@ export function init(options: NodeOptions): NodeClient | undefined { ) { samplingDecision.decision = false; } + + // There are situations where the Next.js Node.js server forwards requests for the Edge Runtime server (e.g. in + // middleware) and this causes spans for Sentry ingest requests to be created. These are not exempt from our tracing + // because we didn't get the chance to do `suppressTracing`, since this happens outside of userland. + // We need to drop these spans. + if ( + typeof spanAttributes[SEMATTRS_HTTP_TARGET] === 'string' && + spanAttributes[SEMATTRS_HTTP_TARGET].includes('sentry_key') && + spanAttributes[SEMATTRS_HTTP_TARGET].includes('sentry_client') + ) { + samplingDecision.decision = false; + } + }); + + client?.on('spanStart', span => { + const spanAttributes = spanToJSON(span).data; + + // What we do in this glorious piece of code, is hoist any information about parameterized routes from spans emitted + // by Next.js via the `next.route` attribute, up to the transaction by setting the http.route attribute. + if (spanAttributes?.['next.route']) { + const rootSpan = getRootSpan(span); + const rootSpanAttributes = spanToJSON(rootSpan).data; + + // Only hoist the http.route attribute if the transaction doesn't already have it + if (rootSpanAttributes?.[SEMATTRS_HTTP_METHOD] && !rootSpanAttributes?.[SEMATTRS_HTTP_ROUTE]) { + rootSpan.setAttribute(SEMATTRS_HTTP_ROUTE, spanAttributes['next.route']); + } + } + + // We want to skip span data inference for any spans generated by Next.js. Reason being that Next.js emits spans + // with patterns (e.g. http.server spans) that will produce confusing data. + if (spanAttributes?.['next.span_type'] !== undefined) { + span.setAttribute('sentry.skip_span_data_inference', true); + } + + // We want to rename these spans because they look like "GET /path/to/route" and we already emit spans that look + // like this with our own http instrumentation. + if (spanAttributes?.['next.span_type'] === 'BaseServer.handleRequest') { + span.updateName('next server handler'); // This is all lowercase because the spans that Next.js emits by itself generally look like this. + } }); getGlobalScope().addEventProcessor( @@ -153,6 +197,15 @@ export function init(options: NodeOptions): NodeClient | undefined { return null; } + // We only want to use our HTTP integration/instrumentation for app router requests, which are marked with the `sentry.rsc` attribute. + if ( + (event.contexts?.trace?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] === 'auto.http.otel.http' || + event.contexts?.trace?.data?.['next.span_type'] === 'BaseServer.handleRequest') && + event.contexts?.trace?.data?.['sentry.rsc'] !== true + ) { + return null; + } + // Filter out transactions for requests to the tunnel route if ( globalWithInjectedValues.__sentryRewritesTunnelPath__ && diff --git a/packages/node/package.json b/packages/node/package.json index 446b3bfd1228..81849e43a8d7 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/node", - "version": "8.14.0", + "version": "8.16.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", @@ -69,35 +69,35 @@ "@opentelemetry/context-async-hooks": "^1.25.1", "@opentelemetry/core": "^1.25.1", "@opentelemetry/instrumentation": "^0.52.1", - "@opentelemetry/instrumentation-connect": "0.37.0", - "@opentelemetry/instrumentation-express": "0.40.1", - "@opentelemetry/instrumentation-fastify": "0.37.0", - "@opentelemetry/instrumentation-graphql": "0.41.0", - "@opentelemetry/instrumentation-hapi": "0.39.0", + "@opentelemetry/instrumentation-connect": "0.38.0", + "@opentelemetry/instrumentation-express": "0.41.0", + "@opentelemetry/instrumentation-fastify": "0.38.0", + "@opentelemetry/instrumentation-graphql": "0.42.0", + "@opentelemetry/instrumentation-hapi": "0.40.0", "@opentelemetry/instrumentation-http": "0.52.1", - "@opentelemetry/instrumentation-ioredis": "0.41.0", - "@opentelemetry/instrumentation-koa": "0.41.0", - "@opentelemetry/instrumentation-mongodb": "0.45.0", - "@opentelemetry/instrumentation-mongoose": "0.39.0", - "@opentelemetry/instrumentation-mysql": "0.39.0", - "@opentelemetry/instrumentation-mysql2": "0.39.0", - "@opentelemetry/instrumentation-nestjs-core": "0.38.0", - "@opentelemetry/instrumentation-pg": "0.42.0", - "@opentelemetry/instrumentation-redis-4": "0.40.0", + "@opentelemetry/instrumentation-ioredis": "0.42.0", + "@opentelemetry/instrumentation-koa": "0.42.0", + "@opentelemetry/instrumentation-mongodb": "0.46.0", + "@opentelemetry/instrumentation-mongoose": "0.40.0", + "@opentelemetry/instrumentation-mysql": "0.40.0", + "@opentelemetry/instrumentation-mysql2": "0.40.0", + "@opentelemetry/instrumentation-nestjs-core": "0.39.0", + "@opentelemetry/instrumentation-pg": "0.43.0", + "@opentelemetry/instrumentation-redis-4": "0.41.0", "@opentelemetry/resources": "^1.25.1", "@opentelemetry/sdk-trace-base": "^1.25.1", "@opentelemetry/semantic-conventions": "^1.25.1", "@prisma/instrumentation": "5.16.1", - "@sentry/core": "8.14.0", - "@sentry/opentelemetry": "8.14.0", - "@sentry/types": "8.14.0", - "@sentry/utils": "8.14.0" + "@sentry/core": "8.16.0", + "@sentry/opentelemetry": "8.16.0", + "@sentry/types": "8.16.0", + "@sentry/utils": "8.16.0" }, "devDependencies": { "@types/node": "^14.18.0" }, "optionalDependencies": { - "opentelemetry-instrumentation-fetch-node": "1.2.0" + "opentelemetry-instrumentation-fetch-node": "1.2.3" }, "scripts": { "build": "run-p build:transpile build:types", diff --git a/packages/node/src/integrations/http.ts b/packages/node/src/integrations/http.ts index c135c32816a2..418fa8aa7853 100644 --- a/packages/node/src/integrations/http.ts +++ b/packages/node/src/integrations/http.ts @@ -43,6 +43,25 @@ interface HttpOptions { */ ignoreIncomingRequests?: (url: string) => boolean; + /** + * Additional instrumentation options that are passed to the underlying HttpInstrumentation. + */ + instrumentation?: { + requestHook?: (span: Span, req: ClientRequest | HTTPModuleRequestIncomingMessage) => void; + responseHook?: (span: Span, response: HTTPModuleRequestIncomingMessage | ServerResponse) => void; + applyCustomAttributesOnSpan?: ( + span: Span, + request: ClientRequest | HTTPModuleRequestIncomingMessage, + response: HTTPModuleRequestIncomingMessage | ServerResponse, + ) => void; + + /** + * You can pass any configuration through to the underlying instrumention. + * Note that there are no semver guarantees for this! + */ + _experimentalConfig?: ConstructorParameters[0]; + }; + /** Allows to pass a custom version of HttpInstrumentation. We use this for Next.js. */ _instrumentation?: typeof HttpInstrumentation; } @@ -63,6 +82,7 @@ export const instrumentHttp = Object.assign( const _InstrumentationClass = _httpOptions._instrumentation || HttpInstrumentation; _httpInstrumentation = new _InstrumentationClass({ + ..._httpOptions.instrumentation?._experimentalConfig, ignoreOutgoingRequestHook: request => { const url = getRequestUrl(request); @@ -107,6 +127,7 @@ export const instrumentHttp = Object.assign( // both, incoming requests and "client" requests made within the app trigger the requestHook // we only want to isolate and further annotate incoming requests (IncomingMessage) if (_isClientRequest(req)) { + _httpOptions.instrumentation?.requestHook?.(span, req); return; } @@ -134,17 +155,21 @@ export const instrumentHttp = Object.assign( const bestEffortTransactionName = `${httpMethod} ${httpTarget}`; isolationScope.setTransactionName(bestEffortTransactionName); + + _httpOptions.instrumentation?.requestHook?.(span, req); }, - responseHook: () => { + responseHook: (span, res) => { const client = getClient(); if (client && client.getOptions().autoSessionTracking) { setImmediate(() => { client['_captureRequestSession'](); }); } + + _httpOptions.instrumentation?.responseHook?.(span, res); }, applyCustomAttributesOnSpan: ( - _span: Span, + span: Span, request: ClientRequest | HTTPModuleRequestIncomingMessage, response: HTTPModuleRequestIncomingMessage | ServerResponse, ) => { @@ -152,6 +177,8 @@ export const instrumentHttp = Object.assign( if (_breadcrumbs) { _addRequestBreadcrumb(request, response); } + + _httpOptions.instrumentation?.applyCustomAttributesOnSpan?.(span, request, response); }, }); diff --git a/packages/node/src/integrations/tracing/koa.ts b/packages/node/src/integrations/tracing/koa.ts index 071238eb7094..7db0225b6fc8 100644 --- a/packages/node/src/integrations/tracing/koa.ts +++ b/packages/node/src/integrations/tracing/koa.ts @@ -30,7 +30,8 @@ export const instrumentKoa = generateInstrumentOnce( } const attributes = spanToJSON(span).data; const route = attributes && attributes[SEMATTRS_HTTP_ROUTE]; - const method = info.context.request.method.toUpperCase() || 'GET'; + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const method: string = info?.context?.request?.method?.toUpperCase() || 'GET'; if (route) { getIsolationScope().setTransactionName(`${method} ${route}`); } diff --git a/packages/node/src/sdk/index.ts b/packages/node/src/sdk/index.ts index 4c30ba2bd2cf..2ac62a2b5b91 100644 --- a/packages/node/src/sdk/index.ts +++ b/packages/node/src/sdk/index.ts @@ -113,12 +113,12 @@ export function initWithoutDefaultIntegrations(options: NodeOptions | undefined * Initialize Sentry for Node, without performance instrumentation. */ function _init( - options: NodeOptions | undefined = {}, + _options: NodeOptions | undefined = {}, getDefaultIntegrationsImpl: (options: Options) => Integration[], ): NodeClient { - const clientOptions = getClientOptions(options, getDefaultIntegrationsImpl); + const options = getClientOptions(_options, getDefaultIntegrationsImpl); - if (clientOptions.debug === true) { + if (options.debug === true) { if (DEBUG_BUILD) { logger.enable(); } else { @@ -139,7 +139,7 @@ function _init( const scope = getCurrentScope(); scope.update(options.initialScope); - const client = new NodeClient(clientOptions); + const client = new NodeClient(options); // The client is on the current scope, from where it generally is inherited getCurrentScope().setClient(client); diff --git a/packages/node/test/integrations/express.test.ts b/packages/node/test/integrations/express.test.ts index 592ab7677db0..3071dce968d5 100644 --- a/packages/node/test/integrations/express.test.ts +++ b/packages/node/test/integrations/express.test.ts @@ -74,7 +74,7 @@ describe('expressErrorHandler()', () => { }); it('autoSessionTracking is enabled + requestHandler is not used -> does not set requestSession status on Crash', done => { - const options = getDefaultNodeClientOptions({ autoSessionTracking: false, release: '3.3' }); + const options = getDefaultNodeClientOptions({ autoSessionTracking: true, release: '3.3' }); client = new NodeClient(options); setCurrentClient(client); diff --git a/packages/node/test/sdk/init.test.ts b/packages/node/test/sdk/init.test.ts index 235cb8d23b86..49246f4284ae 100644 --- a/packages/node/test/sdk/init.test.ts +++ b/packages/node/test/sdk/init.test.ts @@ -1,6 +1,6 @@ import type { Integration } from '@sentry/types'; -import { getClient } from '../../src/'; +import { getClient, getIsolationScope } from '../../src/'; import * as auto from '../../src/integrations/tracing'; import { init } from '../../src/sdk'; import { NodeClient } from '../../src/sdk/client'; @@ -34,112 +34,162 @@ describe('init()', () => { jest.clearAllMocks(); }); - it("doesn't install default integrations if told not to", () => { - init({ dsn: PUBLIC_DSN, defaultIntegrations: false }); + describe('integrations', () => { + it("doesn't install default integrations if told not to", () => { + init({ dsn: PUBLIC_DSN, defaultIntegrations: false }); - const client = getClient(); + const client = getClient(); - expect(client?.getOptions()).toEqual( - expect.objectContaining({ - integrations: [], - }), - ); + expect(client?.getOptions()).toEqual( + expect.objectContaining({ + integrations: [], + }), + ); - expect(mockAutoPerformanceIntegrations).toHaveBeenCalledTimes(0); - }); + expect(mockAutoPerformanceIntegrations).toHaveBeenCalledTimes(0); + }); - it('installs merged default integrations, with overrides provided through options', () => { - const mockDefaultIntegrations = [ - new MockIntegration('Some mock integration 2.1'), - new MockIntegration('Some mock integration 2.2'), - ]; + it('installs merged default integrations, with overrides provided through options', () => { + const mockDefaultIntegrations = [ + new MockIntegration('Some mock integration 2.1'), + new MockIntegration('Some mock integration 2.2'), + ]; - const mockIntegrations = [ - new MockIntegration('Some mock integration 2.1'), - new MockIntegration('Some mock integration 2.3'), - ]; + const mockIntegrations = [ + new MockIntegration('Some mock integration 2.1'), + new MockIntegration('Some mock integration 2.3'), + ]; - init({ dsn: PUBLIC_DSN, integrations: mockIntegrations, defaultIntegrations: mockDefaultIntegrations }); + init({ dsn: PUBLIC_DSN, integrations: mockIntegrations, defaultIntegrations: mockDefaultIntegrations }); - expect(mockDefaultIntegrations[0]?.setupOnce as jest.Mock).toHaveBeenCalledTimes(0); - expect(mockDefaultIntegrations[1]?.setupOnce as jest.Mock).toHaveBeenCalledTimes(1); - expect(mockIntegrations[0]?.setupOnce as jest.Mock).toHaveBeenCalledTimes(1); - expect(mockIntegrations[1]?.setupOnce as jest.Mock).toHaveBeenCalledTimes(1); - expect(mockAutoPerformanceIntegrations).toHaveBeenCalledTimes(0); - }); + expect(mockDefaultIntegrations[0]?.setupOnce as jest.Mock).toHaveBeenCalledTimes(0); + expect(mockDefaultIntegrations[1]?.setupOnce as jest.Mock).toHaveBeenCalledTimes(1); + expect(mockIntegrations[0]?.setupOnce as jest.Mock).toHaveBeenCalledTimes(1); + expect(mockIntegrations[1]?.setupOnce as jest.Mock).toHaveBeenCalledTimes(1); + expect(mockAutoPerformanceIntegrations).toHaveBeenCalledTimes(0); + }); - it('installs integrations returned from a callback function', () => { - const mockDefaultIntegrations = [ - new MockIntegration('Some mock integration 3.1'), - new MockIntegration('Some mock integration 3.2'), - ]; - - const newIntegration = new MockIntegration('Some mock integration 3.3'); - - init({ - dsn: PUBLIC_DSN, - defaultIntegrations: mockDefaultIntegrations, - integrations: integrations => { - const newIntegrations = [...integrations]; - newIntegrations[1] = newIntegration; - return newIntegrations; - }, + it('installs integrations returned from a callback function', () => { + const mockDefaultIntegrations = [ + new MockIntegration('Some mock integration 3.1'), + new MockIntegration('Some mock integration 3.2'), + ]; + + const newIntegration = new MockIntegration('Some mock integration 3.3'); + + init({ + dsn: PUBLIC_DSN, + defaultIntegrations: mockDefaultIntegrations, + integrations: integrations => { + const newIntegrations = [...integrations]; + newIntegrations[1] = newIntegration; + return newIntegrations; + }, + }); + + expect(mockDefaultIntegrations[0]?.setupOnce as jest.Mock).toHaveBeenCalledTimes(1); + expect(mockDefaultIntegrations[1]?.setupOnce as jest.Mock).toHaveBeenCalledTimes(0); + expect(newIntegration.setupOnce as jest.Mock).toHaveBeenCalledTimes(1); + expect(mockAutoPerformanceIntegrations).toHaveBeenCalledTimes(0); }); - expect(mockDefaultIntegrations[0]?.setupOnce as jest.Mock).toHaveBeenCalledTimes(1); - expect(mockDefaultIntegrations[1]?.setupOnce as jest.Mock).toHaveBeenCalledTimes(0); - expect(newIntegration.setupOnce as jest.Mock).toHaveBeenCalledTimes(1); - expect(mockAutoPerformanceIntegrations).toHaveBeenCalledTimes(0); + it('installs performance default instrumentations if tracing is enabled', () => { + const autoPerformanceIntegration = new MockIntegration('Some mock integration 4.4'); + + mockAutoPerformanceIntegrations.mockReset().mockImplementation(() => [autoPerformanceIntegration]); + + const mockIntegrations = [ + new MockIntegration('Some mock integration 4.1'), + new MockIntegration('Some mock integration 4.3'), + ]; + + init({ + dsn: PUBLIC_DSN, + integrations: mockIntegrations, + enableTracing: true, + }); + + expect(mockIntegrations[0]?.setupOnce as jest.Mock).toHaveBeenCalledTimes(1); + expect(mockIntegrations[1]?.setupOnce as jest.Mock).toHaveBeenCalledTimes(1); + expect(autoPerformanceIntegration.setupOnce as jest.Mock).toHaveBeenCalledTimes(1); + expect(mockAutoPerformanceIntegrations).toHaveBeenCalledTimes(1); + + const client = getClient(); + expect(client?.getOptions()).toEqual( + expect.objectContaining({ + integrations: expect.arrayContaining([mockIntegrations[0], mockIntegrations[1], autoPerformanceIntegration]), + }), + ); + }); }); - it('installs performance default instrumentations if tracing is enabled', () => { - const autoPerformanceIntegration = new MockIntegration('Some mock integration 4.4'); + describe('OpenTelemetry', () => { + it('sets up OpenTelemetry by default', () => { + init({ dsn: PUBLIC_DSN }); + + const client = getClient(); - mockAutoPerformanceIntegrations.mockReset().mockImplementation(() => [autoPerformanceIntegration]); + expect(client?.traceProvider).toBeDefined(); + }); - const mockIntegrations = [ - new MockIntegration('Some mock integration 4.1'), - new MockIntegration('Some mock integration 4.3'), - ]; + it('allows to opt-out of OpenTelemetry setup', () => { + init({ dsn: PUBLIC_DSN, skipOpenTelemetrySetup: true }); - init({ - dsn: PUBLIC_DSN, - integrations: mockIntegrations, - enableTracing: true, + const client = getClient(); + + expect(client?.traceProvider).not.toBeDefined(); }); + }); + + it('returns intiated client', () => { + const client = init({ dsn: PUBLIC_DSN, skipOpenTelemetrySetup: true }); - expect(mockIntegrations[0]?.setupOnce as jest.Mock).toHaveBeenCalledTimes(1); - expect(mockIntegrations[1]?.setupOnce as jest.Mock).toHaveBeenCalledTimes(1); - expect(autoPerformanceIntegration.setupOnce as jest.Mock).toHaveBeenCalledTimes(1); - expect(mockAutoPerformanceIntegrations).toHaveBeenCalledTimes(1); - - const client = getClient(); - expect(client?.getOptions()).toEqual( - expect.objectContaining({ - integrations: expect.arrayContaining([mockIntegrations[0], mockIntegrations[1], autoPerformanceIntegration]), - }), - ); + expect(client).toBeInstanceOf(NodeClient); }); - it('sets up OpenTelemetry by default', () => { - init({ dsn: PUBLIC_DSN }); + describe('autoSessionTracking', () => { + it('does not track session by default if no release is set', () => { + // On CI, we always infer the release, so this does not work + if (process.env.CI) { + return; + } + init({ dsn: PUBLIC_DSN }); - const client = getClient(); + const session = getIsolationScope().getSession(); + expect(session).toBeUndefined(); + }); - expect(client?.traceProvider).toBeDefined(); - }); + it('tracks session by default if release is set', () => { + init({ dsn: PUBLIC_DSN, release: '1.2.3' }); - it('allows to opt-out of OpenTelemetry setup', () => { - init({ dsn: PUBLIC_DSN, skipOpenTelemetrySetup: true }); + const session = getIsolationScope().getSession(); + expect(session).toBeDefined(); + }); - const client = getClient(); + it('does not track session if no release is set even if autoSessionTracking=true', () => { + // On CI, we always infer the release, so this does not work + if (process.env.CI) { + return; + } - expect(client?.traceProvider).not.toBeDefined(); - }); + init({ dsn: PUBLIC_DSN, autoSessionTracking: true }); - it('returns intiated client', () => { - const client = init({ dsn: PUBLIC_DSN, skipOpenTelemetrySetup: true }); + const session = getIsolationScope().getSession(); + expect(session).toBeUndefined(); + }); - expect(client).toBeInstanceOf(NodeClient); + it('does not track session if autoSessionTracking=false', () => { + init({ dsn: PUBLIC_DSN, autoSessionTracking: false, release: '1.2.3' }); + + const session = getIsolationScope().getSession(); + expect(session).toBeUndefined(); + }); + + it('tracks session by default if autoSessionTracking=true & release is set', () => { + init({ dsn: PUBLIC_DSN, release: '1.2.3', autoSessionTracking: true }); + + const session = getIsolationScope().getSession(); + expect(session).toBeDefined(); + }); }); }); diff --git a/packages/node/test/sdk/scope.test.ts b/packages/node/test/sdk/scope.test.ts index 09f21aac067d..5e84e412f912 100644 --- a/packages/node/test/sdk/scope.test.ts +++ b/packages/node/test/sdk/scope.test.ts @@ -88,7 +88,11 @@ describe('Unit | Scope', () => { it('allows to set & get a client', () => { const scope = new Scope(); expect(scope.getClient()).toBeUndefined(); - const client = {} as Client; + const client = { + emit() { + // noop + }, + } as unknown as Client; scope.setClient(client); expect(scope.getClient()).toBe(client); }); @@ -108,7 +112,10 @@ describe('Unit | Scope', () => { getEventProcessors() { return [eventProcessor]; }, - } as Client; + emit() { + // noop + }, + } as unknown as Client; const processedEvent = await prepareEvent( options, event, diff --git a/packages/nuxt/README.md b/packages/nuxt/README.md index e33201b21518..c269bda64ba1 100644 --- a/packages/nuxt/README.md +++ b/packages/nuxt/README.md @@ -10,13 +10,14 @@ [![npm dm](https://img.shields.io/npm/dm/@sentry/nuxt.svg)](https://www.npmjs.com/package/@sentry/nuxt) [![npm dt](https://img.shields.io/npm/dt/@sentry/nuxt.svg)](https://www.npmjs.com/package/@sentry/nuxt) -**This SDK is under active development and not yet published!** +**This SDK is under active development! Feel free to already try it but expect breaking changes** ## Links todo: link official SDK docs -- [Official SDK Docs](https://docs.sentry.io/platforms/javascript/) +- [Official Browser SDK Docs](https://docs.sentry.io/platforms/javascript/) +- [Official Node SDK Docs](https://docs.sentry.io/platforms/node/) ## Compatibility @@ -37,7 +38,7 @@ Take a look at the sections below if you want to customize your SDK configuratio If the setup through the wizard doesn't work for you, you can also set up the SDK manually. -### 1. Prerequesits & Installation +### 1. Prerequisites & Installation 1. Install the Sentry Nuxt SDK: @@ -62,25 +63,31 @@ export default defineNuxtConfig({ }); ``` -2. Add a `sentry.client.config.(js|ts)` file to the root of your project: +### 3. Client-side setup + +Add a `sentry.client.config.(js|ts)` file to the root of your project: ```javascript import * as Sentry from '@sentry/nuxt'; -if (!import.meta.env.SSR) { - Sentry.init({ - dsn: env.DSN, - replaysSessionSampleRate: 0.1, - replaysOnErrorSampleRate: 1.0, - }); -} +Sentry.init({ + dsn: env.DSN, +}); ``` -### 3. Server-side Setup +### 4. Server-side setup + +Add a `sentry.server.config.(js|ts)` file to the root of your project: + +```javascript +import * as Sentry from '@sentry/nuxt'; -todo: add server-side setup +Sentry.init({ + dsn: env.DSN, +}); +``` -### 4. Vite Setup +### 5. Vite Setup todo: add vite setup diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index 141335277238..b99eef90471e 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/nuxt", - "version": "8.14.0", + "version": "8.16.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,14 +43,14 @@ }, "dependencies": { "@nuxt/kit": "^3.12.2", - "@sentry/browser": "8.14.0", - "@sentry/core": "8.14.0", - "@sentry/node": "8.14.0", - "@sentry/opentelemetry": "8.14.0", - "@sentry/types": "8.14.0", - "@sentry/utils": "8.14.0", + "@sentry/browser": "8.16.0", + "@sentry/core": "8.16.0", + "@sentry/node": "8.16.0", + "@sentry/opentelemetry": "8.16.0", + "@sentry/types": "8.16.0", + "@sentry/utils": "8.16.0", "@sentry/vite-plugin": "2.20.1", - "@sentry/vue": "8.14.0" + "@sentry/vue": "8.16.0" }, "devDependencies": { "@nuxt/module-builder": "0.8.0", diff --git a/packages/nuxt/rollup.npm.config.mjs b/packages/nuxt/rollup.npm.config.mjs index e800fdbba474..a672e9e43eb3 100644 --- a/packages/nuxt/rollup.npm.config.mjs +++ b/packages/nuxt/rollup.npm.config.mjs @@ -2,6 +2,6 @@ import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollu export default makeNPMConfigVariants( makeBaseNPMConfig({ - entrypoints: ['src/index.client.ts', 'src/client/index.ts'], + entrypoints: ['src/index.server.ts', 'src/index.client.ts', 'src/client/index.ts', 'src/server/index.ts'], }), ); diff --git a/packages/nuxt/src/common/snippets.ts b/packages/nuxt/src/common/snippets.ts deleted file mode 100644 index 5b8a3f1f3ea1..000000000000 --- a/packages/nuxt/src/common/snippets.ts +++ /dev/null @@ -1,47 +0,0 @@ -import * as fs from 'fs'; -import * as path from 'path'; - -/** Returns an import snippet */ -export function buildSdkInitFileImportSnippet(filePath: string): string { - const posixPath = filePath.split(path.sep).join(path.posix.sep); - - // normalize to forward slashed for Windows-based systems - const normalizedPath = posixPath.replace(/\\/g, '/'); - - return `import '${normalizedPath}';`; -} - -/** - * Script tag inside `nuxt-root.vue` (root component we get from NuxtApp) - */ -export const SCRIPT_TAG = '