diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ae4095e304ab..9577eb15aff7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -886,7 +886,13 @@ jobs: - uses: pnpm/action-setup@v4 with: version: 9.4.0 + - name: Set up Node for Angular 20 + if: matrix.test-application == 'angular-20' + uses: actions/setup-node@v4 + with: + node-version: '20.19.2' - name: Set up Node + if: matrix.test-application != 'angular-20' uses: actions/setup-node@v4 with: node-version-file: 'dev-packages/e2e-tests/package.json' diff --git a/CHANGELOG.md b/CHANGELOG.md index ceb36dfdaffc..1adce32abab6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,45 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 9.23.0 + +### Important changes + +- **feat(browser): option to ignore certain resource types ([#16389](https://github.com/getsentry/sentry-javascript/pull/16389))** + +Adds an option to opt out of certain `resource.*` spans via `ignoreResourceSpans`. + +For example, to opt out of `resource.script` spans: + +```js +Sentry.browserTracingIntegration({ + ignoreResourceSpans: ['resource.script'], +}), +``` + +### Other changes + +- feat: Export `isEnabled` from all SDKs ([#16405](https://github.com/getsentry/sentry-javascript/pull/16405)) +- feat(browser): Disable client when browser extension is detected in `init()` ([#16354](https://github.com/getsentry/sentry-javascript/pull/16354)) +- feat(core): Allow re-use of `captureLog` ([#16352](https://github.com/getsentry/sentry-javascript/pull/16352)) +- feat(core): Export `_INTERNAL_captureSerializedLog` ([#16387](https://github.com/getsentry/sentry-javascript/pull/16387)) +- feat(deps): bump @opentelemetry/semantic-conventions from 1.32.0 to 1.34.0 ([#16393](https://github.com/getsentry/sentry-javascript/pull/16393)) +- feat(deps): bump @prisma/instrumentation from 6.7.0 to 6.8.2 ([#16392](https://github.com/getsentry/sentry-javascript/pull/16392)) +- feat(deps): bump @sentry/cli from 2.43.0 to 2.45.0 ([#16395](https://github.com/getsentry/sentry-javascript/pull/16395)) +- feat(deps): bump @sentry/webpack-plugin from 3.3.1 to 3.5.0 ([#16394](https://github.com/getsentry/sentry-javascript/pull/16394)) +- feat(nextjs): Include `static/chunks/main-*` files for `widenClientFileUpload` ([#16406](https://github.com/getsentry/sentry-javascript/pull/16406)) +- feat(node): Do not add HTTP & fetch span instrumentation if tracing is disabled ([#15730](https://github.com/getsentry/sentry-javascript/pull/15730)) +- feat(nuxt): Added support for nuxt layers ([#16372](https://github.com/getsentry/sentry-javascript/pull/16372)) +- fix(browser): Ensure logs are flushed when sendClientReports=false ([#16351](https://github.com/getsentry/sentry-javascript/pull/16351)) +- fix(browser): Move `browserTracingIntegration` code to `setup` hook ([#16386](https://github.com/getsentry/sentry-javascript/pull/16386)) +- fix(cloudflare): Capture exceptions thrown in hono ([#16355](https://github.com/getsentry/sentry-javascript/pull/16355)) +- fix(node): Don't warn about Spotlight on empty NODE_ENV ([#16381](https://github.com/getsentry/sentry-javascript/pull/16381)) +- fix(node): Suppress Spotlight calls ([#16380](https://github.com/getsentry/sentry-javascript/pull/16380)) +- fix(nuxt): Add `@sentry/nuxt` as external in Rollup ([#16407](https://github.com/getsentry/sentry-javascript/pull/16407)) +- fix(opentelemetry): Ensure `withScope` keeps span active & `_getTraceInfoFromScope` works ([#16385](https://github.com/getsentry/sentry-javascript/pull/16385)) + +Work in this release was contributed by @Xenossolitarius. Thank you for your contribution! + ## 9.22.0 ### Important changes diff --git a/dev-packages/browser-integration-tests/suites/manual-client/skip-init-browser-extension/test.ts b/dev-packages/browser-integration-tests/suites/manual-client/skip-init-browser-extension/test.ts index 5098b4aa6552..d949e5d2b19e 100644 --- a/dev-packages/browser-integration-tests/suites/manual-client/skip-init-browser-extension/test.ts +++ b/dev-packages/browser-integration-tests/suites/manual-client/skip-init-browser-extension/test.ts @@ -18,7 +18,12 @@ sentryTest( return !!(window as any).Sentry.isInitialized(); }); - expect(isInitialized).toEqual(false); + const isEnabled = await page.evaluate(() => { + return !!(window as any).Sentry.getClient()?.getOptions().enabled; + }); + + expect(isInitialized).toEqual(true); + expect(isEnabled).toEqual(false); if (hasDebugLogs()) { expect(errorLogs.length).toEqual(1); diff --git a/dev-packages/browser-integration-tests/suites/manual-client/skip-init-chrome-extension/test.ts b/dev-packages/browser-integration-tests/suites/manual-client/skip-init-chrome-extension/test.ts index 411fbd2f9db8..83996a83aad7 100644 --- a/dev-packages/browser-integration-tests/suites/manual-client/skip-init-chrome-extension/test.ts +++ b/dev-packages/browser-integration-tests/suites/manual-client/skip-init-chrome-extension/test.ts @@ -16,7 +16,12 @@ sentryTest('should not initialize when inside a Chrome browser extension', async return !!(window as any).Sentry.isInitialized(); }); - expect(isInitialized).toEqual(false); + const isEnabled = await page.evaluate(() => { + return !!(window as any).Sentry.getClient()?.getOptions().enabled; + }); + + expect(isInitialized).toEqual(true); + expect(isEnabled).toEqual(false); if (hasDebugLogs()) { expect(errorLogs.length).toEqual(1); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/multiple-integrations/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/multiple-integrations/test.ts deleted file mode 100644 index 1789bdf76c12..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/multiple-integrations/test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { expect } from '@playwright/test'; -import { sentryTest } from '../../../../utils/fixtures'; -import { shouldSkipTracingTest } from '../../../../utils/helpers'; - -sentryTest('warns if multiple integrations are used', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } - - const msgs: string[] = []; - - page.on('console', msg => { - msgs.push(msg.text()); - }); - - const url = await getLocalTestUrl({ testDir: __dirname }); - - await page.goto(url); - - expect(msgs).toEqual(['Multiple browserTracingIntegration instances are not supported.']); -}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/resource-spans-ignored/assets/script.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/resource-spans-ignored/assets/script.js new file mode 100644 index 000000000000..eab583b75a75 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/resource-spans-ignored/assets/script.js @@ -0,0 +1,3 @@ +(() => { + // I do nothing. +})(); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/multiple-integrations/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/resource-spans-ignored/init.js similarity index 52% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/multiple-integrations/init.js rename to dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/resource-spans-ignored/init.js index 6d4dd43801b8..70c0b30a03a5 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/multiple-integrations/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/resource-spans-ignored/init.js @@ -4,6 +4,11 @@ window.Sentry = Sentry; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.browserTracingIntegration(), Sentry.browserTracingIntegration()], + integrations: [ + Sentry.browserTracingIntegration({ + ignoreResourceSpans: ['resource.script'], + idleTimeout: 9000, + }), + ], tracesSampleRate: 1, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/resource-spans-ignored/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/resource-spans-ignored/test.ts new file mode 100644 index 000000000000..4bc9621f8395 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/resource-spans-ignored/test.ts @@ -0,0 +1,20 @@ +import type { Route } from '@playwright/test'; +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/core'; +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest('should allow specific types of resource spans to be ignored.', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + await page.route('**/path/to/script.js', (route: Route) => route.fulfill({ path: `${__dirname}/assets/script.js` })); + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + const allSpans = eventData.spans?.filter(({ op }) => op?.startsWith('resource.script')); + + expect(allSpans?.length).toBe(0); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-immediate/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-immediate/init.js new file mode 100644 index 000000000000..0a301839c169 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-immediate/init.js @@ -0,0 +1,14 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.browserTracingIntegration()], + tracePropagationTargets: ['http://sentry-test-site.example'], + tracesSampleRate: 1, + autoSessionTracking: false, +}); + +// fetch directly after init +fetch('http://sentry-test-site.example/0'); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-immediate/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-immediate/test.ts new file mode 100644 index 000000000000..7d11168acb1a --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-immediate/test.ts @@ -0,0 +1,40 @@ +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../../utils/fixtures'; +import { + envelopeRequestParser, + shouldSkipTracingTest, + waitForTransactionRequestOnUrl, +} from '../../../../utils/helpers'; + +sentryTest('should create spans for fetch requests called directly after init', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + await page.route('http://sentry-test-site.example/*', route => route.fulfill({ body: 'ok' })); + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const req = await waitForTransactionRequestOnUrl(page, url); + const tracingEvent = envelopeRequestParser(req); + + const requestSpans = tracingEvent.spans?.filter(({ op }) => op === 'http.client'); + + expect(requestSpans).toHaveLength(1); + + expect(requestSpans![0]).toMatchObject({ + description: 'GET http://sentry-test-site.example/0', + parent_span_id: tracingEvent.contexts?.trace?.span_id, + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: tracingEvent.contexts?.trace?.trace_id, + data: { + 'http.method': 'GET', + 'http.url': 'http://sentry-test-site.example/0', + url: 'http://sentry-test-site.example/0', + 'server.address': 'sentry-test-site.example', + type: 'fetch', + }, + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/angular-20/.editorconfig b/dev-packages/e2e-tests/test-applications/angular-20/.editorconfig new file mode 100644 index 000000000000..f166060da1cb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-20/.editorconfig @@ -0,0 +1,17 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.ts] +quote_type = single +ij_typescript_use_double_quotes = false + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/dev-packages/e2e-tests/test-applications/angular-20/.gitignore b/dev-packages/e2e-tests/test-applications/angular-20/.gitignore new file mode 100644 index 000000000000..315c644a53e8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-20/.gitignore @@ -0,0 +1,44 @@ +# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings + +# System files +.DS_Store +Thumbs.db + +test-results diff --git a/dev-packages/e2e-tests/test-applications/angular-20/.npmrc b/dev-packages/e2e-tests/test-applications/angular-20/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-20/.npmrc @@ -0,0 +1,2 @@ +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/dev-packages/e2e-tests/test-applications/angular-20/README.md b/dev-packages/e2e-tests/test-applications/angular-20/README.md new file mode 100644 index 000000000000..5798a982a95c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-20/README.md @@ -0,0 +1,3 @@ +# Angular 20 + +E2E test app for Angular 20 and `@sentry/angular`. diff --git a/dev-packages/e2e-tests/test-applications/angular-20/angular.json b/dev-packages/e2e-tests/test-applications/angular-20/angular.json new file mode 100644 index 000000000000..09939b0f9b23 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-20/angular.json @@ -0,0 +1,87 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "angular-20": { + "projectType": "application", + "schematics": {}, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:application", + "options": { + "outputPath": "dist/angular-20", + "index": "src/index.html", + "browser": "src/main.ts", + "polyfills": ["zone.js"], + "tsConfig": "tsconfig.app.json", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": ["src/styles.css"], + "scripts": [] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kB", + "maximumError": "1MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "4kB", + "maximumError": "8kB" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "buildTarget": "angular-20:build:production" + }, + "development": { + "buildTarget": "angular-20:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n" + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "polyfills": ["zone.js", "zone.js/testing"], + "tsConfig": "tsconfig.spec.json", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": ["src/styles.css"], + "scripts": [] + } + } + } + } + } +} diff --git a/dev-packages/e2e-tests/test-applications/angular-20/package.json b/dev-packages/e2e-tests/test-applications/angular-20/package.json new file mode 100644 index 000000000000..a43fcaf412b5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-20/package.json @@ -0,0 +1,50 @@ +{ + "name": "angular-20", + "version": "0.0.0", + "scripts": { + "ng": "ng", + "dev": "ng serve", + "proxy": "node start-event-proxy.mjs", + "preview": "http-server dist/angular-20/browser --port 8080", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "test": "playwright test", + "test:build": "pnpm install && pnpm build", + "test:assert": "playwright test", + "clean": "npx rimraf .angular node_modules pnpm-lock.yaml dist" + }, + "private": true, + "dependencies": { + "@angular/animations": "^20.0.0-rc.2", + "@angular/common": "^20.0.0-rc.2", + "@angular/compiler": "^20.0.0-rc.2", + "@angular/core": "^20.0.0-rc.2", + "@angular/forms": "^20.0.0-rc.2", + "@angular/platform-browser": "^20.0.0-rc.2", + "@angular/platform-browser-dynamic": "^20.0.0-rc.2", + "@angular/router": "^20.0.0-rc.2", + "@sentry/angular": "* || latest", + "rxjs": "~7.8.0", + "tslib": "^2.3.0", + "zone.js": "~0.15.0" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^20.0.0-rc.2", + "@angular/cli": "^20.0.0-rc.2", + "@angular/compiler-cli": "^20.0.0-rc.2", + "@playwright/test": "~1.50.0", + "@sentry-internal/test-utils": "link:../../../test-utils", + "@types/jasmine": "~5.1.0", + "http-server": "^14.1.1", + "jasmine-core": "~5.4.0", + "karma": "~6.4.0", + "karma-chrome-launcher": "~3.2.0", + "karma-coverage": "~2.2.0", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.1.0", + "typescript": "~5.8.3" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/dev-packages/e2e-tests/test-applications/angular-20/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/angular-20/playwright.config.mjs new file mode 100644 index 000000000000..0845325879c9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-20/playwright.config.mjs @@ -0,0 +1,8 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; + +const config = getPlaywrightConfig({ + startCommand: `pnpm preview`, + port: 8080, +}); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/angular-20/public/favicon.ico b/dev-packages/e2e-tests/test-applications/angular-20/public/favicon.ico new file mode 100644 index 000000000000..57614f9c9675 Binary files /dev/null and b/dev-packages/e2e-tests/test-applications/angular-20/public/favicon.ico differ diff --git a/dev-packages/e2e-tests/test-applications/angular-20/src/app/app.component.ts b/dev-packages/e2e-tests/test-applications/angular-20/src/app/app.component.ts new file mode 100644 index 000000000000..e912fcc99b04 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-20/src/app/app.component.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; +import { RouterOutlet } from '@angular/router'; + +@Component({ + selector: 'app-root', + standalone: true, + imports: [RouterOutlet], + template: ``, +}) +export class AppComponent { + title = 'angular-20'; +} diff --git a/dev-packages/e2e-tests/test-applications/angular-20/src/app/app.config.ts b/dev-packages/e2e-tests/test-applications/angular-20/src/app/app.config.ts new file mode 100644 index 000000000000..f5cc30f3615b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-20/src/app/app.config.ts @@ -0,0 +1,29 @@ +import { + ApplicationConfig, + ErrorHandler, + inject, + provideAppInitializer, + provideZoneChangeDetection, +} from '@angular/core'; +import { Router, provideRouter } from '@angular/router'; + +import { TraceService, createErrorHandler } from '@sentry/angular'; +import { routes } from './app.routes'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideZoneChangeDetection({ eventCoalescing: true }), + provideRouter(routes), + { + provide: ErrorHandler, + useValue: createErrorHandler(), + }, + { + provide: TraceService, + deps: [Router], + }, + provideAppInitializer(() => { + inject(TraceService); + }), + ], +}; diff --git a/dev-packages/e2e-tests/test-applications/angular-20/src/app/app.routes.ts b/dev-packages/e2e-tests/test-applications/angular-20/src/app/app.routes.ts new file mode 100644 index 000000000000..24bf8b769051 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-20/src/app/app.routes.ts @@ -0,0 +1,42 @@ +import { Routes } from '@angular/router'; +import { cancelGuard } from './cancel-guard.guard'; +import { CancelComponent } from './cancel/cancel.components'; +import { ComponentTrackingComponent } from './component-tracking/component-tracking.components'; +import { HomeComponent } from './home/home.component'; +import { UserComponent } from './user/user.component'; + +export const routes: Routes = [ + { + path: 'users/:id', + component: UserComponent, + }, + { + path: 'home', + component: HomeComponent, + }, + { + path: 'cancel', + component: CancelComponent, + canActivate: [cancelGuard], + }, + { + path: 'component-tracking', + component: ComponentTrackingComponent, + }, + { + path: 'redirect1', + redirectTo: '/redirect2', + }, + { + path: 'redirect2', + redirectTo: '/redirect3', + }, + { + path: 'redirect3', + redirectTo: '/users/456', + }, + { + path: '**', + redirectTo: 'home', + }, +]; diff --git a/dev-packages/e2e-tests/test-applications/angular-20/src/app/cancel-guard.guard.ts b/dev-packages/e2e-tests/test-applications/angular-20/src/app/cancel-guard.guard.ts new file mode 100644 index 000000000000..16ec4a2ab164 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-20/src/app/cancel-guard.guard.ts @@ -0,0 +1,5 @@ +import { ActivatedRouteSnapshot, CanActivateFn, RouterStateSnapshot } from '@angular/router'; + +export const cancelGuard: CanActivateFn = (_next: ActivatedRouteSnapshot, _state: RouterStateSnapshot) => { + return false; +}; diff --git a/dev-packages/e2e-tests/test-applications/angular-20/src/app/cancel/cancel.components.ts b/dev-packages/e2e-tests/test-applications/angular-20/src/app/cancel/cancel.components.ts new file mode 100644 index 000000000000..b6ee1876e035 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-20/src/app/cancel/cancel.components.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-cancel', + standalone: true, + template: `
`, +}) +export class CancelComponent {} diff --git a/dev-packages/e2e-tests/test-applications/angular-20/src/app/component-tracking/component-tracking.components.ts b/dev-packages/e2e-tests/test-applications/angular-20/src/app/component-tracking/component-tracking.components.ts new file mode 100644 index 000000000000..a82e5b1acce6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-20/src/app/component-tracking/component-tracking.components.ts @@ -0,0 +1,21 @@ +import { AfterViewInit, Component, OnInit } from '@angular/core'; +import { TraceClass, TraceMethod, TraceModule } from '@sentry/angular'; +import { SampleComponent } from '../sample-component/sample-component.components'; + +@Component({ + selector: 'app-component-tracking', + standalone: true, + imports: [TraceModule, SampleComponent], + template: ` + + + `, +}) +@TraceClass({ name: 'ComponentTrackingComponent' }) +export class ComponentTrackingComponent implements OnInit, AfterViewInit { + @TraceMethod({ name: 'ngOnInit' }) + ngOnInit() {} + + @TraceMethod() + ngAfterViewInit() {} +} diff --git a/dev-packages/e2e-tests/test-applications/angular-20/src/app/home/home.component.ts b/dev-packages/e2e-tests/test-applications/angular-20/src/app/home/home.component.ts new file mode 100644 index 000000000000..033174fb0d8a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-20/src/app/home/home.component.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; +import { RouterLink } from '@angular/router'; + +@Component({ + selector: 'app-home', + standalone: true, + imports: [RouterLink], + template: ` +
+

Welcome to Sentry's Angular 20 E2E test app

+ + +
+ `, +}) +export class HomeComponent { + throwError() { + throw new Error('Error thrown from Angular 20 E2E test app'); + } +} diff --git a/dev-packages/e2e-tests/test-applications/angular-20/src/app/sample-component/sample-component.components.ts b/dev-packages/e2e-tests/test-applications/angular-20/src/app/sample-component/sample-component.components.ts new file mode 100644 index 000000000000..da09425c7565 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-20/src/app/sample-component/sample-component.components.ts @@ -0,0 +1,12 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-sample-component', + standalone: true, + template: `
Component
`, +}) +export class SampleComponent implements OnInit { + ngOnInit() { + console.log('SampleComponent'); + } +} diff --git a/dev-packages/e2e-tests/test-applications/angular-20/src/app/user/user.component.ts b/dev-packages/e2e-tests/test-applications/angular-20/src/app/user/user.component.ts new file mode 100644 index 000000000000..db02568d395f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-20/src/app/user/user.component.ts @@ -0,0 +1,25 @@ +import { AsyncPipe } from '@angular/common'; +import { Component } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { Observable, map } from 'rxjs'; + +@Component({ + selector: 'app-user', + standalone: true, + imports: [AsyncPipe], + template: ` +

Hello User {{ userId$ | async }}

+ + `, +}) +export class UserComponent { + public userId$: Observable; + + constructor(private route: ActivatedRoute) { + this.userId$ = this.route.paramMap.pipe(map(params => params.get('id') || 'UNKNOWN USER')); + } + + throwError() { + throw new Error('Error thrown from user page'); + } +} diff --git a/dev-packages/e2e-tests/test-applications/angular-20/src/index.html b/dev-packages/e2e-tests/test-applications/angular-20/src/index.html new file mode 100644 index 000000000000..0f546ff0114e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-20/src/index.html @@ -0,0 +1,13 @@ + + + + + Angular 20 + + + + + + + + diff --git a/dev-packages/e2e-tests/test-applications/angular-20/src/main.ts b/dev-packages/e2e-tests/test-applications/angular-20/src/main.ts new file mode 100644 index 000000000000..a0b841afc333 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-20/src/main.ts @@ -0,0 +1,15 @@ +import { bootstrapApplication } from '@angular/platform-browser'; +import { AppComponent } from './app/app.component'; +import { appConfig } from './app/app.config'; + +import * as Sentry from '@sentry/angular'; + +Sentry.init({ + // Cannot use process.env here, so we hardcode the DSN + dsn: 'https://3b6c388182fb435097f41d181be2b2ba@o4504321058471936.ingest.sentry.io/4504321066008576', + tracesSampleRate: 1.0, + integrations: [Sentry.browserTracingIntegration({})], + tunnel: `http://localhost:3031/`, // proxy server +}); + +bootstrapApplication(AppComponent, appConfig).catch(err => console.error(err)); diff --git a/dev-packages/e2e-tests/test-applications/angular-20/src/styles.css b/dev-packages/e2e-tests/test-applications/angular-20/src/styles.css new file mode 100644 index 000000000000..90d4ee0072ce --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-20/src/styles.css @@ -0,0 +1 @@ +/* You can add global styles to this file, and also import other style files */ diff --git a/dev-packages/e2e-tests/test-applications/angular-20/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/angular-20/start-event-proxy.mjs new file mode 100644 index 000000000000..58559fb2f8b8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-20/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'angular-20', +}); diff --git a/dev-packages/e2e-tests/test-applications/angular-20/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/angular-20/tests/errors.test.ts new file mode 100644 index 000000000000..98ae26e195cc --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-20/tests/errors.test.ts @@ -0,0 +1,65 @@ +import { expect, test } from '@playwright/test'; +import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; + +test('sends an error', async ({ page }) => { + const errorPromise = waitForError('angular-20', async errorEvent => { + return !errorEvent.type; + }); + + await page.goto(`/`); + + await page.locator('#errorBtn').click(); + + const error = await errorPromise; + + expect(error).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Error thrown from Angular 20 E2E test app', + mechanism: { + type: 'angular', + handled: false, + }, + }, + ], + }, + transaction: '/home/', + }); +}); + +test('assigns the correct transaction value after a navigation', async ({ page }) => { + const pageloadTxnPromise = waitForTransaction('angular-20', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + const errorPromise = waitForError('angular-20', async errorEvent => { + return !errorEvent.type; + }); + + await page.goto(`/`); + await pageloadTxnPromise; + + await page.waitForTimeout(5000); + + await page.locator('#navLink').click(); + + const [_, error] = await Promise.all([page.locator('#userErrorBtn').click(), errorPromise]); + + expect(error).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Error thrown from user page', + mechanism: { + type: 'angular', + handled: false, + }, + }, + ], + }, + transaction: '/users/:id/', + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/angular-20/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/angular-20/tests/performance.test.ts new file mode 100644 index 000000000000..f790cb10d180 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-20/tests/performance.test.ts @@ -0,0 +1,326 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; +import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; + +test('sends a pageload transaction with a parameterized URL', async ({ page }) => { + const transactionPromise = waitForTransaction('angular-20', 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.angular', + }, + }, + transaction: '/home/', + transaction_info: { + source: 'route', + }, + }); +}); + +test('sends a navigation transaction with a parameterized URL', async ({ page }) => { + const pageloadTxnPromise = waitForTransaction('angular-20', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + const navigationTxnPromise = waitForTransaction('angular-20', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/`); + await pageloadTxnPromise; + + await page.waitForTimeout(5000); + + const [_, navigationTxn] = await Promise.all([page.locator('#navLink').click(), navigationTxnPromise]); + + expect(navigationTxn).toMatchObject({ + contexts: { + trace: { + op: 'navigation', + }, + }, + transaction: '/users/:id/', + transaction_info: { + source: 'route', + }, + }); +}); + +test('sends a navigation transaction even if the pageload span is still active', async ({ page }) => { + const pageloadTxnPromise = waitForTransaction('angular-20', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + const navigationTxnPromise = waitForTransaction('angular-20', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/`); + + // immediately navigate to a different route + const [_, pageloadTxn, navigationTxn] = await Promise.all([ + page.locator('#navLink').click(), + pageloadTxnPromise, + navigationTxnPromise, + ]); + + expect(pageloadTxn).toMatchObject({ + contexts: { + trace: { + op: 'pageload', + origin: 'auto.pageload.angular', + }, + }, + transaction: '/home/', + transaction_info: { + source: 'route', + }, + }); + + expect(navigationTxn).toMatchObject({ + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.angular', + }, + }, + transaction: '/users/:id/', + transaction_info: { + source: 'route', + }, + }); +}); + +test('groups redirects within one navigation root span', async ({ page }) => { + const navigationTxnPromise = waitForTransaction('angular-20', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/`); + + // immediately navigate to a different route + const [_, navigationTxn] = await Promise.all([page.locator('#redirectLink').click(), navigationTxnPromise]); + + expect(navigationTxn).toMatchObject({ + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.angular', + }, + }, + transaction: '/users/:id/', + transaction_info: { + source: 'route', + }, + }); + + const routingSpan = navigationTxn.spans?.find(span => span.op === 'ui.angular.routing'); + + expect(routingSpan).toBeDefined(); + expect(routingSpan?.description).toBe('/redirect1'); +}); + +test.describe('finish routing span', () => { + test('finishes routing span on navigation cancel', async ({ page }) => { + const navigationTxnPromise = waitForTransaction('angular-20', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/`); + + // immediately navigate to a different route + const [_, navigationTxn] = await Promise.all([page.locator('#cancelLink').click(), navigationTxnPromise]); + + expect(navigationTxn).toMatchObject({ + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.angular', + }, + }, + transaction: '/cancel', + transaction_info: { + source: 'url', + }, + }); + + const routingSpan = navigationTxn.spans?.find(span => span.op === 'ui.angular.routing'); + + expect(routingSpan).toBeDefined(); + expect(routingSpan?.description).toBe('/cancel'); + }); + + test('finishes routing span on navigation error', async ({ page }) => { + const navigationTxnPromise = waitForTransaction('angular-20', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/`); + + // immediately navigate to a different route + const [_, navigationTxn] = await Promise.all([page.locator('#nonExistentLink').click(), navigationTxnPromise]); + + const nonExistentRoute = '/non-existent'; + + expect(navigationTxn).toMatchObject({ + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.angular', + }, + }, + transaction: nonExistentRoute, + transaction_info: { + source: 'url', + }, + }); + + const routingSpan = navigationTxn.spans?.find(span => span.op === 'ui.angular.routing'); + + expect(routingSpan).toBeDefined(); + expect(routingSpan?.description).toBe(nonExistentRoute); + }); +}); + +test.describe('TraceDirective', () => { + test('creates a child span with the component name as span name on ngOnInit', async ({ page }) => { + const navigationTxnPromise = waitForTransaction('angular-20', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/`); + + // immediately navigate to a different route + const [_, navigationTxn] = await Promise.all([page.locator('#componentTracking').click(), navigationTxnPromise]); + + const traceDirectiveSpans = navigationTxn.spans?.filter( + span => span?.data && span?.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] === 'auto.ui.angular.trace_directive', + ); + + expect(traceDirectiveSpans).toHaveLength(2); + expect(traceDirectiveSpans).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.angular.init', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_directive', + }, + description: '', // custom component name passed to trace directive + op: 'ui.angular.init', + origin: 'auto.ui.angular.trace_directive', + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + }), + expect.objectContaining({ + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.angular.init', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_directive', + }, + description: '', // fallback selector name + op: 'ui.angular.init', + origin: 'auto.ui.angular.trace_directive', + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + }), + ]), + ); + }); +}); + +test.describe('TraceClass Decorator', () => { + test('adds init span for decorated class', async ({ page }) => { + const navigationTxnPromise = waitForTransaction('angular-20', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/`); + + // immediately navigate to a different route + const [_, navigationTxn] = await Promise.all([page.locator('#componentTracking').click(), navigationTxnPromise]); + + const classDecoratorSpan = navigationTxn.spans?.find( + span => span?.data && span?.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] === 'auto.ui.angular.trace_class_decorator', + ); + + expect(classDecoratorSpan).toBeDefined(); + expect(classDecoratorSpan).toEqual( + expect.objectContaining({ + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.angular.init', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_class_decorator', + }, + description: '', + op: 'ui.angular.init', + origin: 'auto.ui.angular.trace_class_decorator', + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + }), + ); + }); +}); + +test.describe('TraceMethod Decorator', () => { + test('adds name to span description of decorated method `ngOnInit`', async ({ page }) => { + const navigationTxnPromise = waitForTransaction('angular-20', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/`); + + // immediately navigate to a different route + const [_, navigationTxn] = await Promise.all([page.locator('#componentTracking').click(), navigationTxnPromise]); + + const ngInitSpan = navigationTxn.spans?.find(span => span.op === 'ui.angular.ngOnInit'); + + expect(ngInitSpan).toBeDefined(); + expect(ngInitSpan).toEqual( + expect.objectContaining({ + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.angular.ngOnInit', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_method_decorator', + }, + description: '', + op: 'ui.angular.ngOnInit', + origin: 'auto.ui.angular.trace_method_decorator', + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + }), + ); + }); + + test('adds fallback name to span description of decorated method `ngAfterViewInit`', async ({ page }) => { + const navigationTxnPromise = waitForTransaction('angular-20', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/`); + + // immediately navigate to a different route + const [_, navigationTxn] = await Promise.all([page.locator('#componentTracking').click(), navigationTxnPromise]); + + const ngAfterViewInitSpan = navigationTxn.spans?.find(span => span.op === 'ui.angular.ngAfterViewInit'); + + expect(ngAfterViewInitSpan).toBeDefined(); + expect(ngAfterViewInitSpan).toEqual( + expect.objectContaining({ + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.angular.ngAfterViewInit', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_method_decorator', + }, + description: '', + op: 'ui.angular.ngAfterViewInit', + origin: 'auto.ui.angular.trace_method_decorator', + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + }), + ); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/angular-20/tsconfig.app.json b/dev-packages/e2e-tests/test-applications/angular-20/tsconfig.app.json new file mode 100644 index 000000000000..8886e903f8d0 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-20/tsconfig.app.json @@ -0,0 +1,11 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/dev-packages/e2e-tests/test-applications/angular-20/tsconfig.json b/dev-packages/e2e-tests/test-applications/angular-20/tsconfig.json new file mode 100644 index 000000000000..5525117c6744 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-20/tsconfig.json @@ -0,0 +1,27 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "compileOnSave": false, + "compilerOptions": { + "outDir": "./dist/out-tsc", + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "isolatedModules": true, + "esModuleInterop": true, + "experimentalDecorators": true, + "moduleResolution": "bundler", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022" + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/dev-packages/e2e-tests/test-applications/angular-20/tsconfig.spec.json b/dev-packages/e2e-tests/test-applications/angular-20/tsconfig.spec.json new file mode 100644 index 000000000000..e00e30e6d4fb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-20/tsconfig.spec.json @@ -0,0 +1,10 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": ["jasmine"] + }, + "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] +} diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-hono/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-hono/package.json new file mode 100644 index 000000000000..b9667aeef85f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-hono/package.json @@ -0,0 +1,29 @@ +{ + "name": "cloudflare-hono", + "scripts": { + "dev": "wrangler dev", + "build": "wrangler deploy --dry-run --var E2E_TEST_DSN=$E2E_TEST_DSN", + "test": "vitest", + "typecheck": "tsc --noEmit", + "cf-typegen": "wrangler types --env-interface CloudflareBindings", + "test:build": "pnpm install && pnpm build", + "//": "Just checking if it builds correctly and types don't break", + "test:assert": "pnpm typecheck" + }, + "dependencies": { + "@sentry/cloudflare": "latest || *", + "hono": "4.7.10" + }, + "devDependencies": { + "@cloudflare/vitest-pool-workers": "^0.8.31", + "@cloudflare/workers-types": "^4.20250521.0", + "vitest": "3.1.0", + "wrangler": "^4.16.0" + }, + "volta": { + "extends": "../../package.json" + }, + "sentryTest": { + "optional": true + } +} diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-hono/src/env.d.ts b/dev-packages/e2e-tests/test-applications/cloudflare-hono/src/env.d.ts new file mode 100644 index 000000000000..0c9e04919e42 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-hono/src/env.d.ts @@ -0,0 +1,6 @@ +// Generated by Wrangler on Mon Jul 29 2024 21:44:31 GMT-0400 (Eastern Daylight Time) +// by running `wrangler types` + +interface Env { + E2E_TEST_DSN: ''; +} diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-hono/src/index.ts b/dev-packages/e2e-tests/test-applications/cloudflare-hono/src/index.ts new file mode 100644 index 000000000000..7cd667c72408 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-hono/src/index.ts @@ -0,0 +1,34 @@ +import { Hono } from 'hono'; +import * as Sentry from '@sentry/cloudflare'; + +const app = new Hono(); + +app.get('/', ctx => { + return ctx.json({ message: 'Welcome to Hono API' }); +}); + +app.get('/hello/:name', ctx => { + const name = ctx.req.param('name'); + return ctx.json({ message: `Hello, ${name}!` }); +}); + +app.get('/error', () => { + throw new Error('This is a test error'); +}); + +app.onError((err, ctx) => { + console.error(`Error occured: ${err.message}`); + return ctx.json({ error: err.message }, 500); +}); + +app.notFound(ctx => { + return ctx.json({ message: 'Not Found' }, 404); +}); + +export default Sentry.withSentry( + (env: Env) => ({ + dsn: env?.E2E_TEST_DSN, + tracesSampleRate: 1.0, + }), + app, +); diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-hono/test/env.d.ts b/dev-packages/e2e-tests/test-applications/cloudflare-hono/test/env.d.ts new file mode 100644 index 000000000000..3b9f82b9628f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-hono/test/env.d.ts @@ -0,0 +1,4 @@ +declare module 'cloudflare:test' { + // ProvidedEnv controls the type of `import("cloudflare:test").env` + interface ProvidedEnv extends Env {} +} diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-hono/test/index.test.ts b/dev-packages/e2e-tests/test-applications/cloudflare-hono/test/index.test.ts new file mode 100644 index 000000000000..2ae93f9b1fd5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-hono/test/index.test.ts @@ -0,0 +1,74 @@ +import { describe, expect, it } from 'vitest'; +import app from '../src/index'; +import { SELF, createExecutionContext, env, waitOnExecutionContext } from 'cloudflare:test'; + +describe('Hono app on Cloudflare Workers', () => { + describe('Unit Tests', () => { + it('should return welcome message', async () => { + const res = await app.request('/', {}, env); + expect(res.status).toBe(200); + const data = await res.json(); + expect(data).toEqual({ message: 'Welcome to Hono API' }); + }); + + it('should greet a user with their name', async () => { + const res = await app.request('/hello/tester', {}, env); + expect(res.status).toBe(200); + const data = await res.json(); + expect(data).toEqual({ message: 'Hello, tester!' }); + }); + + it('should handle errors with custom error handler', async () => { + const res = await app.request('/error', {}, env); + expect(res.status).toBe(500); + const data = await res.json(); + expect(data).toHaveProperty('error', 'This is a test error'); + }); + + it('should handle 404 with custom not found handler', async () => { + const res = await app.request('/non-existent-route', {}, env); + expect(res.status).toBe(404); + const data = await res.json(); + expect(data).toEqual({ message: 'Not Found' }); + }); + }); + + // Integration test style with worker.fetch + describe('Integration Tests', () => { + it('should fetch the root endpoint', async () => { + // Create request and context + const request = new Request('http://localhost/'); + const ctx = createExecutionContext(); + + const response = await app.fetch(request, env, ctx); + + await waitOnExecutionContext(ctx); + + expect(response.status).toBe(200); + const data = await response.json(); + expect(data).toEqual({ message: 'Welcome to Hono API' }); + }); + + it('should handle a parameter route', async () => { + // Create request and context + const request = new Request('http://localhost/hello/cloudflare'); + const ctx = createExecutionContext(); + + const response = await app.fetch(request, env, ctx); + + await waitOnExecutionContext(ctx); + + expect(response.status).toBe(200); + const data = await response.json(); + expect(data).toEqual({ message: 'Hello, cloudflare!' }); + }); + + it('should handle errors gracefully', async () => { + const response = await SELF.fetch('http://localhost/error'); + + expect(response.status).toBe(500); + const data = await response.json(); + expect(data).toHaveProperty('error', 'This is a test error'); + }); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-hono/test/tsconfig.json b/dev-packages/e2e-tests/test-applications/cloudflare-hono/test/tsconfig.json new file mode 100644 index 000000000000..bc019a7e2bfb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-hono/test/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "types": ["@cloudflare/workers-types/experimental", "@cloudflare/vitest-pool-workers"] + }, + "include": ["./**/*.ts", "../src/env.d.ts"], + "exclude": [] +} diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-hono/tsconfig.json b/dev-packages/e2e-tests/test-applications/cloudflare-hono/tsconfig.json new file mode 100644 index 000000000000..e2025cec5039 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-hono/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "skipLibCheck": true, + "lib": [ + "ESNext" + ], + "jsx": "react-jsx", + "jsxImportSource": "hono/jsx" + }, + "include": ["src/**/*"], + "exclude": ["test", "node_modules"] +} diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-hono/vitest.config.ts b/dev-packages/e2e-tests/test-applications/cloudflare-hono/vitest.config.ts new file mode 100644 index 000000000000..4466287fbe5b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-hono/vitest.config.ts @@ -0,0 +1,12 @@ +import { defineWorkersProject } from '@cloudflare/vitest-pool-workers/config' + +export default defineWorkersProject(() => { + return { + test: { + globals: true, + poolOptions: { + workers: { wrangler: { configPath: './wrangler.toml' } }, + }, + }, + } +}) diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-hono/wrangler.toml b/dev-packages/e2e-tests/test-applications/cloudflare-hono/wrangler.toml new file mode 100644 index 000000000000..9fdfb60c18b9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-hono/wrangler.toml @@ -0,0 +1,7 @@ +name = "cloudflare-hono" +main = "src/index.ts" +compatibility_date = "2023-10-30" +compatibility_flags = ["nodejs_compat"] + +# [vars] +# E2E_TEST_DSN = "" diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/react-router.config.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework/react-router.config.ts index 73b647e4eea6..bb1f96469dd2 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework/react-router.config.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/react-router.config.ts @@ -2,6 +2,5 @@ import type { Config } from '@react-router/dev/config'; export default { ssr: true, - // todo: check why this messes up client tracing in tests - // prerender: ['/performance/static'], + prerender: ['/performance/static'], } satisfies Config; diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/pageload.client.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/pageload.client.test.ts index e283ea522c4a..b18ae44e0e71 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/pageload.client.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/pageload.client.test.ts @@ -5,7 +5,7 @@ import { APP_NAME } from '../constants'; test.describe('client - pageload performance', () => { test('should send pageload transaction', async ({ page }) => { const txPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return transactionEvent.transaction === '/performance'; + return transactionEvent.transaction === '/performance/'; }); await page.goto(`/performance`); @@ -29,7 +29,7 @@ test.describe('client - pageload performance', () => { spans: expect.any(Array), start_timestamp: expect.any(Number), timestamp: expect.any(Number), - transaction: '/performance', + transaction: '/performance/', type: 'transaction', transaction_info: { source: 'url' }, measurements: expect.any(Object), @@ -103,10 +103,9 @@ test.describe('client - pageload performance', () => { }); }); - // todo: this page is currently not prerendered (see react-router.config.ts) test('should send pageload transaction for prerendered pages', async ({ page }) => { const txPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return transactionEvent.transaction === '/performance/static'; + return transactionEvent.transaction === '/performance/static/'; }); await page.goto(`/performance/static`); @@ -114,7 +113,7 @@ test.describe('client - pageload performance', () => { const transaction = await txPromise; expect(transaction).toMatchObject({ - transaction: '/performance/static', + transaction: '/performance/static/', contexts: { trace: { span_id: expect.any(String), diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/trace-propagation.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/trace-propagation.test.ts index 6a9623171236..7562297b2d4d 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/trace-propagation.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/trace-propagation.test.ts @@ -33,4 +33,11 @@ test.describe('Trace propagation', () => { expect(clientTx.contexts?.trace?.trace_id).toEqual(serverTx.contexts?.trace?.trace_id); expect(clientTx.contexts?.trace?.parent_span_id).toBe(serverTx.contexts?.trace?.span_id); }); + + test('should not have trace connection for prerendered pages', async ({ page }) => { + await page.goto('/performance/static'); + + const sentryTraceElement = await page.$('meta[name="sentry-trace"]'); + expect(sentryTraceElement).toBeNull(); + }); }); diff --git a/dev-packages/e2e-tests/test-applications/remix-hydrogen/package.json b/dev-packages/e2e-tests/test-applications/remix-hydrogen/package.json index 48f0becca3f7..6890e46da127 100644 --- a/dev-packages/e2e-tests/test-applications/remix-hydrogen/package.json +++ b/dev-packages/e2e-tests/test-applications/remix-hydrogen/package.json @@ -20,7 +20,7 @@ "@sentry/cloudflare": "latest || *", "@sentry/remix": "latest || *", "@sentry/vite-plugin": "^3.1.2", - "@shopify/hydrogen": "^2025.1.0", + "@shopify/hydrogen": "2025.4.0", "@shopify/remix-oxygen": "^2.0.10", "graphql": "^16.6.0", "graphql-tag": "^2.12.6", @@ -34,7 +34,7 @@ "@remix-run/dev": "^2.15.2", "@remix-run/eslint-config": "^2.15.2", "@sentry-internal/test-utils": "link:../../../test-utils", - "@shopify/cli": "^3.74.1", + "@shopify/cli": "3.74.1", "@shopify/hydrogen-codegen": "^0.3.1", "@shopify/mini-oxygen": "3.2.0", "@shopify/oxygen-workers-types": "^4.1.2", diff --git a/dev-packages/opentelemetry-v2-tests/package.json b/dev-packages/opentelemetry-v2-tests/package.json index 80f43c01c982..216989ffb022 100644 --- a/dev-packages/opentelemetry-v2-tests/package.json +++ b/dev-packages/opentelemetry-v2-tests/package.json @@ -16,7 +16,7 @@ "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.200.0", "@opentelemetry/sdk-trace-base": "^2.0.0", - "@opentelemetry/semantic-conventions": "^1.30.0" + "@opentelemetry/semantic-conventions": "^1.34.0" }, "volta": { "extends": "../../package.json" diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index 9e02765c66c3..29105cfb4b18 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -63,6 +63,7 @@ export { eventFiltersIntegration, initOpenTelemetry, isInitialized, + isEnabled, kafkaIntegration, koaIntegration, knexIntegration, diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts index 644d3345ec8f..942951c165da 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -14,6 +14,7 @@ export { createTransport, getClient, isInitialized, + isEnabled, generateInstrumentOnce, getCurrentScope, getGlobalScope, diff --git a/packages/browser-utils/src/metrics/browserMetrics.ts b/packages/browser-utils/src/metrics/browserMetrics.ts index 71470a0d8706..d5ca039c65f0 100644 --- a/packages/browser-utils/src/metrics/browserMetrics.ts +++ b/packages/browser-utils/src/metrics/browserMetrics.ts @@ -300,6 +300,13 @@ interface AddPerformanceEntriesOptions { * sent as a standalone span instead. */ recordClsOnPageloadSpan: boolean; + + /** + * Resource spans with `op`s matching strings in the array will not be emitted. + * + * Default: [] + */ + ignoreResourceSpans: Array<'resouce.script' | 'resource.css' | 'resource.img' | 'resource.other' | string>; } /** Add performance related spans to a transaction */ @@ -355,7 +362,15 @@ export function addPerformanceEntries(span: Span, options: AddPerformanceEntries break; } case 'resource': { - _addResourceSpans(span, entry as PerformanceResourceTiming, entry.name, startTime, duration, timeOrigin); + _addResourceSpans( + span, + entry as PerformanceResourceTiming, + entry.name, + startTime, + duration, + timeOrigin, + options.ignoreResourceSpans, + ); break; } // Ignore other entry types. @@ -568,6 +583,7 @@ export function _addResourceSpans( startTime: number, duration: number, timeOrigin: number, + ignoreResourceSpans?: Array, ): void { // we already instrument based on fetch and xhr, so we don't need to // duplicate spans here. @@ -575,6 +591,11 @@ export function _addResourceSpans( return; } + const op = entry.initiatorType ? `resource.${entry.initiatorType}` : 'resource.other'; + if (ignoreResourceSpans?.includes(op)) { + return; + } + const parsedUrl = parseUrl(resourceUrl); const attributes: SpanAttributes = { @@ -616,7 +637,7 @@ export function _addResourceSpans( startAndEndSpan(span, startTimestamp, endTimestamp, { name: resourceUrl.replace(WINDOW.location.origin, ''), - op: entry.initiatorType ? `resource.${entry.initiatorType}` : 'resource.other', + op, attributes, }); } diff --git a/packages/browser-utils/test/browser/browserMetrics.test.ts b/packages/browser-utils/test/browser/browserMetrics.test.ts index 99cf451f824e..87646a690f0e 100644 --- a/packages/browser-utils/test/browser/browserMetrics.test.ts +++ b/packages/browser-utils/test/browser/browserMetrics.test.ts @@ -271,6 +271,53 @@ describe('_addResourceSpans', () => { } }); + it('allows resource spans to be ignored via ignoreResourceSpans', () => { + const spans: Span[] = []; + const ignoredResourceSpans = ['resource.other', 'resource.script']; + + getClient()?.on('spanEnd', span => { + spans.push(span); + }); + + const table = [ + { + initiatorType: undefined, + op: 'resource.other', + }, + { + initiatorType: 'css', + op: 'resource.css', + }, + { + initiatorType: 'css', + op: 'resource.css', + }, + { + initiatorType: 'image', + op: 'resource.image', + }, + { + initiatorType: 'script', + op: 'resource.script', + }, + ]; + for (const row of table) { + const { initiatorType } = row; + const entry = mockPerformanceResourceTiming({ + initiatorType, + nextHopProtocol: 'http/1.1', + }); + _addResourceSpans(span, entry, 'https://example.com/assets/to/me', 123, 234, 465, ignoredResourceSpans); + } + expect(spans).toHaveLength(table.length - ignoredResourceSpans.length); + const spanOps = new Set( + spans.map(s => { + return spanToJSON(s).op; + }), + ); + expect(spanOps).toEqual(new Set(['resource.css', 'resource.image'])); + }); + it('allows for enter size of 0', () => { const spans: Span[] = []; diff --git a/packages/browser/src/client.ts b/packages/browser/src/client.ts index 73cbd55d42db..5c50e53e708b 100644 --- a/packages/browser/src/client.ts +++ b/packages/browser/src/client.ts @@ -21,15 +21,24 @@ import { eventFromException, eventFromMessage } from './eventbuilder'; import { WINDOW } from './helpers'; import type { BrowserTransportOptions } from './transports/types'; +/** + * A magic string that build tooling can leverage in order to inject a release value into the SDK. + */ +declare const __SENTRY_RELEASE__: string | undefined; + const DEFAULT_FLUSH_INTERVAL = 5000; +type BrowserSpecificOptions = BrowserClientReplayOptions & + BrowserClientProfilingOptions & { + /** If configured, this URL will be used as base URL for lazy loading integration. */ + cdnBaseUrl?: string; + }; /** * Configuration options for the Sentry Browser SDK. * @see @sentry/core Options for more information. */ export type BrowserOptions = Options & - BrowserClientReplayOptions & - BrowserClientProfilingOptions & { + BrowserSpecificOptions & { /** * Important: Only set this option if you know what you are doing! * @@ -54,12 +63,7 @@ export type BrowserOptions = Options & * Configuration options for the Sentry Browser SDK Client class * @see BrowserClient for more information. */ -export type BrowserClientOptions = ClientOptions & - BrowserClientReplayOptions & - BrowserClientProfilingOptions & { - /** If configured, this URL will be used as base URL for lazy loading integration. */ - cdnBaseUrl?: string; - }; +export type BrowserClientOptions = ClientOptions & BrowserSpecificOptions; /** * The Sentry Browser SDK Client. @@ -75,51 +79,47 @@ export class BrowserClient extends Client { * @param options Configuration options for this SDK. */ public constructor(options: BrowserClientOptions) { - const opts = { - // We default this to true, as it is the safer scenario - parentSpanIsAlwaysRootSpan: true, - ...options, - }; + const opts = applyDefaultOptions(options); const sdkSource = WINDOW.SENTRY_SDK_SOURCE || getSDKSource(); applySdkMetadata(opts, 'browser', ['browser'], sdkSource); super(opts); - // eslint-disable-next-line @typescript-eslint/no-this-alias - const client = this; - const { sendDefaultPii, _experiments } = client._options; + const { sendDefaultPii, sendClientReports, _experiments } = this._options; const enableLogs = _experiments?.enableLogs; - if (opts.sendClientReports && WINDOW.document) { + if (WINDOW.document && (sendClientReports || enableLogs)) { WINDOW.document.addEventListener('visibilitychange', () => { if (WINDOW.document.visibilityState === 'hidden') { - this._flushOutcomes(); + if (sendClientReports) { + this._flushOutcomes(); + } if (enableLogs) { - _INTERNAL_flushLogsBuffer(client); + _INTERNAL_flushLogsBuffer(this); } } }); } if (enableLogs) { - client.on('flush', () => { - _INTERNAL_flushLogsBuffer(client); + this.on('flush', () => { + _INTERNAL_flushLogsBuffer(this); }); - client.on('afterCaptureLog', () => { - if (client._logFlushIdleTimeout) { - clearTimeout(client._logFlushIdleTimeout); + this.on('afterCaptureLog', () => { + if (this._logFlushIdleTimeout) { + clearTimeout(this._logFlushIdleTimeout); } - client._logFlushIdleTimeout = setTimeout(() => { - _INTERNAL_flushLogsBuffer(client); + this._logFlushIdleTimeout = setTimeout(() => { + _INTERNAL_flushLogsBuffer(this); }, DEFAULT_FLUSH_INTERVAL); }); } if (sendDefaultPii) { - client.on('postprocessEvent', addAutoIpAddressToUser); - client.on('beforeSendSession', addAutoIpAddressToSession); + this.on('postprocessEvent', addAutoIpAddressToUser); + this.on('beforeSendSession', addAutoIpAddressToSession); } } @@ -155,3 +155,17 @@ export class BrowserClient extends Client { return super._prepareEvent(event, hint, currentScope, isolationScope); } } + +/** Exported only for tests. */ +export function applyDefaultOptions>(optionsArg: T): T { + return { + release: + typeof __SENTRY_RELEASE__ === 'string' // This allows build tooling to find-and-replace __SENTRY_RELEASE__ to inject a release value + ? __SENTRY_RELEASE__ + : WINDOW.SENTRY_RELEASE?.id, // This supports the variable that sentry-webpack-plugin injects + sendClientReports: true, + // We default this to true, as it is the safer scenario + parentSpanIsAlwaysRootSpan: true, + ...optionsArg, + }; +} diff --git a/packages/browser/src/exports.ts b/packages/browser/src/exports.ts index b454f29556c2..27e1ea34b8b7 100644 --- a/packages/browser/src/exports.ts +++ b/packages/browser/src/exports.ts @@ -32,6 +32,7 @@ export { flush, getClient, isInitialized, + isEnabled, getCurrentScope, getIsolationScope, getGlobalScope, diff --git a/packages/browser/src/sdk.ts b/packages/browser/src/sdk.ts index 56f3ace8f193..2101e4e36074 100644 --- a/packages/browser/src/sdk.ts +++ b/packages/browser/src/sdk.ts @@ -1,18 +1,14 @@ import type { Client, Integration, Options } from '@sentry/core'; import { - consoleSandbox, dedupeIntegration, functionToStringIntegration, getIntegrationsToSetup, - getLocationHref, inboundFiltersIntegration, initAndBind, stackParserFromStackParserOptions, } from '@sentry/core'; import type { BrowserClientOptions, BrowserOptions } from './client'; import { BrowserClient } from './client'; -import { DEBUG_BUILD } from './debug-build'; -import { WINDOW } from './helpers'; import { breadcrumbsIntegration } from './integrations/breadcrumbs'; import { browserApiErrorsIntegration } from './integrations/browserapierrors'; import { browserSessionIntegration } from './integrations/browsersession'; @@ -21,22 +17,7 @@ import { httpContextIntegration } from './integrations/httpcontext'; import { linkedErrorsIntegration } from './integrations/linkederrors'; import { defaultStackParser } from './stack-parsers'; import { makeFetchTransport } from './transports/fetch'; - -type ExtensionProperties = { - chrome?: Runtime; - browser?: Runtime; - nw?: unknown; -}; -type Runtime = { - runtime?: { - id?: string; - }; -}; - -/** - * A magic string that build tooling can leverage in order to inject a release value into the SDK. - */ -declare const __SENTRY_RELEASE__: string | undefined; +import { checkAndWarnIfIsEmbeddedBrowserExtension } from './utils/detectBrowserExtension'; /** Get the default integrations for the browser SDK. */ export function getDefaultIntegrations(_options: Options): Integration[] { @@ -59,40 +40,6 @@ export function getDefaultIntegrations(_options: Options): Integration[] { ]; } -/** Exported only for tests. */ -export function applyDefaultOptions(optionsArg: BrowserOptions = {}): BrowserOptions { - const defaultOptions: BrowserOptions = { - defaultIntegrations: getDefaultIntegrations(optionsArg), - release: - typeof __SENTRY_RELEASE__ === 'string' // This allows build tooling to find-and-replace __SENTRY_RELEASE__ to inject a release value - ? __SENTRY_RELEASE__ - : WINDOW.SENTRY_RELEASE?.id, // This supports the variable that sentry-webpack-plugin injects - sendClientReports: true, - }; - - return { - ...defaultOptions, - ...dropTopLevelUndefinedKeys(optionsArg), - }; -} - -/** - * In contrast to the regular `dropUndefinedKeys` method, - * this one does not deep-drop keys, but only on the top level. - */ -function dropTopLevelUndefinedKeys(obj: T): Partial { - const mutatetedObj: Partial = {}; - - for (const k of Object.getOwnPropertyNames(obj)) { - const key = k as keyof T; - if (obj[key] !== undefined) { - mutatetedObj[key] = obj[key]; - } - } - - return mutatetedObj; -} - /** * The Sentry Browser SDK Client. * @@ -139,19 +86,21 @@ function dropTopLevelUndefinedKeys(obj: T): Partial { * * @see {@link BrowserOptions} for documentation on configuration options. */ -export function init(browserOptions: BrowserOptions = {}): Client | undefined { - if (!browserOptions.skipBrowserExtensionCheck && _checkForBrowserExtension()) { - return; - } +export function init(options: BrowserOptions = {}): Client | undefined { + const shouldDisableBecauseIsBrowserExtenstion = + !options.skipBrowserExtensionCheck && checkAndWarnIfIsEmbeddedBrowserExtension(); - const options = applyDefaultOptions(browserOptions); const clientOptions: BrowserClientOptions = { ...options, + enabled: shouldDisableBecauseIsBrowserExtenstion ? false : options.enabled, stackParser: stackParserFromStackParserOptions(options.stackParser || defaultStackParser), - integrations: getIntegrationsToSetup(options), + integrations: getIntegrationsToSetup({ + integrations: options.integrations, + defaultIntegrations: + options.defaultIntegrations == null ? getDefaultIntegrations(options) : options.defaultIntegrations, + }), transport: options.transport || makeFetchTransport, }; - return initAndBind(BrowserClient, clientOptions); } @@ -170,48 +119,3 @@ export function forceLoad(): void { export function onLoad(callback: () => void): void { callback(); } - -function _isEmbeddedBrowserExtension(): boolean { - if (typeof WINDOW.window === 'undefined') { - // No need to show the error if we're not in a browser window environment (e.g. service workers) - return false; - } - - const _window = WINDOW as typeof WINDOW & ExtensionProperties; - - // Running the SDK in NW.js, which appears like a browser extension but isn't, is also fine - // see: https://github.com/getsentry/sentry-javascript/issues/12668 - if (_window.nw) { - return false; - } - - const extensionObject = _window['chrome'] || _window['browser']; - - if (!extensionObject?.runtime?.id) { - return false; - } - - const href = getLocationHref(); - const extensionProtocols = ['chrome-extension', 'moz-extension', 'ms-browser-extension', 'safari-web-extension']; - - // Running the SDK in a dedicated extension page and calling Sentry.init is fine; no risk of data leakage - const isDedicatedExtensionPage = - WINDOW === WINDOW.top && extensionProtocols.some(protocol => href.startsWith(`${protocol}://`)); - - return !isDedicatedExtensionPage; -} - -function _checkForBrowserExtension(): true | void { - if (_isEmbeddedBrowserExtension()) { - if (DEBUG_BUILD) { - consoleSandbox(() => { - // eslint-disable-next-line no-console - console.error( - '[Sentry] You cannot use Sentry.init() in a browser extension, see: https://docs.sentry.io/platforms/javascript/best-practices/browser-extensions/', - ); - }); - } - - return true; - } -} diff --git a/packages/browser/src/tracing/browserTracingIntegration.ts b/packages/browser/src/tracing/browserTracingIntegration.ts index 0a4579f40774..3f38bdb6a8be 100644 --- a/packages/browser/src/tracing/browserTracingIntegration.ts +++ b/packages/browser/src/tracing/browserTracingIntegration.ts @@ -3,7 +3,6 @@ import type { Client, IntegrationFn, Span, StartSpanOptions, TransactionSource, import { addNonEnumerableProperty, browserPerformanceTimeOrigin, - consoleSandbox, generateTraceId, getClient, getCurrentScope, @@ -145,6 +144,13 @@ export interface BrowserTracingOptions { */ enableHTTPTimings: boolean; + /** + * Resource spans with `op`s matching strings in the array will not be emitted. + * + * Default: [] + */ + ignoreResourceSpans: Array; + /** * Link the currently started trace to a previous trace (e.g. a prior pageload, navigation or * manually started span). When enabled, this option will allow you to navigate between traces @@ -227,14 +233,13 @@ const DEFAULT_BROWSER_TRACING_OPTIONS: BrowserTracingOptions = { enableLongTask: true, enableLongAnimationFrame: true, enableInp: true, + ignoreResourceSpans: [], linkPreviousTrace: 'in-memory', consistentTraceSampling: false, _experiments: {}, ...defaultRequestInstrumentationOptions, }; -let _hasBeenInitialized = false; - /** * The Browser Tracing integration automatically instruments browser pageload/navigation * actions as transactions, and captures requests, metrics and errors as spans. @@ -245,14 +250,10 @@ let _hasBeenInitialized = false; * We explicitly export the proper type here, as this has to be extended in some cases. */ export const browserTracingIntegration = ((_options: Partial = {}) => { - if (_hasBeenInitialized) { - consoleSandbox(() => { - // eslint-disable-next-line no-console - console.warn('Multiple browserTracingIntegration instances are not supported.'); - }); - } - - _hasBeenInitialized = true; + const latestRoute: RouteInfo = { + name: undefined, + source: undefined, + }; /** * This is just a small wrapper that makes `document` optional. @@ -260,8 +261,6 @@ export const browserTracingIntegration = ((_options: Partial void); /** Create routing idle transaction. */ function _createRouteSpan(client: Client, startSpanOptions: StartSpanOptions): void { @@ -340,8 +316,10 @@ export const browserTracingIntegration = ((_options: Partial { - _collectWebVitals(); - addPerformanceEntries(span, { recordClsOnPageloadSpan: !enableStandaloneClsSpans }); + // This will generally always be defined here, because it is set in `setup()` of the integration + // but technically, it is optional, so we guard here to be extra safe + _collectWebVitals?.(); + addPerformanceEntries(span, { recordClsOnPageloadSpan: !enableStandaloneClsSpans, ignoreResourceSpans }); setActiveIdleSpan(client, undefined); // A trace should stay consistent over the entire timespan of one route - even after the pageload/navigation ended. @@ -378,8 +356,29 @@ export const browserTracingIntegration = ((_options: Partial { + // eslint-disable-next-line no-console + console.error( + '[Sentry] You cannot use Sentry.init() in a browser extension, see: https://docs.sentry.io/platforms/javascript/best-practices/browser-extensions/', + ); + }); + } + + return true; + } + + return false; +} + +function _isEmbeddedBrowserExtension(): boolean { + if (typeof WINDOW.window === 'undefined') { + // No need to show the error if we're not in a browser window environment (e.g. service workers) + return false; + } + + const _window = WINDOW as typeof WINDOW & ExtensionProperties; + + // Running the SDK in NW.js, which appears like a browser extension but isn't, is also fine + // see: https://github.com/getsentry/sentry-javascript/issues/12668 + if (_window.nw) { + return false; + } + + const extensionObject = _window['chrome'] || _window['browser']; + + if (!extensionObject?.runtime?.id) { + return false; + } + + const href = getLocationHref(); + const extensionProtocols = ['chrome-extension', 'moz-extension', 'ms-browser-extension', 'safari-web-extension']; + + // Running the SDK in a dedicated extension page and calling Sentry.init is fine; no risk of data leakage + const isDedicatedExtensionPage = + WINDOW === WINDOW.top && extensionProtocols.some(protocol => href.startsWith(`${protocol}://`)); + + return !isDedicatedExtensionPage; +} diff --git a/packages/browser/test/client.test.ts b/packages/browser/test/client.test.ts index a90f8cdbc388..c0f4e649501a 100644 --- a/packages/browser/test/client.test.ts +++ b/packages/browser/test/client.test.ts @@ -4,7 +4,7 @@ import * as sentryCore from '@sentry/core'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { BrowserClient } from '../src/client'; +import { applyDefaultOptions, BrowserClient } from '../src/client'; import { WINDOW } from '../src/helpers'; import { getDefaultBrowserClientOptions } from './helper/browser-client-options'; @@ -118,3 +118,70 @@ describe('BrowserClient', () => { }); }); }); + +describe('applyDefaultOptions', () => { + it('works with empty options', () => { + const options = {}; + const actual = applyDefaultOptions(options); + + expect(actual).toEqual({ + release: undefined, + sendClientReports: true, + parentSpanIsAlwaysRootSpan: true, + }); + }); + + it('works with options', () => { + const options = { + tracesSampleRate: 0.5, + release: '1.0.0', + }; + const actual = applyDefaultOptions(options); + + expect(actual).toEqual({ + release: '1.0.0', + sendClientReports: true, + tracesSampleRate: 0.5, + parentSpanIsAlwaysRootSpan: true, + }); + }); + + it('picks up release from WINDOW.SENTRY_RELEASE.id', () => { + const releaseBefore = WINDOW.SENTRY_RELEASE; + + WINDOW.SENTRY_RELEASE = { id: '1.0.0' }; + const options = { + tracesSampleRate: 0.5, + }; + const actual = applyDefaultOptions(options); + + expect(actual).toEqual({ + release: '1.0.0', + sendClientReports: true, + tracesSampleRate: 0.5, + parentSpanIsAlwaysRootSpan: true, + }); + + WINDOW.SENTRY_RELEASE = releaseBefore; + }); + + it('passed in release takes precedence over WINDOW.SENTRY_RELEASE.id', () => { + const releaseBefore = WINDOW.SENTRY_RELEASE; + + WINDOW.SENTRY_RELEASE = { id: '1.0.0' }; + const options = { + release: '2.0.0', + tracesSampleRate: 0.5, + }; + const actual = applyDefaultOptions(options); + + expect(actual).toEqual({ + release: '2.0.0', + sendClientReports: true, + tracesSampleRate: 0.5, + parentSpanIsAlwaysRootSpan: true, + }); + + WINDOW.SENTRY_RELEASE = releaseBefore; + }); +}); diff --git a/packages/browser/test/sdk.test.ts b/packages/browser/test/sdk.test.ts index 342b008bfc18..b7972797182f 100644 --- a/packages/browser/test/sdk.test.ts +++ b/packages/browser/test/sdk.test.ts @@ -7,10 +7,10 @@ import type { Integration } from '@sentry/core'; import * as SentryCore from '@sentry/core'; import { createTransport, resolvedSyncPromise } from '@sentry/core'; import type { Mock } from 'vitest'; -import { afterAll, afterEach, beforeEach, describe, expect, it, test, vi } from 'vitest'; +import { afterEach, describe, expect, it, test, vi } from 'vitest'; import type { BrowserOptions } from '../src'; import { WINDOW } from '../src'; -import { applyDefaultOptions, getDefaultIntegrations, init } from '../src/sdk'; +import { init } from '../src/sdk'; const PUBLIC_DSN = 'https://username@domain/123'; @@ -32,15 +32,11 @@ export class MockIntegration implements Integration { } describe('init', () => { - beforeEach(() => { - vi.clearAllMocks(); + afterEach(() => { + vi.restoreAllMocks(); }); - afterAll(() => { - vi.resetAllMocks(); - }); - - test('installs default integrations', () => { + test('installs passed default integrations', () => { const DEFAULT_INTEGRATIONS: Integration[] = [ new MockIntegration('MockIntegration 0.1'), new MockIntegration('MockIntegration 0.2'), @@ -134,7 +130,7 @@ describe('init', () => { Object.defineProperty(WINDOW, 'browser', { value: undefined, writable: true }); Object.defineProperty(WINDOW, 'nw', { value: undefined, writable: true }); Object.defineProperty(WINDOW, 'window', { value: WINDOW, writable: true }); - vi.clearAllMocks(); + vi.restoreAllMocks(); }); it('logs a browser extension error if executed inside a Chrome extension', () => { @@ -151,8 +147,6 @@ describe('init', () => { expect(consoleErrorSpy).toHaveBeenCalledWith( '[Sentry] You cannot use Sentry.init() in a browser extension, see: https://docs.sentry.io/platforms/javascript/best-practices/browser-extensions/', ); - - consoleErrorSpy.mockRestore(); }); it('logs a browser extension error if executed inside a Firefox/Safari extension', () => { @@ -166,8 +160,6 @@ describe('init', () => { expect(consoleErrorSpy).toHaveBeenCalledWith( '[Sentry] You cannot use Sentry.init() in a browser extension, see: https://docs.sentry.io/platforms/javascript/best-practices/browser-extensions/', ); - - consoleErrorSpy.mockRestore(); }); it.each(['chrome-extension', 'moz-extension', 'ms-browser-extension', 'safari-web-extension'])( @@ -224,7 +216,7 @@ describe('init', () => { consoleErrorSpy.mockRestore(); }); - it("doesn't return a client on initialization error", () => { + it('returns a disabled client on initialization error', () => { const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); Object.defineProperty(WINDOW, 'chrome', { @@ -234,7 +226,9 @@ describe('init', () => { const client = init(options); - expect(client).toBeUndefined(); + expect(client).toBeDefined(); + expect(SentryCore.isEnabled()).toBe(false); + expect(client!['_isEnabled']()).toBe(false); consoleErrorSpy.mockRestore(); }); @@ -245,97 +239,3 @@ describe('init', () => { expect(client).not.toBeUndefined(); }); }); - -describe('applyDefaultOptions', () => { - test('it works with empty options', () => { - const options = {}; - const actual = applyDefaultOptions(options); - - expect(actual).toEqual({ - defaultIntegrations: expect.any(Array), - release: undefined, - sendClientReports: true, - }); - - expect((actual.defaultIntegrations as { name: string }[]).map(i => i.name)).toEqual( - getDefaultIntegrations(options).map(i => i.name), - ); - }); - - test('it works with options', () => { - const options = { - tracesSampleRate: 0.5, - release: '1.0.0', - }; - const actual = applyDefaultOptions(options); - - expect(actual).toEqual({ - defaultIntegrations: expect.any(Array), - release: '1.0.0', - sendClientReports: true, - tracesSampleRate: 0.5, - }); - - expect((actual.defaultIntegrations as { name: string }[]).map(i => i.name)).toEqual( - getDefaultIntegrations(options).map(i => i.name), - ); - }); - - test('it works with defaultIntegrations=false', () => { - const options = { - defaultIntegrations: false, - } as const; - const actual = applyDefaultOptions(options); - - expect(actual.defaultIntegrations).toStrictEqual(false); - }); - - test('it works with defaultIntegrations=[]', () => { - const options = { - defaultIntegrations: [], - }; - const actual = applyDefaultOptions(options); - - expect(actual.defaultIntegrations).toEqual([]); - }); - - test('it works with tracesSampleRate=undefined', () => { - const options = { - tracesSampleRate: undefined, - } as const; - const actual = applyDefaultOptions(options); - - // Not defined, not even undefined - expect('tracesSampleRate' in actual).toBe(false); - }); - - test('it works with tracesSampleRate=null', () => { - const options = { - tracesSampleRate: null, - } as any; - const actual = applyDefaultOptions(options); - - expect(actual.tracesSampleRate).toStrictEqual(null); - }); - - test('it works with tracesSampleRate=0', () => { - const options = { - tracesSampleRate: 0, - } as const; - const actual = applyDefaultOptions(options); - - expect(actual.tracesSampleRate).toStrictEqual(0); - }); - - test('it does not deep-drop undefined keys', () => { - const options = { - obj: { - prop: undefined, - }, - } as any; - const actual = applyDefaultOptions(options) as any; - - expect('prop' in actual.obj).toBe(true); - expect(actual.obj.prop).toStrictEqual(undefined); - }); -}); diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index b1ad295b1257..70104de6d7c3 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -33,6 +33,7 @@ export { createTransport, getClient, isInitialized, + isEnabled, generateInstrumentOnce, getCurrentScope, getGlobalScope, diff --git a/packages/cloudflare/src/handler.ts b/packages/cloudflare/src/handler.ts index 62956cff62cf..d3d1f80dbbd5 100644 --- a/packages/cloudflare/src/handler.ts +++ b/packages/cloudflare/src/handler.ts @@ -26,6 +26,7 @@ import { init } from './sdk'; * @param handler {ExportedHandler} The handler to wrap. * @returns The wrapped handler. */ +// eslint-disable-next-line complexity export function withSentry( optionsCallback: (env: Env) => CloudflareOptions, handler: ExportedHandler, @@ -47,6 +48,26 @@ export function withSentry>) { diff --git a/packages/cloudflare/src/index.ts b/packages/cloudflare/src/index.ts index 64a6f57fccc2..6754ffd04f7c 100644 --- a/packages/cloudflare/src/index.ts +++ b/packages/cloudflare/src/index.ts @@ -33,6 +33,7 @@ export { flush, getClient, isInitialized, + isEnabled, getCurrentScope, getGlobalScope, getIsolationScope, diff --git a/packages/cloudflare/test/handler.test.ts b/packages/cloudflare/test/handler.test.ts index 6ae688f316f9..bced0fdbe277 100644 --- a/packages/cloudflare/test/handler.test.ts +++ b/packages/cloudflare/test/handler.test.ts @@ -7,6 +7,17 @@ import * as SentryCore from '@sentry/core'; import { beforeEach, describe, expect, test, vi } from 'vitest'; import { CloudflareClient } from '../src/client'; import { withSentry } from '../src/handler'; +import { markAsInstrumented } from '../src/instrument'; + +// Custom type for hono-like apps (cloudflare handlers) that include errorHandler and onError +type HonoLikeApp = ExportedHandler< + Env, + QueueHandlerMessage, + CfHostMetadata +> & { + onError?: () => void; + errorHandler?: (err: Error) => Response; +}; const MOCK_ENV = { SENTRY_DSN: 'https://public@dsn.ingest.sentry.io/1337', @@ -931,6 +942,86 @@ describe('withSentry', () => { }); }); }); + + describe('hono errorHandler', () => { + test('captures errors handled by the errorHandler', async () => { + const captureExceptionSpy = vi.spyOn(SentryCore, 'captureException'); + const error = new Error('test hono error'); + + const honoApp = { + fetch(_request, _env, _context) { + return new Response('test'); + }, + onError() {}, // hono-like onError + errorHandler(err: Error) { + return new Response(`Error: ${err.message}`, { status: 500 }); + }, + } satisfies HonoLikeApp; + + withSentry(env => ({ dsn: env.SENTRY_DSN }), honoApp); + + // simulates hono's error handling + const errorHandlerResponse = honoApp.errorHandler?.(error); + + expect(captureExceptionSpy).toHaveBeenCalledTimes(1); + expect(captureExceptionSpy).toHaveBeenLastCalledWith(error, { + mechanism: { handled: false, type: 'cloudflare' }, + }); + expect(errorHandlerResponse?.status).toBe(500); + }); + + test('preserves the original errorHandler functionality', async () => { + const originalErrorHandlerSpy = vi.fn().mockImplementation((err: Error) => { + return new Response(`Error: ${err.message}`, { status: 500 }); + }); + + const error = new Error('test hono error'); + + const honoApp = { + fetch(_request, _env, _context) { + return new Response('test'); + }, + onError() {}, // hono-like onError + errorHandler: originalErrorHandlerSpy, + } satisfies HonoLikeApp; + + withSentry(env => ({ dsn: env.SENTRY_DSN }), honoApp); + + // Call the errorHandler directly to simulate Hono's error handling + const errorHandlerResponse = honoApp.errorHandler?.(error); + + expect(originalErrorHandlerSpy).toHaveBeenCalledTimes(1); + expect(originalErrorHandlerSpy).toHaveBeenLastCalledWith(error); + expect(errorHandlerResponse?.status).toBe(500); + }); + + test('does not instrument an already instrumented errorHandler', async () => { + const captureExceptionSpy = vi.spyOn(SentryCore, 'captureException'); + const error = new Error('test hono error'); + + // Create a handler with an errorHandler that's already been instrumented + const originalErrorHandler = (err: Error) => { + return new Response(`Error: ${err.message}`, { status: 500 }); + }; + + // Mark as instrumented before wrapping + markAsInstrumented(originalErrorHandler); + + const honoApp = { + fetch(_request, _env, _context) { + return new Response('test'); + }, + onError() {}, // hono-like onError + errorHandler: originalErrorHandler, + } satisfies HonoLikeApp; + + withSentry(env => ({ dsn: env.SENTRY_DSN }), honoApp); + + // The errorHandler should not have been wrapped again + honoApp.errorHandler?.(error); + expect(captureExceptionSpy).not.toHaveBeenCalled(); + }); + }); }); function createMockExecutionContext(): ExecutionContext { diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index 0dda6c86fd26..d9abd8f5d0d2 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -1,7 +1,7 @@ /* eslint-disable max-lines */ import { getEnvelopeEndpointWithUrlEncodedAuth } from './api'; import { DEFAULT_ENVIRONMENT } from './constants'; -import { getCurrentScope, getIsolationScope, getTraceContextFromScope } from './currentScopes'; +import { getCurrentScope, getIsolationScope, getTraceContextFromScope, withScope } from './currentScopes'; import { DEBUG_BUILD } from './debug-build'; import { createEventEnvelope, createSessionEnvelope } from './envelope'; import type { IntegrationIndex } from './integration'; @@ -36,8 +36,7 @@ import { getPossibleEventMessages } from './utils/eventUtils'; import { merge } from './utils/merge'; import { parseSampleRate } from './utils/parseSampleRate'; import { prepareEvent } from './utils/prepareEvent'; -import { _getSpanForScope } from './utils/spanOnScope'; -import { showSpanDropWarning, spanToTraceContext } from './utils/spanUtils'; +import { getActiveSpan, showSpanDropWarning, spanToTraceContext } from './utils/spanUtils'; import { convertSpanJsonToTransactionEvent, convertTransactionEventToSpanJson } from './utils/transactionEvent'; import { createClientReportEnvelope } from './utils-hoist/clientreport'; import { dsnToString, makeDsn } from './utils-hoist/dsn'; @@ -1325,10 +1324,12 @@ export function _getTraceInfoFromScope( return [undefined, undefined]; } - const span = _getSpanForScope(scope); - const traceContext = span ? spanToTraceContext(span) : getTraceContextFromScope(scope); - const dynamicSamplingContext = span - ? getDynamicSamplingContextFromSpan(span) - : getDynamicSamplingContextFromScope(client, scope); - return [dynamicSamplingContext, traceContext]; + return withScope(scope, () => { + const span = getActiveSpan(); + const traceContext = span ? spanToTraceContext(span) : getTraceContextFromScope(scope); + const dynamicSamplingContext = span + ? getDynamicSamplingContextFromSpan(span) + : getDynamicSamplingContextFromScope(client, scope); + return [dynamicSamplingContext, traceContext]; + }); } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index a67f003aac56..986d18a972d2 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -119,7 +119,7 @@ export { trpcMiddleware } from './trpc'; export { wrapMcpServerWithSentry } from './mcp-server'; export { captureFeedback } from './feedback'; export type { ReportDialogOptions } from './report-dialog'; -export { _INTERNAL_captureLog, _INTERNAL_flushLogsBuffer } from './logs/exports'; +export { _INTERNAL_captureLog, _INTERNAL_flushLogsBuffer, _INTERNAL_captureSerializedLog } from './logs/exports'; export { consoleLoggingIntegration } from './logs/console-integration'; export type { FeatureFlag } from './featureFlags'; diff --git a/packages/core/src/logs/exports.ts b/packages/core/src/logs/exports.ts index a2451c6cc6c0..9a738d503a80 100644 --- a/packages/core/src/logs/exports.ts +++ b/packages/core/src/logs/exports.ts @@ -61,12 +61,34 @@ export function logAttributeToSerializedLogAttribute(value: unknown): Serialized } } +/** + * Captures a serialized log event and adds it to the log buffer for the given client. + * + * @param client - A client. Uses the current client if not provided. + * @param serializedLog - The serialized log event to capture. + * + * @experimental This method will experience breaking changes. This is not yet part of + * the stable Sentry SDK API and can be changed or removed without warning. + */ +export function _INTERNAL_captureSerializedLog(client: Client, serializedLog: SerializedLog): void { + const logBuffer = _INTERNAL_getLogBuffer(client); + if (logBuffer === undefined) { + GLOBAL_OBJ._sentryClientToLogBufferMap?.set(client, [serializedLog]); + } else { + GLOBAL_OBJ._sentryClientToLogBufferMap?.set(client, [...logBuffer, serializedLog]); + if (logBuffer.length >= MAX_LOG_BUFFER_SIZE) { + _INTERNAL_flushLogsBuffer(client, logBuffer); + } + } +} + /** * Captures a log event and sends it to Sentry. * * @param log - The log event to capture. * @param scope - A scope. Uses the current scope if not provided. * @param client - A client. Uses the current client if not provided. + * @param captureSerializedLog - A function to capture the serialized log. * * @experimental This method will experience breaking changes. This is not yet part of * the stable Sentry SDK API and can be changed or removed without warning. @@ -75,6 +97,7 @@ export function _INTERNAL_captureLog( beforeLog: Log, client: Client | undefined = getClient(), scope = getCurrentScope(), + captureSerializedLog: (client: Client, log: SerializedLog) => void = _INTERNAL_captureSerializedLog, ): void { if (!client) { DEBUG_BUILD && logger.warn('No client available to capture log.'); @@ -151,15 +174,7 @@ export function _INTERNAL_captureLog( ), }; - const logBuffer = _INTERNAL_getLogBuffer(client); - if (logBuffer === undefined) { - GLOBAL_OBJ._sentryClientToLogBufferMap?.set(client, [serializedLog]); - } else { - GLOBAL_OBJ._sentryClientToLogBufferMap?.set(client, [...logBuffer, serializedLog]); - if (logBuffer.length >= MAX_LOG_BUFFER_SIZE) { - _INTERNAL_flushLogsBuffer(client, logBuffer); - } - } + captureSerializedLog(client, serializedLog); client.emit('afterCaptureLog', log); } diff --git a/packages/core/test/lib/integration.test.ts b/packages/core/test/lib/integration.test.ts index 27ca88049b2a..3e4c26e0b959 100644 --- a/packages/core/test/lib/integration.test.ts +++ b/packages/core/test/lib/integration.test.ts @@ -22,14 +22,12 @@ class MockIntegration implements Integration { // Only for testing - tag to keep separate instances straight when testing deduplication public tag?: string; + public setupOnce = vi.fn(() => {}); + public constructor(name: string, tag?: string) { this.name = name; this.tag = tag; } - - public setupOnce(): void { - // noop - } } type TestCase = [ @@ -74,6 +72,31 @@ describe('getIntegrationsToSetup', () => { }); expect(integrations.map(i => i.name)).toEqual(expected); }); + + test('it uses passed integration over default intergation', () => { + const integrationDefault = new MockIntegration('ChaseSquirrels'); + const integration1 = new MockIntegration('ChaseSquirrels'); + + const integrations = getIntegrationsToSetup({ + defaultIntegrations: [integrationDefault], + integrations: [integration1], + }); + + expect(integrations).toEqual([integration1]); + }); + + test('it uses last passed integration only', () => { + const integrationDefault = new MockIntegration('ChaseSquirrels'); + const integration1 = new MockIntegration('ChaseSquirrels'); + const integration2 = new MockIntegration('ChaseSquirrels'); + + const integrations = getIntegrationsToSetup({ + defaultIntegrations: [integrationDefault], + integrations: [integration1, integration2], + }); + + expect(integrations).toEqual([integration2]); + }); }); describe('deduping', () => { diff --git a/packages/core/test/lib/tracing/trace.test.ts b/packages/core/test/lib/tracing/trace.test.ts index 0e0d16fb6ec4..83f4b150a6d6 100644 --- a/packages/core/test/lib/tracing/trace.test.ts +++ b/packages/core/test/lib/tracing/trace.test.ts @@ -1781,6 +1781,19 @@ describe('getActiveSpan', () => { const result = getActiveSpan(); expect(result).toBe(staticSpan); }); + + it('handles active span when passing scopes to withScope', () => { + const [scope, span] = startSpan({ name: 'outer' }, span => { + return [getCurrentScope(), span]; + }); + + const spanOnScope = withScope(scope, () => { + return getActiveSpan(); + }); + + expect(spanOnScope).toBeDefined(); + expect(spanOnScope).toBe(span); + }); }); describe('withActiveSpan()', () => { diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts index f95d36771b24..f388de7cb5ee 100644 --- a/packages/deno/src/index.ts +++ b/packages/deno/src/index.ts @@ -33,6 +33,7 @@ export { flush, getClient, isInitialized, + isEnabled, getCurrentScope, getGlobalScope, getIsolationScope, diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json index e9da408c67b3..eb5c18783e24 100644 --- a/packages/gatsby/package.json +++ b/packages/gatsby/package.json @@ -47,7 +47,7 @@ "dependencies": { "@sentry/core": "9.22.0", "@sentry/react": "9.22.0", - "@sentry/webpack-plugin": "3.3.1" + "@sentry/webpack-plugin": "3.5.0" }, "peerDependencies": { "gatsby": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0", diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts index 7376978a5226..f5d593312743 100644 --- a/packages/google-cloud-serverless/src/index.ts +++ b/packages/google-cloud-serverless/src/index.ts @@ -14,6 +14,7 @@ export { createTransport, getClient, isInitialized, + isEnabled, generateInstrumentOnce, getCurrentScope, getGlobalScope, diff --git a/packages/nestjs/package.json b/packages/nestjs/package.json index ca439183048c..e13dbd717cdc 100644 --- a/packages/nestjs/package.json +++ b/packages/nestjs/package.json @@ -48,7 +48,7 @@ "@opentelemetry/core": "^1.30.1", "@opentelemetry/instrumentation": "0.57.2", "@opentelemetry/instrumentation-nestjs-core": "0.44.1", - "@opentelemetry/semantic-conventions": "^1.30.0", + "@opentelemetry/semantic-conventions": "^1.34.0", "@sentry/core": "9.22.0", "@sentry/node": "9.22.0" }, diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 28e85accecf2..a34c83f2f909 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -77,7 +77,7 @@ }, "dependencies": { "@opentelemetry/api": "^1.9.0", - "@opentelemetry/semantic-conventions": "^1.30.0", + "@opentelemetry/semantic-conventions": "^1.34.0", "@rollup/plugin-commonjs": "28.0.1", "@sentry-internal/browser-utils": "9.22.0", "@sentry/core": "9.22.0", @@ -85,7 +85,7 @@ "@sentry/opentelemetry": "9.22.0", "@sentry/react": "9.22.0", "@sentry/vercel-edge": "9.22.0", - "@sentry/webpack-plugin": "3.3.1", + "@sentry/webpack-plugin": "3.5.0", "chalk": "3.0.0", "resolve": "1.22.8", "rollup": "4.35.0", diff --git a/packages/nextjs/src/config/webpackPluginOptions.ts b/packages/nextjs/src/config/webpackPluginOptions.ts index 335c7bea8976..641efd77524c 100644 --- a/packages/nextjs/src/config/webpackPluginOptions.ts +++ b/packages/nextjs/src/config/webpackPluginOptions.ts @@ -41,11 +41,15 @@ export function getWebpackPluginOptions( ); } - // TODO: We should think about uploading these when `widenClientFileUpload` is `true`. They may be useful in some situations. + // We want to include main-* files if widenClientFileUpload is true as they have proven to be useful + if (!sentryBuildOptions.widenClientFileUpload) { + sourcemapUploadIgnore.push(path.posix.join(distDirAbsPath, 'static', 'chunks', 'main-*')); + } + + // Always ignore framework, polyfills, and webpack files sourcemapUploadIgnore.push( path.posix.join(distDirAbsPath, 'static', 'chunks', 'framework-*'), path.posix.join(distDirAbsPath, 'static', 'chunks', 'framework.*'), - path.posix.join(distDirAbsPath, 'static', 'chunks', 'main-*'), path.posix.join(distDirAbsPath, 'static', 'chunks', 'polyfills-*'), path.posix.join(distDirAbsPath, 'static', 'chunks', 'webpack-*'), ); diff --git a/packages/nextjs/test/config/webpack/webpackPluginOptions.test.ts b/packages/nextjs/test/config/webpack/webpackPluginOptions.test.ts index 1dd0cfa95d5b..76ab58be9b64 100644 --- a/packages/nextjs/test/config/webpack/webpackPluginOptions.test.ts +++ b/packages/nextjs/test/config/webpack/webpackPluginOptions.test.ts @@ -153,9 +153,9 @@ describe('getWebpackPluginOptions()', () => { expect(generatedPluginOptions.sourcemaps).toMatchObject({ assets: ['/my/project/dir/.next/static/chunks/pages/**', '/my/project/dir/.next/static/chunks/app/**'], ignore: [ + '/my/project/dir/.next/static/chunks/main-*', '/my/project/dir/.next/static/chunks/framework-*', '/my/project/dir/.next/static/chunks/framework.*', - '/my/project/dir/.next/static/chunks/main-*', '/my/project/dir/.next/static/chunks/polyfills-*', '/my/project/dir/.next/static/chunks/webpack-*', ], @@ -170,7 +170,6 @@ describe('getWebpackPluginOptions()', () => { ignore: [ '/my/project/dir/.next/static/chunks/framework-*', '/my/project/dir/.next/static/chunks/framework.*', - '/my/project/dir/.next/static/chunks/main-*', '/my/project/dir/.next/static/chunks/polyfills-*', '/my/project/dir/.next/static/chunks/webpack-*', ], @@ -197,7 +196,6 @@ describe('getWebpackPluginOptions()', () => { ignore: [ 'C:/my/windows/project/dir/.dist/v1/static/chunks/framework-*', 'C:/my/windows/project/dir/.dist/v1/static/chunks/framework.*', - 'C:/my/windows/project/dir/.dist/v1/static/chunks/main-*', 'C:/my/windows/project/dir/.dist/v1/static/chunks/polyfills-*', 'C:/my/windows/project/dir/.dist/v1/static/chunks/webpack-*', ], diff --git a/packages/node/package.json b/packages/node/package.json index 88572cc9d500..12dccfc9555c 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -93,8 +93,8 @@ "@opentelemetry/instrumentation-undici": "0.10.1", "@opentelemetry/resources": "^1.30.1", "@opentelemetry/sdk-trace-base": "^1.30.1", - "@opentelemetry/semantic-conventions": "^1.30.0", - "@prisma/instrumentation": "6.7.0", + "@opentelemetry/semantic-conventions": "^1.34.0", + "@prisma/instrumentation": "6.8.2", "@sentry/core": "9.22.0", "@sentry/opentelemetry": "9.22.0", "import-in-the-middle": "^1.13.1", diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 03705bda89ba..5a933002bc23 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -65,6 +65,7 @@ export { export { addBreadcrumb, isInitialized, + isEnabled, getGlobalScope, lastEventId, close, diff --git a/packages/node/src/integrations/http/SentryHttpInstrumentation.ts b/packages/node/src/integrations/http/SentryHttpInstrumentation.ts index 03c1260a2e5a..ac82134fd07f 100644 --- a/packages/node/src/integrations/http/SentryHttpInstrumentation.ts +++ b/packages/node/src/integrations/http/SentryHttpInstrumentation.ts @@ -77,8 +77,8 @@ export type SentryHttpInstrumentationOptions = InstrumentationConfig & { * Do not capture the request body for incoming HTTP requests to URLs where the given callback returns `true`. * This can be useful for long running requests where the body is not needed and we want to avoid capturing it. * - * @param url Contains the entire URL, including query string (if any), protocol, host, etc. of the outgoing request. - * @param request Contains the {@type RequestOptions} object used to make the outgoing request. + * @param url Contains the entire URL, including query string (if any), protocol, host, etc. of the incoming request. + * @param request Contains the {@type RequestOptions} object used to make the incoming request. */ ignoreIncomingRequestBody?: (url: string, request: http.RequestOptions) => boolean; diff --git a/packages/node/src/integrations/http/index.ts b/packages/node/src/integrations/http/index.ts index 72326e21e6f1..96e9b84315be 100644 --- a/packages/node/src/integrations/http/index.ts +++ b/packages/node/src/integrations/http/index.ts @@ -3,7 +3,8 @@ import { diag } from '@opentelemetry/api'; import type { HttpInstrumentationConfig } from '@opentelemetry/instrumentation-http'; import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; import type { Span } from '@sentry/core'; -import { defineIntegration, getClient } from '@sentry/core'; +import { defineIntegration, getClient, hasSpansEnabled } from '@sentry/core'; +import { NODE_VERSION } from '../../nodeVersion'; import { generateInstrumentOnce } from '../../otel/instrument'; import type { NodeClient } from '../../sdk/client'; import type { HTTPModuleRequestIncomingMessage } from '../../transports/http-module'; @@ -85,8 +86,8 @@ interface HttpOptions { * Do not capture the request body for incoming HTTP requests to URLs where the given callback returns `true`. * This can be useful for long running requests where the body is not needed and we want to avoid capturing it. * - * @param url Contains the entire URL, including query string (if any), protocol, host, etc. of the outgoing request. - * @param request Contains the {@type RequestOptions} object used to make the outgoing request. + * @param url Contains the entire URL, including query string (if any), protocol, host, etc. of the incoming request. + * @param request Contains the {@type RequestOptions} object used to make the incoming request. */ ignoreIncomingRequestBody?: (url: string, request: RequestOptions) => boolean; @@ -159,8 +160,22 @@ export const instrumentOtelHttp = generateInstrumentOnce = {}): boolean { // If `spans` is passed in, it takes precedence - // Else, we by default emit spans, unless `skipOpenTelemetrySetup` is set to `true` - return typeof options.spans === 'boolean' ? options.spans : !clientOptions.skipOpenTelemetrySetup; + // Else, we by default emit spans, unless `skipOpenTelemetrySetup` is set to `true` or spans are not enabled + if (typeof options.spans === 'boolean') { + return options.spans; + } + + if (clientOptions.skipOpenTelemetrySetup) { + return false; + } + + // IMPORTANT: We only disable span instrumentation when spans are not enabled _and_ we are on Node 22+, + // as otherwise the necessary diagnostics channel is not available yet + if (!hasSpansEnabled(clientOptions) && NODE_VERSION.major >= 22) { + return false; + } + + return true; } /** diff --git a/packages/node/src/integrations/node-fetch/index.ts b/packages/node/src/integrations/node-fetch/index.ts index d62d1e7c88c2..e85ce34dfb35 100644 --- a/packages/node/src/integrations/node-fetch/index.ts +++ b/packages/node/src/integrations/node-fetch/index.ts @@ -1,7 +1,7 @@ import type { UndiciInstrumentationConfig } from '@opentelemetry/instrumentation-undici'; import { UndiciInstrumentation } from '@opentelemetry/instrumentation-undici'; import type { IntegrationFn } from '@sentry/core'; -import { defineIntegration, getClient, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { defineIntegration, getClient, hasSpansEnabled, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { generateInstrumentOnce } from '../../otel/instrument'; import type { NodeClient } from '../../sdk/client'; import type { NodeClientOptions } from '../../types'; @@ -86,8 +86,10 @@ function getAbsoluteUrl(origin: string, path: string = '/'): string { function _shouldInstrumentSpans(options: NodeFetchOptions, clientOptions: Partial = {}): boolean { // If `spans` is passed in, it takes precedence - // Else, we by default emit spans, unless `skipOpenTelemetrySetup` is set to `true` - return typeof options.spans === 'boolean' ? options.spans : !clientOptions.skipOpenTelemetrySetup; + // Else, we by default emit spans, unless `skipOpenTelemetrySetup` is set to `true` or spans are not enabled + return typeof options.spans === 'boolean' + ? options.spans + : !clientOptions.skipOpenTelemetrySetup && hasSpansEnabled(clientOptions); } function getConfigWithDefaults(options: Partial = {}): UndiciInstrumentationConfig { diff --git a/packages/node/src/integrations/spotlight.ts b/packages/node/src/integrations/spotlight.ts index 49a169076798..4e36f3692fb0 100644 --- a/packages/node/src/integrations/spotlight.ts +++ b/packages/node/src/integrations/spotlight.ts @@ -1,6 +1,6 @@ import * as http from 'node:http'; import type { Client, Envelope, IntegrationFn } from '@sentry/core'; -import { defineIntegration, logger, serializeEnvelope } from '@sentry/core'; +import { defineIntegration, logger, serializeEnvelope, suppressTracing } from '@sentry/core'; type SpotlightConnectionOptions = { /** @@ -20,7 +20,12 @@ const _spotlightIntegration = ((options: Partial = { return { name: INTEGRATION_NAME, setup(client) { - if (typeof process === 'object' && process.env && process.env.NODE_ENV !== 'development') { + if ( + typeof process === 'object' && + process.env && + process.env.NODE_ENV && + process.env.NODE_ENV !== 'development' + ) { logger.warn("[Spotlight] It seems you're not in dev mode. Do you really want to have Spotlight enabled?"); } connectToSpotlight(client, _options); @@ -52,40 +57,40 @@ function connectToSpotlight(client: Client, options: Required { + const req = http.request( + { + method: 'POST', + path: spotlightUrl.pathname, + hostname: spotlightUrl.hostname, + port: spotlightUrl.port, + headers: { + 'Content-Type': 'application/x-sentry-envelope', + }, }, - }, - res => { - if (res.statusCode && res.statusCode >= 200 && res.statusCode < 400) { - // Reset failed requests counter on success - failedRequests = 0; - } - res.on('data', () => { - // Drain socket - }); - - res.on('end', () => { - // Drain socket - }); - res.setEncoding('utf8'); - }, - ); - - req.on('error', () => { - failedRequests++; - logger.warn('[Spotlight] Failed to send envelope to Spotlight Sidecar'); + res => { + if (res.statusCode && res.statusCode >= 200 && res.statusCode < 400) { + // Reset failed requests counter on success + failedRequests = 0; + } + res.on('data', () => { + // Drain socket + }); + + res.on('end', () => { + // Drain socket + }); + res.setEncoding('utf8'); + }, + ); + + req.on('error', () => { + failedRequests++; + logger.warn('[Spotlight] Failed to send envelope to Spotlight Sidecar'); + }); + req.write(serializedEnvelope); + req.end(); }); - req.write(serializedEnvelope); - req.end(); }); } @@ -97,22 +102,3 @@ function parseSidecarUrl(url: string): URL | undefined { return undefined; } } - -type HttpRequestImpl = typeof http.request; -type WrappedHttpRequest = HttpRequestImpl & { __sentry_original__: HttpRequestImpl }; - -/** - * We want to get an unpatched http request implementation to avoid capturing our own calls. - */ -export function getNativeHttpRequest(): HttpRequestImpl { - const { request } = http; - if (isWrapped(request)) { - return request.__sentry_original__; - } - - return request; -} - -function isWrapped(impl: HttpRequestImpl): impl is WrappedHttpRequest { - return '__sentry_original__' in impl; -} diff --git a/packages/node/test/integrations/http.test.ts b/packages/node/test/integrations/http.test.ts index 6d4bea24661f..89052a348ea4 100644 --- a/packages/node/test/integrations/http.test.ts +++ b/packages/node/test/integrations/http.test.ts @@ -1,19 +1,30 @@ import { describe, expect, it } from 'vitest'; import { _shouldInstrumentSpans } from '../../src/integrations/http'; +import { conditionalTest } from '../helpers/conditional'; describe('httpIntegration', () => { describe('_shouldInstrumentSpans', () => { it.each([ - [{}, {}, true], [{ spans: true }, {}, true], [{ spans: false }, {}, false], [{ spans: true }, { skipOpenTelemetrySetup: true }, true], [{ spans: false }, { skipOpenTelemetrySetup: true }, false], [{}, { skipOpenTelemetrySetup: true }, false], - [{}, { skipOpenTelemetrySetup: false }, true], + [{}, { tracesSampleRate: 0, skipOpenTelemetrySetup: true }, false], + [{}, { tracesSampleRate: 0 }, true], ])('returns the correct value for options=%j and clientOptions=%j', (options, clientOptions, expected) => { const actual = _shouldInstrumentSpans(options, clientOptions); expect(actual).toBe(expected); }); + + conditionalTest({ min: 22 })('returns false without tracesSampleRate on Node >=22', () => { + const actual = _shouldInstrumentSpans({}, {}); + expect(actual).toBe(false); + }); + + conditionalTest({ max: 21 })('returns true without tracesSampleRate on Node <22', () => { + const actual = _shouldInstrumentSpans({}, {}); + expect(actual).toBe(true); + }); }); }); diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module.ts index a7e81a49d045..377508e860a2 100644 --- a/packages/nuxt/src/module.ts +++ b/packages/nuxt/src/module.ts @@ -35,7 +35,7 @@ export default defineNuxtModule({ const moduleDirResolver = createResolver(import.meta.url); const buildDirResolver = createResolver(nuxt.options.buildDir); - const clientConfigFile = findDefaultSdkInitFile('client'); + const clientConfigFile = findDefaultSdkInitFile('client', nuxt); if (clientConfigFile) { // Inject the client-side Sentry config file with a side effect import @@ -59,7 +59,7 @@ export default defineNuxtModule({ addPlugin({ src: moduleDirResolver.resolve('./runtime/plugins/sentry.client'), mode: 'client' }); } - const serverConfigFile = findDefaultSdkInitFile('server'); + const serverConfigFile = findDefaultSdkInitFile('server', nuxt); if (serverConfigFile) { addServerPlugin(moduleDirResolver.resolve('./runtime/plugins/sentry.server')); diff --git a/packages/nuxt/src/vite/addServerConfig.ts b/packages/nuxt/src/vite/addServerConfig.ts index 771c534705cb..83e59d439ecf 100644 --- a/packages/nuxt/src/vite/addServerConfig.ts +++ b/packages/nuxt/src/vite/addServerConfig.ts @@ -8,6 +8,7 @@ import type { SentryNuxtModuleOptions } from '../common/types'; import { constructFunctionReExport, constructWrappedFunctionExportQuery, + getExternalOptionsWithSentryNuxt, getFilenameFromNodeStartCommand, QUERY_END_INDICATOR, removeSentryQueryFromPath, @@ -130,6 +131,13 @@ function injectServerConfigPlugin(nitro: Nitro, serverConfigFile: string, debug? return { name: 'rollup-plugin-inject-sentry-server-config', + options(opts) { + return { + ...opts, + external: getExternalOptionsWithSentryNuxt(opts.external), + }; + }, + buildStart() { const configPath = createResolver(nitro.options.srcDir).resolve(`/${serverConfigFile}`); diff --git a/packages/nuxt/src/vite/utils.ts b/packages/nuxt/src/vite/utils.ts index ea2db2bc21b8..a0e88c892a25 100644 --- a/packages/nuxt/src/vite/utils.ts +++ b/packages/nuxt/src/vite/utils.ts @@ -1,29 +1,50 @@ import { consoleSandbox } from '@sentry/core'; import * as fs from 'fs'; +import type { Nuxt } from 'nuxt/schema'; import * as path from 'path'; +import type { ExternalOption } from 'rollup'; /** * Find the default SDK init file for the given type (client or server). * The sentry.server.config file is prioritized over the instrument.server file. */ -export function findDefaultSdkInitFile(type: 'server' | 'client'): string | undefined { +export function findDefaultSdkInitFile(type: 'server' | 'client', nuxt?: Nuxt): string | undefined { const possibleFileExtensions = ['ts', 'js', 'mjs', 'cjs', 'mts', 'cts']; - const cwd = process.cwd(); + const relativePaths: string[] = []; - const filePaths: string[] = []; if (type === 'server') { for (const ext of possibleFileExtensions) { - // order is important here - we want to prioritize the server.config file - filePaths.push(path.join(cwd, `sentry.${type}.config.${ext}`)); - filePaths.push(path.join(cwd, 'public', `instrument.${type}.${ext}`)); + relativePaths.push(`sentry.${type}.config.${ext}`); + relativePaths.push(path.join('public', `instrument.${type}.${ext}`)); } } else { for (const ext of possibleFileExtensions) { - filePaths.push(path.join(cwd, `sentry.${type}.config.${ext}`)); + relativePaths.push(`sentry.${type}.config.${ext}`); } } - return filePaths.find(filename => fs.existsSync(filename)); + // Get layers from highest priority to lowest + const layers = [...(nuxt?.options._layers ?? [])].reverse(); + + for (const layer of layers) { + for (const relativePath of relativePaths) { + const fullPath = path.resolve(layer.cwd, relativePath); + if (fs.existsSync(fullPath)) { + return fullPath; + } + } + } + + // As a fallback, also check CWD (left for pure compatibility) + const cwd = process.cwd(); + for (const relativePath of relativePaths) { + const fullPath = path.resolve(cwd, relativePath); + if (fs.existsSync(fullPath)) { + return fullPath; + } + } + + return undefined; } /** @@ -52,6 +73,35 @@ export function removeSentryQueryFromPath(url: string): string { return url.replace(regex, ''); } +/** + * Add @sentry/nuxt to the external options of the Rollup configuration to prevent Rollup bundling all dependencies + * that would result in adding imports from OpenTelemetry libraries etc. to the server build. + */ +export function getExternalOptionsWithSentryNuxt(previousExternal: ExternalOption | undefined): ExternalOption { + const sentryNuxt = /^@sentry\/nuxt$/; + let external: ExternalOption; + + if (typeof previousExternal === 'function') { + external = new Proxy(previousExternal, { + apply(target, thisArg, args: [string, string | undefined, boolean]) { + const [source] = args; + if (sentryNuxt.test(source)) { + return true; + } + return Reflect.apply(target, thisArg, args); + }, + }); + } else if (Array.isArray(previousExternal)) { + external = [sentryNuxt, ...previousExternal]; + } else if (previousExternal) { + external = [sentryNuxt, previousExternal]; + } else { + external = sentryNuxt; + } + + return external; +} + /** * Extracts and sanitizes function re-export and function wrap query parameters from a query string. * If it is a default export, it is not considered for re-exporting. diff --git a/packages/nuxt/test/vite/utils.test.ts b/packages/nuxt/test/vite/utils.test.ts index 7ffd7654549e..9419c5f0a545 100644 --- a/packages/nuxt/test/vite/utils.test.ts +++ b/packages/nuxt/test/vite/utils.test.ts @@ -1,10 +1,12 @@ import * as fs from 'fs'; +import type { Nuxt } from 'nuxt/schema'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { constructFunctionReExport, constructWrappedFunctionExportQuery, extractFunctionReexportQueryParameters, findDefaultSdkInitFile, + getExternalOptionsWithSentryNuxt, getFilenameFromNodeStartCommand, QUERY_END_INDICATOR, removeSentryQueryFromPath, @@ -69,6 +71,75 @@ describe('findDefaultSdkInitFile', () => { const result = findDefaultSdkInitFile('server'); expect(result).toMatch('packages/nuxt/sentry.server.config.js'); }); + + it('should return the latest layer config file path if client config exists', () => { + vi.spyOn(fs, 'existsSync').mockImplementation(filePath => { + return !(filePath instanceof URL) && filePath.includes('sentry.client.config.ts'); + }); + + const nuxtMock = { + options: { + _layers: [ + { + cwd: 'packages/nuxt/module', + }, + { + cwd: 'packages/nuxt', + }, + ], + }, + } as Nuxt; + + const result = findDefaultSdkInitFile('client', nuxtMock); + expect(result).toMatch('packages/nuxt/sentry.client.config.ts'); + }); + + it('should return the latest layer config file path if server config exists', () => { + vi.spyOn(fs, 'existsSync').mockImplementation(filePath => { + return ( + !(filePath instanceof URL) && + (filePath.includes('sentry.server.config.ts') || filePath.includes('instrument.server.ts')) + ); + }); + + const nuxtMock = { + options: { + _layers: [ + { + cwd: 'packages/nuxt/module', + }, + { + cwd: 'packages/nuxt', + }, + ], + }, + } as Nuxt; + + const result = findDefaultSdkInitFile('server', nuxtMock); + expect(result).toMatch('packages/nuxt/sentry.server.config.ts'); + }); + + it('should return the latest layer config file path if client config exists in former layer', () => { + vi.spyOn(fs, 'existsSync').mockImplementation(filePath => { + return !(filePath instanceof URL) && filePath.includes('nuxt/sentry.client.config.ts'); + }); + + const nuxtMock = { + options: { + _layers: [ + { + cwd: 'packages/nuxt/module', + }, + { + cwd: 'packages/nuxt', + }, + ], + }, + } as Nuxt; + + const result = findDefaultSdkInitFile('client', nuxtMock); + expect(result).toMatch('packages/nuxt/sentry.client.config.ts'); + }); }); describe('getFilenameFromPath', () => { @@ -296,3 +367,60 @@ export { foo_sentryWrapped as foo }; expect(result).toBe(''); }); }); + +describe('getExternalOptionsWithSentryNuxt', () => { + it('should return sentryExternals when previousExternal is undefined', () => { + const result = getExternalOptionsWithSentryNuxt(undefined); + expect(result).toEqual(/^@sentry\/nuxt$/); + }); + + it('should merge sentryExternals with array previousExternal', () => { + const previousExternal = [/vue/, 'react']; + const result = getExternalOptionsWithSentryNuxt(previousExternal); + expect(result).toEqual([/^@sentry\/nuxt$/, /vue/, 'react']); + }); + + it('should create array with sentryExternals and non-array previousExternal', () => { + const previousExternal = 'vue'; + const result = getExternalOptionsWithSentryNuxt(previousExternal); + expect(result).toEqual([/^@sentry\/nuxt$/, 'vue']); + }); + + it('should create a proxy when previousExternal is a function', () => { + const mockExternalFn = vi.fn().mockReturnValue(false); + const result = getExternalOptionsWithSentryNuxt(mockExternalFn); + + expect(typeof result).toBe('function'); + expect(result).toBeInstanceOf(Function); + }); + + it('should return true from proxied function when source is @sentry/nuxt', () => { + const mockExternalFn = vi.fn().mockReturnValue(false); + const result = getExternalOptionsWithSentryNuxt(mockExternalFn); + + // @ts-expect-error - result is a function + const output = result('@sentry/nuxt', undefined, false); + expect(output).toBe(true); + expect(mockExternalFn).not.toHaveBeenCalled(); + }); + + it('should return false from proxied function and call function when source just includes @sentry/nuxt', () => { + const mockExternalFn = vi.fn().mockReturnValue(false); + const result = getExternalOptionsWithSentryNuxt(mockExternalFn); + + // @ts-expect-error - result is a function + const output = result('@sentry/nuxt/dist/index.js', undefined, false); + expect(output).toBe(false); + expect(mockExternalFn).toHaveBeenCalledWith('@sentry/nuxt/dist/index.js', undefined, false); + }); + + it('should call original function when source does not include @sentry/nuxt', () => { + const mockExternalFn = vi.fn().mockReturnValue(false); + const result = getExternalOptionsWithSentryNuxt(mockExternalFn); + + // @ts-expect-error - result is a function + const output = result('vue', undefined, false); + expect(output).toBe(false); + expect(mockExternalFn).toHaveBeenCalledWith('vue', undefined, false); + }); +}); diff --git a/packages/opentelemetry/package.json b/packages/opentelemetry/package.json index b6b378d1d3da..b50d85981653 100644 --- a/packages/opentelemetry/package.json +++ b/packages/opentelemetry/package.json @@ -47,7 +47,7 @@ "@opentelemetry/core": "^1.30.1 || ^2.0.0", "@opentelemetry/instrumentation": "^0.57.1 || ^0.200.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.0.0", - "@opentelemetry/semantic-conventions": "^1.30.0" + "@opentelemetry/semantic-conventions": "^1.34.0" }, "devDependencies": { "@opentelemetry/api": "^1.9.0", @@ -55,7 +55,7 @@ "@opentelemetry/core": "^1.30.1", "@opentelemetry/instrumentation": "^0.57.2", "@opentelemetry/sdk-trace-base": "^1.30.1", - "@opentelemetry/semantic-conventions": "^1.30.0" + "@opentelemetry/semantic-conventions": "^1.34.0" }, "scripts": { "build": "run-p build:transpile build:types", diff --git a/packages/opentelemetry/src/asyncContextStrategy.ts b/packages/opentelemetry/src/asyncContextStrategy.ts index 695175bc3fa1..9f7b38d0b43d 100644 --- a/packages/opentelemetry/src/asyncContextStrategy.ts +++ b/packages/opentelemetry/src/asyncContextStrategy.ts @@ -8,7 +8,7 @@ import { } from './constants'; import { continueTrace, startInactiveSpan, startSpan, startSpanManual, withActiveSpan } from './trace'; import type { CurrentScopes } from './types'; -import { getScopesFromContext } from './utils/contextData'; +import { getContextFromScope, getScopesFromContext } from './utils/contextData'; import { getActiveSpan } from './utils/getActiveSpan'; import { getTraceData } from './utils/getTraceData'; import { suppressTracing } from './utils/suppressTracing'; @@ -48,7 +48,7 @@ export function setOpenTelemetryContextAsyncContextStrategy(): void { } function withSetScope(scope: Scope, callback: (scope: Scope) => T): T { - const ctx = api.context.active(); + const ctx = getContextFromScope(scope) || api.context.active(); // We depend on the otelContextManager to handle the context/hub // We set the `SENTRY_FORK_SET_SCOPE_CONTEXT_KEY` context value, which is picked up by diff --git a/packages/opentelemetry/test/trace.test.ts b/packages/opentelemetry/test/trace.test.ts index 74b37809f4ae..f9aed823a4a4 100644 --- a/packages/opentelemetry/test/trace.test.ts +++ b/packages/opentelemetry/test/trace.test.ts @@ -1342,6 +1342,21 @@ describe('trace', () => { }); }); }); + + describe('scope passing', () => { + it('handles active span when passing scopes to withScope', () => { + const [scope, span] = startSpan({ name: 'outer' }, span => { + return [getCurrentScope(), span]; + }); + + const spanOnScope = withScope(scope, () => { + return getActiveSpan(); + }); + + expect(spanOnScope).toBeDefined(); + expect(spanOnScope).toBe(span); + }); + }); }); describe('trace (tracing disabled)', () => { diff --git a/packages/react-router/package.json b/packages/react-router/package.json index e488a2029acc..ac3872ff45c0 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -37,9 +37,9 @@ "@opentelemetry/api": "^1.9.0", "@opentelemetry/core": "^1.30.1", "@opentelemetry/instrumentation": "0.57.2", - "@opentelemetry/semantic-conventions": "^1.30.0", + "@opentelemetry/semantic-conventions": "^1.34.0", "@sentry/browser": "9.22.0", - "@sentry/cli": "^2.43.0", + "@sentry/cli": "^2.45.0", "@sentry/core": "9.22.0", "@sentry/node": "9.22.0", "@sentry/vite-plugin": "^3.2.4", diff --git a/packages/react/src/reactrouterv6-compat-utils.tsx b/packages/react/src/reactrouterv6-compat-utils.tsx index 97109e191fcf..d9d0cbe45e8c 100644 --- a/packages/react/src/reactrouterv6-compat-utils.tsx +++ b/packages/react/src/reactrouterv6-compat-utils.tsx @@ -236,7 +236,9 @@ export function createReactRouterV6CompatibleTracingIntegration( return { ...integration, - setup() { + setup(client) { + integration.setup(client); + _useEffect = useEffect; _useLocation = useLocation; _useNavigationType = useNavigationType; diff --git a/packages/remix/package.json b/packages/remix/package.json index be1bf30ad1d9..52c657ab1999 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -66,9 +66,9 @@ "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/instrumentation": "^0.57.2", - "@opentelemetry/semantic-conventions": "^1.30.0", + "@opentelemetry/semantic-conventions": "^1.34.0", "@remix-run/router": "1.x", - "@sentry/cli": "^2.43.0", + "@sentry/cli": "^2.45.0", "@sentry/core": "9.22.0", "@sentry/node": "9.22.0", "@sentry/opentelemetry": "9.22.0", diff --git a/packages/remix/src/cloudflare/index.ts b/packages/remix/src/cloudflare/index.ts index f503adaf0711..3d3d17e1da27 100644 --- a/packages/remix/src/cloudflare/index.ts +++ b/packages/remix/src/cloudflare/index.ts @@ -55,6 +55,7 @@ export { flush, getClient, isInitialized, + isEnabled, getCurrentScope, getGlobalScope, getIsolationScope, diff --git a/packages/remix/src/server/index.ts b/packages/remix/src/server/index.ts index f90ff55eca6c..0aa30b3c93cc 100644 --- a/packages/remix/src/server/index.ts +++ b/packages/remix/src/server/index.ts @@ -52,6 +52,7 @@ export { eventFiltersIntegration, initOpenTelemetry, isInitialized, + isEnabled, knexIntegration, kafkaIntegration, koaIntegration, diff --git a/packages/solidstart/src/server/index.ts b/packages/solidstart/src/server/index.ts index 06e97a20a96a..f11b9bb51077 100644 --- a/packages/solidstart/src/server/index.ts +++ b/packages/solidstart/src/server/index.ts @@ -55,6 +55,7 @@ export { eventFiltersIntegration, initOpenTelemetry, isInitialized, + isEnabled, knexIntegration, kafkaIntegration, koaIntegration, diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index 5e49fa45fed3..8cc952bfb7e7 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -57,6 +57,7 @@ export { eventFiltersIntegration, initOpenTelemetry, isInitialized, + isEnabled, knexIntegration, kafkaIntegration, koaIntegration, diff --git a/packages/sveltekit/src/worker/index.ts b/packages/sveltekit/src/worker/index.ts index 9fc8429e5864..3614922072ec 100644 --- a/packages/sveltekit/src/worker/index.ts +++ b/packages/sveltekit/src/worker/index.ts @@ -46,6 +46,7 @@ export { // eslint-disable-next-line deprecation/deprecation inboundFiltersIntegration, isInitialized, + isEnabled, lastEventId, linkedErrorsIntegration, requestDataIntegration, diff --git a/packages/tanstackstart-react/package.json b/packages/tanstackstart-react/package.json index d8930ee14811..b95d4b34db00 100644 --- a/packages/tanstackstart-react/package.json +++ b/packages/tanstackstart-react/package.json @@ -51,7 +51,7 @@ }, "dependencies": { "@opentelemetry/api": "^1.9.0", - "@opentelemetry/semantic-conventions": "^1.30.0", + "@opentelemetry/semantic-conventions": "^1.34.0", "@sentry-internal/browser-utils": "9.22.0", "@sentry/core": "9.22.0", "@sentry/node": "9.22.0", diff --git a/packages/vercel-edge/package.json b/packages/vercel-edge/package.json index 2af29ebfa807..88be3c33b114 100644 --- a/packages/vercel-edge/package.json +++ b/packages/vercel-edge/package.json @@ -47,7 +47,7 @@ "@opentelemetry/core": "^1.30.1", "@opentelemetry/resources": "^1.30.1", "@opentelemetry/sdk-trace-base": "^1.30.1", - "@opentelemetry/semantic-conventions": "^1.28.0", + "@opentelemetry/semantic-conventions": "^1.34.0", "@sentry/opentelemetry": "9.22.0" }, "scripts": { diff --git a/packages/vercel-edge/src/index.ts b/packages/vercel-edge/src/index.ts index c98cf8ed253d..ff1231c8a1f8 100644 --- a/packages/vercel-edge/src/index.ts +++ b/packages/vercel-edge/src/index.ts @@ -33,6 +33,7 @@ export { flush, getClient, isInitialized, + isEnabled, getCurrentScope, getGlobalScope, getIsolationScope, diff --git a/yarn.lock b/yarn.lock index 67dbf24aac88..1b5d8e707ce3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5744,10 +5744,10 @@ resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz#337fb2bca0453d0726696e745f50064411f646d6" integrity sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA== -"@opentelemetry/semantic-conventions@^1.27.0", "@opentelemetry/semantic-conventions@^1.28.0", "@opentelemetry/semantic-conventions@^1.29.0", "@opentelemetry/semantic-conventions@^1.30.0": - version "1.32.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.32.0.tgz#a15e8f78f32388a7e4655e7f539570e40958ca3f" - integrity sha512-s0OpmpQFSfMrmedAn9Lhg4KWJELHCU6uU9dtIJ28N8UGhf9Y55im5X8fEzwhwDwiSqN+ZPSNrDJF7ivf/AuRPQ== +"@opentelemetry/semantic-conventions@^1.27.0", "@opentelemetry/semantic-conventions@^1.29.0", "@opentelemetry/semantic-conventions@^1.34.0": + version "1.34.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.34.0.tgz#8b6a46681b38a4d5947214033ac48128328c1738" + integrity sha512-aKcOkyrorBGlajjRdVoJWHTxfxO1vCNHLJVlSDaRHDIdjU+pX8IYQPvPDkYiujKLbRnWU+1TBwEt0QRgSm4SGA== "@opentelemetry/sql-common@^0.40.1": version "0.40.1" @@ -5879,10 +5879,10 @@ resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.28.tgz#d45e01c4a56f143ee69c54dd6b12eade9e270a73" integrity sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw== -"@prisma/instrumentation@6.7.0": - version "6.7.0" - resolved "https://registry.yarnpkg.com/@prisma/instrumentation/-/instrumentation-6.7.0.tgz#5fd97be1f89e9d9268148424a812deaea491f80a" - integrity sha512-3NuxWlbzYNevgPZbV0ktA2z6r0bfh0g22ONTxcK09a6+6MdIPjHsYx1Hnyu4yOq+j7LmupO5J69hhuOnuvj8oQ== +"@prisma/instrumentation@6.8.2": + version "6.8.2" + resolved "https://registry.yarnpkg.com/@prisma/instrumentation/-/instrumentation-6.8.2.tgz#77a87a37f67ab35eaaf8ff629f889e9e11a465ac" + integrity sha512-5NCTbZjw7a+WIZ/ey6G8SY+YKcyM2zBF0hOT1muvqC9TbVtTCr5Qv3RL/2iNDOzLUHEvo4I1uEfioyfuNOGK8Q== dependencies: "@opentelemetry/instrumentation" "^0.52.0 || ^0.53.0 || ^0.54.0 || ^0.55.0 || ^0.56.0 || ^0.57.0" @@ -6474,16 +6474,16 @@ resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-3.2.4.tgz#c0877df6e5ce227bf51754bf27da2fa5227af847" integrity sha512-yBzRn3GEUSv1RPtE4xB4LnuH74ZxtdoRJ5cmQ9i6mzlmGDxlrnKuvem5++AolZTE9oJqAD3Tx2rd1PqmpWnLoA== -"@sentry/babel-plugin-component-annotate@3.3.1": - version "3.3.1" - resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-3.3.1.tgz#baecd89396cbb4659565a4e8efe7f0a71b19262a" - integrity sha512-5GOxGT7lZN+I8A7Vp0rWY+726FDKEw8HnFiebe51rQrMbfGfCu2Aw9uSM0nT9OG6xhV6WvGccIcCszTPs4fUZQ== - "@sentry/babel-plugin-component-annotate@3.4.0": version "3.4.0" resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-3.4.0.tgz#f47a7652e16f84556df82cbc38f0004bca1335d1" integrity sha512-tSzfc3aE7m0PM0Aj7HBDet5llH9AB9oc+tBQ8AvOqUSnWodLrNCuWeQszJ7mIBovD3figgCU3h0cvI6U5cDtsg== +"@sentry/babel-plugin-component-annotate@3.5.0": + version "3.5.0" + resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-3.5.0.tgz#1b0d01f903b725da876117d551610085c3dd21c7" + integrity sha512-s2go8w03CDHbF9luFGtBHKJp4cSpsQzNVqgIa9Pfa4wnjipvrK6CxVT4icpLA3YO6kg5u622Yoa5GF3cJdippw== + "@sentry/bundler-plugin-core@2.22.6": version "2.22.6" resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-2.22.6.tgz#a1ea1fd43700a3ece9e7db016997e79a2782b87d" @@ -6512,13 +6512,13 @@ magic-string "0.30.8" unplugin "1.0.1" -"@sentry/bundler-plugin-core@3.3.1": - version "3.3.1" - resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-3.3.1.tgz#67c5017dc8a70f629c14e88420c6ede4e51c2047" - integrity sha512-Dd6xaWb293j9otEJ1yJqG2Ra6zB49OPzMNdIkdP8wdY+S9UFQE5PyKTyredmPY7hqCc005OrUQZolIIo9Zl13A== +"@sentry/bundler-plugin-core@3.4.0": + version "3.4.0" + resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-3.4.0.tgz#3a3459aba94cbeb093347f5730f15df25153fd0a" + integrity sha512-X1Q41AsQ6xcT6hB4wYmBDBukndKM/inT4IsR7pdKLi7ICpX2Qq6lisamBAEPCgEvnLpazSFguaiC0uiwMKAdqw== dependencies: "@babel/core" "^7.18.5" - "@sentry/babel-plugin-component-annotate" "3.3.1" + "@sentry/babel-plugin-component-annotate" "3.4.0" "@sentry/cli" "2.42.2" dotenv "^16.3.1" find-up "^5.0.0" @@ -6526,13 +6526,13 @@ magic-string "0.30.8" unplugin "1.0.1" -"@sentry/bundler-plugin-core@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-3.4.0.tgz#3a3459aba94cbeb093347f5730f15df25153fd0a" - integrity sha512-X1Q41AsQ6xcT6hB4wYmBDBukndKM/inT4IsR7pdKLi7ICpX2Qq6lisamBAEPCgEvnLpazSFguaiC0uiwMKAdqw== +"@sentry/bundler-plugin-core@3.5.0": + version "3.5.0" + resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-3.5.0.tgz#b62af5be1b1a862e7062181655829c556c7d7c0b" + integrity sha512-zDzPrhJqAAy2VzV4g540qAZH4qxzisstK2+NIJPZUUKztWRWUV2cMHsyUtdctYgloGkLyGpZJBE3RE6dmP/xqQ== dependencies: "@babel/core" "^7.18.5" - "@sentry/babel-plugin-component-annotate" "3.4.0" + "@sentry/babel-plugin-component-annotate" "3.5.0" "@sentry/cli" "2.42.2" dotenv "^16.3.1" find-up "^5.0.0" @@ -6545,75 +6545,75 @@ resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.42.2.tgz#a32a4f226e717122b37d9969e8d4d0e14779f720" integrity sha512-GtJSuxER7Vrp1IpxdUyRZzcckzMnb4N5KTW7sbTwUiwqARRo+wxS+gczYrS8tdgtmXs5XYhzhs+t4d52ITHMIg== -"@sentry/cli-darwin@2.43.0": - version "2.43.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.43.0.tgz#544caac85069a34d435b0723d8ac6bb6a530ab33" - integrity sha512-0MYvRHJowXOMNY5W6XF4p9GQNH3LuQ+IHAQwVbZOsfwnEv8e20rf9BiPPzmJ9sIjZSWYR4yIqm6dBp6ABJFbGQ== +"@sentry/cli-darwin@2.45.0": + version "2.45.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.45.0.tgz#e3d6feae4fadcfdf91db9c7b9c4689a66d3d8d19" + integrity sha512-p4Uxfv/L2fQdP3/wYnKVVz9gzZJf/1Xp9D+6raax/3Bu5y87yHYUqcdt98y/VAXQD4ofp2QgmhGUVPofvQNZmg== "@sentry/cli-linux-arm64@2.42.2": version "2.42.2" resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.42.2.tgz#1c06c83ff21f51ec23acf5be3b1f8c7553bf86b1" integrity sha512-BOxzI7sgEU5Dhq3o4SblFXdE9zScpz6EXc5Zwr1UDZvzgXZGosUtKVc7d1LmkrHP8Q2o18HcDWtF3WvJRb5Zpw== -"@sentry/cli-linux-arm64@2.43.0": - version "2.43.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.43.0.tgz#e110758c7cf6ced78d8b0b3c8c56802984b0fb98" - integrity sha512-7URSaNjbEJQZyYJ33XK3pVKl6PU2oO9ETF6R/4Cz2FmU3fecACLKVldv7+OuNl9aspLZ62mnPMDvT732/Fp2Ug== +"@sentry/cli-linux-arm64@2.45.0": + version "2.45.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.45.0.tgz#384c8e17f7e7dc007d164033d0e7c75aa83a2e9b" + integrity sha512-gUcLoEjzg7AIc4QQGEZwRHri+EHf3Gcms9zAR1VHiNF3/C/jL4WeDPJF2YiWAQt6EtH84tHiyhw1Ab/R8XFClg== "@sentry/cli-linux-arm@2.42.2": version "2.42.2" resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.42.2.tgz#00cadc359ae3c051efb3e63873c033c61dbd1ca1" integrity sha512-7udCw+YL9lwq+9eL3WLspvnuG+k5Icg92YE7zsteTzWLwgPVzaxeZD2f8hwhsu+wmL+jNqbpCRmktPteh3i2mg== -"@sentry/cli-linux-arm@2.43.0": - version "2.43.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.43.0.tgz#00d36a7347480b8b693cf0ed5cff270ad40045e7" - integrity sha512-c2Fwb6HrFL1nbaGV4uRhHC1wEJPR+wfpKN5y06PgSNNbd10YrECAB3tqBHXC8CEmhuDyFR+ORGZ7VbswfCWEEQ== +"@sentry/cli-linux-arm@2.45.0": + version "2.45.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.45.0.tgz#b9d6f86f3934b4d9ced5b45a8158ff2ac2bdd25d" + integrity sha512-6sEskFLlFKJ+e0MOYgIclBTUX5jYMyYhHIxXahEkI/4vx6JO0uvpyRAkUJRpJkRh/lPog0FM+tbP3so+VxB2qQ== "@sentry/cli-linux-i686@2.42.2": version "2.42.2" resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.42.2.tgz#3b817b715dd806c20dfbffd539725ad8089c310a" integrity sha512-Sw/dQp5ZPvKnq3/y7wIJyxTUJYPGoTX/YeMbDs8BzDlu9to2LWV3K3r7hE7W1Lpbaw4tSquUHiQjP5QHCOS7aQ== -"@sentry/cli-linux-i686@2.43.0": - version "2.43.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.43.0.tgz#83a31f47a13d8b3fae2716f86412fae2ffec5cf4" - integrity sha512-bFo/tpMZeMJ275HPGmAENREchnBxhALOOpZAphSyalUu3pGZ+EETEtlSLrKyVNJo26Dye5W7GlrYUV9+rkyCtg== +"@sentry/cli-linux-i686@2.45.0": + version "2.45.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.45.0.tgz#39e22beb84cfa26e11bdc198364315fdfb4da4d5" + integrity sha512-VmmOaEAzSW23YdGNdy/+oQjCNAMY+HmOGA77A25/ep/9AV7PQB6FI7xO5Y1PVvlkxZFJ23e373njSsEeg4uDZw== "@sentry/cli-linux-x64@2.42.2": version "2.42.2" resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.42.2.tgz#ddf906bc3071cc79ce6e633eddcb76bb9068e688" integrity sha512-mU4zUspAal6TIwlNLBV5oq6yYqiENnCWSxtSQVzWs0Jyq97wtqGNG9U+QrnwjJZ+ta/hvye9fvL2X25D/RxHQw== -"@sentry/cli-linux-x64@2.43.0": - version "2.43.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.43.0.tgz#dac1641c0c862d5d9f45c2b2c0d77b0281c53261" - integrity sha512-EbAmKXUNU/Ii4pNGVRCepU6ks1M43wStMKx3pibrUTllrrCwqYKyPxRRdoFYySHkduwCxnoKZcLEg9vWZ3qS6A== +"@sentry/cli-linux-x64@2.45.0": + version "2.45.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.45.0.tgz#25cd3699297f9433835fb5edd42dad722c11f041" + integrity sha512-a0Oj68mrb25a0WjX/ShZ6AAd4PPiuLcgyzQr7bl2+DvYxIOajwkGbR+CZFEhOVZcfhTnixKy/qIXEzApEPHPQg== -"@sentry/cli-win32-arm64@2.43.0": - version "2.43.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.43.0.tgz#ec499fc49c380252984ae5cde6b59ca0dbad79ef" - integrity sha512-KmJRCdQQGLSErJvrcGcN+yWo68m+5OdluhyJHsVYMOQknwu8YMOWLm12EIa+4t4GclDvwg5xcxLccCuiWMJUZw== +"@sentry/cli-win32-arm64@2.45.0": + version "2.45.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.45.0.tgz#50c7d29ea2169bdb4d98bbde81c5f7dac0dd3955" + integrity sha512-vn+CwS4p+52pQSLNPoi20ZOrQmv01ZgAmuMnjkh1oUZfTyBAwWLrAh6Cy4cztcN8DfL5dOWKQBo8DBKURE4ttg== "@sentry/cli-win32-i686@2.42.2": version "2.42.2" resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.42.2.tgz#9036085c7c6ce455ad45fda411c55ff39c06eb95" integrity sha512-iHvFHPGqgJMNqXJoQpqttfsv2GI3cGodeTq4aoVLU/BT3+hXzbV0x1VpvvEhncJkDgDicJpFLM8sEPHb3b8abw== -"@sentry/cli-win32-i686@2.43.0": - version "2.43.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.43.0.tgz#bf686d387e91d7897cfbd96a681a52816f90fb0b" - integrity sha512-ZWxZdOyZX7NJ/CTskzg+dJ2xTpobFLXVNMOMq0HiwdhqXP2zYYJzKnIt3mHNJYA40zYFODGSgxIamodjpB8BuA== +"@sentry/cli-win32-i686@2.45.0": + version "2.45.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.45.0.tgz#201075c4aec37a3e797160e0b468641245437f0c" + integrity sha512-8mMoDdlwxtcdNIMtteMK7dbi7054jak8wKSHJ5yzMw8UmWxC5thc/gXBc1uPduiaI56VjoJV+phWHBKCD+6I4w== "@sentry/cli-win32-x64@2.42.2": version "2.42.2" resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.42.2.tgz#7d6464b63f32c9f97fff428f246b1f039b402233" integrity sha512-vPPGHjYoaGmfrU7xhfFxG7qlTBacroz5NdT+0FmDn6692D8IvpNXl1K+eV3Kag44ipJBBeR8g1HRJyx/F/9ACw== -"@sentry/cli-win32-x64@2.43.0": - version "2.43.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.43.0.tgz#9d5e7b9888c0e639d3e62e4dd7a58468103b6bce" - integrity sha512-S/IRQYAziEnjpyROhnqzTqShDq3m8jcevXx+q5f49uQnFbfYcTgS1sdrEPqqao/K2boOWbffxYtTkvBiB/piQQ== +"@sentry/cli-win32-x64@2.45.0": + version "2.45.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.45.0.tgz#2075e9e1ea3c3609e0fa1a758ca033e94e1c600f" + integrity sha512-ZvK9cIqFaq7vZ0jkHJ/xh5au6902Dr+AUxSk6L6vCL7JCe2p93KGL/4d8VFB5PD/P7Y9b+105G/e0QIFKzpeOw== "@sentry/cli@2.42.2": version "2.42.2" @@ -6634,10 +6634,10 @@ "@sentry/cli-win32-i686" "2.42.2" "@sentry/cli-win32-x64" "2.42.2" -"@sentry/cli@^2.36.1", "@sentry/cli@^2.43.0": - version "2.43.0" - resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.43.0.tgz#2b0dcb749a1529faeb2c239962307c5a2e33f817" - integrity sha512-gBE3bkx+PBJxopTrzIJLX4xHe5S0w87q5frIveWKDZ5ulVIU6YWnVumay0y07RIEweUEj3IYva1qH6HG2abfiA== +"@sentry/cli@^2.36.1", "@sentry/cli@^2.45.0": + version "2.45.0" + resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.45.0.tgz#35feed7a2fee54faf25daed73001a2a2a3143396" + integrity sha512-4sWu7zgzgHAjIxIjXUA/66qgeEf5ZOlloO+/JaGD5qXNSW0G7KMTR6iYjReNKMgdBCTH6bUUt9qiuA+Ex9Masw== dependencies: https-proxy-agent "^5.0.0" node-fetch "^2.6.7" @@ -6645,14 +6645,14 @@ proxy-from-env "^1.1.0" which "^2.0.2" optionalDependencies: - "@sentry/cli-darwin" "2.43.0" - "@sentry/cli-linux-arm" "2.43.0" - "@sentry/cli-linux-arm64" "2.43.0" - "@sentry/cli-linux-i686" "2.43.0" - "@sentry/cli-linux-x64" "2.43.0" - "@sentry/cli-win32-arm64" "2.43.0" - "@sentry/cli-win32-i686" "2.43.0" - "@sentry/cli-win32-x64" "2.43.0" + "@sentry/cli-darwin" "2.45.0" + "@sentry/cli-linux-arm" "2.45.0" + "@sentry/cli-linux-arm64" "2.45.0" + "@sentry/cli-linux-i686" "2.45.0" + "@sentry/cli-linux-x64" "2.45.0" + "@sentry/cli-win32-arm64" "2.45.0" + "@sentry/cli-win32-i686" "2.45.0" + "@sentry/cli-win32-x64" "2.45.0" "@sentry/rollup-plugin@3.4.0": version "3.4.0" @@ -6678,12 +6678,12 @@ "@sentry/bundler-plugin-core" "3.2.4" unplugin "1.0.1" -"@sentry/webpack-plugin@3.3.1": - version "3.3.1" - resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-3.3.1.tgz#b257e1cb5f939b68f5050e9c4ea040d7366a55de" - integrity sha512-AFRnGNUnlIvq3M+ADdfWb+DIXWKK6yYEkVPAyOppkjO+cL/19gjXMdvAwv+CMFts28YCFKF8Kr3pamUiCmwodA== +"@sentry/webpack-plugin@3.5.0": + version "3.5.0" + resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-3.5.0.tgz#cde95534f1e945a4002d47465aeda01d382cd279" + integrity sha512-xvclj0QY2HyU7uJLzOlHSrZQBDwfnGKJxp8mmlU4L7CwmK+8xMCqlO7tYZoqE4K/wU3c2xpXql70x8qmvNMxzQ== dependencies: - "@sentry/bundler-plugin-core" "3.3.1" + "@sentry/bundler-plugin-core" "3.5.0" unplugin "1.0.1" uuid "^9.0.0"