diff --git a/.cursor/environment.json b/.cursor/environment.json new file mode 100644 index 000000000000..0828f53fbd4a --- /dev/null +++ b/.cursor/environment.json @@ -0,0 +1,12 @@ +{ + "name": "Sentry JavaScript SDK Development", + "install": "curl https://get.volta.sh | bash && export VOLTA_HOME=\"$HOME/.volta\" && export PATH=\"$VOLTA_HOME/bin:$PATH\" && export VOLTA_FEATURE_PNPM=1 && yarn install", + "start": "export VOLTA_HOME=\"$HOME/.volta\" && export PATH=\"$VOLTA_HOME/bin:$PATH\" && export VOLTA_FEATURE_PNPM=1", + "terminals": [ + { + "name": "Development", + "command": "export VOLTA_HOME=\"$HOME/.volta\" && export PATH=\"$VOLTA_HOME/bin:$PATH\" && export VOLTA_FEATURE_PNPM=1 && echo 'Volta setup complete. Node version:' && node --version && echo 'Yarn version:' && yarn --version", + "description": "Main development terminal with Volta environment configured" + } + ] +} diff --git a/.cursor/rules/sdk_dependency_upgrades.mdc b/.cursor/rules/sdk_dependency_upgrades.mdc index 826249108f01..becf0805eb91 100644 --- a/.cursor/rules/sdk_dependency_upgrades.mdc +++ b/.cursor/rules/sdk_dependency_upgrades.mdc @@ -3,6 +3,7 @@ description: Use this rule if you are looking to upgrade a dependency in the Sen globs: alwaysApply: false --- + # Yarn v1 Dependency Upgrades ## Upgrade Process @@ -45,10 +46,12 @@ Avoid upgrading top-level dependencies (defined in `package.json`), especially i **STOP UPGRADE IMMEDIATELY** if upgrading any dependency with `opentelemetry` in the name and the new version or any of its dependencies uses forbidden OpenTelemetry versions. **FORBIDDEN VERSION PATTERNS:** + - `2.x.x` versions (e.g., `2.0.0`, `2.1.0`) - `0.2xx.x` versions (e.g., `0.200.0`, `0.201.0`) When upgrading OpenTelemetry dependencies: + 1. Check the dependency's `package.json` after upgrade 2. Verify the package itself doesn't use forbidden version patterns 3. Verify none of its dependencies use `@opentelemetry/*` packages with forbidden version patterns @@ -153,6 +156,7 @@ yarn info versions ``` The `yarn info` command provides detailed dependency information without requiring installation, making it particularly useful for: + - Verifying OpenTelemetry packages don't introduce forbidden version patterns (`2.x.x` or `0.2xx.x`) - Checking what dependencies a package will bring in before upgrading - Understanding package version history and compatibility diff --git a/.cursor/rules/sdk_development.mdc b/.cursor/rules/sdk_development.mdc index 988703ddac81..088c94f47a23 100644 --- a/.cursor/rules/sdk_development.mdc +++ b/.cursor/rules/sdk_development.mdc @@ -12,12 +12,13 @@ You are working on the Sentry JavaScript SDK, a critical production SDK used by **CRITICAL**: All changes must pass these checks before committing: 1. **Always run `yarn lint`** - Fix all linting issues -2. **Always run `yarn test`** - Ensure all tests pass +2. **Always run `yarn test`** - Ensure all tests pass 3. **Always run `yarn build:dev`** - Verify TypeScript compilation ## Development Commands ### Build Commands + - `yarn build` - Full production build with package verification - `yarn build:dev` - Development build (transpile + types) - `yarn build:dev:watch` - Development build in watch mode (recommended) @@ -26,13 +27,11 @@ You are working on the Sentry JavaScript SDK, a critical production SDK used by - `yarn build:bundle` - Build browser bundles only ### Testing -- `yarn test` - Run all tests (excludes integration tests) -- `yarn test:unit` - Run unit tests only -- `yarn test:pr` - Run tests affected by changes (CI mode) -- `yarn test:pr:browser` - Run affected browser-specific tests -- `yarn test:pr:node` - Run affected Node.js-specific tests + +- `yarn test` - Run all unit tests ### Linting and Formatting + - `yarn lint` - Run ESLint and Prettier checks - `yarn fix` - Auto-fix linting and formatting issues - `yarn lint:es-compatibility` - Check ES compatibility @@ -42,12 +41,17 @@ You are working on the Sentry JavaScript SDK, a critical production SDK used by This repository uses **Git Flow**. See [docs/gitflow.md](docs/gitflow.md) for details. ### Key Rules + - **All PRs target `develop` branch** (NOT `master`) - `master` represents the last released state - Never merge directly into `master` (except emergency fixes) - Avoid changing `package.json` files on `develop` during pending releases +- Never update dependencies, package.json content or build scripts unless explicitly asked for +- When asked to do a task on a set of files, always make sure that all occurences in the codebase are covered. Double check that no files have been forgotten. +- Unless explicitly asked for, make sure to cover all files, including files in `src/` and `test/` directories. ### Branch Naming + - Features: `feat/descriptive-name` - Releases: `release/X.Y.Z` @@ -56,27 +60,33 @@ This repository uses **Git Flow**. See [docs/gitflow.md](docs/gitflow.md) for de This is a Lerna monorepo with 40+ packages in the `@sentry/*` namespace. ### Core Packages + - `packages/core/` - Base SDK with interfaces, type definitions, core functionality -- `packages/types/` - Shared TypeScript type definitions (active) +- `packages/types/` - Shared TypeScript type definitions - this is deprecated, never modify this package - `packages/browser-utils/` - Browser-specific utilities and instrumentation +- `packages/node-core/` - Node Core SDK which contains most of the node-specific logic, excluding OpenTelemetry instrumentation. ### Platform SDKs + - `packages/browser/` - Browser SDK with bundled variants -- `packages/node/` - Node.js SDK with server-side integrations +- `packages/node/` - Node.js SDK. All general Node code should go into node-core, the node package itself only contains OpenTelemetry instrumentation on top of that. - `packages/bun/`, `packages/deno/`, `packages/cloudflare/` - Runtime-specific SDKs ### Framework Integrations + - Framework packages: `packages/{framework}/` (react, vue, angular, etc.) - Client/server entry points where applicable (nextjs, nuxt, sveltekit) - Integration tests use Playwright (Remix, browser-integration-tests) ### User Experience Packages + - `packages/replay-internal/` - Session replay functionality - `packages/replay-canvas/` - Canvas recording for replay - `packages/replay-worker/` - Web worker support for replay - `packages/feedback/` - User feedback integration ### Development Packages (`dev-packages/`) + - `browser-integration-tests/` - Playwright browser tests - `e2e-tests/` - End-to-end tests for 70+ framework combinations - `node-integration-tests/` - Node.js integration tests @@ -88,13 +98,16 @@ This is a Lerna monorepo with 40+ packages in the `@sentry/*` namespace. ## Development Guidelines ### Build System + - Uses Rollup for bundling (`rollup.*.config.mjs`) - TypeScript with multiple tsconfig files per package - Lerna manages package dependencies and publishing - Vite for testing with `vitest` ### Package Structure Pattern + Each package typically contains: + - `src/index.ts` - Main entry point - `src/sdk.ts` - SDK initialization logic - `rollup.npm.config.mjs` - Build configuration @@ -102,16 +115,25 @@ Each package typically contains: - `test/` directory with corresponding test files ### Key Development Notes + - Uses Volta for Node.js/Yarn version management - Requires initial `yarn build` after `yarn install` for TypeScript linking - Integration tests use Playwright extensively -- Native profiling requires Python <3.12 for binary builds +- Never change the volta, yarn, or package manager setup in general unless explicitly asked for + +### Notes for Background Tasks + +- Make sure to use [volta](https://volta.sh/) for development. Volta is used to manage the node, yarn and pnpm version used. +- Make sure that [PNPM support is enabled in volta](https://docs.volta.sh/advanced/pnpm). This means that the `VOLTA_FEATURE_PNPM` environment variable has to be set to `1`. +- Yarn, Node and PNPM have to be used through volta, in the versions defined by the volta config. NEVER change any versions unless explicitly asked to. ## Testing Single Packages + - Test specific package: `cd packages/{package-name} && yarn test` - Build specific package: `yarn build:dev:filter @sentry/{package-name}` ## Code Style Rules + - Follow existing code conventions in each package - Check imports and dependencies - only use libraries already in the codebase - Look at neighboring files for patterns and style @@ -119,10 +141,12 @@ Each package typically contains: - Follow security best practices ## Before Every Commit Checklist + 1. ✅ `yarn lint` (fix all issues) 2. ✅ `yarn test` (all tests pass) 3. ✅ `yarn build:dev` (builds successfully) 4. ✅ Target `develop` branch for PRs (not `master`) ## Documentation Sync -**IMPORTANT**: When editing CLAUDE.md, also update .cursor/rules/sdk_development.mdc and vice versa to keep both files in sync. \ No newline at end of file + +**IMPORTANT**: When editing CLAUDE.md, also update .cursor/rules/sdk_development.mdc and vice versa to keep both files in sync. diff --git a/.eslintrc.js b/.eslintrc.js index 75ab5f048b29..5266f0117a89 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -9,7 +9,7 @@ module.exports = { es2017: true, }, parserOptions: { - ecmaVersion: 2018, + ecmaVersion: 2020, }, extends: ['@sentry-internal/sdk'], ignorePatterns: [ diff --git a/CHANGELOG.md b/CHANGELOG.md index ef2a5393537a..89bc7a377ee5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,78 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 9.40.0 + +### Important Changes + +- **feat(browser): Add debugId sync APIs between web worker and main thread ([#16981](https://github.com/getsentry/sentry-javascript/pull/16981))** + +This release adds two Browser SDK APIs to let the main thread know about debugIds of worker files: + +- `webWorkerIntegration({worker})` to be used in the main thread +- `registerWebWorker({self})` to be used in the web worker + +```js +// main.js +Sentry.init({...}) + +const worker = new MyWorker(...); + +Sentry.addIntegration(Sentry.webWorkerIntegration({ worker })); + +worker.addEventListener('message', e => {...}); +``` + +```js +// worker.js +Sentry.registerWebWorker({ self }); + +self.postMessage(...); +``` + +- **feat(core): Deprecate logger in favor of debug ([#17040](https://github.com/getsentry/sentry-javascript/pull/17040))** + +The internal SDK `logger` export from `@sentry/core` has been deprecated in favor of the `debug` export. `debug` only exposes `log`, `warn`, and `error` methods but is otherwise identical to `logger`. Note that this deprecation does not affect the `logger` export from other packages (like `@sentry/browser` or `@sentry/node`) which is used for Sentry Logging. + +```js +import { logger, debug } from '@sentry/core'; + +// before +logger.info('This is an info message'); + +// after +debug.log('This is an info message'); +``` + +- **feat(node): Add OpenAI integration ([#17022](https://github.com/getsentry/sentry-javascript/pull/17022))** + +This release adds official support for instrumenting OpenAI SDK calls in with Sentry tracing, following OpenTelemetry semantic conventions for Generative AI. It instruments: + +- `client.chat.completions.create()` - For chat-based completions +- `client.responses.create()` - For the responses API + +```js +// The integration respects your `sendDefaultPii` option, but you can override the behavior in the integration options + +Sentry.init({ + dsn: '__DSN__', + integrations: [ + Sentry.openAIIntegration({ + recordInputs: true, // Force recording prompts + recordOutputs: true, // Force recording responses + }), + ], +}); +``` + +### Other Changes + +- feat(node-core): Expand `@opentelemetry/instrumentation` range to cover `0.203.0` ([#17043](https://github.com/getsentry/sentry-javascript/pull/17043)) +- fix(cloudflare): Ensure errors get captured from durable objects ([#16838](https://github.com/getsentry/sentry-javascript/pull/16838)) +- fix(sveltekit): Ensure server errors from streamed responses are sent ([#17044](https://github.com/getsentry/sentry-javascript/pull/17044)) + +Work in this release was contributed by @0xbad0c0d3 and @tommy-gilligan. Thank you for your contributions! + ## 9.39.0 ### Important Changes diff --git a/CLAUDE.md b/CLAUDE.md index 263b9eef97a4..e515c171303e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,24 +2,32 @@ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +# SDK Development Rules + +You are working on the Sentry JavaScript SDK, a critical production SDK used by thousands of applications. Follow these rules strictly. + +## Code Quality Requirements (MANDATORY) + +**CRITICAL**: All changes must pass these checks before committing: + +1. **Always run `yarn lint`** - Fix all linting issues +2. **Always run `yarn test`** - Ensure all tests pass +3. **Always run `yarn build:dev`** - Verify TypeScript compilation + ## Development Commands ### Build Commands - `yarn build` - Full production build with package verification - `yarn build:dev` - Development build (transpile + types) -- `yarn build:dev:watch` - Development build in watch mode (recommended for development) -- `yarn build:dev:filter ` - Build specific package and its dependencies +- `yarn build:dev:watch` - Development build in watch mode (recommended) +- `yarn build:dev:filter ` - Build specific package and dependencies - `yarn build:types:watch` - Watch mode for TypeScript types only - `yarn build:bundle` - Build browser bundles only ### Testing -- `yarn test` - Run all tests (excludes integration tests) -- `yarn test:unit` - Run unit tests only -- `yarn test:pr` - Run tests affected by changes (CI mode) -- `yarn test:pr:browser` - Run affected browser-specific tests -- `yarn test:pr:node` - Run affected Node.js-specific tests +- `yarn test` - Run all unit tests ### Linting and Formatting @@ -27,44 +35,71 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co - `yarn fix` - Auto-fix linting and formatting issues - `yarn lint:es-compatibility` - Check ES compatibility -### Package Management +## Git Flow Branching Strategy + +This repository uses **Git Flow**. See [docs/gitflow.md](docs/gitflow.md) for details. + +### Key Rules + +- **All PRs target `develop` branch** (NOT `master`) +- `master` represents the last released state +- Never merge directly into `master` (except emergency fixes) +- Avoid changing `package.json` files on `develop` during pending releases +- Never update dependencies, package.json content or build scripts unless explicitly asked for +- When asked to do a task on a set of files, always make sure that all occurences in the codebase are covered. Double check that no files have been forgotten. +- Unless explicitly asked for, make sure to cover all files, including files in `src/` and `test/` directories. + +### Branch Naming -- `yarn clean` - Clean build artifacts and caches -- `yarn clean:deps` - Clean and reinstall all dependencies +- Features: `feat/descriptive-name` +- Releases: `release/X.Y.Z` ## Repository Architecture -This is a Lerna monorepo containing 40+ packages in the `@sentry/*` namespace. Key architectural components: +This is a Lerna monorepo with 40+ packages in the `@sentry/*` namespace. ### Core Packages -- `packages/core/` - Base SDK with interfaces, type definitions, and core functionality -- `packages/types/` - Shared TypeScript type definitions (active) +- `packages/core/` - Base SDK with interfaces, type definitions, core functionality +- `packages/types/` - Shared TypeScript type definitions - this is deprecated, never modify this package - `packages/browser-utils/` - Browser-specific utilities and instrumentation +- `packages/node-core/` - Node Core SDK which contains most of the node-specific logic, excluding OpenTelemetry instrumentation. ### Platform SDKs - `packages/browser/` - Browser SDK with bundled variants -- `packages/node/` - Node.js SDK with server-side integrations +- `packages/node/` - Node.js SDK. All general Node code should go into node-core, the node package itself only contains OpenTelemetry instrumentation on top of that. - `packages/bun/`, `packages/deno/`, `packages/cloudflare/` - Runtime-specific SDKs ### Framework Integrations -- Framework packages follow naming: `packages/{framework}/` (react, vue, angular, etc.) -- Each has client/server entry points where applicable (e.g., nextjs, nuxt, sveltekit) -- Integration tests use Playwright (e.g., Remix, browser-integration-tests) +- Framework packages: `packages/{framework}/` (react, vue, angular, etc.) +- Client/server entry points where applicable (nextjs, nuxt, sveltekit) +- Integration tests use Playwright (Remix, browser-integration-tests) ### User Experience Packages - `packages/replay-internal/` - Session replay functionality -- `packages/replay-canvas/` - Canvas recording support for replay +- `packages/replay-canvas/` - Canvas recording for replay - `packages/replay-worker/` - Web worker support for replay - `packages/feedback/` - User feedback integration +### Development Packages (`dev-packages/`) + +- `browser-integration-tests/` - Playwright browser tests +- `e2e-tests/` - End-to-end tests for 70+ framework combinations +- `node-integration-tests/` - Node.js integration tests +- `test-utils/` - Shared testing utilities +- `bundle-analyzer-scenarios/` - Bundle analysis +- `rollup-utils/` - Build utilities +- GitHub Actions packages for CI/CD automation + +## Development Guidelines + ### Build System -- Uses Rollup for bundling with config files: `rollup.*.config.mjs` -- TypeScript with multiple tsconfig files per package (main, test, types) +- Uses Rollup for bundling (`rollup.*.config.mjs`) +- TypeScript with multiple tsconfig files per package - Lerna manages package dependencies and publishing - Vite for testing with `vitest` @@ -75,73 +110,42 @@ Each package typically contains: - `src/index.ts` - Main entry point - `src/sdk.ts` - SDK initialization logic - `rollup.npm.config.mjs` - Build configuration -- `tsconfig.json`, `tsconfig.test.json`, `tsconfig.types.json` - TypeScript configs +- `tsconfig.json`, `tsconfig.test.json`, `tsconfig.types.json` - `test/` directory with corresponding test files -### Development Packages (`dev-packages/`) - -Separate from main packages, containing development and testing utilities: - -- `browser-integration-tests/` - Playwright browser tests -- `e2e-tests/` - End-to-end tests for 70+ framework combinations -- `node-integration-tests/` - Node.js integration tests -- `test-utils/` - Shared testing utilities -- `bundle-analyzer-scenarios/` - Bundle analysis -- `rollup-utils/` - Build utilities -- GitHub Actions packages for CI/CD automation - ### Key Development Notes - Uses Volta for Node.js/Yarn version management - Requires initial `yarn build` after `yarn install` for TypeScript linking - Integration tests use Playwright extensively -- Native profiling requires Python <3.12 for binary builds -- Bundle outputs vary - check `build/bundles/` for specific files after builds +- Never change the volta, yarn, or package manager setup in general unless explicitly asked for -## Git Flow Branching Strategy +### Notes for Background Tasks -This repository uses **Git Flow** branching model. See [detailed documentation](docs/gitflow.md). +- Make sure to use [volta](https://volta.sh/) for development. Volta is used to manage the node, yarn and pnpm version used. +- Make sure that [PNPM support is enabled in volta](https://docs.volta.sh/advanced/pnpm). This means that the `VOLTA_FEATURE_PNPM` environment variable has to be set to `1`. +- Yarn, Node and PNPM have to be used through volta, in the versions defined by the volta config. NEVER change any versions unless explicitly asked to. -### Key Points - -- **All PRs target `develop` branch** (not `master`) -- `master` represents the last released state -- Never merge directly into `master` (except emergency fixes) -- Automated workflow syncs `master` → `develop` after releases -- Avoid changing `package.json` files on `develop` during pending releases - -### Branch Naming - -- Features: `feat/descriptive-name` -- Releases: `release/X.Y.Z` - -## Code Quality Requirements - -**CRITICAL**: This is a production SDK used by thousands of applications. All changes must be: - -### Mandatory Checks - -- **Always run `yarn lint`** - Fix all linting issues before committing -- **Always run `yarn test`** - Ensure all tests pass -- **Run `yarn build`** - Verify build succeeds without errors - -### Before Any Commit +## Testing Single Packages -1. `yarn lint` - Check and fix ESLint/Prettier issues -2. `yarn test` - Run relevant tests for your changes -3. `yarn build:dev` - Verify TypeScript compilation +- Test specific package: `cd packages/{package-name} && yarn test` +- Build specific package: `yarn build:dev:filter @sentry/{package-name}` -### CI/CD Integration +## Code Style Rules -- All PRs automatically run full lint/test/build pipeline -- Failed checks block merging -- Use `yarn test:pr` for testing only affected changes +- Follow existing code conventions in each package +- Check imports and dependencies - only use libraries already in the codebase +- Look at neighboring files for patterns and style +- Never introduce code that exposes secrets or keys +- Follow security best practices -## Testing Single Packages +## Before Every Commit Checklist -To test a specific package: `cd packages/{package-name} && yarn test` -To build a specific package: `yarn build:dev:filter @sentry/{package-name}` +1. ✅ `yarn lint` (fix all issues) +2. ✅ `yarn test` (all tests pass) +3. ✅ `yarn build:dev` (builds successfully) +4. ✅ Target `develop` branch for PRs (not `master`) -## Cursor IDE Integration +## Documentation Sync -For Cursor IDE users, see [.cursor/rules/sdk_development.mdc](.cursor/rules/sdk_development.mdc) for complementary development rules. +**IMPORTANT**: When editing CLAUDE.md, also update .cursor/rules/sdk_development.mdc and vice versa to keep both files in sync. diff --git a/MIGRATION.md b/MIGRATION.md index 7f3160a59466..ac2a46a8d50e 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -7,6 +7,22 @@ These docs walk through how to migrate our JavaScript SDKs through different maj - Upgrading from [SDK 7.x to 8.x](./docs/migration/v7-to-v8.md) - Upgrading from [SDK 8.x to 9.x](#upgrading-from-8x-to-9x) +# Deprecations in 9.x + +## Deprecated `@sentry/core` SDK internal `logger` export + +The internal SDK `logger` export from `@sentry/core` has been deprecated in favor of the `debug` export. `debug` only exposes `log`, `warn`, and `error` methods but is otherwise identical to `logger`. Note that this deprecation does not affect the `logger` export from other packages (like `@sentry/browser` or `@sentry/node`) which is used for Sentry Logging. + +```js +import { logger, debug } from '@sentry/core'; + +// before +logger.info('This is an info message'); + +// after +debug.log('This is an info message'); +``` + # Upgrading from 8.x to 9.x Version 9 of the Sentry JavaScript SDK primarily introduces API cleanup and version support changes. diff --git a/dev-packages/browser-integration-tests/scripts/detectFlakyTests.ts b/dev-packages/browser-integration-tests/scripts/detectFlakyTests.ts index 1105346562c9..8e0aadc6af59 100644 --- a/dev-packages/browser-integration-tests/scripts/detectFlakyTests.ts +++ b/dev-packages/browser-integration-tests/scripts/detectFlakyTests.ts @@ -131,7 +131,7 @@ function getApproximateNumberOfTests(testPath: string): number { const content = fs.readFileSync(path.join(process.cwd(), testPath, 'test.ts'), 'utf-8'); const matches = content.match(/sentryTest\(/g); return Math.max(matches ? matches.length : 1, 1); - } catch (e) { + } catch { console.error(`Could not read file ${testPath}`); return 1; } diff --git a/dev-packages/browser-integration-tests/suites/feedback/attachTo/test.ts b/dev-packages/browser-integration-tests/suites/feedback/attachTo/test.ts index e3f094022bbf..c93ce0453f83 100644 --- a/dev-packages/browser-integration-tests/suites/feedback/attachTo/test.ts +++ b/dev-packages/browser-integration-tests/suites/feedback/attachTo/test.ts @@ -17,7 +17,7 @@ sentryTest('should capture feedback with custom button', async ({ getLocalTestUr try { return getEnvelopeType(req) === 'feedback'; - } catch (err) { + } catch { return false; } }); diff --git a/dev-packages/browser-integration-tests/suites/feedback/captureFeedback/test.ts b/dev-packages/browser-integration-tests/suites/feedback/captureFeedback/test.ts index c94f8363b107..e6eb920f64a5 100644 --- a/dev-packages/browser-integration-tests/suites/feedback/captureFeedback/test.ts +++ b/dev-packages/browser-integration-tests/suites/feedback/captureFeedback/test.ts @@ -17,7 +17,7 @@ sentryTest('should capture feedback', async ({ getLocalTestUrl, page }) => { try { return getEnvelopeType(req) === 'feedback'; - } catch (err) { + } catch { return false; } }); diff --git a/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackAndReplay/hasSampling/test.ts b/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackAndReplay/hasSampling/test.ts index 65d9f18c48a8..66653ce68a82 100644 --- a/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackAndReplay/hasSampling/test.ts +++ b/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackAndReplay/hasSampling/test.ts @@ -25,7 +25,7 @@ sentryTest('should capture feedback', async ({ forceFlushReplay, getLocalTestUrl try { return getEnvelopeType(req) === 'feedback'; - } catch (err) { + } catch { return false; } }); diff --git a/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackCsp/test.ts b/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackCsp/test.ts index 030d5049b0f7..69c715654921 100644 --- a/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackCsp/test.ts +++ b/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackCsp/test.ts @@ -17,7 +17,7 @@ sentryTest('should capture feedback', async ({ getLocalTestUrl, page }) => { try { return getEnvelopeType(req) === 'feedback'; - } catch (err) { + } catch { return false; } }); diff --git a/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadata/init.js b/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadata/init.js index b0f321d1f5cd..d5b05443ca9e 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadata/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadata/init.js @@ -14,7 +14,7 @@ Sentry.init({ moduleMetadataEntries.push(frame.module_metadata); }); }); - } catch (e) { + } catch { // noop } } diff --git a/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadataWithRewriteFrames/init.js b/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadataWithRewriteFrames/init.js index f3fd6e98874d..3c89709b3dfb 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadataWithRewriteFrames/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/moduleMetadata/appliesMetadataWithRewriteFrames/init.js @@ -28,7 +28,7 @@ Sentry.init({ moduleMetadataEntries.push(frame.module_metadata); }); }); - } catch (e) { + } catch { // noop } } diff --git a/dev-packages/browser-integration-tests/suites/integrations/webWorker/assets/worker.js b/dev-packages/browser-integration-tests/suites/integrations/webWorker/assets/worker.js new file mode 100644 index 000000000000..59af46d764e2 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/webWorker/assets/worker.js @@ -0,0 +1,14 @@ +self._sentryDebugIds = { + 'Error at http://sentry-test.io/worker.js': 'worker-debug-id-789', +}; + +self.postMessage({ + _sentryMessage: true, + _sentryDebugIds: self._sentryDebugIds, +}); + +self.addEventListener('message', event => { + if (event.data.type === 'throw-error') { + throw new Error('Worker error for testing'); + } +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/webWorker/init.js b/dev-packages/browser-integration-tests/suites/integrations/webWorker/init.js new file mode 100644 index 000000000000..aa08cd652418 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/webWorker/init.js @@ -0,0 +1,18 @@ +import * as Sentry from '@sentry/browser'; + +// Initialize Sentry with webWorker integration +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', +}); + +const worker = new Worker('/worker.js'); + +Sentry.addIntegration(Sentry.webWorkerIntegration({ worker })); + +const btn = document.getElementById('errWorker'); + +btn.addEventListener('click', () => { + worker.postMessage({ + type: 'throw-error', + }); +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/webWorker/subject.js b/dev-packages/browser-integration-tests/suites/integrations/webWorker/subject.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/dev-packages/browser-integration-tests/suites/integrations/webWorker/template.html b/dev-packages/browser-integration-tests/suites/integrations/webWorker/template.html new file mode 100644 index 000000000000..1c36227c5a3d --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/webWorker/template.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/dev-packages/browser-integration-tests/suites/integrations/webWorker/test.ts b/dev-packages/browser-integration-tests/suites/integrations/webWorker/test.ts new file mode 100644 index 000000000000..cc5a8b3c7cf0 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/webWorker/test.ts @@ -0,0 +1,38 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/core'; +import { sentryTest } from '../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest } from '../../../utils/helpers'; + +sentryTest('Assigns web worker debug IDs when using webWorkerIntegration', async ({ getLocalTestUrl, page }) => { + const bundle = process.env.PW_BUNDLE as string | undefined; + if (bundle != null && !bundle.includes('esm') && !bundle.includes('cjs')) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const errorEventPromise = getFirstSentryEnvelopeRequest(page, url); + + page.route('**/worker.js', route => { + route.fulfill({ + path: `${__dirname}/assets/worker.js`, + }); + }); + + const button = page.locator('#errWorker'); + await button.click(); + + const errorEvent = await errorEventPromise; + + expect(errorEvent.debug_meta?.images).toBeDefined(); + + const debugImages = errorEvent.debug_meta?.images || []; + + expect(debugImages.length).toBe(1); + + debugImages.forEach(image => { + expect(image.type).toBe('sourcemap'); + expect(image.debug_id).toEqual('worker-debug-id-789'); + expect(image.code_file).toEqual('http://sentry-test.io/worker.js'); + }); +}); diff --git a/dev-packages/browser-integration-tests/utils/helpers.ts b/dev-packages/browser-integration-tests/utils/helpers.ts index e4ebd8b19313..5a9d8a351449 100644 --- a/dev-packages/browser-integration-tests/utils/helpers.ts +++ b/dev-packages/browser-integration-tests/utils/helpers.ts @@ -23,7 +23,7 @@ export const envelopeParser = (request: Request | null): unknown[] => { return envelope.split('\n').map(line => { try { return JSON.parse(line); - } catch (error) { + } catch { return line; } }); @@ -172,7 +172,7 @@ export async function runScriptInSandbox( ): Promise { try { await page.addScriptTag({ path: impl.path, content: impl.content }); - } catch (e) { + } catch { // no-op } } diff --git a/dev-packages/e2e-tests/lib/copyToTemp.ts b/dev-packages/e2e-tests/lib/copyToTemp.ts index d6667978b924..830ff76f6077 100644 --- a/dev-packages/e2e-tests/lib/copyToTemp.ts +++ b/dev-packages/e2e-tests/lib/copyToTemp.ts @@ -28,7 +28,7 @@ function fixPackageJson(cwd: string): void { // 2. Fix volta extends if (!packageJson.volta) { - throw new Error('No volta config found, please provide one!'); + throw new Error("No volta config found, please add one to the test app's package.json!"); } if (typeof packageJson.volta.extends === 'string') { diff --git a/dev-packages/e2e-tests/registrySetup.ts b/dev-packages/e2e-tests/registrySetup.ts index d34beb29c8c9..80cbcd10d384 100644 --- a/dev-packages/e2e-tests/registrySetup.ts +++ b/dev-packages/e2e-tests/registrySetup.ts @@ -85,8 +85,15 @@ export function registrySetup(): void { }, ); - if (publishImageContainerRunProcess.status !== 0) { - throw new Error('Publish Image Container failed.'); + const statusCode = publishImageContainerRunProcess.status; + + if (statusCode !== 0) { + if (statusCode === 137) { + throw new Error( + `Publish Image Container failed with exit code ${statusCode}, possibly due to memory issues. Consider increasing the memory limit for the container.`, + ); + } + throw new Error(`Publish Image Container failed with exit code ${statusCode}`); } }); diff --git a/dev-packages/e2e-tests/test-applications/browser-webworker-vite/.npmrc b/dev-packages/e2e-tests/test-applications/browser-webworker-vite/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/browser-webworker-vite/.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/browser-webworker-vite/index.html b/dev-packages/e2e-tests/test-applications/browser-webworker-vite/index.html new file mode 100644 index 000000000000..0ebc79719432 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/browser-webworker-vite/index.html @@ -0,0 +1,21 @@ + + + + + + Vite + TS + + +
+ + + + + + diff --git a/dev-packages/e2e-tests/test-applications/browser-webworker-vite/package.json b/dev-packages/e2e-tests/test-applications/browser-webworker-vite/package.json new file mode 100644 index 000000000000..fcda3617e5c5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/browser-webworker-vite/package.json @@ -0,0 +1,29 @@ +{ + "name": "browser-webworker-vite", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "rm -rf dist && tsc && vite build", + "preview": "vite preview --port 3030", + "test": "playwright test", + "test:build": "pnpm install && pnpm build", + "test:assert": "pnpm test" + }, + "devDependencies": { + "@playwright/test": "~1.53.2", + "@sentry-internal/test-utils": "link:../../../test-utils", + "typescript": "~5.8.3", + "vite": "^7.0.4" + }, + "dependencies": { + "@sentry/browser": "latest || *", + "@sentry/vite-plugin": "^3.5.0" + }, + "volta": { + "node": "20.19.2", + "yarn": "1.22.22", + "pnpm": "9.15.9" + } +} diff --git a/dev-packages/e2e-tests/test-applications/browser-webworker-vite/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/browser-webworker-vite/playwright.config.mjs new file mode 100644 index 000000000000..bf40ebae4467 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/browser-webworker-vite/playwright.config.mjs @@ -0,0 +1,10 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; + +const config = getPlaywrightConfig({ + startCommand: `pnpm preview`, + eventProxyFile: 'start-event-proxy.mjs', + eventProxyPort: 3031, + port: 3030, +}); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/browser-webworker-vite/src/main.ts b/dev-packages/e2e-tests/test-applications/browser-webworker-vite/src/main.ts new file mode 100644 index 000000000000..b017c1bfdc4d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/browser-webworker-vite/src/main.ts @@ -0,0 +1,44 @@ +import MyWorker from './worker.ts?worker'; +import MyWorker2 from './worker2.ts?worker'; +import * as Sentry from '@sentry/browser'; + +Sentry.init({ + dsn: import.meta.env.PUBLIC_E2E_TEST_DSN, + environment: import.meta.env.MODE || 'development', + tracesSampleRate: 1.0, + debug: true, + integrations: [Sentry.browserTracingIntegration()], + tunnel: 'http://localhost:3031/', // proxy server +}); + +const worker = new MyWorker(); +const worker2 = new MyWorker2(); + +const webWorkerIntegration = Sentry.webWorkerIntegration({ worker: [worker, worker2] }); +Sentry.addIntegration(webWorkerIntegration); + +worker.addEventListener('message', event => { + // this is part of the test, do not delete + console.log('received message from worker:', event.data.msg); +}); + +document.querySelector('#trigger-error')!.addEventListener('click', () => { + worker.postMessage({ + msg: 'TRIGGER_ERROR', + }); +}); + +document.querySelector('#trigger-error-2')!.addEventListener('click', () => { + worker2.postMessage({ + msg: 'TRIGGER_ERROR', + }); +}); + +document.querySelector('#trigger-error-3')!.addEventListener('click', async () => { + const Worker3 = await import('./worker3.ts?worker'); + const worker3 = new Worker3.default(); + webWorkerIntegration.addWorker(worker3); + worker3.postMessage({ + msg: 'TRIGGER_ERROR', + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/browser-webworker-vite/src/vite-env.d.ts b/dev-packages/e2e-tests/test-applications/browser-webworker-vite/src/vite-env.d.ts new file mode 100644 index 000000000000..11f02fe2a006 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/browser-webworker-vite/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/dev-packages/e2e-tests/test-applications/browser-webworker-vite/src/worker.ts b/dev-packages/e2e-tests/test-applications/browser-webworker-vite/src/worker.ts new file mode 100644 index 000000000000..455e8e395901 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/browser-webworker-vite/src/worker.ts @@ -0,0 +1,17 @@ +import * as Sentry from '@sentry/browser'; + +// type cast necessary because TS thinks this file is part of the main +// thread where self is of type `Window` instead of `Worker` +Sentry.registerWebWorker({ self: self as unknown as Worker }); + +// Let the main thread know the worker is ready +self.postMessage({ + msg: 'WORKER_READY', +}); + +self.addEventListener('message', event => { + if (event.data.msg === 'TRIGGER_ERROR') { + // This will throw an uncaught error in the worker + throw new Error(`Uncaught error in worker`); + } +}); diff --git a/dev-packages/e2e-tests/test-applications/browser-webworker-vite/src/worker2.ts b/dev-packages/e2e-tests/test-applications/browser-webworker-vite/src/worker2.ts new file mode 100644 index 000000000000..8dfb70b32853 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/browser-webworker-vite/src/worker2.ts @@ -0,0 +1,17 @@ +import * as Sentry from '@sentry/browser'; + +// type cast necessary because TS thinks this file is part of the main +// thread where self is of type `Window` instead of `Worker` +Sentry.registerWebWorker({ self: self as unknown as Worker }); + +// Let the main thread know the worker is ready +self.postMessage({ + msg: 'WORKER_2_READY', +}); + +self.addEventListener('message', event => { + if (event.data.msg === 'TRIGGER_ERROR') { + // This will throw an uncaught error in the worker + throw new Error(`Uncaught error in worker 2`); + } +}); diff --git a/dev-packages/e2e-tests/test-applications/browser-webworker-vite/src/worker3.ts b/dev-packages/e2e-tests/test-applications/browser-webworker-vite/src/worker3.ts new file mode 100644 index 000000000000..d68265c24ab7 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/browser-webworker-vite/src/worker3.ts @@ -0,0 +1,17 @@ +import * as Sentry from '@sentry/browser'; + +// type cast necessary because TS thinks this file is part of the main +// thread where self is of type `Window` instead of `Worker` +Sentry.registerWebWorker({ self: self as unknown as Worker }); + +// Let the main thread know the worker is ready +self.postMessage({ + msg: 'WORKER_3_READY', +}); + +self.addEventListener('message', event => { + if (event.data.msg === 'TRIGGER_ERROR') { + // This will throw an uncaught error in the worker + throw new Error(`Uncaught error in worker 3`); + } +}); diff --git a/dev-packages/e2e-tests/test-applications/browser-webworker-vite/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/browser-webworker-vite/start-event-proxy.mjs new file mode 100644 index 000000000000..102c13c48379 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/browser-webworker-vite/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'browser-webworker-vite', +}); diff --git a/dev-packages/e2e-tests/test-applications/browser-webworker-vite/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/browser-webworker-vite/tests/errors.test.ts new file mode 100644 index 000000000000..e298fa525efb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/browser-webworker-vite/tests/errors.test.ts @@ -0,0 +1,173 @@ +import { expect, test } from '@playwright/test'; +import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; + +test('captures an error with debug ids and pageload trace context', async ({ page }) => { + const errorEventPromise = waitForError('browser-webworker-vite', async event => { + return !event.type && !!event.exception?.values?.[0]; + }); + + const transactionPromise = waitForTransaction('browser-webworker-vite', transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + await page.goto('/'); + + await page.locator('#trigger-error').click(); + + await page.waitForTimeout(1000); + + const errorEvent = await errorEventPromise; + const transactionEvent = await transactionPromise; + + const pageloadTraceId = transactionEvent.contexts?.trace?.trace_id; + const pageloadSpanId = transactionEvent.contexts?.trace?.span_id; + + expect(errorEvent.exception?.values).toHaveLength(1); + expect(errorEvent.exception?.values?.[0]?.value).toBe('Uncaught Error: Uncaught error in worker'); + expect(errorEvent.exception?.values?.[0]?.stacktrace?.frames).toHaveLength(1); + expect(errorEvent.exception?.values?.[0]?.stacktrace?.frames?.[0]?.filename).toMatch(/worker-.+\.js$/); + + expect(errorEvent.transaction).toBe('/'); + expect(transactionEvent.transaction).toBe('/'); + + expect(errorEvent.request).toEqual({ + url: 'http://localhost:3030/', + headers: expect.any(Object), + }); + + expect(errorEvent.contexts?.trace).toEqual({ + trace_id: pageloadTraceId, + span_id: pageloadSpanId, + }); + + expect(errorEvent.debug_meta).toEqual({ + images: [ + { + code_file: expect.stringMatching(/http:\/\/localhost:3030\/assets\/worker-.+\.js/), + debug_id: expect.stringMatching(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/), + type: 'sourcemap', + }, + ], + }); +}); + +test("user worker message handlers don't trigger for sentry messages", async ({ page }) => { + const workerReadyPromise = new Promise(resolve => { + let workerMessageCount = 0; + page.on('console', msg => { + if (msg.text().startsWith('received message from worker:')) { + workerMessageCount++; + } + + if (msg.text() === 'received message from worker: WORKER_READY') { + resolve(workerMessageCount); + } + }); + }); + + await page.goto('/'); + + const workerMessageCount = await workerReadyPromise; + + expect(workerMessageCount).toBe(1); +}); + +test('captures an error from the second eagerly added worker', async ({ page }) => { + const errorEventPromise = waitForError('browser-webworker-vite', async event => { + return !event.type && !!event.exception?.values?.[0]; + }); + + const transactionPromise = waitForTransaction('browser-webworker-vite', transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + await page.goto('/'); + + await page.locator('#trigger-error-2').click(); + + await page.waitForTimeout(1000); + + const errorEvent = await errorEventPromise; + const transactionEvent = await transactionPromise; + + const pageloadTraceId = transactionEvent.contexts?.trace?.trace_id; + const pageloadSpanId = transactionEvent.contexts?.trace?.span_id; + + expect(errorEvent.exception?.values).toHaveLength(1); + expect(errorEvent.exception?.values?.[0]?.value).toBe('Uncaught Error: Uncaught error in worker 2'); + expect(errorEvent.exception?.values?.[0]?.stacktrace?.frames).toHaveLength(1); + expect(errorEvent.exception?.values?.[0]?.stacktrace?.frames?.[0]?.filename).toMatch(/worker2-.+\.js$/); + + expect(errorEvent.transaction).toBe('/'); + expect(transactionEvent.transaction).toBe('/'); + + expect(errorEvent.request).toEqual({ + url: 'http://localhost:3030/', + headers: expect.any(Object), + }); + + expect(errorEvent.contexts?.trace).toEqual({ + trace_id: pageloadTraceId, + span_id: pageloadSpanId, + }); + + expect(errorEvent.debug_meta).toEqual({ + images: [ + { + code_file: expect.stringMatching(/http:\/\/localhost:3030\/assets\/worker2-.+\.js/), + debug_id: expect.stringMatching(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/), + type: 'sourcemap', + }, + ], + }); +}); + +test('captures an error from the third lazily added worker', async ({ page }) => { + const errorEventPromise = waitForError('browser-webworker-vite', async event => { + return !event.type && !!event.exception?.values?.[0]; + }); + + const transactionPromise = waitForTransaction('browser-webworker-vite', transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + await page.goto('/'); + + await page.locator('#trigger-error-3').click(); + + await page.waitForTimeout(1000); + + const errorEvent = await errorEventPromise; + const transactionEvent = await transactionPromise; + + const pageloadTraceId = transactionEvent.contexts?.trace?.trace_id; + const pageloadSpanId = transactionEvent.contexts?.trace?.span_id; + + expect(errorEvent.exception?.values).toHaveLength(1); + expect(errorEvent.exception?.values?.[0]?.value).toBe('Uncaught Error: Uncaught error in worker 3'); + expect(errorEvent.exception?.values?.[0]?.stacktrace?.frames).toHaveLength(1); + expect(errorEvent.exception?.values?.[0]?.stacktrace?.frames?.[0]?.filename).toMatch(/worker3-.+\.js$/); + + expect(errorEvent.transaction).toBe('/'); + expect(transactionEvent.transaction).toBe('/'); + + expect(errorEvent.request).toEqual({ + url: 'http://localhost:3030/', + headers: expect.any(Object), + }); + + expect(errorEvent.contexts?.trace).toEqual({ + trace_id: pageloadTraceId, + span_id: pageloadSpanId, + }); + + expect(errorEvent.debug_meta).toEqual({ + images: [ + { + code_file: expect.stringMatching(/http:\/\/localhost:3030\/assets\/worker3-.+\.js/), + debug_id: expect.stringMatching(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/), + type: 'sourcemap', + }, + ], + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/browser-webworker-vite/tsconfig.json b/dev-packages/e2e-tests/test-applications/browser-webworker-vite/tsconfig.json new file mode 100644 index 000000000000..4f5edc248c88 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/browser-webworker-vite/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/dev-packages/e2e-tests/test-applications/browser-webworker-vite/vite.config.ts b/dev-packages/e2e-tests/test-applications/browser-webworker-vite/vite.config.ts new file mode 100644 index 000000000000..df010d9b426c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/browser-webworker-vite/vite.config.ts @@ -0,0 +1,29 @@ +import { sentryVitePlugin } from '@sentry/vite-plugin'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + build: { + sourcemap: 'hidden', + envPrefix: ['PUBLIC_'], + }, + + plugins: [ + sentryVitePlugin({ + org: process.env.E2E_TEST_SENTRY_ORG_SLUG, + project: process.env.E2E_TEST_SENTRY_PROJECT, + authToken: process.env.E2E_TEST_AUTH_TOKEN, + }), + ], + + worker: { + plugins: () => [ + ...sentryVitePlugin({ + org: process.env.E2E_TEST_SENTRY_ORG_SLUG, + project: process.env.E2E_TEST_SENTRY_PROJECT, + authToken: process.env.E2E_TEST_AUTH_TOKEN, + }), + ], + }, + + envPrefix: ['PUBLIC_'], +}); diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers/.gitignore b/dev-packages/e2e-tests/test-applications/cloudflare-workers/.gitignore new file mode 100644 index 000000000000..e71378008bf1 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers/.gitignore @@ -0,0 +1 @@ +.wrangler diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers/package.json b/dev-packages/e2e-tests/test-applications/cloudflare-workers/package.json index b195ec7e2716..91a49e0788f4 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-workers/package.json +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers/package.json @@ -4,28 +4,35 @@ "private": true, "scripts": { "deploy": "wrangler deploy", - "dev": "wrangler dev --var E2E_TEST_DSN=$E2E_TEST_DSN", - "build": "wrangler deploy --dry-run --var E2E_TEST_DSN=$E2E_TEST_DSN", - "test": "vitest", + "dev": "wrangler dev --var \"E2E_TEST_DSN:$E2E_TEST_DSN\" --log-level=$(test $CI && echo 'none' || echo 'log')", + "build": "wrangler deploy --dry-run", + "test": "vitest --run", "typecheck": "tsc --noEmit", "cf-typegen": "wrangler types", "test:build": "pnpm install && pnpm build", - "test:assert": "pnpm typecheck" + "test:assert": "pnpm test:dev && pnpm test:prod", + "test:prod": "TEST_ENV=production playwright test", + "test:dev": "TEST_ENV=development playwright test" }, "dependencies": { "@sentry/cloudflare": "latest || *" }, "devDependencies": { - "@cloudflare/vitest-pool-workers": "^0.4.5", + "@playwright/test": "~1.50.0", + "@cloudflare/vitest-pool-workers": "^0.8.19", "@cloudflare/workers-types": "^4.20240725.0", + "@sentry-internal/test-utils": "link:../../../test-utils", "typescript": "^5.5.2", - "vitest": "1.6.1", - "wrangler": "4.22.0" + "vitest": "~3.2.0", + "wrangler": "^4.23.0", + "ws": "^8.18.3" }, "volta": { "extends": "../../package.json" }, - "sentryTest": { - "optional": true + "pnpm": { + "overrides": { + "strip-literal": "~2.0.0" + } } } diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers/playwright.config.ts b/dev-packages/e2e-tests/test-applications/cloudflare-workers/playwright.config.ts new file mode 100644 index 000000000000..c69c955fafd8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers/playwright.config.ts @@ -0,0 +1,21 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; +const testEnv = process.env.TEST_ENV; + +if (!testEnv) { + throw new Error('No test env defined'); +} + +const APP_PORT = 38787; + +const config = getPlaywrightConfig( + { + startCommand: `pnpm dev --port ${APP_PORT}`, + port: APP_PORT, + }, + { + // This comes with the risk of tests leaking into each other but the tests run quite slow so we should parallelize + workers: '100%', + }, +); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers/src/index.ts b/dev-packages/e2e-tests/test-applications/cloudflare-workers/src/index.ts index a3366168fa08..f329cea238e8 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-workers/src/index.ts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers/src/index.ts @@ -11,15 +11,100 @@ * Learn more at https://developers.cloudflare.com/workers/ */ import * as Sentry from '@sentry/cloudflare'; +import { DurableObject } from "cloudflare:workers"; + +class MyDurableObjectBase extends DurableObject { + private throwOnExit = new WeakMap(); + async throwException(): Promise { + throw new Error('Should be recorded in Sentry.'); + } + + async fetch(request: Request) { + const { pathname } = new URL(request.url); + switch (pathname) { + case '/throwException': { + await this.throwException(); + break; + } + case '/ws': + const webSocketPair = new WebSocketPair(); + const [client, server] = Object.values(webSocketPair); + this.ctx.acceptWebSocket(server); + return new Response(null, { status: 101, webSocket: client }); + } + return new Response('DO is fine'); + } + + webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): void | Promise { + if (message === 'throwException') { + throw new Error('Should be recorded in Sentry: webSocketMessage'); + } else if (message === 'throwOnExit') { + this.throwOnExit.set(ws, new Error('Should be recorded in Sentry: webSocketClose')); + } + } + + webSocketClose(ws: WebSocket): void | Promise { + if (this.throwOnExit.has(ws)) { + const error = this.throwOnExit.get(ws)!; + this.throwOnExit.delete(ws); + throw error; + } + } +} + +export const MyDurableObject = Sentry.instrumentDurableObjectWithSentry( + (env: Env) => ({ + dsn: env.E2E_TEST_DSN, + environment: 'qa', // dynamic sampling bias to keep transactions + tunnel: `http://localhost:3031/`, // proxy server + tracesSampleRate: 1.0, + sendDefaultPii: true, + transportOptions: { + // We are doing a lot of events at once in this test + bufferSize: 1000, + }, + }), + MyDurableObjectBase, +); export default Sentry.withSentry( (env: Env) => ({ dsn: env.E2E_TEST_DSN, - // Set tracesSampleRate to 1.0 to capture 100% of spans for tracing. + environment: 'qa', // dynamic sampling bias to keep transactions + tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1.0, + sendDefaultPii: true, + transportOptions: { + // We are doing a lot of events at once in this test + bufferSize: 1000, + }, }), { - async fetch(request, env, ctx) { + async fetch(request, env) { + const url = new URL(request.url); + switch (url.pathname) { + case '/rpc/throwException': + { + const id = env.MY_DURABLE_OBJECT.idFromName('foo'); + const stub = env.MY_DURABLE_OBJECT.get(id) as DurableObjectStub; + try { + await stub.throwException(); + } catch (e) { + //We will catch this to be sure not to log inside withSentry + return new Response(null, { status: 500 }); + } + } + break; + case '/throwException': + throw new Error('To be recorded in Sentry.'); + default: + if (url.pathname.startsWith('/pass-to-object/')) { + const id = env.MY_DURABLE_OBJECT.idFromName('foo'); + const stub = env.MY_DURABLE_OBJECT.get(id) as DurableObjectStub; + url.pathname = url.pathname.replace('/pass-to-object/', ''); + return stub.fetch(new Request(url, request)); + } + } return new Response('Hello World!'); }, } satisfies ExportedHandler, diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/cloudflare-workers/start-event-proxy.mjs new file mode 100644 index 000000000000..738ec64293b5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import {startEventProxyServer} from '@sentry-internal/test-utils' + +startEventProxyServer({ + port: 3031, + proxyServerName: 'cloudflare-workers', +}) diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers/test/index.spec.ts b/dev-packages/e2e-tests/test-applications/cloudflare-workers/test/index.spec.ts deleted file mode 100644 index 21c9d1b7999a..000000000000 --- a/dev-packages/e2e-tests/test-applications/cloudflare-workers/test/index.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import worker from '../src/index'; -// test/index.spec.ts -import { SELF, createExecutionContext, env, waitOnExecutionContext } from 'cloudflare:test'; - -// For now, you'll need to do something like this to get a correctly-typed -// `Request` to pass to `worker.fetch()`. -const IncomingRequest = Request; - -describe('Hello World worker', () => { - it('responds with Hello World! (unit style)', async () => { - const request = new IncomingRequest('http://example.com'); - // Create an empty context to pass to `worker.fetch()`. - const ctx = createExecutionContext(); - const response = await worker.fetch(request, env, ctx); - // Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions - await waitOnExecutionContext(ctx); - expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`); - }); - - it('responds with Hello World! (integration style)', async () => { - const response = await SELF.fetch('https://example.com'); - expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`); - }); -}); diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers/test/tsconfig.json b/dev-packages/e2e-tests/test-applications/cloudflare-workers/test/tsconfig.json deleted file mode 100644 index bc019a7e2bfb..000000000000 --- a/dev-packages/e2e-tests/test-applications/cloudflare-workers/test/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "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-workers/tests/index.test.ts b/dev-packages/e2e-tests/test-applications/cloudflare-workers/tests/index.test.ts new file mode 100644 index 000000000000..ac8f2e38952e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers/tests/index.test.ts @@ -0,0 +1,67 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; +import {WebSocket} from 'ws' + +test('Index page', async ({ baseURL }) => { + const result = await fetch(baseURL!); + expect(result.status).toBe(200); + await expect(result.text()).resolves.toBe('Hello World!'); +}) + +test('worker\'s withSentry', async ({baseURL}) => { + const eventWaiter = waitForError('cloudflare-workers', (event) => { + return event.exception?.values?.[0]?.mechanism?.type === 'cloudflare'; + }); + const response = await fetch(`${baseURL}/throwException`); + expect(response.status).toBe(500); + const event = await eventWaiter; + expect(event.exception?.values?.[0]?.value).toBe('To be recorded in Sentry.'); +}) + +test('RPC method which throws an exception to be logged to sentry', async ({baseURL}) => { + const eventWaiter = waitForError('cloudflare-workers', (event) => { + return event.exception?.values?.[0]?.mechanism?.type === 'cloudflare_durableobject'; + }); + const response = await fetch(`${baseURL}/rpc/throwException`); + expect(response.status).toBe(500); + const event = await eventWaiter; + expect(event.exception?.values?.[0]?.value).toBe('Should be recorded in Sentry.'); +}); +test('Request processed by DurableObject\'s fetch is recorded', async ({baseURL}) => { + const eventWaiter = waitForError('cloudflare-workers', (event) => { + return event.exception?.values?.[0]?.mechanism?.type === 'cloudflare_durableobject'; + }); + const response = await fetch(`${baseURL}/pass-to-object/throwException`); + expect(response.status).toBe(500); + const event = await eventWaiter; + expect(event.exception?.values?.[0]?.value).toBe('Should be recorded in Sentry.'); +}); +test('Websocket.webSocketMessage', async ({baseURL}) => { + const eventWaiter = waitForError('cloudflare-workers', (event) => { + return event.exception?.values?.[0]?.mechanism?.type === 'cloudflare_durableobject'; + }); + const url = new URL('/pass-to-object/ws', baseURL); + url.protocol = url.protocol.replace('http', 'ws'); + const socket = new WebSocket(url.toString()); + socket.addEventListener('open', () => { + socket.send('throwException') + }); + const event = await eventWaiter; + socket.close(); + expect(event.exception?.values?.[0]?.value).toBe('Should be recorded in Sentry: webSocketMessage'); +}) + +test('Websocket.webSocketClose', async ({baseURL}) => { + const eventWaiter = waitForError('cloudflare-workers', (event) => { + return event.exception?.values?.[0]?.mechanism?.type === 'cloudflare_durableobject'; + }); + const url = new URL('/pass-to-object/ws', baseURL); + url.protocol = url.protocol.replace('http', 'ws'); + const socket = new WebSocket(url.toString()); + socket.addEventListener('open', () => { + socket.send('throwOnExit') + socket.close() + }); + const event = await eventWaiter; + expect(event.exception?.values?.[0]?.value).toBe('Should be recorded in Sentry: webSocketClose'); +}) diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers/tests/tsconfig.json b/dev-packages/e2e-tests/test-applications/cloudflare-workers/tests/tsconfig.json new file mode 100644 index 000000000000..978ecd87b7ce --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers/tests/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "types": ["@cloudflare/vitest-pool-workers"] + }, + "include": ["./**/*.ts", "../worker-configuration.d.ts"], + "exclude": [] +} diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers/tsconfig.json b/dev-packages/e2e-tests/test-applications/cloudflare-workers/tsconfig.json index 79207ab7ae9a..ca1f83e3bc15 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-workers/tsconfig.json +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers/tsconfig.json @@ -2,103 +2,43 @@ "compilerOptions": { /* Visit https://aka.ms/tsconfig.json to read more about this file */ - /* Projects */ - // "incremental": true, /* Enable incremental compilation */ - // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ - // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ - // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ - // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ - // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ - - /* Language and Environment */ - "target": "es2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, - "lib": ["es2021"] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, - "jsx": "react-jsx" /* Specify what JSX code is generated. */, - // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ - // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ - // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ - // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ - // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ - // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ - // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ - // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ - - /* Modules */ - "module": "es2022" /* Specify what module code is generated. */, - // "rootDir": "./", /* Specify the root folder within your source files. */ - "moduleResolution": "Bundler" /* Specify how TypeScript looks up a file from a given module specifier. */, - // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ - // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ - // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ - // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ + /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "target": "es2021", + /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + "lib": ["es2021"], + /* Specify what JSX code is generated. */ + "jsx": "react-jsx", + + /* Specify what module code is generated. */ + "module": "es2022", + /* Specify how TypeScript looks up a file from a given module specifier. */ + "moduleResolution": "Bundler", + /* Enable importing .json files */ + "resolveJsonModule": true, + + /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ + "allowJs": true, + /* Enable error reporting in type-checked JavaScript files. */ + "checkJs": false, + + /* Disable emitting files from a compilation. */ + "noEmit": true, + + /* Ensure that each file can be safely transpiled without relying on other imports. */ + "isolatedModules": true, + /* Allow 'import x from y' when a module doesn't have a default export. */ + "allowSyntheticDefaultImports": true, + /* Ensure that casing is correct in imports. */ + "forceConsistentCasingInFileNames": true, + + /* Enable all strict type-checking options. */ + "strict": true, + + /* Skip type checking all .d.ts files. */ + "skipLibCheck": true, "types": [ - "@cloudflare/workers-types/2023-07-01" - ] /* Specify type package names to be included without being referenced in a source file. */, - // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - "resolveJsonModule": true /* Enable importing .json files */, - // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ - - /* JavaScript Support */ - "allowJs": true /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */, - "checkJs": false /* Enable error reporting in type-checked JavaScript files. */, - // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ - - /* Emit */ - // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ - // "declarationMap": true, /* Create sourcemaps for d.ts files. */ - // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ - // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ - // "outDir": "./", /* Specify an output folder for all emitted files. */ - // "removeComments": true, /* Disable emitting comments. */ - "noEmit": true /* Disable emitting files from a compilation. */, - // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ - // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ - // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ - // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ - // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ - // "newLine": "crlf", /* Set the newline character for emitting files. */ - // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ - // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ - // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ - // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ - // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ - // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ - - /* Interop Constraints */ - "isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */, - "allowSyntheticDefaultImports": true /* Allow 'import x from y' when a module doesn't have a default export. */, - // "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, - // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, - - /* Type Checking */ - "strict": true /* Enable all strict type-checking options. */, - // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ - // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ - // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ - // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ - // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ - // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ - // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ - // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ - // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ - // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ - // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ - // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ - // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ - // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ - // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ - // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ - // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ - // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ - - /* Completeness */ - // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ + "./worker-configuration.d.ts" + ] }, "exclude": ["test"], "include": ["worker-configuration.d.ts", "src/**/*.ts"] diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers/worker-configuration.d.ts b/dev-packages/e2e-tests/test-applications/cloudflare-workers/worker-configuration.d.ts index 0c9e04919e42..08a92a61d05d 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-workers/worker-configuration.d.ts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers/worker-configuration.d.ts @@ -3,4 +3,5 @@ interface Env { E2E_TEST_DSN: ''; + MY_DURABLE_OBJECT: DurableObjectNamespace } diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-workers/wrangler.toml b/dev-packages/e2e-tests/test-applications/cloudflare-workers/wrangler.toml index 2fc762f4025c..70b0ae034580 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-workers/wrangler.toml +++ b/dev-packages/e2e-tests/test-applications/cloudflare-workers/wrangler.toml @@ -53,15 +53,15 @@ compatibility_flags = ["nodejs_compat"] # Bind a Durable Object. Durable objects are a scale-to-zero compute primitive based on the actor model. # Durable Objects can live for as long as needed. Use these when you need a long-running "server", such as in realtime apps. # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#durable-objects -# [[durable_objects.bindings]] -# name = "MY_DURABLE_OBJECT" -# class_name = "MyDurableObject" +[[durable_objects.bindings]] +name = "MY_DURABLE_OBJECT" +class_name = "MyDurableObject" # Durable Object migrations. # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#migrations -# [[migrations]] -# tag = "v1" -# new_classes = ["MyDurableObject"] +[[migrations]] +tag = "v1" +new_sqlite_classes = ["MyDurableObject"] # Bind a Hyperdrive configuration. Use to accelerate access to your existing databases from Cloudflare Workers. # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#hyperdrive diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/app/controllers/index.ts b/dev-packages/e2e-tests/test-applications/ember-classic/app/controllers/index.ts index c49ff8d94147..418b51ccee07 100644 --- a/dev-packages/e2e-tests/test-applications/ember-classic/app/controllers/index.ts +++ b/dev-packages/e2e-tests/test-applications/ember-classic/app/controllers/index.ts @@ -22,7 +22,7 @@ export default class IndexController extends Controller { public createCaughtEmberError(): void { try { throw new Error('Looks like you have a caught EmberError'); - } catch (e) { + } catch { // do nothing } } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/pages/crashed-session-page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/crashed-session-page.tsx index 277293e77aed..ddfdc73680db 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-13/pages/crashed-session-page.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/crashed-session-page.tsx @@ -6,7 +6,7 @@ export default function CrashedPage() { // @ts-expect-error window.onerror(null, null, null, null, new Error('Crashed')); } - } catch (_e) { + } catch { // no-empty } return

Crashed

; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/.npmrc b/dev-packages/e2e-tests/test-applications/nextjs-15/.npmrc index 070f80f05092..a3160f4de175 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15/.npmrc +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/.npmrc @@ -1,2 +1,4 @@ @sentry:registry=http://127.0.0.1:4873 @sentry-internal:registry=http://127.0.0.1:4873 +public-hoist-pattern[]=*import-in-the-middle* +public-hoist-pattern[]=*require-in-the-middle* diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/package.json b/dev-packages/e2e-tests/test-applications/nextjs-15/package.json index 19acabbba0c4..8216f06f7be6 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/package.json @@ -7,10 +7,12 @@ "clean": "npx rimraf node_modules pnpm-lock.yaml", "test:prod": "TEST_ENV=production playwright test", "test:dev": "TEST_ENV=development playwright test", + "test:dev-turbo": "TEST_ENV=dev-turbopack playwright test", "test:build": "pnpm install && pnpm build", "test:build-canary": "pnpm install && pnpm add next@canary && pnpm add react@beta && pnpm add react-dom@beta && pnpm build", "//": "15.0.0-canary.194 is the canary release attached to Next.js RC 1. We need to use the canary version instead of the RC because PPR will not work without. The specific react version is also attached to RC 1.", "test:build-latest": "pnpm install && pnpm add next@15.0.0-canary.194 && pnpm add react@19.0.0-rc-cd22717c-20241013 && pnpm add react-dom@19.0.0-rc-cd22717c-20241013 && pnpm build", + "test:build-turbo": "pnpm install && pnpm add next@15.4.2-canary.1 && next build --turbopack", "test:assert": "pnpm test:prod && pnpm test:dev" }, "dependencies": { @@ -19,7 +21,7 @@ "@types/react": "18.0.26", "@types/react-dom": "18.0.9", "ai": "^3.0.0", - "next": "15.3.0-canary.33", + "next": "15.4.2-canary.1", "react": "beta", "react-dom": "beta", "typescript": "~5.0.0", @@ -41,6 +43,11 @@ { "build-command": "test:build-latest", "label": "nextjs-15 (latest)" + }, + { + "build-command": "test:build-turbo", + "assert-command": "pnpm test:prod && pnpm test:dev-turbo", + "label": "nextjs-15 (turbo)" } ] } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/nextjs-15/playwright.config.mjs index c675d003853a..f2aa01e3e3c8 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15/playwright.config.mjs +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/playwright.config.mjs @@ -5,8 +5,24 @@ if (!testEnv) { throw new Error('No test env defined'); } +const getStartCommand = () => { + if (testEnv === 'dev-turbopack') { + return 'pnpm next dev -p 3030 --turbopack'; + } + + if (testEnv === 'development') { + return 'pnpm next dev -p 3030'; + } + + if (testEnv === 'production') { + return 'pnpm next start -p 3030'; + } + + throw new Error(`Unknown test env: ${testEnv}`); +}; + const config = getPlaywrightConfig({ - startCommand: testEnv === 'development' ? 'pnpm next dev -p 3030' : 'pnpm next start -p 3030', + startCommand: getStartCommand(), port: 3030, }); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/tests/prefetch-spans.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/prefetch-spans.test.ts index b59a45f31f8b..550f6726e789 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15/tests/prefetch-spans.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/prefetch-spans.test.ts @@ -2,7 +2,10 @@ import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; test('Prefetch client spans should have a http.request.prefetch attribute', async ({ page }) => { - test.skip(process.env.TEST_ENV === 'development', "Prefetch requests don't have the prefetch header in dev mode"); + test.skip( + process.env.TEST_ENV === 'development' || process.env.TEST_ENV === 'dev-turbopack', + "Prefetch requests don't have the prefetch header in dev mode", + ); const pageloadTransactionPromise = waitForTransaction('nextjs-15', async transactionEvent => { return transactionEvent?.transaction === '/prefetching'; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/assert-build.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/assert-build.ts index 75cdeb30ee10..8d6ab7ffbfef 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/assert-build.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/assert-build.ts @@ -13,7 +13,7 @@ const getLatestNextVersion = async () => { const response = await fetch('https://registry.npmjs.org/next/latest'); const data = await response.json(); return data.version as string; - } catch (error) { + } catch { return '0.0.0'; } }; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/package.json b/dev-packages/e2e-tests/test-applications/nextjs-turbo/package.json index 9102de60706b..e4f02f7438f3 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-turbo/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/package.json @@ -9,17 +9,17 @@ "test:dev": "TEST_ENV=development playwright test", "test:build": "pnpm install && pnpm build", "test:build-canary": "pnpm install && pnpm add next@canary && pnpm add react@canary && pnpm add react-dom@canary && pnpm build", - "test:build-latest": "pnpm install && pnpm add next@latest && pnpm add react@rc && pnpm add react-dom@rc && pnpm build", + "test:build-latest": "pnpm install && pnpm add next@latest && pnpm build", "test:assert": "pnpm test:prod && pnpm test:dev" }, "dependencies": { "@sentry/nextjs": "latest || *", "@types/node": "^18.19.1", - "@types/react": "18.0.26", - "@types/react-dom": "18.0.9", + "@types/react": "^19", + "@types/react-dom": "^19", "next": "^15.3.5", - "react": "rc", - "react-dom": "rc", + "react": "^19", + "react-dom": "^19", "typescript": "~5.0.0" }, "devDependencies": { diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-cross-usage/package.json b/dev-packages/e2e-tests/test-applications/react-router-7-cross-usage/package.json index e4e47733bf77..dc61a85199e9 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-cross-usage/package.json +++ b/dev-packages/e2e-tests/test-applications/react-router-7-cross-usage/package.json @@ -9,7 +9,7 @@ "express": "4.20.0", "react": "18.2.0", "react-dom": "18.2.0", - "react-router-dom": "^7.2.0", + "react-router-dom": "7.6.0", "react-scripts": "5.0.1", "typescript": "~5.0.0" }, diff --git a/dev-packages/node-core-integration-tests/scripts/clean.js b/dev-packages/node-core-integration-tests/scripts/clean.js index 0610e39f92d4..e6fdb4c4f6e8 100644 --- a/dev-packages/node-core-integration-tests/scripts/clean.js +++ b/dev-packages/node-core-integration-tests/scripts/clean.js @@ -13,7 +13,7 @@ for (const path of paths) { // eslint-disable-next-line no-console console.log(`docker compose down @ ${path}`); execSync('docker compose down --volumes', { stdio: 'inherit', cwd: path }); - } catch (_) { + } catch { // } } diff --git a/dev-packages/node-core-integration-tests/utils/runner.ts b/dev-packages/node-core-integration-tests/utils/runner.ts index 97b1efa2dbb4..273680607ffb 100644 --- a/dev-packages/node-core-integration-tests/utils/runner.ts +++ b/dev-packages/node-core-integration-tests/utils/runner.ts @@ -497,7 +497,7 @@ export function createRunner(...paths: string[]) { try { const envelope = JSON.parse(cleanedLine) as Envelope; newEnvelope(envelope); - } catch (_) { + } catch { // } } diff --git a/dev-packages/node-integration-tests/scripts/clean.js b/dev-packages/node-integration-tests/scripts/clean.js index 0610e39f92d4..e6fdb4c4f6e8 100644 --- a/dev-packages/node-integration-tests/scripts/clean.js +++ b/dev-packages/node-integration-tests/scripts/clean.js @@ -13,7 +13,7 @@ for (const path of paths) { // eslint-disable-next-line no-console console.log(`docker compose down @ ${path}`); execSync('docker compose down --volumes', { stdio: 'inherit', cwd: path }); - } catch (_) { + } catch { // } } diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/instrument-with-options.mjs b/dev-packages/node-integration-tests/suites/tracing/openai/instrument-with-options.mjs new file mode 100644 index 000000000000..35f97fd84093 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/openai/instrument-with-options.mjs @@ -0,0 +1,16 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + sendDefaultPii: false, + transport: loggingTransport, + integrations: [ + Sentry.openAIIntegration({ + recordInputs: true, + recordOutputs: true, + }), + ], +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/instrument-with-pii.mjs b/dev-packages/node-integration-tests/suites/tracing/openai/instrument-with-pii.mjs new file mode 100644 index 000000000000..a53a13af7738 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/openai/instrument-with-pii.mjs @@ -0,0 +1,11 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + sendDefaultPii: true, + transport: loggingTransport, + integrations: [Sentry.openAIIntegration()], +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/openai/instrument.mjs new file mode 100644 index 000000000000..f3fbac9d1274 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/openai/instrument.mjs @@ -0,0 +1,11 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + sendDefaultPii: false, + transport: loggingTransport, + integrations: [Sentry.openAIIntegration()], +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/scenario.mjs b/dev-packages/node-integration-tests/suites/tracing/openai/scenario.mjs new file mode 100644 index 000000000000..3958517bea40 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/openai/scenario.mjs @@ -0,0 +1,108 @@ +import { instrumentOpenAiClient } from '@sentry/core'; +import * as Sentry from '@sentry/node'; + +class MockOpenAI { + constructor(config) { + this.apiKey = config.apiKey; + + this.chat = { + completions: { + create: async params => { + // Simulate processing time + await new Promise(resolve => setTimeout(resolve, 10)); + + if (params.model === 'error-model') { + const error = new Error('Model not found'); + error.status = 404; + error.headers = { 'x-request-id': 'mock-request-123' }; + throw error; + } + + return { + id: 'chatcmpl-mock123', + object: 'chat.completion', + created: 1677652288, + model: params.model, + system_fingerprint: 'fp_44709d6fcb', + choices: [ + { + index: 0, + message: { + role: 'assistant', + content: 'Hello from OpenAI mock!', + }, + finish_reason: 'stop', + }, + ], + usage: { + prompt_tokens: 10, + completion_tokens: 15, + total_tokens: 25, + }, + }; + }, + }, + }; + + this.responses = { + create: async params => { + await new Promise(resolve => setTimeout(resolve, 10)); + + return { + id: 'resp_mock456', + object: 'response', + created: 1677652290, + model: params.model, + input_text: params.input, + output_text: `Response to: ${params.input}`, + finish_reason: 'stop', + usage: { + input_tokens: 5, + output_tokens: 8, + total_tokens: 13, + }, + }; + }, + }; + } +} + +async function run() { + await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { + const mockClient = new MockOpenAI({ + apiKey: 'mock-api-key', + }); + + const client = instrumentOpenAiClient(mockClient); + + // First test: basic chat completion + await client.chat.completions.create({ + model: 'gpt-3.5-turbo', + messages: [ + { role: 'system', content: 'You are a helpful assistant.' }, + { role: 'user', content: 'What is the capital of France?' }, + ], + temperature: 0.7, + max_tokens: 100, + }); + + // Second test: responses API + await client.responses.create({ + model: 'gpt-3.5-turbo', + input: 'Translate this to French: Hello', + instructions: 'You are a translator', + }); + + // Third test: error handling + try { + await client.chat.completions.create({ + model: 'error-model', + messages: [{ role: 'user', content: 'This will fail' }], + }); + } catch { + // Error is expected and handled + } + }); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/openai/test.ts b/dev-packages/node-integration-tests/suites/tracing/openai/test.ts new file mode 100644 index 000000000000..ec6f97a6aa00 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/openai/test.ts @@ -0,0 +1,183 @@ +import { afterAll, describe, expect } from 'vitest'; +import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner'; + +describe('OpenAI integration', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + const EXPECTED_TRANSACTION_DEFAULT_PII_FALSE = { + transaction: 'main', + spans: expect.arrayContaining([ + // First span - basic chat completion without PII + expect.objectContaining({ + data: { + 'gen_ai.operation.name': 'chat', + 'sentry.op': 'gen_ai.chat', + 'sentry.origin': 'manual', + 'gen_ai.system': 'openai', + 'gen_ai.request.model': 'gpt-3.5-turbo', + 'gen_ai.request.temperature': 0.7, + 'gen_ai.response.model': 'gpt-3.5-turbo', + 'gen_ai.response.id': 'chatcmpl-mock123', + 'gen_ai.response.finish_reasons': '["stop"]', + 'gen_ai.usage.input_tokens': 10, + 'gen_ai.usage.output_tokens': 15, + 'gen_ai.usage.total_tokens': 25, + 'openai.response.id': 'chatcmpl-mock123', + 'openai.response.model': 'gpt-3.5-turbo', + 'openai.response.timestamp': '2023-03-01T06:31:28.000Z', + 'openai.usage.completion_tokens': 15, + 'openai.usage.prompt_tokens': 10, + }, + description: 'chat gpt-3.5-turbo', + op: 'gen_ai.chat', + origin: 'manual', + status: 'ok', + }), + // Second span - responses API + expect.objectContaining({ + data: { + 'gen_ai.operation.name': 'chat', + 'sentry.op': 'gen_ai.chat', + 'sentry.origin': 'manual', + 'gen_ai.system': 'openai', + 'gen_ai.request.model': 'gpt-3.5-turbo', + 'gen_ai.response.model': 'gpt-3.5-turbo', + 'gen_ai.response.id': 'resp_mock456', + 'gen_ai.usage.input_tokens': 5, + 'gen_ai.usage.output_tokens': 8, + 'gen_ai.usage.total_tokens': 13, + 'openai.response.id': 'resp_mock456', + 'openai.response.model': 'gpt-3.5-turbo', + 'openai.usage.completion_tokens': 8, + 'openai.usage.prompt_tokens': 5, + }, + description: 'chat gpt-3.5-turbo', + op: 'gen_ai.chat', + origin: 'manual', + status: 'ok', + }), + // Third span - error handling + expect.objectContaining({ + data: { + 'gen_ai.operation.name': 'chat', + 'sentry.op': 'gen_ai.chat', + 'sentry.origin': 'manual', + 'gen_ai.system': 'openai', + 'gen_ai.request.model': 'error-model', + }, + description: 'chat error-model', + op: 'gen_ai.chat', + origin: 'manual', + status: 'unknown_error', + }), + ]), + }; + + const EXPECTED_TRANSACTION_DEFAULT_PII_TRUE = { + transaction: 'main', + spans: expect.arrayContaining([ + // First span - basic chat completion with PII + expect.objectContaining({ + data: { + 'gen_ai.operation.name': 'chat', + 'sentry.op': 'gen_ai.chat', + 'sentry.origin': 'manual', + 'gen_ai.system': 'openai', + 'gen_ai.request.model': 'gpt-3.5-turbo', + 'gen_ai.request.temperature': 0.7, + 'gen_ai.request.messages': + '[{"role":"system","content":"You are a helpful assistant."},{"role":"user","content":"What is the capital of France?"}]', + 'gen_ai.response.model': 'gpt-3.5-turbo', + 'gen_ai.response.id': 'chatcmpl-mock123', + 'gen_ai.response.finish_reasons': '["stop"]', + 'gen_ai.response.text': '["Hello from OpenAI mock!"]', + 'gen_ai.usage.input_tokens': 10, + 'gen_ai.usage.output_tokens': 15, + 'gen_ai.usage.total_tokens': 25, + 'openai.response.id': 'chatcmpl-mock123', + 'openai.response.model': 'gpt-3.5-turbo', + 'openai.response.timestamp': '2023-03-01T06:31:28.000Z', + 'openai.usage.completion_tokens': 15, + 'openai.usage.prompt_tokens': 10, + }, + description: 'chat gpt-3.5-turbo', + op: 'gen_ai.chat', + origin: 'manual', + status: 'ok', + }), + // Second span - responses API with PII + expect.objectContaining({ + data: { + 'gen_ai.operation.name': 'chat', + 'sentry.op': 'gen_ai.chat', + 'sentry.origin': 'manual', + 'gen_ai.system': 'openai', + 'gen_ai.request.model': 'gpt-3.5-turbo', + 'gen_ai.request.messages': '"Translate this to French: Hello"', + 'gen_ai.response.text': 'Response to: Translate this to French: Hello', + 'gen_ai.response.model': 'gpt-3.5-turbo', + 'gen_ai.response.id': 'resp_mock456', + 'gen_ai.usage.input_tokens': 5, + 'gen_ai.usage.output_tokens': 8, + 'gen_ai.usage.total_tokens': 13, + 'openai.response.id': 'resp_mock456', + 'openai.response.model': 'gpt-3.5-turbo', + 'openai.usage.completion_tokens': 8, + 'openai.usage.prompt_tokens': 5, + }, + description: 'chat gpt-3.5-turbo', + op: 'gen_ai.chat', + origin: 'manual', + status: 'ok', + }), + // Third span - error handling with PII + expect.objectContaining({ + data: { + 'gen_ai.operation.name': 'chat', + 'sentry.op': 'gen_ai.chat', + 'sentry.origin': 'manual', + 'gen_ai.system': 'openai', + 'gen_ai.request.model': 'error-model', + 'gen_ai.request.messages': '[{"role":"user","content":"This will fail"}]', + }, + description: 'chat error-model', + op: 'gen_ai.chat', + origin: 'manual', + status: 'unknown_error', + }), + ]), + }; + + const EXPECTED_TRANSACTION_WITH_OPTIONS = { + transaction: 'main', + spans: expect.arrayContaining([ + // Check that custom options are respected + expect.objectContaining({ + data: expect.objectContaining({ + 'gen_ai.request.messages': expect.any(String), // Should include messages when recordInputs: true + 'gen_ai.response.text': expect.any(String), // Should include response text when recordOutputs: true + }), + }), + ]), + }; + + createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { + test('creates openai related spans with sendDefaultPii: false', async () => { + await createRunner().expect({ transaction: EXPECTED_TRANSACTION_DEFAULT_PII_FALSE }).start().completed(); + }); + }); + + createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument-with-pii.mjs', (createRunner, test) => { + test('creates openai related spans with sendDefaultPii: true', async () => { + await createRunner().expect({ transaction: EXPECTED_TRANSACTION_DEFAULT_PII_TRUE }).start().completed(); + }); + }); + + createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument-with-options.mjs', (createRunner, test) => { + test('creates openai related spans with custom options', async () => { + await createRunner().expect({ transaction: EXPECTED_TRANSACTION_WITH_OPTIONS }).start().completed(); + }); + }); +}); diff --git a/dev-packages/node-integration-tests/utils/runner.ts b/dev-packages/node-integration-tests/utils/runner.ts index 1006d71bf3f0..44fc82c220a0 100644 --- a/dev-packages/node-integration-tests/utils/runner.ts +++ b/dev-packages/node-integration-tests/utils/runner.ts @@ -506,7 +506,7 @@ export function createRunner(...paths: string[]) { try { const envelope = JSON.parse(cleanedLine) as Envelope; newEnvelope(envelope); - } catch (_) { + } catch { // } } diff --git a/dev-packages/opentelemetry-v2-tests/package.json b/dev-packages/opentelemetry-v2-tests/package.json index 6172d387e4ad..5bd8cd2f40db 100644 --- a/dev-packages/opentelemetry-v2-tests/package.json +++ b/dev-packages/opentelemetry-v2-tests/package.json @@ -16,7 +16,7 @@ "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^2.0.0", "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.200.0", + "@opentelemetry/instrumentation": "^0.203.0", "@opentelemetry/sdk-trace-base": "^2.0.0", "@opentelemetry/semantic-conventions": "^1.34.0" }, diff --git a/dev-packages/opentelemetry-v2-tests/test/helpers/initOtel.ts b/dev-packages/opentelemetry-v2-tests/test/helpers/initOtel.ts index 50d35295ba60..b45e49e28d79 100644 --- a/dev-packages/opentelemetry-v2-tests/test/helpers/initOtel.ts +++ b/dev-packages/opentelemetry-v2-tests/test/helpers/initOtel.ts @@ -7,7 +7,7 @@ import { ATTR_SERVICE_VERSION, SEMRESATTRS_SERVICE_NAMESPACE, } from '@opentelemetry/semantic-conventions'; -import { getClient, logger, SDK_VERSION } from '@sentry/core'; +import { debug as debugLogger, getClient, SDK_VERSION } from '@sentry/core'; import { wrapContextManagerClass } from '../../../../packages/opentelemetry/src/contextManager'; import { DEBUG_BUILD } from '../../../../packages/opentelemetry/src/debug-build'; import { SentryPropagator } from '../../../../packages/opentelemetry/src/propagator'; @@ -25,21 +25,23 @@ export function initOtel(): void { if (!client) { DEBUG_BUILD && - logger.warn( + debugLogger.warn( 'No client available, skipping OpenTelemetry setup. This probably means that `Sentry.init()` was not called before `initOtel()`.', ); return; } if (client.getOptions().debug) { - const otelLogger = new Proxy(logger as typeof logger & { verbose: (typeof logger)['debug'] }, { - get(target, prop, receiver) { - const actualProp = prop === 'verbose' ? 'debug' : prop; - return Reflect.get(target, actualProp, receiver); + diag.setLogger( + { + error: debugLogger.error, + warn: debugLogger.warn, + info: debugLogger.log, + debug: debugLogger.log, + verbose: debugLogger.log, }, - }); - - diag.setLogger(otelLogger, DiagLogLevel.DEBUG); + DiagLogLevel.DEBUG, + ); } setupEventContextTrace(client); diff --git a/dev-packages/opentelemetry-v2-tests/test/integration/transactions.test.ts b/dev-packages/opentelemetry-v2-tests/test/integration/transactions.test.ts index 7e2bf79f6ec0..4c9909e09d9f 100644 --- a/dev-packages/opentelemetry-v2-tests/test/integration/transactions.test.ts +++ b/dev-packages/opentelemetry-v2-tests/test/integration/transactions.test.ts @@ -4,8 +4,8 @@ import { TraceState } from '@opentelemetry/core'; import type { Event, TransactionEvent } from '@sentry/core'; import { addBreadcrumb, + debug, getClient, - logger, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, setTag, @@ -438,7 +438,7 @@ describe('Integration | Transactions', () => { vi.setSystemTime(now); const logs: unknown[] = []; - vi.spyOn(logger, 'log').mockImplementation(msg => logs.push(msg)); + vi.spyOn(debug, 'log').mockImplementation(msg => logs.push(msg)); mockSdkInit({ tracesSampleRate: 1, beforeSendTransaction }); @@ -502,7 +502,7 @@ describe('Integration | Transactions', () => { vi.setSystemTime(now); const logs: unknown[] = []; - vi.spyOn(logger, 'log').mockImplementation(msg => logs.push(msg)); + vi.spyOn(debug, 'log').mockImplementation(msg => logs.push(msg)); const transactions: Event[] = []; @@ -554,7 +554,7 @@ describe('Integration | Transactions', () => { vi.setSystemTime(now); const logs: unknown[] = []; - vi.spyOn(logger, 'log').mockImplementation(msg => logs.push(msg)); + vi.spyOn(debug, 'log').mockImplementation(msg => logs.push(msg)); const transactions: Event[] = []; @@ -605,7 +605,7 @@ describe('Integration | Transactions', () => { vi.setSystemTime(now); const logs: unknown[] = []; - vi.spyOn(logger, 'log').mockImplementation(msg => logs.push(msg)); + vi.spyOn(debug, 'log').mockImplementation(msg => logs.push(msg)); const transactions: Event[] = []; diff --git a/package.json b/package.json index 591424f91ec5..f2cbefa5faa4 100644 --- a/package.json +++ b/package.json @@ -22,12 +22,12 @@ "clean:all": "run-s clean:build clean:tarballs clean:caches clean:deps clean:watchman", "fix": "run-s fix:prettier fix:lerna", "fix:lerna": "lerna run fix", - "fix:prettier": "prettier \"**/*.{md,css,yml,yaml}\" \"packages/**/**.{ts,js,mjs,cjs,mts,cts,jsx,tsx,astro,vue}\" \"dev-packages/!(e2e-tests)/**/**.{ts,js,mjs,cjs,mts,cts,jsx,tsx,astro,vue}\" --write", + "fix:prettier": "prettier \"**/*.{md,css,yml,yaml,mdc}\" \"packages/**/**.{ts,js,mjs,cjs,mts,cts,jsx,tsx,astro,vue}\" \"dev-packages/!(e2e-tests)/**/**.{ts,js,mjs,cjs,mts,cts,jsx,tsx,astro,vue}\" --write", "changelog": "ts-node ./scripts/get-commit-list.ts", "link:yarn": "lerna exec yarn link", "lint": "run-s lint:prettier lint:lerna", "lint:lerna": "lerna run lint", - "lint:prettier": "prettier \"**/*.{md,css,yml,yaml}\" \"packages/**/**.{ts,js,mjs,cjs,mts,cts,jsx,tsx,astro,vue}\" --check", + "lint:prettier": "prettier \"**/*.{md,css,yml,yaml,mdc}\" \"packages/**/**.{ts,js,mjs,cjs,mts,cts,jsx,tsx,astro,vue}\" --check", "lint:es-compatibility": "lerna run lint:es-compatibility", "dedupe-deps:check": "yarn-deduplicate yarn.lock --list --fail", "dedupe-deps:fix": "yarn-deduplicate yarn.lock", @@ -165,6 +165,13 @@ "options": { "proseWrap": "preserve" } + }, + { + "files": "*.mdc", + "options": { + "parser": "markdown", + "proseWrap": "preserve" + } } ] } diff --git a/packages/angular/patch-vitest.ts b/packages/angular/patch-vitest.ts index 476d40860786..26f695db9512 100644 --- a/packages/angular/patch-vitest.ts +++ b/packages/angular/patch-vitest.ts @@ -85,7 +85,7 @@ function wrapTestInZone(testBody: string | any[] | undefined) { enumerable: false, }); wrappedFunc.length = testBody.length; - } catch (e) { + } catch { return testBody.length === 0 ? () => testProxyZone.run(testBody, null) : (done: any) => testProxyZone.run(testBody, null, [done]); diff --git a/packages/angular/src/sdk.ts b/packages/angular/src/sdk.ts index 5a4ffbbde7ee..99dbb0354e82 100755 --- a/packages/angular/src/sdk.ts +++ b/packages/angular/src/sdk.ts @@ -12,10 +12,10 @@ import { import type { Client, Integration } from '@sentry/core'; import { applySdkMetadata, + debug, dedupeIntegration, functionToStringIntegration, inboundFiltersIntegration, - logger, } from '@sentry/core'; import { IS_DEBUG_BUILD } from './flags'; @@ -68,7 +68,7 @@ function checkAndSetAngularVersion(): void { if (angularVersion) { if (angularVersion < ANGULAR_MINIMUM_VERSION) { IS_DEBUG_BUILD && - logger.warn( + debug.warn( `This Sentry SDK does not officially support Angular ${angularVersion}.`, `This SDK only supports Angular ${ANGULAR_MINIMUM_VERSION} and above.`, "If you're using lower Angular versions, check the Angular Version Compatibility table in our docs: https://docs.sentry.io/platforms/javascript/guides/angular/#angular-version-compatibility.", diff --git a/packages/angular/src/tracing.ts b/packages/angular/src/tracing.ts index 952eabbc8d6c..86d7a4771c9c 100644 --- a/packages/angular/src/tracing.ts +++ b/packages/angular/src/tracing.ts @@ -21,7 +21,7 @@ import { startInactiveSpan, } from '@sentry/browser'; import type { Integration, Span } from '@sentry/core'; -import { logger, stripUrlQueryAndFragment, timestampInSeconds } from '@sentry/core'; +import { debug, stripUrlQueryAndFragment, timestampInSeconds } from '@sentry/core'; import type { Observable } from 'rxjs'; import { Subscription } from 'rxjs'; import { filter, tap } from 'rxjs/operators'; @@ -75,7 +75,7 @@ export class TraceService implements OnDestroy { tap(navigationEvent => { if (!instrumentationInitialized) { IS_DEBUG_BUILD && - logger.error('Angular integration has tracing enabled, but Tracing integration is not configured'); + debug.error('Angular integration has tracing enabled, but Tracing integration is not configured'); return; } diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index 1139f58d092c..0b92c8a4a6f8 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -84,6 +84,7 @@ export { nodeContextIntegration, onUncaughtExceptionIntegration, onUnhandledRejectionIntegration, + openAIIntegration, parameterize, postgresIntegration, postgresJsIntegration, diff --git a/packages/astro/src/server/middleware.ts b/packages/astro/src/server/middleware.ts index 5aabfa8d7351..fb2f2e572fa4 100644 --- a/packages/astro/src/server/middleware.ts +++ b/packages/astro/src/server/middleware.ts @@ -1,8 +1,8 @@ import type { RequestEventData, Scope, SpanAttributes } from '@sentry/core'; import { addNonEnumerableProperty, + debug, extractQueryParamsFromUrl, - logger, objectify, stripUrlQueryAndFragment, vercelWaitUntil, @@ -228,7 +228,7 @@ async function instrumentRequest( try { await flush(2000); } catch (e) { - logger.log('Error while flushing events:\n', e); + debug.log('Error while flushing events:\n', e); } })(), ); diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts index 7d2abeaf6a12..7cf8e17f0dd7 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -51,6 +51,7 @@ export { nativeNodeFetchIntegration, onUncaughtExceptionIntegration, onUnhandledRejectionIntegration, + openAIIntegration, modulesIntegration, contextLinesIntegration, nodeContextIntegration, diff --git a/packages/aws-serverless/src/sdk.ts b/packages/aws-serverless/src/sdk.ts index 7d278414c5be..dafaf780ed99 100644 --- a/packages/aws-serverless/src/sdk.ts +++ b/packages/aws-serverless/src/sdk.ts @@ -1,7 +1,7 @@ import type { Integration, Options, Scope, Span } from '@sentry/core'; import { applySdkMetadata, - logger, + debug, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, } from '@sentry/core'; @@ -127,7 +127,7 @@ export function tryPatchHandler(taskRoot: string, handlerPath: string): void { const handlerDesc = basename(handlerPath); const match = handlerDesc.match(/^([^.]*)\.(.*)$/); if (!match) { - DEBUG_BUILD && logger.error(`Bad handler ${handlerDesc}`); + DEBUG_BUILD && debug.error(`Bad handler ${handlerDesc}`); return; } @@ -138,7 +138,7 @@ export function tryPatchHandler(taskRoot: string, handlerPath: string): void { const handlerDir = handlerPath.substring(0, handlerPath.indexOf(handlerDesc)); obj = tryRequire(taskRoot, handlerDir, handlerMod); } catch (e) { - DEBUG_BUILD && logger.error(`Cannot require ${handlerPath} in ${taskRoot}`, e); + DEBUG_BUILD && debug.error(`Cannot require ${handlerPath} in ${taskRoot}`, e); return; } @@ -150,17 +150,17 @@ export function tryPatchHandler(taskRoot: string, handlerPath: string): void { functionName = name; }); if (!obj) { - DEBUG_BUILD && logger.error(`${handlerPath} is undefined or not exported`); + DEBUG_BUILD && debug.error(`${handlerPath} is undefined or not exported`); return; } if (typeof obj !== 'function') { - DEBUG_BUILD && logger.error(`${handlerPath} is not a function`); + DEBUG_BUILD && debug.error(`${handlerPath} is not a function`); return; } // Check for prototype pollution if (functionName === '__proto__' || functionName === 'constructor' || functionName === 'prototype') { - DEBUG_BUILD && logger.error(`Invalid handler name: ${functionName}`); + DEBUG_BUILD && debug.error(`Invalid handler name: ${functionName}`); return; } @@ -317,7 +317,7 @@ export function wrapHandler( span.end(); } await flush(options.flushTimeout).catch(e => { - DEBUG_BUILD && logger.error(e); + DEBUG_BUILD && debug.error(e); }); } return rv; diff --git a/packages/aws-serverless/test/sdk.test.ts b/packages/aws-serverless/test/sdk.test.ts index f05deee57d88..9c1e7f584b8d 100644 --- a/packages/aws-serverless/test/sdk.test.ts +++ b/packages/aws-serverless/test/sdk.test.ts @@ -265,7 +265,7 @@ describe('AWSLambda', () => { try { await wrappedHandler(fakeEvent, fakeContext, fakeCallback); - } catch (e) { + } catch { const fakeTransactionContext = { name: 'functionName', op: 'function.aws.lambda', @@ -376,7 +376,7 @@ describe('AWSLambda', () => { try { await wrappedHandler(fakeEvent, fakeContext, fakeCallback); - } catch (e) { + } catch { const fakeTransactionContext = { name: 'functionName', op: 'function.aws.lambda', @@ -458,7 +458,7 @@ describe('AWSLambda', () => { try { await wrappedHandler(fakeEvent, fakeContext, fakeCallback); - } catch (e) { + } catch { const fakeTransactionContext = { name: 'functionName', op: 'function.aws.lambda', @@ -489,7 +489,7 @@ describe('AWSLambda', () => { try { await wrappedHandler(fakeEvent, fakeContext, fakeCallback); - } catch (e) { + } catch { expect(mockCaptureException).toBeCalledWith(error, expect.any(Function)); const scopeFunction = mockCaptureException.mock.calls[0][1]; diff --git a/packages/browser-utils/src/getNativeImplementation.ts b/packages/browser-utils/src/getNativeImplementation.ts index 398a40045119..410d2abf4de0 100644 --- a/packages/browser-utils/src/getNativeImplementation.ts +++ b/packages/browser-utils/src/getNativeImplementation.ts @@ -1,4 +1,4 @@ -import { isNativeFunction, logger } from '@sentry/core'; +import { debug, isNativeFunction } from '@sentry/core'; import { DEBUG_BUILD } from './debug-build'; import { WINDOW } from './types'; @@ -53,7 +53,7 @@ export function getNativeImplementation( if (getOriginalFunction(fn)) { return fn; } - } catch (e) { + } catch { // Just accessing custom props in some Selenium environments // can cause a "Permission denied" exception (see raven-js#495). // Bail on wrapping and return the function as-is (defers to window.onerror). diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index 963d8ab38546..0bc523506454 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -73,3 +73,4 @@ export { openFeatureIntegration, OpenFeatureIntegrationHook } from './integratio export { unleashIntegration } from './integrations/featureFlags/unleash'; export { statsigIntegration } from './integrations/featureFlags/statsig'; export { diagnoseSdkConnectivity } from './diagnose-sdk'; +export { webWorkerIntegration, registerWebWorker } from './integrations/webWorker'; diff --git a/packages/browser/src/integrations/breadcrumbs.ts b/packages/browser/src/integrations/breadcrumbs.ts index 636f091e5e06..dfbfb581aeca 100644 --- a/packages/browser/src/integrations/breadcrumbs.ts +++ b/packages/browser/src/integrations/breadcrumbs.ts @@ -19,13 +19,13 @@ import { addBreadcrumb, addConsoleInstrumentationHandler, addFetchInstrumentationHandler, + debug, defineIntegration, getBreadcrumbLogLevelFromHttpStatusCode, getClient, getComponentName, getEventDescription, htmlTreeAsString, - logger, parseUrl, safeJoin, severityLevelFromString, @@ -142,7 +142,7 @@ function _getDomBreadcrumbHandler( typeof dom === 'object' && typeof dom.maxStringLength === 'number' ? dom.maxStringLength : undefined; if (maxStringLength && maxStringLength > MAX_ALLOWED_STRING_LENGTH) { DEBUG_BUILD && - logger.warn( + debug.warn( `\`dom.maxStringLength\` cannot exceed ${MAX_ALLOWED_STRING_LENGTH}, but a value of ${maxStringLength} was configured. Sentry will use ${MAX_ALLOWED_STRING_LENGTH} instead.`, ); maxStringLength = MAX_ALLOWED_STRING_LENGTH; @@ -159,7 +159,7 @@ function _getDomBreadcrumbHandler( target = htmlTreeAsString(element, { keyAttrs, maxStringLength }); componentName = getComponentName(element); - } catch (e) { + } catch { target = ''; } diff --git a/packages/browser/src/integrations/browserapierrors.ts b/packages/browser/src/integrations/browserapierrors.ts index 113c969b9ebc..6db6d40c67c2 100644 --- a/packages/browser/src/integrations/browserapierrors.ts +++ b/packages/browser/src/integrations/browserapierrors.ts @@ -256,7 +256,7 @@ function _wrapEventTarget(target: string, integrationOptions: BrowserApiErrorsOp if (originalEventHandler) { originalRemoveEventListener.call(this, eventName, originalEventHandler, options); } - } catch (e) { + } catch { // ignore, accessing __sentry_wrapped__ will throw in some Selenium environments } return originalRemoveEventListener.call(this, eventName, fn, options); diff --git a/packages/browser/src/integrations/browsersession.ts b/packages/browser/src/integrations/browsersession.ts index ac74951b6596..78a9228e3b29 100644 --- a/packages/browser/src/integrations/browsersession.ts +++ b/packages/browser/src/integrations/browsersession.ts @@ -1,4 +1,4 @@ -import { captureSession, defineIntegration, logger, startSession } from '@sentry/core'; +import { captureSession, debug, defineIntegration, startSession } from '@sentry/core'; import { addHistoryInstrumentationHandler } from '@sentry-internal/browser-utils'; import { DEBUG_BUILD } from '../debug-build'; import { WINDOW } from '../helpers'; @@ -15,7 +15,7 @@ export const browserSessionIntegration = defineIntegration(() => { setupOnce() { if (typeof WINDOW.document === 'undefined') { DEBUG_BUILD && - logger.warn('Using the `browserSessionIntegration` in non-browser environments is not supported.'); + debug.warn('Using the `browserSessionIntegration` in non-browser environments is not supported.'); return; } diff --git a/packages/browser/src/integrations/featureFlags/unleash/integration.ts b/packages/browser/src/integrations/featureFlags/unleash/integration.ts index 699c797edecf..c822b49f8810 100644 --- a/packages/browser/src/integrations/featureFlags/unleash/integration.ts +++ b/packages/browser/src/integrations/featureFlags/unleash/integration.ts @@ -3,9 +3,9 @@ import { _INTERNAL_addFeatureFlagToActiveSpan, _INTERNAL_copyFlagsFromScopeToEvent, _INTERNAL_insertFlagToScope, + debug, defineIntegration, fill, - logger, } from '@sentry/core'; import { DEBUG_BUILD } from '../../../debug-build'; import type { UnleashClient, UnleashClientClass } from './types'; @@ -73,7 +73,7 @@ function _wrappedIsEnabled( _INTERNAL_insertFlagToScope(toggleName, result); _INTERNAL_addFeatureFlagToActiveSpan(toggleName, result); } else if (DEBUG_BUILD) { - logger.error( + debug.error( `[Feature Flags] UnleashClient.isEnabled does not match expected signature. arg0: ${toggleName} (${typeof toggleName}), result: ${result} (${typeof result})`, ); } diff --git a/packages/browser/src/integrations/globalhandlers.ts b/packages/browser/src/integrations/globalhandlers.ts index fa4420a0416e..74e78c37679e 100644 --- a/packages/browser/src/integrations/globalhandlers.ts +++ b/packages/browser/src/integrations/globalhandlers.ts @@ -3,12 +3,12 @@ import { addGlobalErrorInstrumentationHandler, addGlobalUnhandledRejectionInstrumentationHandler, captureEvent, + debug, defineIntegration, getClient, getLocationHref, isPrimitive, isString, - logger, UNKNOWN_FUNCTION, } from '@sentry/core'; import type { BrowserClient } from '../client'; @@ -188,7 +188,7 @@ function _enhanceEventWithInitialFrame( } function globalHandlerLog(type: string): void { - DEBUG_BUILD && logger.log(`Global Handler attached: ${type}`); + DEBUG_BUILD && debug.log(`Global Handler attached: ${type}`); } function getOptions(): { stackParser: StackParser; attachStacktrace?: boolean } { diff --git a/packages/browser/src/integrations/httpclient.ts b/packages/browser/src/integrations/httpclient.ts index 11763b34d581..9aaa476b7618 100644 --- a/packages/browser/src/integrations/httpclient.ts +++ b/packages/browser/src/integrations/httpclient.ts @@ -3,11 +3,11 @@ import { addExceptionMechanism, addFetchInstrumentationHandler, captureEvent, + debug, defineIntegration, getClient, GLOBAL_OBJ, isSentryRequestUrl, - logger, supportsNativeFetch, } from '@sentry/core'; import { addXhrInstrumentationHandler, SENTRY_XHR_DATA_KEY } from '@sentry-internal/browser-utils'; @@ -333,7 +333,7 @@ function _wrapXHR(client: Client, options: HttpClientOptions): void { try { _xhrResponseHandler(options, xhr, method, headers, error || virtualError); } catch (e) { - DEBUG_BUILD && logger.warn('Error while extracting response event form XHR response', e); + DEBUG_BUILD && debug.warn('Error while extracting response event form XHR response', e); } }); } diff --git a/packages/browser/src/integrations/spotlight.ts b/packages/browser/src/integrations/spotlight.ts index 233b85ace310..481648f31138 100644 --- a/packages/browser/src/integrations/spotlight.ts +++ b/packages/browser/src/integrations/spotlight.ts @@ -1,5 +1,5 @@ import type { Client, Envelope, Event, IntegrationFn } from '@sentry/core'; -import { defineIntegration, logger, serializeEnvelope } from '@sentry/core'; +import { debug, defineIntegration, serializeEnvelope } from '@sentry/core'; import { getNativeImplementation } from '@sentry-internal/browser-utils'; import { DEBUG_BUILD } from '../debug-build'; import type { WINDOW } from '../helpers'; @@ -20,7 +20,7 @@ const _spotlightIntegration = ((options: Partial = { return { name: INTEGRATION_NAME, setup: () => { - DEBUG_BUILD && logger.log('Using Sidecar URL', sidecarUrl); + DEBUG_BUILD && debug.log('Using Sidecar URL', sidecarUrl); }, // We don't want to send interaction transactions/root spans created from // clicks within Spotlight to Sentry. Neither do we want them to be sent to @@ -38,7 +38,7 @@ function setupSidecarForwarding(client: Client, sidecarUrl: string): void { client.on('beforeEnvelope', (envelope: Envelope) => { if (failCount > 3) { - logger.warn('[Spotlight] Disabled Sentry -> Spotlight integration due to too many failed requests:', failCount); + debug.warn('[Spotlight] Disabled Sentry -> Spotlight integration due to too many failed requests:', failCount); return; } @@ -58,7 +58,7 @@ function setupSidecarForwarding(client: Client, sidecarUrl: string): void { }, err => { failCount++; - logger.error( + debug.error( "Sentry SDK can't connect to Sidecar is it running? See: https://spotlightjs.com/sidecar/npx/", err, ); @@ -81,8 +81,7 @@ export function isSpotlightInteraction(event: Event): boolean { return Boolean( event.type === 'transaction' && event.spans && - event.contexts && - event.contexts.trace && + event.contexts?.trace && event.contexts.trace.op === 'ui.action.click' && event.spans.some(({ description }) => description?.includes('#sentry-spotlight')), ); diff --git a/packages/browser/src/integrations/webWorker.ts b/packages/browser/src/integrations/webWorker.ts new file mode 100644 index 000000000000..f422f372a463 --- /dev/null +++ b/packages/browser/src/integrations/webWorker.ts @@ -0,0 +1,150 @@ +import type { Integration, IntegrationFn } from '@sentry/core'; +import { debug, defineIntegration, isPlainObject } from '@sentry/core'; +import { DEBUG_BUILD } from '../debug-build'; +import { WINDOW } from '../helpers'; + +export const INTEGRATION_NAME = 'WebWorker'; + +interface WebWorkerMessage { + _sentryMessage: boolean; + _sentryDebugIds?: Record; +} + +interface WebWorkerIntegrationOptions { + worker: Worker | Array; +} + +interface WebWorkerIntegration extends Integration { + addWorker: (worker: Worker) => void; +} + +/** + * Use this integration to set up Sentry with web workers. + * + * IMPORTANT: This integration must be added **before** you start listening to + * any messages from the worker. Otherwise, your message handlers will receive + * messages from the Sentry SDK which you need to ignore. + * + * This integration only has an effect, if you call `Sentry.registerWorker(self)` + * from within the worker(s) you're adding to the integration. + * + * Given that you want to initialize the SDK as early as possible, you most likely + * want to add this integration **after** initializing the SDK: + * + * @example: + * ```ts filename={main.js} + * import * as Sentry from '@sentry/'; + * + * // some time earlier: + * Sentry.init(...) + * + * // 1. Initialize the worker + * const worker = new Worker(new URL('./worker.ts', import.meta.url)); + * + * // 2. Add the integration + * const webWorkerIntegration = Sentry.webWorkerIntegration({ worker }); + * Sentry.addIntegration(webWorkerIntegration); + * + * // 3. Register message listeners on the worker + * worker.addEventListener('message', event => { + * // ... + * }); + * ``` + * + * If you initialize multiple workers at the same time, you can also pass an array of workers + * to the integration: + * + * ```ts filename={main.js} + * const webWorkerIntegration = Sentry.webWorkerIntegration({ worker: [worker1, worker2] }); + * Sentry.addIntegration(webWorkerIntegration); + * ``` + * + * If you have any additional workers that you initialize at a later point, + * you can add them to the integration as follows: + * + * ```ts filename={main.js} + * const webWorkerIntegration = Sentry.webWorkerIntegration({ worker: worker1 }); + * Sentry.addIntegration(webWorkerIntegration); + * + * // sometime later: + * webWorkerIntegration.addWorker(worker2); + * ``` + * + * Of course, you can also directly add the integration in Sentry.init: + * ```ts filename={main.js} + * import * as Sentry from '@sentry/'; + * + * // 1. Initialize the worker + * const worker = new Worker(new URL('./worker.ts', import.meta.url)); + * + * // 2. Initialize the SDK + * Sentry.init({ + * integrations: [Sentry.webWorkerIntegration({ worker })] + * }); + * + * // 3. Register message listeners on the worker + * worker.addEventListener('message', event => { + * // ... + * }); + * ``` + * + * @param options {WebWorkerIntegrationOptions} Integration options: + * - `worker`: The worker instance. + */ +export const webWorkerIntegration = defineIntegration(({ worker }: WebWorkerIntegrationOptions) => ({ + name: INTEGRATION_NAME, + setupOnce: () => { + (Array.isArray(worker) ? worker : [worker]).forEach(w => listenForSentryDebugIdMessages(w)); + }, + addWorker: (worker: Worker) => listenForSentryDebugIdMessages(worker), +})) as IntegrationFn; + +function listenForSentryDebugIdMessages(worker: Worker): void { + worker.addEventListener('message', event => { + if (isSentryDebugIdMessage(event.data)) { + event.stopImmediatePropagation(); // other listeners should not receive this message + DEBUG_BUILD && debug.log('Sentry debugId web worker message received', event.data); + WINDOW._sentryDebugIds = { + ...event.data._sentryDebugIds, + // debugIds of the main thread have precedence over the worker's in case of a collision. + ...WINDOW._sentryDebugIds, + }; + } + }); +} + +interface RegisterWebWorkerOptions { + self: Worker & { _sentryDebugIds?: Record }; +} + +/** + * Use this function to register the worker with the Sentry SDK. + * + * @example + * ```ts filename={worker.js} + * import * as Sentry from '@sentry/'; + * + * // Do this as early as possible in your worker. + * Sentry.registerWorker({ self }); + * + * // continue setting up your worker + * self.postMessage(...) + * ``` + * @param options {RegisterWebWorkerOptions} Integration options: + * - `self`: The worker instance you're calling this function from (self). + */ +export function registerWebWorker({ self }: RegisterWebWorkerOptions): void { + self.postMessage({ + _sentryMessage: true, + _sentryDebugIds: self._sentryDebugIds ?? undefined, + }); +} + +function isSentryDebugIdMessage(eventData: unknown): eventData is WebWorkerMessage { + return ( + isPlainObject(eventData) && + eventData._sentryMessage === true && + '_sentryDebugIds' in eventData && + (isPlainObject(eventData._sentryDebugIds) || eventData._sentryDebugIds === undefined) + ); +} diff --git a/packages/browser/src/profiling/integration.ts b/packages/browser/src/profiling/integration.ts index 0f676bc1156c..7ad77d8920e5 100644 --- a/packages/browser/src/profiling/integration.ts +++ b/packages/browser/src/profiling/integration.ts @@ -1,5 +1,5 @@ import type { EventEnvelope, IntegrationFn, Profile, Span } from '@sentry/core'; -import { defineIntegration, getActiveSpan, getRootSpan, logger } from '@sentry/core'; +import { debug, defineIntegration, getActiveSpan, getRootSpan } from '@sentry/core'; import { DEBUG_BUILD } from '../debug-build'; import { startProfileForSpan } from './startProfileForSpan'; import type { ProfiledEvent } from './utils'; @@ -53,12 +53,12 @@ const _browserProfilingIntegration = (() => { const start_timestamp = context?.profile?.['start_timestamp']; if (typeof profile_id !== 'string') { - DEBUG_BUILD && logger.log('[Profiling] cannot find profile for a span without a profile context'); + DEBUG_BUILD && debug.log('[Profiling] cannot find profile for a span without a profile context'); continue; } if (!profile_id) { - DEBUG_BUILD && logger.log('[Profiling] cannot find profile for a span without a profile context'); + DEBUG_BUILD && debug.log('[Profiling] cannot find profile for a span without a profile context'); continue; } @@ -69,7 +69,7 @@ const _browserProfilingIntegration = (() => { const profile = takeProfileFromGlobalCache(profile_id); if (!profile) { - DEBUG_BUILD && logger.log(`[Profiling] Could not retrieve profile for span: ${profile_id}`); + DEBUG_BUILD && debug.log(`[Profiling] Could not retrieve profile for span: ${profile_id}`); continue; } diff --git a/packages/browser/src/profiling/startProfileForSpan.ts b/packages/browser/src/profiling/startProfileForSpan.ts index 548508331646..b60a207abbce 100644 --- a/packages/browser/src/profiling/startProfileForSpan.ts +++ b/packages/browser/src/profiling/startProfileForSpan.ts @@ -1,5 +1,5 @@ import type { Span } from '@sentry/core'; -import { getCurrentScope, logger, spanToJSON, timestampInSeconds, uuid4 } from '@sentry/core'; +import { debug, getCurrentScope, spanToJSON, timestampInSeconds, uuid4 } from '@sentry/core'; import { DEBUG_BUILD } from '../debug-build'; import { WINDOW } from '../helpers'; import type { JSSelfProfile } from './jsSelfProfiling'; @@ -26,7 +26,7 @@ export function startProfileForSpan(span: Span): void { } if (DEBUG_BUILD) { - logger.log(`[Profiling] started profiling span: ${spanToJSON(span).description}`); + debug.log(`[Profiling] started profiling span: ${spanToJSON(span).description}`); } // We create "unique" span names to avoid concurrent spans with same names @@ -62,7 +62,7 @@ export function startProfileForSpan(span: Span): void { } if (processedProfile) { if (DEBUG_BUILD) { - logger.log('[Profiling] profile for:', spanToJSON(span).description, 'already exists, returning early'); + debug.log('[Profiling] profile for:', spanToJSON(span).description, 'already exists, returning early'); } return; } @@ -76,13 +76,13 @@ export function startProfileForSpan(span: Span): void { } if (DEBUG_BUILD) { - logger.log(`[Profiling] stopped profiling of span: ${spanToJSON(span).description}`); + debug.log(`[Profiling] stopped profiling of span: ${spanToJSON(span).description}`); } // In case of an overlapping span, stopProfiling may return null and silently ignore the overlapping profile. if (!profile) { if (DEBUG_BUILD) { - logger.log( + debug.log( `[Profiling] profiler returned null profile for: ${spanToJSON(span).description}`, 'this may indicate an overlapping span or a call to stopProfiling with a profile title that was never started', ); @@ -94,7 +94,7 @@ export function startProfileForSpan(span: Span): void { }) .catch(error => { if (DEBUG_BUILD) { - logger.log('[Profiling] error while stopping profiler:', error); + debug.log('[Profiling] error while stopping profiler:', error); } }); } @@ -102,7 +102,7 @@ export function startProfileForSpan(span: Span): void { // Enqueue a timeout to prevent profiles from running over max duration. let maxDurationTimeoutID: number | undefined = WINDOW.setTimeout(() => { if (DEBUG_BUILD) { - logger.log('[Profiling] max profile duration elapsed, stopping profiling for:', spanToJSON(span).description); + debug.log('[Profiling] max profile duration elapsed, stopping profiling for:', spanToJSON(span).description); } // If the timeout exceeds, we want to stop profiling, but not finish the span // eslint-disable-next-line @typescript-eslint/no-floating-promises diff --git a/packages/browser/src/profiling/utils.ts b/packages/browser/src/profiling/utils.ts index 5b81364df727..66b202c8517f 100644 --- a/packages/browser/src/profiling/utils.ts +++ b/packages/browser/src/profiling/utils.ts @@ -2,11 +2,11 @@ import type { DebugImage, Envelope, Event, EventEnvelope, Profile, Span, ThreadCpuProfile } from '@sentry/core'; import { browserPerformanceTimeOrigin, + debug, DEFAULT_ENVIRONMENT, forEachEnvelopeItem, getClient, getDebugImagesForResources, - logger, spanToJSON, timestampInSeconds, uuid4, @@ -98,13 +98,13 @@ export interface ProfiledEvent extends Event { } function getTraceId(event: Event): string { - const traceId: unknown = event.contexts?.trace?.['trace_id']; + const traceId: unknown = event.contexts?.trace?.trace_id; // Log a warning if the profile has an invalid traceId (should be uuidv4). // All profiles and transactions are rejected if this is the case and we want to // warn users that this is happening if they enable debug flag if (typeof traceId === 'string' && traceId.length !== 32) { if (DEBUG_BUILD) { - logger.log(`[Profiling] Invalid traceId: ${traceId} on profiled event`); + debug.log(`[Profiling] Invalid traceId: ${traceId} on profiled event`); } } if (typeof traceId !== 'string') { @@ -333,7 +333,7 @@ export function findProfiledTransactionsFromEnvelope(envelope: Envelope): Event[ for (let j = 1; j < item.length; j++) { const event = item[j] as Event; - if (event?.contexts && event.contexts['profile'] && event.contexts['profile']['profile_id']) { + if (event?.contexts?.profile?.profile_id) { events.push(item[j] as Event); } } @@ -364,7 +364,7 @@ export function isValidSampleRate(rate: unknown): boolean { // we need to check NaN explicitly because it's of type 'number' and therefore wouldn't get caught by this typecheck if ((typeof rate !== 'number' && typeof rate !== 'boolean') || (typeof rate === 'number' && isNaN(rate))) { DEBUG_BUILD && - logger.warn( + debug.warn( `[Profiling] Invalid sample rate. Sample rate must be a boolean or a number between 0 and 1. Got ${JSON.stringify( rate, )} of type ${JSON.stringify(typeof rate)}.`, @@ -379,7 +379,7 @@ export function isValidSampleRate(rate: unknown): boolean { // in case sampleRate is a boolean, it will get automatically cast to 1 if it's true and 0 if it's false if (rate < 0 || rate > 1) { - DEBUG_BUILD && logger.warn(`[Profiling] Invalid sample rate. Sample rate must be between 0 and 1. Got ${rate}.`); + DEBUG_BUILD && debug.warn(`[Profiling] Invalid sample rate. Sample rate must be between 0 and 1. Got ${rate}.`); return false; } return true; @@ -391,14 +391,14 @@ function isValidProfile(profile: JSSelfProfile): profile is JSSelfProfile & { pr // Log a warning if the profile has less than 2 samples so users can know why // they are not seeing any profiling data and we cant avoid the back and forth // of asking them to provide us with a dump of the profile data. - logger.log('[Profiling] Discarding profile because it contains less than 2 samples'); + debug.log('[Profiling] Discarding profile because it contains less than 2 samples'); } return false; } if (!profile.frames.length) { if (DEBUG_BUILD) { - logger.log('[Profiling] Discarding profile because it contains no frames'); + debug.log('[Profiling] Discarding profile because it contains no frames'); } return false; } @@ -428,9 +428,7 @@ export function startJSSelfProfile(): JSSelfProfiler | undefined { if (!isJSProfilerSupported(JSProfilerConstructor)) { if (DEBUG_BUILD) { - logger.log( - '[Profiling] Profiling is not supported by this browser, Profiler interface missing on window object.', - ); + debug.log('[Profiling] Profiling is not supported by this browser, Profiler interface missing on window object.'); } return; } @@ -447,10 +445,10 @@ export function startJSSelfProfile(): JSSelfProfiler | undefined { return new JSProfilerConstructor({ sampleInterval: samplingIntervalMS, maxBufferSize: maxSamples }); } catch (e) { if (DEBUG_BUILD) { - logger.log( + debug.log( "[Profiling] Failed to initialize the Profiling constructor, this is likely due to a missing 'Document-Policy': 'js-profiling' header.", ); - logger.log('[Profiling] Disabling profiling for current user session.'); + debug.log('[Profiling] Disabling profiling for current user session.'); } PROFILING_CONSTRUCTOR_FAILED = true; } @@ -465,14 +463,14 @@ export function shouldProfileSpan(span: Span): boolean { // If constructor failed once, it will always fail, so we can early return. if (PROFILING_CONSTRUCTOR_FAILED) { if (DEBUG_BUILD) { - logger.log('[Profiling] Profiling has been disabled for the duration of the current user session.'); + debug.log('[Profiling] Profiling has been disabled for the duration of the current user session.'); } return false; } if (!span.isRecording()) { if (DEBUG_BUILD) { - logger.log('[Profiling] Discarding profile because transaction was not sampled.'); + debug.log('[Profiling] Discarding profile because transaction was not sampled.'); } return false; } @@ -480,7 +478,7 @@ export function shouldProfileSpan(span: Span): boolean { const client = getClient(); const options = client?.getOptions(); if (!options) { - DEBUG_BUILD && logger.log('[Profiling] Profiling disabled, no options found.'); + DEBUG_BUILD && debug.log('[Profiling] Profiling disabled, no options found.'); return false; } @@ -490,14 +488,14 @@ export function shouldProfileSpan(span: Span): boolean { // Since this is coming from the user (or from a function provided by the user), who knows what we might get. (The // only valid values are booleans or numbers between 0 and 1.) if (!isValidSampleRate(profilesSampleRate)) { - DEBUG_BUILD && logger.warn('[Profiling] Discarding profile because of invalid sample rate.'); + DEBUG_BUILD && debug.warn('[Profiling] Discarding profile because of invalid sample rate.'); return false; } // if the function returned 0 (or false), or if `profileSampleRate` is 0, it's a sign the profile should be dropped if (!profilesSampleRate) { DEBUG_BUILD && - logger.log( + debug.log( '[Profiling] Discarding profile because a negative sampling decision was inherited or profileSampleRate is set to 0', ); return false; @@ -509,7 +507,7 @@ export function shouldProfileSpan(span: Span): boolean { // Check if we should sample this profile if (!sampled) { DEBUG_BUILD && - logger.log( + debug.log( `[Profiling] Discarding profile because it's not included in the random sample (sampling rate = ${Number( profilesSampleRate, )})`, diff --git a/packages/browser/src/report-dialog.ts b/packages/browser/src/report-dialog.ts index 99b9cbd7733b..03255a7db91d 100644 --- a/packages/browser/src/report-dialog.ts +++ b/packages/browser/src/report-dialog.ts @@ -1,5 +1,5 @@ import type { ReportDialogOptions } from '@sentry/core'; -import { getClient, getCurrentScope, getReportDialogEndpoint, lastEventId, logger } from '@sentry/core'; +import { debug, getClient, getCurrentScope, getReportDialogEndpoint, lastEventId } from '@sentry/core'; import { DEBUG_BUILD } from './debug-build'; import { WINDOW } from './helpers'; @@ -14,7 +14,7 @@ export function showReportDialog(options: ReportDialogOptions = {}): void { // doesn't work without a document (React Native) if (!injectionPoint) { - DEBUG_BUILD && logger.error('[showReportDialog] Global document not defined'); + DEBUG_BUILD && debug.error('[showReportDialog] Global document not defined'); return; } @@ -23,7 +23,7 @@ export function showReportDialog(options: ReportDialogOptions = {}): void { const dsn = client?.getDsn(); if (!dsn) { - DEBUG_BUILD && logger.error('[showReportDialog] DSN not configured'); + DEBUG_BUILD && debug.error('[showReportDialog] DSN not configured'); return; } diff --git a/packages/browser/src/tracing/backgroundtab.ts b/packages/browser/src/tracing/backgroundtab.ts index e6241794d6d1..f8aeca761d85 100644 --- a/packages/browser/src/tracing/backgroundtab.ts +++ b/packages/browser/src/tracing/backgroundtab.ts @@ -1,4 +1,4 @@ -import { getActiveSpan, getRootSpan, logger, SPAN_STATUS_ERROR, spanToJSON } from '@sentry/core'; +import { debug, getActiveSpan, getRootSpan, SPAN_STATUS_ERROR, spanToJSON } from '@sentry/core'; import { DEBUG_BUILD } from '../debug-build'; import { WINDOW } from '../helpers'; @@ -22,7 +22,7 @@ export function registerBackgroundTabDetection(): void { const { op, status } = spanToJSON(rootSpan); if (DEBUG_BUILD) { - logger.log(`[Tracing] Transaction: ${cancelledStatus} -> since tab moved to the background, op: ${op}`); + debug.log(`[Tracing] Transaction: ${cancelledStatus} -> since tab moved to the background, op: ${op}`); } // We should not set status if it is already set, this prevent important statuses like @@ -36,6 +36,6 @@ export function registerBackgroundTabDetection(): void { } }); } else { - DEBUG_BUILD && logger.warn('[Tracing] Could not set up background tab detection due to lack of global document'); + DEBUG_BUILD && debug.warn('[Tracing] Could not set up background tab detection due to lack of global document'); } } diff --git a/packages/browser/src/tracing/browserTracingIntegration.ts b/packages/browser/src/tracing/browserTracingIntegration.ts index f010030c47c3..2a38f7afe7be 100644 --- a/packages/browser/src/tracing/browserTracingIntegration.ts +++ b/packages/browser/src/tracing/browserTracingIntegration.ts @@ -4,6 +4,7 @@ import { addNonEnumerableProperty, browserPerformanceTimeOrigin, dateTimestampInSeconds, + debug, generateTraceId, getClient, getCurrentScope, @@ -11,7 +12,6 @@ import { getIsolationScope, getLocationHref, GLOBAL_OBJ, - logger, parseStringToURLObject, propagationContextFromHeaders, registerSpanErrorInstrumentation, @@ -478,7 +478,7 @@ export const browserTracingIntegration = ((_options: Partial { + return { + ...((await importActual()) as any), + debug: { + log: vi.fn(), + }, + }; +}); + +// Mock debug build +vi.mock('../../src/debug-build', () => ({ + DEBUG_BUILD: true, +})); + +// Mock helpers +vi.mock('../../src/helpers', () => ({ + WINDOW: { + _sentryDebugIds: undefined, + }, +})); + +describe('webWorkerIntegration', () => { + const mockDebugLog = SentryCore.debug.log as any; + + let mockWorker: { + addEventListener: ReturnType; + postMessage: ReturnType; + _sentryDebugIds?: Record; + }; + + let mockWorker2: { + addEventListener: ReturnType; + postMessage: ReturnType; + _sentryDebugIds?: Record; + }; + + let mockEvent: { + data: any; + stopImmediatePropagation: ReturnType; + }; + + beforeEach(() => { + vi.clearAllMocks(); + + // Reset WINDOW mock + (helpers.WINDOW as any)._sentryDebugIds = undefined; + + // Setup mock worker + mockWorker = { + addEventListener: vi.fn(), + postMessage: vi.fn(), + }; + + mockWorker2 = { + addEventListener: vi.fn(), + postMessage: vi.fn(), + }; + + // Setup mock event + mockEvent = { + data: {}, + stopImmediatePropagation: vi.fn(), + }; + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('creates integration with correct name', () => { + const integration = webWorkerIntegration({ worker: mockWorker as any }); + + expect(integration.name).toBe(INTEGRATION_NAME); + expect(integration.name).toBe('WebWorker'); + expect(typeof integration.setupOnce).toBe('function'); + }); + + describe('setupOnce', () => { + it('adds message event listener to the worker', () => { + const integration = webWorkerIntegration({ worker: mockWorker as any }); + + integration.setupOnce!(); + + expect(mockWorker.addEventListener).toHaveBeenCalledWith('message', expect.any(Function)); + }); + + it('adds message event listener to multiple workers passed to the integration', () => { + const integration = webWorkerIntegration({ worker: [mockWorker, mockWorker2] as any }); + integration.setupOnce!(); + expect(mockWorker.addEventListener).toHaveBeenCalledWith('message', expect.any(Function)); + expect(mockWorker2.addEventListener).toHaveBeenCalledWith('message', expect.any(Function)); + }); + + it('adds message event listener to a worker added later', () => { + const integration = webWorkerIntegration({ worker: mockWorker as any }); + integration.setupOnce!(); + integration.addWorker(mockWorker2 as any); + expect(mockWorker2.addEventListener).toHaveBeenCalledWith('message', expect.any(Function)); + }); + + describe('message handler', () => { + let messageHandler: (event: any) => void; + + beforeEach(() => { + const integration = webWorkerIntegration({ worker: mockWorker as any }); + integration.setupOnce!(); + + // Extract the message handler from the addEventListener call + expect(mockWorker.addEventListener.mock.calls).toBeDefined(); + messageHandler = mockWorker.addEventListener.mock.calls![0]![1]; + }); + + it('ignores non-Sentry messages', () => { + mockEvent.data = { someData: 'value' }; + + messageHandler(mockEvent); + + expect(mockEvent.stopImmediatePropagation).not.toHaveBeenCalled(); + expect(mockDebugLog).not.toHaveBeenCalled(); + }); + + it('ignores plain objects without _sentryMessage flag', () => { + mockEvent.data = { + someData: 'value', + _sentry: {}, + }; + + messageHandler(mockEvent); + + expect(mockEvent.stopImmediatePropagation).not.toHaveBeenCalled(); + expect(mockDebugLog).not.toHaveBeenCalled(); + }); + + it('processes valid Sentry messages', () => { + mockEvent.data = { + _sentryMessage: true, + _sentryDebugIds: { 'file1.js': 'debug-id-1' }, + }; + + messageHandler(mockEvent); + + expect(mockEvent.stopImmediatePropagation).toHaveBeenCalled(); + expect(mockDebugLog).toHaveBeenCalledWith('Sentry debugId web worker message received', mockEvent.data); + }); + + it('merges debug IDs with worker precedence for new IDs', () => { + (helpers.WINDOW as any)._sentryDebugIds = undefined; + + mockEvent.data = { + _sentryMessage: true, + _sentryDebugIds: { + 'worker-file1.js': 'worker-debug-1', + 'worker-file2.js': 'worker-debug-2', + }, + }; + + messageHandler(mockEvent); + + expect((helpers.WINDOW as any)._sentryDebugIds).toEqual({ + 'worker-file1.js': 'worker-debug-1', + 'worker-file2.js': 'worker-debug-2', + }); + }); + + it('gives main thread precedence over worker for conflicting debug IDs', () => { + (helpers.WINDOW as any)._sentryDebugIds = { + 'shared-file.js': 'main-debug-id', + 'main-only.js': 'main-debug-2', + }; + + mockEvent.data = { + _sentryMessage: true, + _sentryDebugIds: { + 'shared-file.js': 'worker-debug-id', // Should be overridden + 'worker-only.js': 'worker-debug-3', // Should be kept + }, + }; + + messageHandler(mockEvent); + + expect((helpers.WINDOW as any)._sentryDebugIds).toEqual({ + 'shared-file.js': 'main-debug-id', // Main thread wins + 'main-only.js': 'main-debug-2', // Main thread preserved + 'worker-only.js': 'worker-debug-3', // Worker added + }); + }); + + it('handles empty debug IDs from worker', () => { + (helpers.WINDOW as any)._sentryDebugIds = { 'main.js': 'main-debug' }; + + mockEvent.data = { + _sentryMessage: true, + _sentryDebugIds: {}, + }; + + messageHandler(mockEvent); + + expect((helpers.WINDOW as any)._sentryDebugIds).toEqual({ + 'main.js': 'main-debug', + }); + }); + }); + }); +}); + +describe('registerWebWorker', () => { + let mockWorkerSelf: { + postMessage: ReturnType; + _sentryDebugIds?: Record; + }; + + beforeEach(() => { + vi.clearAllMocks(); + + mockWorkerSelf = { + postMessage: vi.fn(), + }; + }); + + it('posts message with _sentryMessage flag', () => { + registerWebWorker({ self: mockWorkerSelf as any }); + + expect(mockWorkerSelf.postMessage).toHaveBeenCalledTimes(1); + expect(mockWorkerSelf.postMessage).toHaveBeenCalledWith({ + _sentryMessage: true, + _sentryDebugIds: undefined, + }); + }); + + it('includes debug IDs when available', () => { + mockWorkerSelf._sentryDebugIds = { + 'worker-file1.js': 'debug-id-1', + 'worker-file2.js': 'debug-id-2', + }; + + registerWebWorker({ self: mockWorkerSelf as any }); + + expect(mockWorkerSelf.postMessage).toHaveBeenCalledTimes(1); + expect(mockWorkerSelf.postMessage).toHaveBeenCalledWith({ + _sentryMessage: true, + _sentryDebugIds: { + 'worker-file1.js': 'debug-id-1', + 'worker-file2.js': 'debug-id-2', + }, + }); + }); + + it('handles undefined debug IDs', () => { + mockWorkerSelf._sentryDebugIds = undefined; + + registerWebWorker({ self: mockWorkerSelf as any }); + + expect(mockWorkerSelf.postMessage).toHaveBeenCalledTimes(1); + expect(mockWorkerSelf.postMessage).toHaveBeenCalledWith({ + _sentryMessage: true, + _sentryDebugIds: undefined, + }); + }); +}); + +describe('registerWebWorker and webWorkerIntegration', () => { + beforeEach(() => {}); + + it('work together (with multiple workers)', () => { + (helpers.WINDOW as any)._sentryDebugIds = { + 'Error at \n /main-file1.js': 'main-debug-1', + 'Error at \n /main-file2.js': 'main-debug-2', + 'Error at \n /shared-file.js': 'main-debug-id', + }; + + let cb1: ((arg0: any) => any) | undefined = undefined; + let cb2: ((arg0: any) => any) | undefined = undefined; + let cb3: ((arg0: any) => any) | undefined = undefined; + + // Setup mock worker + const mockWorker = { + _sentryDebugIds: { + 'Error at \n /worker-file1.js': 'worker-debug-1', + 'Error at \n /worker-file2.js': 'worker-debug-2', + 'Error at \n /shared-file.js': 'worker-debug-id', + }, + addEventListener: vi.fn((_, l) => (cb1 = l)), + postMessage: vi.fn(message => { + // @ts-expect-error - cb is defined + cb1({ data: message, stopImmediatePropagation: vi.fn() }); + }), + }; + + const mockWorker2 = { + _sentryDebugIds: { + 'Error at \n /worker-2-file1.js': 'worker-2-debug-1', + 'Error at \n /worker-2-file2.js': 'worker-2-debug-2', + }, + + addEventListener: vi.fn((_, l) => (cb2 = l)), + postMessage: vi.fn(message => { + // @ts-expect-error - cb is defined + cb2({ data: message, stopImmediatePropagation: vi.fn() }); + }), + }; + + const mockWorker3 = { + _sentryDebugIds: { + 'Error at \n /worker-3-file1.js': 'worker-3-debug-1', + 'Error at \n /worker-3-file2.js': 'worker-3-debug-2', + }, + addEventListener: vi.fn((_, l) => (cb3 = l)), + postMessage: vi.fn(message => { + // @ts-expect-error - cb is defined + cb3({ data: message, stopImmediatePropagation: vi.fn() }); + }), + }; + + const integration = webWorkerIntegration({ worker: [mockWorker as any, mockWorker2 as any] }); + integration.setupOnce!(); + + registerWebWorker({ self: mockWorker as any }); + registerWebWorker({ self: mockWorker2 as any }); + + expect(mockWorker.addEventListener).toHaveBeenCalledWith('message', expect.any(Function)); + expect(mockWorker2.addEventListener).toHaveBeenCalledWith('message', expect.any(Function)); + + expect(mockWorker.postMessage).toHaveBeenCalledWith({ + _sentryMessage: true, + _sentryDebugIds: mockWorker._sentryDebugIds, + }); + + expect((helpers.WINDOW as any)._sentryDebugIds).toEqual({ + 'Error at \n /main-file1.js': 'main-debug-1', + 'Error at \n /main-file2.js': 'main-debug-2', + 'Error at \n /shared-file.js': 'main-debug-id', + 'Error at \n /worker-file1.js': 'worker-debug-1', + 'Error at \n /worker-file2.js': 'worker-debug-2', + 'Error at \n /worker-2-file1.js': 'worker-2-debug-1', + 'Error at \n /worker-2-file2.js': 'worker-2-debug-2', + }); + + integration.addWorker(mockWorker3 as any); + registerWebWorker({ self: mockWorker3 as any }); + + expect(mockWorker3.addEventListener).toHaveBeenCalledWith('message', expect.any(Function)); + + expect(mockWorker3.postMessage).toHaveBeenCalledWith({ + _sentryMessage: true, + _sentryDebugIds: mockWorker3._sentryDebugIds, + }); + + expect((helpers.WINDOW as any)._sentryDebugIds).toEqual({ + 'Error at \n /main-file1.js': 'main-debug-1', + 'Error at \n /main-file2.js': 'main-debug-2', + 'Error at \n /shared-file.js': 'main-debug-id', + 'Error at \n /worker-file1.js': 'worker-debug-1', + 'Error at \n /worker-file2.js': 'worker-debug-2', + 'Error at \n /worker-2-file1.js': 'worker-2-debug-1', + 'Error at \n /worker-2-file2.js': 'worker-2-debug-2', + 'Error at \n /worker-3-file1.js': 'worker-3-debug-1', + 'Error at \n /worker-3-file2.js': 'worker-3-debug-2', + }); + }); +}); diff --git a/packages/bun/README.md b/packages/bun/README.md index 0f6f37bd6384..1867f5a0fef9 100644 --- a/packages/bun/README.md +++ b/packages/bun/README.md @@ -59,17 +59,3 @@ Sentry.captureEvent({ ], }); ``` - -It's not possible to capture unhandled exceptions, unhandled promise rejections now - Bun is working on adding support -for it. [Github Issue](https://github.com/oven-sh/bun/issues/5091) follow this issue. To report errors to Sentry, you -have to manually try-catch and call `Sentry.captureException` in the catch block. - -```ts -import * as Sentry from '@sentry/bun'; - -try { - throw new Error('test'); -} catch (e) { - Sentry.captureException(e); -} -``` diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index d027539931cc..024e3e3af5e8 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -71,6 +71,7 @@ export { nativeNodeFetchIntegration, onUncaughtExceptionIntegration, onUnhandledRejectionIntegration, + openAIIntegration, modulesIntegration, contextLinesIntegration, nodeContextIntegration, diff --git a/packages/cloudflare/src/durableobject.ts b/packages/cloudflare/src/durableobject.ts index 0e919977025d..c9de8763750c 100644 --- a/packages/cloudflare/src/durableobject.ts +++ b/packages/cloudflare/src/durableobject.ts @@ -25,16 +25,21 @@ type MethodWrapperOptions = { }; // eslint-disable-next-line @typescript-eslint/no-explicit-any -function wrapMethodWithSentry any>( +type OriginalMethod = (...args: any[]) => any; + +function wrapMethodWithSentry( wrapperOptions: MethodWrapperOptions, handler: T, callback?: (...args: Parameters) => void, + noMark?: true, ): T { if (isInstrumented(handler)) { return handler; } - markAsInstrumented(handler); + if (!noMark) { + markAsInstrumented(handler); + } return new Proxy(handler, { apply(target, thisArg, args: Parameters) { @@ -221,8 +226,46 @@ export function instrumentDurableObjectWithSentry< ); } } + const instrumentedPrototype = instrumentPrototype(target, options, context); + Object.setPrototypeOf(obj, instrumentedPrototype); return obj; }, }); } + +function instrumentPrototype( + target: T, + options: CloudflareOptions, + context: MethodWrapperOptions['context'], +): T { + return new Proxy(target.prototype, { + get(target, prop, receiver) { + const value = Reflect.get(target, prop, receiver); + if (prop === 'constructor' || typeof value !== 'function') { + return value; + } + const wrapped = wrapMethodWithSentry( + { options, context, spanName: prop.toString(), spanOp: 'rpc' }, + value, + undefined, + true, + ); + const instrumented = new Proxy(wrapped, { + get(target, p, receiver) { + if ('__SENTRY_INSTRUMENTED__' === p) { + return true; + } + return Reflect.get(target, p, receiver); + }, + }); + Object.defineProperty(receiver, prop, { + value: instrumented, + enumerable: true, + writable: true, + configurable: true, + }); + return instrumented; + }, + }); +} diff --git a/packages/cloudflare/src/handler.ts b/packages/cloudflare/src/handler.ts index 001ab55049ab..354233154a0b 100644 --- a/packages/cloudflare/src/handler.ts +++ b/packages/cloudflare/src/handler.ts @@ -233,7 +233,7 @@ export function withSentry { - it('instrumentDurableObjectWithSentry generic functionality', () => { +describe('instrumentDurableObjectWithSentry', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + it('Generic functionality', () => { const options = vi.fn(); const instrumented = instrumentDurableObjectWithSentry(options, vi.fn()); expect(instrumented).toBeTypeOf('function'); expect(() => Reflect.construct(instrumented, [])).not.toThrow(); expect(options).toHaveBeenCalledOnce(); }); - it('all available durable object methods are instrumented', () => { - const testClass = vi.fn(() => ({ - customMethod: vi.fn(), - fetch: vi.fn(), - alarm: vi.fn(), - webSocketMessage: vi.fn(), - webSocketClose: vi.fn(), - webSocketError: vi.fn(), - })); + it('Instruments prototype methods and defines implementation in the object', () => { + const testClass = class { + method() {} + }; + const obj = Reflect.construct(instrumentDurableObjectWithSentry(vi.fn(), testClass as any), []) as any; + expect(obj.method).toBe(obj.method); + }); + it('Instruments prototype methods without "sticking" to the options', () => { + const initCore = vi.spyOn(SentryCore, 'initAndBind'); + vi.spyOn(SentryCore, 'getClient').mockReturnValue(undefined); + const options = vi + .fn() + .mockReturnValueOnce({ + orgId: 1, + }) + .mockReturnValueOnce({ + orgId: 2, + }); + const testClass = class { + method() {} + }; + (Reflect.construct(instrumentDurableObjectWithSentry(options, testClass as any), []) as any).method(); + (Reflect.construct(instrumentDurableObjectWithSentry(options, testClass as any), []) as any).method(); + expect(initCore).nthCalledWith(1, expect.any(Function), expect.objectContaining({ orgId: 1 })); + expect(initCore).nthCalledWith(2, expect.any(Function), expect.objectContaining({ orgId: 2 })); + }); + it('All available durable object methods are instrumented', () => { + const testClass = class { + propertyFunction = vi.fn(); + + rpcMethod() {} + + fetch() {} + + alarm() {} + + webSocketMessage() {} + + webSocketClose() {} + + webSocketError() {} + }; const instrumented = instrumentDurableObjectWithSentry(vi.fn(), testClass as any); - const dObject: any = Reflect.construct(instrumented, []); - for (const method of Object.getOwnPropertyNames(dObject)) { - expect(isInstrumented(dObject[method]), `Method ${method} is instrumented`).toBeTruthy(); + const obj = Reflect.construct(instrumented, []); + expect(Object.getPrototypeOf(obj), 'Prototype is instrumented').not.toBe(testClass.prototype); + for (const method_name of [ + 'propertyFunction', + 'fetch', + 'alarm', + 'webSocketMessage', + 'webSocketClose', + 'webSocketError', + 'rpcMethod', + ]) { + expect(isInstrumented((obj as any)[method_name]), `Method ${method_name} is instrumented`).toBeTruthy(); } }); it('flush performs after all waitUntil promises are finished', async () => { diff --git a/packages/core/src/breadcrumbs.ts b/packages/core/src/breadcrumbs.ts index 78157a40e2e3..99deafdd8d2d 100644 --- a/packages/core/src/breadcrumbs.ts +++ b/packages/core/src/breadcrumbs.ts @@ -1,6 +1,6 @@ import { getClient, getIsolationScope } from './currentScopes'; import type { Breadcrumb, BreadcrumbHint } from './types-hoist/breadcrumb'; -import { consoleSandbox } from './utils/logger'; +import { consoleSandbox } from './utils/debug-logger'; import { dateTimestampInSeconds } from './utils/time'; /** diff --git a/packages/core/src/carrier.ts b/packages/core/src/carrier.ts index b6d30082f847..9fde3f7b74f0 100644 --- a/packages/core/src/carrier.ts +++ b/packages/core/src/carrier.ts @@ -3,7 +3,7 @@ import type { AsyncContextStrategy } from './asyncContext/types'; import type { Client } from './client'; import type { Scope } from './scope'; import type { SerializedLog } from './types-hoist/log'; -import type { Logger } from './utils/logger'; +import type { Logger } from './utils/debug-logger'; import { SDK_VERSION } from './utils/version'; import { GLOBAL_OBJ } from './utils/worldwide'; @@ -27,6 +27,7 @@ export interface SentryCarrier { defaultIsolationScope?: Scope; defaultCurrentScope?: Scope; /** @deprecated Logger is no longer set. Instead, we keep enabled state in loggerSettings. */ + // eslint-disable-next-line deprecation/deprecation logger?: Logger; loggerSettings?: { enabled: boolean }; /** diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index fb7717c6a178..ab450788459c 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -33,11 +33,11 @@ import type { Span, SpanAttributes, SpanContextData, SpanJSON } from './types-ho import type { StartSpanOptions } from './types-hoist/startSpanOptions'; import type { Transport, TransportMakeRequestResponse } from './types-hoist/transport'; import { createClientReportEnvelope } from './utils/clientreport'; +import { debug } from './utils/debug-logger'; import { dsnToString, makeDsn } from './utils/dsn'; import { addItemToEnvelope, createAttachmentEnvelopeItem } from './utils/envelope'; import { getPossibleEventMessages } from './utils/eventUtils'; import { isParameterizedString, isPlainObject, isPrimitive, isThenable } from './utils/is'; -import { debug } from './utils/logger'; import { merge } from './utils/merge'; import { checkOrSetAlreadyCaught, uuid4 } from './utils/misc'; import { parseSampleRate } from './utils/parseSampleRate'; diff --git a/packages/core/src/eventProcessors.ts b/packages/core/src/eventProcessors.ts index 0dea0b88286d..3bebe71b9316 100644 --- a/packages/core/src/eventProcessors.ts +++ b/packages/core/src/eventProcessors.ts @@ -1,8 +1,8 @@ import { DEBUG_BUILD } from './debug-build'; import type { Event, EventHint } from './types-hoist/event'; import type { EventProcessor } from './types-hoist/eventprocessor'; +import { debug } from './utils/debug-logger'; import { isThenable } from './utils/is'; -import { debug } from './utils/logger'; import { SyncPromise } from './utils/syncpromise'; /** diff --git a/packages/core/src/exports.ts b/packages/core/src/exports.ts index d70a542ca284..a5dc716d8124 100644 --- a/packages/core/src/exports.ts +++ b/packages/core/src/exports.ts @@ -10,8 +10,8 @@ import type { Primitive } from './types-hoist/misc'; import type { Session, SessionContext } from './types-hoist/session'; import type { SeverityLevel } from './types-hoist/severity'; import type { User } from './types-hoist/user'; +import { debug } from './utils/debug-logger'; import { isThenable } from './utils/is'; -import { debug } from './utils/logger'; import { uuid4 } from './utils/misc'; import type { ExclusiveEventHintOrCaptureContext } from './utils/prepareEvent'; import { parseEventHintOrCaptureContext } from './utils/prepareEvent'; diff --git a/packages/core/src/fetch.ts b/packages/core/src/fetch.ts index cf82eca1e6c1..502e24711994 100644 --- a/packages/core/src/fetch.ts +++ b/packages/core/src/fetch.ts @@ -204,7 +204,7 @@ function endSpan(span: Span, handlerData: HandlerDataFetch): void { if (handlerData.response) { setHttpStatus(span, handlerData.response.status); - const contentLength = handlerData.response?.headers && handlerData.response.headers.get('content-length'); + const contentLength = handlerData.response?.headers?.get('content-length'); if (contentLength) { const contentLengthNum = parseInt(contentLength); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 8852e2c9293f..3e020fc6aa77 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -124,7 +124,9 @@ export type { ReportDialogOptions } from './report-dialog'; export { _INTERNAL_captureLog, _INTERNAL_flushLogsBuffer, _INTERNAL_captureSerializedLog } from './logs/exports'; export { consoleLoggingIntegration } from './logs/console-integration'; export { addVercelAiProcessors } from './utils/vercel-ai'; - +export { instrumentOpenAiClient } from './utils/openai'; +export { OPENAI_INTEGRATION_NAME } from './utils/openai/constants'; +export type { OpenAiClient, OpenAiOptions, InstrumentedMethod } from './utils/openai/types'; export type { FeatureFlag } from './utils/featureFlags'; export { _INTERNAL_copyFlagsFromScopeToEvent, @@ -165,8 +167,10 @@ export { isVueViewModel, } from './utils/is'; export { isBrowser } from './utils/isBrowser'; -export { CONSOLE_LEVELS, consoleSandbox, debug, logger, originalConsoleMethods } from './utils/logger'; -export type { Logger } from './utils/logger'; +// eslint-disable-next-line deprecation/deprecation +export { CONSOLE_LEVELS, consoleSandbox, debug, logger, originalConsoleMethods } from './utils/debug-logger'; +// eslint-disable-next-line deprecation/deprecation +export type { Logger, SentryDebugLogger } from './utils/debug-logger'; export { addContextToFrame, addExceptionMechanism, diff --git a/packages/core/src/instrument/console.ts b/packages/core/src/instrument/console.ts index 141f3bcb33c9..e96de345d202 100644 --- a/packages/core/src/instrument/console.ts +++ b/packages/core/src/instrument/console.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/ban-types */ import type { ConsoleLevel, HandlerDataConsole } from '../types-hoist/instrument'; -import { CONSOLE_LEVELS, originalConsoleMethods } from '../utils/logger'; +import { CONSOLE_LEVELS, originalConsoleMethods } from '../utils/debug-logger'; import { fill } from '../utils/object'; import { GLOBAL_OBJ } from '../utils/worldwide'; import { addHandler, maybeInstrument, triggerHandlers } from './handlers'; diff --git a/packages/core/src/instrument/fetch.ts b/packages/core/src/instrument/fetch.ts index be079280215a..0780b25bb29f 100644 --- a/packages/core/src/instrument/fetch.ts +++ b/packages/core/src/instrument/fetch.ts @@ -172,7 +172,7 @@ async function resolveResponse(res: Response | undefined, onFinishedResolving: ( onFinishedResolving(); readingActive = false; } - } catch (error) { + } catch { readingActive = false; } finally { clearTimeout(chunkTimeout); diff --git a/packages/core/src/instrument/handlers.ts b/packages/core/src/instrument/handlers.ts index fbde79985120..86c5a90b7c52 100644 --- a/packages/core/src/instrument/handlers.ts +++ b/packages/core/src/instrument/handlers.ts @@ -1,5 +1,5 @@ import { DEBUG_BUILD } from '../debug-build'; -import { debug } from '../utils/logger'; +import { debug } from '../utils/debug-logger'; import { getFunctionName } from '../utils/stacktrace'; export type InstrumentHandlerType = diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts index 5354649532d0..0cc9fe2630fe 100644 --- a/packages/core/src/integration.ts +++ b/packages/core/src/integration.ts @@ -4,7 +4,7 @@ import { DEBUG_BUILD } from './debug-build'; import type { Event, EventHint } from './types-hoist/event'; import type { Integration, IntegrationFn } from './types-hoist/integration'; import type { Options } from './types-hoist/options'; -import { debug } from './utils/logger'; +import { debug } from './utils/debug-logger'; export const installedIntegrations: string[] = []; diff --git a/packages/core/src/integrations/captureconsole.ts b/packages/core/src/integrations/captureconsole.ts index 83863349e14a..e491efedd83f 100644 --- a/packages/core/src/integrations/captureconsole.ts +++ b/packages/core/src/integrations/captureconsole.ts @@ -4,7 +4,7 @@ import { addConsoleInstrumentationHandler } from '../instrument/console'; import { defineIntegration } from '../integration'; import type { CaptureContext } from '../scope'; import type { IntegrationFn } from '../types-hoist/integration'; -import { CONSOLE_LEVELS } from '../utils/logger'; +import { CONSOLE_LEVELS } from '../utils/debug-logger'; import { addExceptionMechanism } from '../utils/misc'; import { severityLevelFromString } from '../utils/severity'; import { safeJoin } from '../utils/string'; diff --git a/packages/core/src/integrations/console.ts b/packages/core/src/integrations/console.ts index 3aa1cd8d9f3e..dda44543cc03 100644 --- a/packages/core/src/integrations/console.ts +++ b/packages/core/src/integrations/console.ts @@ -3,7 +3,7 @@ import { getClient } from '../currentScopes'; import { addConsoleInstrumentationHandler } from '../instrument/console'; import { defineIntegration } from '../integration'; import type { ConsoleLevel } from '../types-hoist/instrument'; -import { CONSOLE_LEVELS } from '../utils/logger'; +import { CONSOLE_LEVELS } from '../utils/debug-logger'; import { severityLevelFromString } from '../utils/severity'; import { safeJoin } from '../utils/string'; import { GLOBAL_OBJ } from '../utils/worldwide'; diff --git a/packages/core/src/integrations/dedupe.ts b/packages/core/src/integrations/dedupe.ts index dfca4698f788..4379c8f6ff73 100644 --- a/packages/core/src/integrations/dedupe.ts +++ b/packages/core/src/integrations/dedupe.ts @@ -4,7 +4,7 @@ import type { Event } from '../types-hoist/event'; import type { Exception } from '../types-hoist/exception'; import type { IntegrationFn } from '../types-hoist/integration'; import type { StackFrame } from '../types-hoist/stackframe'; -import { debug } from '../utils/logger'; +import { debug } from '../utils/debug-logger'; import { getFramesFromEvent } from '../utils/stacktrace'; const INTEGRATION_NAME = 'Dedupe'; @@ -27,7 +27,7 @@ const _dedupeIntegration = (() => { DEBUG_BUILD && debug.warn('Event dropped due to being a duplicate of previously captured event.'); return null; } - } catch (_oO) {} // eslint-disable-line no-empty + } catch {} // eslint-disable-line no-empty return (previousEvent = currentEvent); }, @@ -170,11 +170,11 @@ function _isSameFingerprint(currentEvent: Event, previousEvent: Event): boolean // Otherwise, compare the two try { return !!(currentFingerprint.join('') === previousFingerprint.join('')); - } catch (_oO) { + } catch { return false; } } function _getExceptionFromEvent(event: Event): Exception | undefined { - return event.exception?.values && event.exception.values[0]; + return event.exception?.values?.[0]; } diff --git a/packages/core/src/integrations/eventFilters.ts b/packages/core/src/integrations/eventFilters.ts index 729758a7438e..84ae5d4c4139 100644 --- a/packages/core/src/integrations/eventFilters.ts +++ b/packages/core/src/integrations/eventFilters.ts @@ -3,8 +3,8 @@ import { defineIntegration } from '../integration'; import type { Event } from '../types-hoist/event'; import type { IntegrationFn } from '../types-hoist/integration'; import type { StackFrame } from '../types-hoist/stackframe'; +import { debug } from '../utils/debug-logger'; import { getPossibleEventMessages } from '../utils/eventUtils'; -import { debug } from '../utils/logger'; import { getEventDescription } from '../utils/misc'; import { stringMatchesSomePattern } from '../utils/string'; @@ -211,7 +211,7 @@ function _getEventFilterUrl(event: Event): string | null { .find(value => value.mechanism?.parent_id === undefined && value.stacktrace?.frames?.length); const frames = rootException?.stacktrace?.frames; return frames ? _getLastValidUrl(frames) : null; - } catch (oO) { + } catch { DEBUG_BUILD && debug.error(`Cannot extract url for event ${getEventDescription(event)}`); return null; } diff --git a/packages/core/src/integrations/extraerrordata.ts b/packages/core/src/integrations/extraerrordata.ts index 21b47e9fdb9c..291648244f6c 100644 --- a/packages/core/src/integrations/extraerrordata.ts +++ b/packages/core/src/integrations/extraerrordata.ts @@ -4,8 +4,8 @@ import type { Contexts } from '../types-hoist/context'; import type { ExtendedError } from '../types-hoist/error'; import type { Event, EventHint } from '../types-hoist/event'; import type { IntegrationFn } from '../types-hoist/integration'; +import { debug } from '../utils/debug-logger'; import { isError, isPlainObject } from '../utils/is'; -import { debug } from '../utils/logger'; import { normalize } from '../utils/normalize'; import { addNonEnumerableProperty } from '../utils/object'; import { truncate } from '../utils/string'; diff --git a/packages/core/src/integrations/rewriteframes.ts b/packages/core/src/integrations/rewriteframes.ts index c903016b0531..555fb492ce10 100644 --- a/packages/core/src/integrations/rewriteframes.ts +++ b/packages/core/src/integrations/rewriteframes.ts @@ -75,7 +75,7 @@ export const rewriteFramesIntegration = defineIntegration((options: RewriteFrame })), }, }; - } catch (_oO) { + } catch { return event; } } @@ -84,7 +84,7 @@ export const rewriteFramesIntegration = defineIntegration((options: RewriteFrame function _processStacktrace(stacktrace?: Stacktrace): Stacktrace { return { ...stacktrace, - frames: stacktrace?.frames && stacktrace.frames.map(f => iteratee(f)), + frames: stacktrace?.frames?.map(f => iteratee(f)), }; } diff --git a/packages/core/src/integrations/supabase.ts b/packages/core/src/integrations/supabase.ts index 66ddf955419d..360f31142652 100644 --- a/packages/core/src/integrations/supabase.ts +++ b/packages/core/src/integrations/supabase.ts @@ -9,8 +9,8 @@ import { defineIntegration } from '../integration'; import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../semanticAttributes'; import { setHttpStatus, SPAN_STATUS_ERROR, SPAN_STATUS_OK, startSpan } from '../tracing'; import type { IntegrationFn } from '../types-hoist/integration'; +import { debug } from '../utils/debug-logger'; import { isPlainObject } from '../utils/is'; -import { debug } from '../utils/logger'; const AUTH_OPERATIONS_TO_INSTRUMENT = [ 'reauthenticate', diff --git a/packages/core/src/logs/console-integration.ts b/packages/core/src/logs/console-integration.ts index 3affabd412e9..d2cdc8fa1d48 100644 --- a/packages/core/src/logs/console-integration.ts +++ b/packages/core/src/logs/console-integration.ts @@ -5,8 +5,8 @@ import { defineIntegration } from '../integration'; import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../semanticAttributes'; import type { ConsoleLevel } from '../types-hoist/instrument'; import type { IntegrationFn } from '../types-hoist/integration'; +import { CONSOLE_LEVELS, debug } from '../utils/debug-logger'; import { isPrimitive } from '../utils/is'; -import { CONSOLE_LEVELS, debug } from '../utils/logger'; import { normalize } from '../utils/normalize'; import { GLOBAL_OBJ } from '../utils/worldwide'; import { _INTERNAL_captureLog } from './exports'; diff --git a/packages/core/src/logs/exports.ts b/packages/core/src/logs/exports.ts index b060721128ad..23246a7e1251 100644 --- a/packages/core/src/logs/exports.ts +++ b/packages/core/src/logs/exports.ts @@ -6,8 +6,8 @@ import { DEBUG_BUILD } from '../debug-build'; import type { Scope, ScopeData } from '../scope'; import type { Log, SerializedLog, SerializedLogAttributeValue } from '../types-hoist/log'; import { mergeScopeData } from '../utils/applyScopeDataToEvent'; +import { consoleSandbox, debug } from '../utils/debug-logger'; import { isParameterizedString } from '../utils/is'; -import { consoleSandbox, debug } from '../utils/logger'; import { _getSpanForScope } from '../utils/spanOnScope'; import { timestampInSeconds } from '../utils/time'; import { SEVERITY_TEXT_TO_SEVERITY_NUMBER } from './constants'; diff --git a/packages/core/src/mcp-server.ts b/packages/core/src/mcp-server.ts index fe53d228bd60..ded30dd50928 100644 --- a/packages/core/src/mcp-server.ts +++ b/packages/core/src/mcp-server.ts @@ -6,7 +6,7 @@ import { } from './semanticAttributes'; import { startSpan, withActiveSpan } from './tracing'; import type { Span } from './types-hoist/span'; -import { debug } from './utils/logger'; +import { debug } from './utils/debug-logger'; import { getActiveSpan } from './utils/spanUtils'; interface MCPTransport { diff --git a/packages/core/src/metadata.ts b/packages/core/src/metadata.ts index c88d7a36406d..190db6dd55fa 100644 --- a/packages/core/src/metadata.ts +++ b/packages/core/src/metadata.ts @@ -72,7 +72,7 @@ export function addMetadataToStackFrames(parser: StackParser, event: Event): voi } } }); - } catch (_) { + } catch { // To save bundle size we're just try catching here instead of checking for the existence of all the different objects. } } @@ -92,7 +92,7 @@ export function stripMetadataFromStackFrames(event: Event): void { delete frame.module_metadata; } }); - } catch (_) { + } catch { // To save bundle size we're just try catching here instead of checking for the existence of all the different objects. } } diff --git a/packages/core/src/profiling.ts b/packages/core/src/profiling.ts index ccc8d587bab6..b49a30070683 100644 --- a/packages/core/src/profiling.ts +++ b/packages/core/src/profiling.ts @@ -1,7 +1,7 @@ import { getClient } from './currentScopes'; import { DEBUG_BUILD } from './debug-build'; import type { Profiler, ProfilingIntegration } from './types-hoist/profiling'; -import { debug } from './utils/logger'; +import { debug } from './utils/debug-logger'; function isProfilingIntegrationWithProfiler( integration: ProfilingIntegration | undefined, diff --git a/packages/core/src/scope.ts b/packages/core/src/scope.ts index dfcc8019ace2..8b1e21acfb4a 100644 --- a/packages/core/src/scope.ts +++ b/packages/core/src/scope.ts @@ -16,8 +16,8 @@ import type { SeverityLevel } from './types-hoist/severity'; import type { Span } from './types-hoist/span'; import type { PropagationContext } from './types-hoist/tracing'; import type { User } from './types-hoist/user'; +import { debug } from './utils/debug-logger'; import { isPlainObject } from './utils/is'; -import { debug } from './utils/logger'; import { merge } from './utils/merge'; import { uuid4 } from './utils/misc'; import { generateTraceId } from './utils/propagationContext'; diff --git a/packages/core/src/sdk.ts b/packages/core/src/sdk.ts index db191df436c5..bc96e90e9ed8 100644 --- a/packages/core/src/sdk.ts +++ b/packages/core/src/sdk.ts @@ -2,7 +2,7 @@ import type { Client } from './client'; import { getCurrentScope } from './currentScopes'; import { DEBUG_BUILD } from './debug-build'; import type { ClientOptions } from './types-hoist/options'; -import { consoleSandbox, debug } from './utils/logger'; +import { consoleSandbox, debug } from './utils/debug-logger'; /** A class object that can instantiate Client objects. */ export type ClientClass = new (options: O) => F; diff --git a/packages/core/src/server-runtime-client.ts b/packages/core/src/server-runtime-client.ts index 6dd8a4f045a3..246e0a88bc51 100644 --- a/packages/core/src/server-runtime-client.ts +++ b/packages/core/src/server-runtime-client.ts @@ -13,9 +13,9 @@ import type { ClientOptions } from './types-hoist/options'; import type { ParameterizedString } from './types-hoist/parameterize'; import type { SeverityLevel } from './types-hoist/severity'; import type { BaseTransportOptions } from './types-hoist/transport'; +import { debug } from './utils/debug-logger'; import { eventFromMessage, eventFromUnknownInput } from './utils/eventbuilder'; import { isPrimitive } from './utils/is'; -import { debug } from './utils/logger'; import { uuid4 } from './utils/misc'; import { resolvedSyncPromise } from './utils/syncpromise'; diff --git a/packages/core/src/tracing/errors.ts b/packages/core/src/tracing/errors.ts index a2d5af2560c5..cf7aafbac8ea 100644 --- a/packages/core/src/tracing/errors.ts +++ b/packages/core/src/tracing/errors.ts @@ -1,7 +1,7 @@ import { DEBUG_BUILD } from '../debug-build'; import { addGlobalErrorInstrumentationHandler } from '../instrument/globalError'; import { addGlobalUnhandledRejectionInstrumentationHandler } from '../instrument/globalUnhandledRejection'; -import { debug } from '../utils/logger'; +import { debug } from '../utils/debug-logger'; import { getActiveSpan, getRootSpan } from '../utils/spanUtils'; import { SPAN_STATUS_ERROR } from './spanstatus'; diff --git a/packages/core/src/tracing/idleSpan.ts b/packages/core/src/tracing/idleSpan.ts index e9a7906f9b0d..11045e0da1af 100644 --- a/packages/core/src/tracing/idleSpan.ts +++ b/packages/core/src/tracing/idleSpan.ts @@ -4,8 +4,8 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON } from '../semanticAt import type { DynamicSamplingContext } from '../types-hoist/envelope'; import type { Span } from '../types-hoist/span'; import type { StartSpanOptions } from '../types-hoist/startSpanOptions'; +import { debug } from '../utils/debug-logger'; import { hasSpansEnabled } from '../utils/hasSpansEnabled'; -import { debug } from '../utils/logger'; import { _setSpanForScope } from '../utils/spanOnScope'; import { getActiveSpan, diff --git a/packages/core/src/tracing/logSpans.ts b/packages/core/src/tracing/logSpans.ts index 4cc08fd2388d..f3bcb02e497c 100644 --- a/packages/core/src/tracing/logSpans.ts +++ b/packages/core/src/tracing/logSpans.ts @@ -1,6 +1,6 @@ import { DEBUG_BUILD } from '../debug-build'; import type { Span } from '../types-hoist/span'; -import { debug } from '../utils/logger'; +import { debug } from '../utils/debug-logger'; import { getRootSpan, spanIsSampled, spanToJSON } from '../utils/spanUtils'; /** diff --git a/packages/core/src/tracing/measurement.ts b/packages/core/src/tracing/measurement.ts index 115325e41594..bf9a02e88ba0 100644 --- a/packages/core/src/tracing/measurement.ts +++ b/packages/core/src/tracing/measurement.ts @@ -5,7 +5,7 @@ import { } from '../semanticAttributes'; import type { Measurements, MeasurementUnit } from '../types-hoist/measurement'; import type { TimedEvent } from '../types-hoist/timedEvent'; -import { debug } from '../utils/logger'; +import { debug } from '../utils/debug-logger'; import { getActiveSpan, getRootSpan } from '../utils/spanUtils'; /** diff --git a/packages/core/src/tracing/sampling.ts b/packages/core/src/tracing/sampling.ts index 87c056d613a3..27b32970d74c 100644 --- a/packages/core/src/tracing/sampling.ts +++ b/packages/core/src/tracing/sampling.ts @@ -1,8 +1,8 @@ import { DEBUG_BUILD } from '../debug-build'; import type { Options } from '../types-hoist/options'; import type { SamplingContext } from '../types-hoist/samplingcontext'; +import { debug } from '../utils/debug-logger'; import { hasSpansEnabled } from '../utils/hasSpansEnabled'; -import { debug } from '../utils/logger'; import { parseSampleRate } from '../utils/parseSampleRate'; /** diff --git a/packages/core/src/tracing/sentrySpan.ts b/packages/core/src/tracing/sentrySpan.ts index 01c0417c48b4..af1a1f2b8ffc 100644 --- a/packages/core/src/tracing/sentrySpan.ts +++ b/packages/core/src/tracing/sentrySpan.ts @@ -25,7 +25,7 @@ import type { import type { SpanStatus } from '../types-hoist/spanStatus'; import type { TimedEvent } from '../types-hoist/timedEvent'; import type { TransactionSource } from '../types-hoist/transaction'; -import { debug } from '../utils/logger'; +import { debug } from '../utils/debug-logger'; import { generateSpanId, generateTraceId } from '../utils/propagationContext'; import { convertSpanLinksForEnvelope, diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index 7062cf3bbb47..ba6df741508c 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -11,9 +11,9 @@ import type { DynamicSamplingContext } from '../types-hoist/envelope'; import type { ClientOptions } from '../types-hoist/options'; import type { SentrySpanArguments, Span, SpanTimeInput } from '../types-hoist/span'; import type { StartSpanOptions } from '../types-hoist/startSpanOptions'; +import { debug } from '../utils/debug-logger'; import { handleCallbackErrors } from '../utils/handleCallbackErrors'; import { hasSpansEnabled } from '../utils/hasSpansEnabled'; -import { debug } from '../utils/logger'; import { parseSampleRate } from '../utils/parseSampleRate'; import { generateTraceId } from '../utils/propagationContext'; import { _getSpanForScope, _setSpanForScope } from '../utils/spanOnScope'; diff --git a/packages/core/src/transports/base.ts b/packages/core/src/transports/base.ts index d37b7cbb1b05..c475c338db2f 100644 --- a/packages/core/src/transports/base.ts +++ b/packages/core/src/transports/base.ts @@ -7,13 +7,13 @@ import type { TransportMakeRequestResponse, TransportRequestExecutor, } from '../types-hoist/transport'; +import { debug } from '../utils/debug-logger'; import { createEnvelope, envelopeItemTypeToDataCategory, forEachEnvelopeItem, serializeEnvelope, } from '../utils/envelope'; -import { debug } from '../utils/logger'; import { type PromiseBuffer, makePromiseBuffer, SENTRY_BUFFER_FULL_ERROR } from '../utils/promisebuffer'; import { type RateLimits, isRateLimited, updateRateLimits } from '../utils/ratelimit'; import { resolvedSyncPromise } from '../utils/syncpromise'; diff --git a/packages/core/src/transports/offline.ts b/packages/core/src/transports/offline.ts index e8b7437b9f3a..88211a4378e7 100644 --- a/packages/core/src/transports/offline.ts +++ b/packages/core/src/transports/offline.ts @@ -1,8 +1,8 @@ import { DEBUG_BUILD } from '../debug-build'; import type { Envelope } from '../types-hoist/envelope'; import type { InternalBaseTransportOptions, Transport, TransportMakeRequestResponse } from '../types-hoist/transport'; +import { debug } from '../utils/debug-logger'; import { envelopeContainsItemType } from '../utils/envelope'; -import { debug } from '../utils/logger'; import { parseRetryAfterHeader } from '../utils/ratelimit'; export const MIN_DELAY = 100; // 100 ms diff --git a/packages/core/src/trpc.ts b/packages/core/src/trpc.ts index 611dded043b6..beac8c5b4c4c 100644 --- a/packages/core/src/trpc.ts +++ b/packages/core/src/trpc.ts @@ -70,7 +70,7 @@ export function trpcMiddleware(options: SentryTrpcMiddlewareOptions = {}) { const rawRes = await getRawInput(); trpcContext.input = normalize(rawRes); - } catch (err) { + } catch { // noop } } diff --git a/packages/core/src/utils/baggage.ts b/packages/core/src/utils/baggage.ts index 6696169aad79..b483207ba8f2 100644 --- a/packages/core/src/utils/baggage.ts +++ b/packages/core/src/utils/baggage.ts @@ -1,7 +1,7 @@ import { DEBUG_BUILD } from '../debug-build'; import type { DynamicSamplingContext } from '../types-hoist/envelope'; +import { debug } from './debug-logger'; import { isString } from './is'; -import { debug } from './logger'; export const SENTRY_BAGGAGE_KEY_PREFIX = 'sentry-'; diff --git a/packages/core/src/utils/browser.ts b/packages/core/src/utils/browser.ts index bd9775c594ab..c051cd70f234 100644 --- a/packages/core/src/utils/browser.ts +++ b/packages/core/src/utils/browser.ts @@ -56,7 +56,7 @@ export function htmlTreeAsString( } return out.reverse().join(separator); - } catch (_oO) { + } catch { return ''; } } @@ -134,7 +134,7 @@ function _htmlElementAsString(el: unknown, keyAttrs?: string[]): string { export function getLocationHref(): string { try { return WINDOW.document.location.href; - } catch (oO) { + } catch { return ''; } } diff --git a/packages/core/src/utils/cookie.ts b/packages/core/src/utils/cookie.ts index 2a9d21654ba6..218342ae36d3 100644 --- a/packages/core/src/utils/cookie.ts +++ b/packages/core/src/utils/cookie.ts @@ -66,7 +66,7 @@ export function parseCookie(str: string): Record { try { obj[key] = val.indexOf('%') !== -1 ? decodeURIComponent(val) : val; - } catch (e) { + } catch { obj[key] = val; } } diff --git a/packages/core/src/utils/logger.ts b/packages/core/src/utils/debug-logger.ts similarity index 95% rename from packages/core/src/utils/logger.ts rename to packages/core/src/utils/debug-logger.ts index 590d02afb34d..36e3169b1d52 100644 --- a/packages/core/src/utils/logger.ts +++ b/packages/core/src/utils/debug-logger.ts @@ -3,7 +3,11 @@ import { DEBUG_BUILD } from '../debug-build'; import type { ConsoleLevel } from '../types-hoist/instrument'; import { GLOBAL_OBJ } from './worldwide'; -/** A Sentry Logger instance. */ +/** + * A Sentry Logger instance. + * + * @deprecated Use {@link debug} instead with the {@link SentryDebugLogger} type. + */ export interface Logger { disable(): void; enable(): void; @@ -146,6 +150,8 @@ function _getLoggerSettings(): { enabled: boolean } { /** * This is a logger singleton which either logs things or no-ops if logging is not enabled. * The logger is a singleton on the carrier, to ensure that a consistent logger is used throughout the SDK. + * + * @deprecated Use {@link debug} instead. */ export const logger = { /** Enable logging. */ @@ -168,6 +174,7 @@ export const logger = { assert, /** Log a trace. */ trace, + // eslint-disable-next-line deprecation/deprecation } satisfies Logger; /** diff --git a/packages/core/src/utils/dsn.ts b/packages/core/src/utils/dsn.ts index 57bb07a53014..63e6472c10c7 100644 --- a/packages/core/src/utils/dsn.ts +++ b/packages/core/src/utils/dsn.ts @@ -1,6 +1,6 @@ import { DEBUG_BUILD } from '../debug-build'; import type { DsnComponents, DsnLike, DsnProtocol } from '../types-hoist/dsn'; -import { consoleSandbox, debug } from './logger'; +import { consoleSandbox, debug } from './debug-logger'; /** Regular expression used to extract org ID from a DSN host. */ const ORG_ID_REGEX = /^o(\d+)\./; diff --git a/packages/core/src/utils/envelope.ts b/packages/core/src/utils/envelope.ts index 0f6af9440643..ffda9434d886 100644 --- a/packages/core/src/utils/envelope.ts +++ b/packages/core/src/utils/envelope.ts @@ -112,7 +112,7 @@ export function serializeEnvelope(envelope: Envelope): string | Uint8Array { let stringifiedPayload: string; try { stringifiedPayload = JSON.stringify(payload); - } catch (e) { + } catch { // In case, despite all our efforts to keep `payload` circular-dependency-free, `JSON.stringify()` still // fails, we try again after normalizing it again with infinite normalization depth. This of course has a // performance impact but in this case a performance hit is better than throwing. diff --git a/packages/core/src/utils/eventUtils.ts b/packages/core/src/utils/eventUtils.ts index 3b474548a300..4391e63f72f8 100644 --- a/packages/core/src/utils/eventUtils.ts +++ b/packages/core/src/utils/eventUtils.ts @@ -19,7 +19,7 @@ export function getPossibleEventMessages(event: Event): string[] { possibleMessages.push(`${lastException.type}: ${lastException.value}`); } } - } catch (e) { + } catch { // ignore errors here } diff --git a/packages/core/src/utils/eventbuilder.ts b/packages/core/src/utils/eventbuilder.ts index 7156a0227615..24889187712f 100644 --- a/packages/core/src/utils/eventbuilder.ts +++ b/packages/core/src/utils/eventbuilder.ts @@ -82,7 +82,7 @@ function getObjectClassName(obj: unknown): string | undefined | void { try { const prototype: unknown | null = Object.getPrototypeOf(obj); return prototype ? prototype.constructor.name : undefined; - } catch (e) { + } catch { // ignore errors here } } diff --git a/packages/core/src/utils/featureFlags.ts b/packages/core/src/utils/featureFlags.ts index c0cef4bde2e2..3e7be22704b6 100644 --- a/packages/core/src/utils/featureFlags.ts +++ b/packages/core/src/utils/featureFlags.ts @@ -1,7 +1,7 @@ import { getCurrentScope } from '../currentScopes'; import { DEBUG_BUILD } from '../debug-build'; import { type Event } from '../types-hoist/event'; -import { debug } from '../utils/logger'; +import { debug } from './debug-logger'; import { getActiveSpan, spanToJSON } from './spanUtils'; /** diff --git a/packages/core/src/utils/gen-ai-attributes.ts b/packages/core/src/utils/gen-ai-attributes.ts new file mode 100644 index 000000000000..cf8a073a4313 --- /dev/null +++ b/packages/core/src/utils/gen-ai-attributes.ts @@ -0,0 +1,148 @@ +/** + * OpenAI Integration Telemetry Attributes + * Based on OpenTelemetry Semantic Conventions for Generative AI + * @see https://opentelemetry.io/docs/specs/semconv/gen-ai/ + */ + +// ============================================================================= +// OPENTELEMETRY SEMANTIC CONVENTIONS FOR GENAI +// ============================================================================= + +/** + * The Generative AI system being used + * For OpenAI, this should always be "openai" + */ +export const GEN_AI_SYSTEM_ATTRIBUTE = 'gen_ai.system'; + +/** + * The name of the model as requested + * Examples: "gpt-4", "gpt-3.5-turbo" + */ +export const GEN_AI_REQUEST_MODEL_ATTRIBUTE = 'gen_ai.request.model'; + +/** + * Whether streaming was enabled for the request + */ +export const GEN_AI_REQUEST_STREAM_ATTRIBUTE = 'gen_ai.request.stream'; + +/** + * The temperature setting for the model request + */ +export const GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE = 'gen_ai.request.temperature'; + +/** + * The maximum number of tokens requested + */ +export const GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE = 'gen_ai.request.max_tokens'; + +/** + * The frequency penalty setting for the model request + */ +export const GEN_AI_REQUEST_FREQUENCY_PENALTY_ATTRIBUTE = 'gen_ai.request.frequency_penalty'; + +/** + * The presence penalty setting for the model request + */ +export const GEN_AI_REQUEST_PRESENCE_PENALTY_ATTRIBUTE = 'gen_ai.request.presence_penalty'; + +/** + * The top_p (nucleus sampling) setting for the model request + */ +export const GEN_AI_REQUEST_TOP_P_ATTRIBUTE = 'gen_ai.request.top_p'; + +/** + * The top_k setting for the model request + */ +export const GEN_AI_REQUEST_TOP_K_ATTRIBUTE = 'gen_ai.request.top_k'; + +/** + * Stop sequences for the model request + */ +export const GEN_AI_REQUEST_STOP_SEQUENCES_ATTRIBUTE = 'gen_ai.request.stop_sequences'; + +/** + * Array of reasons why the model stopped generating tokens + */ +export const GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE = 'gen_ai.response.finish_reasons'; + +/** + * The name of the model that generated the response + */ +export const GEN_AI_RESPONSE_MODEL_ATTRIBUTE = 'gen_ai.response.model'; + +/** + * The unique identifier for the response + */ +export const GEN_AI_RESPONSE_ID_ATTRIBUTE = 'gen_ai.response.id'; + +/** + * The number of tokens used in the prompt + */ +export const GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE = 'gen_ai.usage.input_tokens'; + +/** + * The number of tokens used in the response + */ +export const GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE = 'gen_ai.usage.output_tokens'; + +/** + * The total number of tokens used (input + output) + */ +export const GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE = 'gen_ai.usage.total_tokens'; + +/** + * The operation name for OpenAI API calls + */ +export const GEN_AI_OPERATION_NAME_ATTRIBUTE = 'gen_ai.operation.name'; + +/** + * The prompt messages sent to OpenAI (stringified JSON) + * Only recorded when recordInputs is enabled + */ +export const GEN_AI_REQUEST_MESSAGES_ATTRIBUTE = 'gen_ai.request.messages'; + +/** + * The response text from OpenAI (stringified JSON array) + * Only recorded when recordOutputs is enabled + */ +export const GEN_AI_RESPONSE_TEXT_ATTRIBUTE = 'gen_ai.response.text'; + +// ============================================================================= +// OPENAI-SPECIFIC ATTRIBUTES +// ============================================================================= + +/** + * The response ID from OpenAI + */ +export const OPENAI_RESPONSE_ID_ATTRIBUTE = 'openai.response.id'; + +/** + * The response model from OpenAI + */ +export const OPENAI_RESPONSE_MODEL_ATTRIBUTE = 'openai.response.model'; + +/** + * The response timestamp from OpenAI (ISO string) + */ +export const OPENAI_RESPONSE_TIMESTAMP_ATTRIBUTE = 'openai.response.timestamp'; + +/** + * The number of completion tokens used (OpenAI specific) + */ +export const OPENAI_USAGE_COMPLETION_TOKENS_ATTRIBUTE = 'openai.usage.completion_tokens'; + +/** + * The number of prompt tokens used (OpenAI specific) + */ +export const OPENAI_USAGE_PROMPT_TOKENS_ATTRIBUTE = 'openai.usage.prompt_tokens'; + +// ============================================================================= +// OPENAI OPERATIONS +// ============================================================================= + +/** + * OpenAI API operations + */ +export const OPENAI_OPERATIONS = { + CHAT: 'chat', +} as const; diff --git a/packages/core/src/utils/is.ts b/packages/core/src/utils/is.ts index 5fa8a3d1877b..9ec498983d4a 100644 --- a/packages/core/src/utils/is.ts +++ b/packages/core/src/utils/is.ts @@ -182,7 +182,7 @@ export function isSyntheticEvent(wat: unknown): boolean { export function isInstanceOf(wat: any, base: any): boolean { try { return wat instanceof base; - } catch (_e) { + } catch { return false; } } diff --git a/packages/core/src/utils/misc.ts b/packages/core/src/utils/misc.ts index 78b8a9d6aa74..607eff129fe5 100644 --- a/packages/core/src/utils/misc.ts +++ b/packages/core/src/utils/misc.ts @@ -45,7 +45,7 @@ export function uuid4(crypto = getCrypto()): string { return typedArray[0]!; }; } - } catch (_) { + } catch { // some runtimes can crash invoking crypto // https://github.com/getsentry/sentry-javascript/issues/8935 } @@ -222,7 +222,7 @@ export function checkOrSetAlreadyCaught(exception: unknown): boolean { // set it this way rather than by assignment so that it's not ennumerable and therefore isn't recorded by the // `ExtraErrorData` integration addNonEnumerableProperty(exception as { [key: string]: unknown }, '__sentry_captured__', true); - } catch (err) { + } catch { // `exception` is a primitive, so we can't mark it seen } diff --git a/packages/core/src/utils/node.ts b/packages/core/src/utils/node.ts index d7747d6ae6c0..6060700c2b03 100644 --- a/packages/core/src/utils/node.ts +++ b/packages/core/src/utils/node.ts @@ -50,7 +50,7 @@ export function loadModule(moduleName: string, existingModule: any = module): try { mod = dynamicRequire(existingModule, moduleName); - } catch (e) { + } catch { // no-empty } @@ -58,7 +58,7 @@ export function loadModule(moduleName: string, existingModule: any = module): try { const { cwd } = dynamicRequire(existingModule, 'process'); mod = dynamicRequire(existingModule, `${cwd()}/node_modules/${moduleName}`) as T; - } catch (e) { + } catch { // no-empty } } diff --git a/packages/core/src/utils/normalize.ts b/packages/core/src/utils/normalize.ts index 2aa5dae7fae7..ba78d0cdb043 100644 --- a/packages/core/src/utils/normalize.ts +++ b/packages/core/src/utils/normalize.ts @@ -134,7 +134,7 @@ function visit( const jsonValue = valueWithToJSON.toJSON(); // We need to normalize the return value of `.toJSON()` in case it has circular references return visit('', jsonValue, remainingDepth - 1, maxProperties, memo); - } catch (err) { + } catch { // pass (The built-in `toJSON` failed, but we can still try to do it ourselves) } } @@ -296,7 +296,7 @@ export function normalizeUrlToBase(url: string, basePath: string): string { let newUrl = url; try { newUrl = decodeURI(url); - } catch (_Oo) { + } catch { // Sometime this breaks } return ( diff --git a/packages/core/src/utils/object.ts b/packages/core/src/utils/object.ts index 208bd4d0ec8f..e212b9f3b833 100644 --- a/packages/core/src/utils/object.ts +++ b/packages/core/src/utils/object.ts @@ -2,8 +2,8 @@ import { DEBUG_BUILD } from '../debug-build'; import type { WrappedFunction } from '../types-hoist/wrappedfunction'; import { htmlTreeAsString } from './browser'; +import { debug } from './debug-logger'; import { isElement, isError, isEvent, isInstanceOf, isPrimitive } from './is'; -import { debug } from './logger'; import { truncate } from './string'; /** @@ -61,7 +61,7 @@ export function addNonEnumerableProperty(obj: object, name: string, value: unkno writable: true, configurable: true, }); - } catch (o_O) { + } catch { DEBUG_BUILD && debug.log(`Failed to add non-enumerable property "${name}" to object`, obj); } } @@ -78,7 +78,7 @@ export function markFunctionWrapped(wrapped: WrappedFunction, original: WrappedF const proto = original.prototype || {}; wrapped.prototype = original.prototype = proto; addNonEnumerableProperty(wrapped, '__sentry_original__', original); - } catch (o_O) {} // eslint-disable-line no-empty + } catch {} // eslint-disable-line no-empty } /** @@ -151,7 +151,7 @@ export function convertToPlainObject(value: V): function serializeEventTarget(target: unknown): string { try { return isElement(target) ? htmlTreeAsString(target) : Object.prototype.toString.call(target); - } catch (_oO) { + } catch { return ''; } } diff --git a/packages/core/src/utils/openai/constants.ts b/packages/core/src/utils/openai/constants.ts new file mode 100644 index 000000000000..e552616cc1db --- /dev/null +++ b/packages/core/src/utils/openai/constants.ts @@ -0,0 +1,5 @@ +export const OPENAI_INTEGRATION_NAME = 'OpenAI'; + +// https://platform.openai.com/docs/quickstart?api-mode=responses +// https://platform.openai.com/docs/quickstart?api-mode=chat +export const INSTRUMENTED_METHODS = ['responses.create', 'chat.completions.create'] as const; diff --git a/packages/core/src/utils/openai/index.ts b/packages/core/src/utils/openai/index.ts new file mode 100644 index 000000000000..2b5fdbef9c11 --- /dev/null +++ b/packages/core/src/utils/openai/index.ts @@ -0,0 +1,282 @@ +import { getCurrentScope } from '../../currentScopes'; +import { captureException } from '../../exports'; +import { startSpan } from '../../tracing/trace'; +import type { Span, SpanAttributeValue } from '../../types-hoist/span'; +import { + GEN_AI_OPERATION_NAME_ATTRIBUTE, + GEN_AI_REQUEST_FREQUENCY_PENALTY_ATTRIBUTE, + GEN_AI_REQUEST_MESSAGES_ATTRIBUTE, + GEN_AI_REQUEST_MODEL_ATTRIBUTE, + GEN_AI_REQUEST_PRESENCE_PENALTY_ATTRIBUTE, + GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE, + GEN_AI_REQUEST_TOP_P_ATTRIBUTE, + GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE, + GEN_AI_RESPONSE_ID_ATTRIBUTE, + GEN_AI_RESPONSE_MODEL_ATTRIBUTE, + GEN_AI_RESPONSE_TEXT_ATTRIBUTE, + GEN_AI_SYSTEM_ATTRIBUTE, + GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE, + GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE, + GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE, + OPENAI_RESPONSE_ID_ATTRIBUTE, + OPENAI_RESPONSE_MODEL_ATTRIBUTE, + OPENAI_RESPONSE_TIMESTAMP_ATTRIBUTE, + OPENAI_USAGE_COMPLETION_TOKENS_ATTRIBUTE, + OPENAI_USAGE_PROMPT_TOKENS_ATTRIBUTE, +} from '../gen-ai-attributes'; +import { OPENAI_INTEGRATION_NAME } from './constants'; +import type { + InstrumentedMethod, + OpenAiChatCompletionObject, + OpenAiClient, + OpenAiIntegration, + OpenAiOptions, + OpenAiResponse, + OpenAIResponseObject, +} from './types'; +import { + buildMethodPath, + getOperationName, + getSpanOperation, + isChatCompletionResponse, + isResponsesApiResponse, + shouldInstrument, +} from './utils'; + +/** + * Extract request attributes from method arguments + */ +function extractRequestAttributes(args: unknown[], methodPath: string): Record { + const attributes: Record = { + [GEN_AI_SYSTEM_ATTRIBUTE]: 'openai', + [GEN_AI_OPERATION_NAME_ATTRIBUTE]: getOperationName(methodPath), + }; + + if (args.length > 0 && typeof args[0] === 'object' && args[0] !== null) { + const params = args[0] as Record; + + attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE] = params.model ?? 'unknown'; + if ('temperature' in params) attributes[GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE] = params.temperature; + if ('top_p' in params) attributes[GEN_AI_REQUEST_TOP_P_ATTRIBUTE] = params.top_p; + if ('frequency_penalty' in params) + attributes[GEN_AI_REQUEST_FREQUENCY_PENALTY_ATTRIBUTE] = params.frequency_penalty; + if ('presence_penalty' in params) attributes[GEN_AI_REQUEST_PRESENCE_PENALTY_ATTRIBUTE] = params.presence_penalty; + } else { + attributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE] = 'unknown'; + } + + return attributes; +} + +/** + * Helper function to set token usage attributes + */ +function setTokenUsageAttributes( + span: Span, + promptTokens?: number, + completionTokens?: number, + totalTokens?: number, +): void { + if (promptTokens !== undefined) { + span.setAttributes({ + [OPENAI_USAGE_PROMPT_TOKENS_ATTRIBUTE]: promptTokens, + [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: promptTokens, + }); + } + if (completionTokens !== undefined) { + span.setAttributes({ + [OPENAI_USAGE_COMPLETION_TOKENS_ATTRIBUTE]: completionTokens, + [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: completionTokens, + }); + } + if (totalTokens !== undefined) { + span.setAttributes({ + [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: totalTokens, + }); + } +} + +/** + * Helper function to set common response attributes (ID, model, timestamp) + */ +function setCommonResponseAttributes(span: Span, id?: string, model?: string, timestamp?: number): void { + if (id) { + span.setAttributes({ + [OPENAI_RESPONSE_ID_ATTRIBUTE]: id, + [GEN_AI_RESPONSE_ID_ATTRIBUTE]: id, + }); + } + if (model) { + span.setAttributes({ + [OPENAI_RESPONSE_MODEL_ATTRIBUTE]: model, + [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: model, + }); + } + if (timestamp) { + span.setAttributes({ + [OPENAI_RESPONSE_TIMESTAMP_ATTRIBUTE]: new Date(timestamp * 1000).toISOString(), + }); + } +} + +/** + * Add attributes for Chat Completion responses + */ +function addChatCompletionAttributes(span: Span, response: OpenAiChatCompletionObject): void { + setCommonResponseAttributes(span, response.id, response.model, response.created); + if (response.usage) { + setTokenUsageAttributes( + span, + response.usage.prompt_tokens, + response.usage.completion_tokens, + response.usage.total_tokens, + ); + } + if (Array.isArray(response.choices)) { + const finishReasons = response.choices + .map(choice => choice.finish_reason) + .filter((reason): reason is string => reason !== null); + if (finishReasons.length > 0) { + span.setAttributes({ + [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: JSON.stringify(finishReasons), + }); + } + } +} + +/** + * Add attributes for Responses API responses + */ +function addResponsesApiAttributes(span: Span, response: OpenAIResponseObject): void { + setCommonResponseAttributes(span, response.id, response.model, response.created_at); + if (response.status) { + span.setAttributes({ + [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: JSON.stringify([response.status]), + }); + } + if (response.usage) { + setTokenUsageAttributes( + span, + response.usage.input_tokens, + response.usage.output_tokens, + response.usage.total_tokens, + ); + } +} + +/** + * Add response attributes to spans + * This currently supports both Chat Completion and Responses API responses + */ +function addResponseAttributes(span: Span, result: unknown, recordOutputs?: boolean): void { + if (!result || typeof result !== 'object') return; + + const response = result as OpenAiResponse; + + if (isChatCompletionResponse(response)) { + addChatCompletionAttributes(span, response); + if (recordOutputs && response.choices?.length) { + const responseTexts = response.choices.map(choice => choice.message?.content || ''); + span.setAttributes({ [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: JSON.stringify(responseTexts) }); + } + } else if (isResponsesApiResponse(response)) { + addResponsesApiAttributes(span, response); + if (recordOutputs && response.output_text) { + span.setAttributes({ [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: response.output_text }); + } + } +} + +// Extract and record AI request inputs, if present. This is intentionally separate from response attributes. +function addRequestAttributes(span: Span, params: Record): void { + if ('messages' in params) { + span.setAttributes({ [GEN_AI_REQUEST_MESSAGES_ATTRIBUTE]: JSON.stringify(params.messages) }); + } + if ('input' in params) { + span.setAttributes({ [GEN_AI_REQUEST_MESSAGES_ATTRIBUTE]: JSON.stringify(params.input) }); + } +} + +function getOptionsFromIntegration(): OpenAiOptions { + const scope = getCurrentScope(); + const client = scope.getClient(); + const integration = client?.getIntegrationByName(OPENAI_INTEGRATION_NAME) as OpenAiIntegration | undefined; + const shouldRecordInputsAndOutputs = integration ? Boolean(client?.getOptions().sendDefaultPii) : false; + + return { + recordInputs: integration?.options?.recordInputs ?? shouldRecordInputsAndOutputs, + recordOutputs: integration?.options?.recordOutputs ?? shouldRecordInputsAndOutputs, + }; +} + +/** + * Instrument a method with Sentry spans + * Following Sentry AI Agents Manual Instrumentation conventions + * @see https://docs.sentry.io/platforms/javascript/guides/node/tracing/instrumentation/ai-agents-module/#manual-instrumentation + */ +function instrumentMethod( + originalMethod: (...args: T) => Promise, + methodPath: InstrumentedMethod, + context: unknown, + options?: OpenAiOptions, +): (...args: T) => Promise { + return async function instrumentedMethod(...args: T): Promise { + const finalOptions = options || getOptionsFromIntegration(); + const requestAttributes = extractRequestAttributes(args, methodPath); + const model = (requestAttributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE] as string) || 'unknown'; + const operationName = getOperationName(methodPath); + + return startSpan( + { + name: `${operationName} ${model}`, + op: getSpanOperation(methodPath), + attributes: requestAttributes as Record, + }, + async (span: Span) => { + try { + if (finalOptions.recordInputs && args[0] && typeof args[0] === 'object') { + addRequestAttributes(span, args[0] as Record); + } + + const result = await originalMethod.apply(context, args); + // TODO: Add streaming support + addResponseAttributes(span, result, finalOptions.recordOutputs); + return result; + } catch (error) { + captureException(error); + throw error; + } + }, + ); + }; +} + +/** + * Create a deep proxy for OpenAI client instrumentation + */ +function createDeepProxy(target: object, currentPath = '', options?: OpenAiOptions): OpenAiClient { + return new Proxy(target, { + get(obj: object, prop: string): unknown { + const value = (obj as Record)[prop]; + const methodPath = buildMethodPath(currentPath, String(prop)); + + if (typeof value === 'function' && shouldInstrument(methodPath)) { + return instrumentMethod(value as (...args: unknown[]) => Promise, methodPath, obj, options); + } + + if (value && typeof value === 'object') { + return createDeepProxy(value as object, methodPath, options); + } + + return value; + }, + }); +} + +/** + * Instrument an OpenAI client with Sentry tracing + * Can be used across Node.js, Cloudflare Workers, and Vercel Edge + */ +export function instrumentOpenAiClient(client: OpenAiClient, options?: OpenAiOptions): OpenAiClient { + return createDeepProxy(client, '', options); +} diff --git a/packages/core/src/utils/openai/types.ts b/packages/core/src/utils/openai/types.ts new file mode 100644 index 000000000000..c9a3870a959e --- /dev/null +++ b/packages/core/src/utils/openai/types.ts @@ -0,0 +1,143 @@ +import type { INSTRUMENTED_METHODS } from './constants'; + +/** + * Attribute values may be any non-nullish primitive value except an object. + * + * null or undefined attribute values are invalid and will result in undefined behavior. + */ +export type AttributeValue = + | string + | number + | boolean + | Array + | Array + | Array; + +export interface OpenAiOptions { + /** + * Enable or disable input recording. Enabled if `sendDefaultPii` is `true` + */ + recordInputs?: boolean; + /** + * Enable or disable output recording. Enabled if `sendDefaultPii` is `true` + */ + recordOutputs?: boolean; +} + +export interface OpenAiClient { + responses?: { + create: (...args: unknown[]) => Promise; + }; + chat?: { + completions?: { + create: (...args: unknown[]) => Promise; + }; + }; +} + +/** + * @see https://platform.openai.com/docs/api-reference/chat/object + */ +export interface OpenAiChatCompletionObject { + id: string; + object: 'chat.completion'; + created: number; + model: string; + choices: Array<{ + index: number; + message: { + role: 'assistant' | 'user' | 'system' | string; + content: string | null; + refusal?: string | null; + annotations?: Array; // Depends on whether annotations are enabled + }; + logprobs?: unknown | null; + finish_reason: string | null; + }>; + usage: { + prompt_tokens: number; + completion_tokens: number; + total_tokens: number; + prompt_tokens_details?: { + cached_tokens?: number; + audio_tokens?: number; + }; + completion_tokens_details?: { + reasoning_tokens?: number; + audio_tokens?: number; + accepted_prediction_tokens?: number; + rejected_prediction_tokens?: number; + }; + }; + service_tier?: string; + system_fingerprint?: string; +} + +/** + * @see https://platform.openai.com/docs/api-reference/responses/object + */ +export interface OpenAIResponseObject { + id: string; + object: 'response'; + created_at: number; + status: 'in_progress' | 'completed' | 'failed' | 'cancelled'; + error: string | null; + incomplete_details: unknown | null; + instructions: unknown | null; + max_output_tokens: number | null; + model: string; + output: Array<{ + type: 'message'; + id: string; + status: 'completed' | string; + role: 'assistant' | string; + content: Array<{ + type: 'output_text'; + text: string; + annotations: Array; + }>; + }>; + output_text: string; // Direct text output field + parallel_tool_calls: boolean; + previous_response_id: string | null; + reasoning: { + effort: string | null; + summary: string | null; + }; + store: boolean; + temperature: number; + text: { + format: { + type: 'text' | string; + }; + }; + tool_choice: 'auto' | string; + tools: Array; + top_p: number; + truncation: 'disabled' | string; + usage: { + input_tokens: number; + input_tokens_details?: { + cached_tokens?: number; + }; + output_tokens: number; + output_tokens_details?: { + reasoning_tokens?: number; + }; + total_tokens: number; + }; + user: string | null; + metadata: Record; +} + +export type OpenAiResponse = OpenAiChatCompletionObject | OpenAIResponseObject; + +/** + * OpenAI Integration interface for type safety + */ +export interface OpenAiIntegration { + name: string; + options: OpenAiOptions; +} + +export type InstrumentedMethod = (typeof INSTRUMENTED_METHODS)[number]; diff --git a/packages/core/src/utils/openai/utils.ts b/packages/core/src/utils/openai/utils.ts new file mode 100644 index 000000000000..b7d5e12ecf62 --- /dev/null +++ b/packages/core/src/utils/openai/utils.ts @@ -0,0 +1,63 @@ +import { OPENAI_OPERATIONS } from '../gen-ai-attributes'; +import { INSTRUMENTED_METHODS } from './constants'; +import type { InstrumentedMethod, OpenAiChatCompletionObject, OpenAIResponseObject } from './types'; + +/** + * Maps OpenAI method paths to Sentry operation names + */ +export function getOperationName(methodPath: string): string { + if (methodPath.includes('chat.completions')) { + return OPENAI_OPERATIONS.CHAT; + } + if (methodPath.includes('responses')) { + // The responses API is also a chat operation + return OPENAI_OPERATIONS.CHAT; + } + return methodPath.split('.').pop() || 'unknown'; +} + +/** + * Get the span operation for OpenAI methods + * Following Sentry's convention: "gen_ai.{operation_name}" + */ +export function getSpanOperation(methodPath: string): string { + return `gen_ai.${getOperationName(methodPath)}`; +} + +/** + * Check if a method path should be instrumented + */ +export function shouldInstrument(methodPath: string): methodPath is InstrumentedMethod { + return INSTRUMENTED_METHODS.includes(methodPath as InstrumentedMethod); +} + +/** + * Build method path from current traversal + */ +export function buildMethodPath(currentPath: string, prop: string): string { + return currentPath ? `${currentPath}.${prop}` : prop; +} + +/** + * Check if response is a Chat Completion object + */ +export function isChatCompletionResponse(response: unknown): response is OpenAiChatCompletionObject { + return ( + response !== null && + typeof response === 'object' && + 'object' in response && + (response as Record).object === 'chat.completion' + ); +} + +/** + * Check if response is a Responses API object + */ +export function isResponsesApiResponse(response: unknown): response is OpenAIResponseObject { + return ( + response !== null && + typeof response === 'object' && + 'object' in response && + (response as Record).object === 'response' + ); +} diff --git a/packages/core/src/utils/spanUtils.ts b/packages/core/src/utils/spanUtils.ts index 361be95b6510..b6761c9930e7 100644 --- a/packages/core/src/utils/spanUtils.ts +++ b/packages/core/src/utils/spanUtils.ts @@ -14,11 +14,11 @@ import type { TraceContext } from '../types-hoist/context'; import type { SpanLink, SpanLinkJSON } from '../types-hoist/link'; import type { Span, SpanAttributes, SpanJSON, SpanOrigin, SpanTimeInput } from '../types-hoist/span'; import type { SpanStatus } from '../types-hoist/spanStatus'; -import { consoleSandbox } from '../utils/logger'; import { addNonEnumerableProperty } from '../utils/object'; import { generateSpanId } from '../utils/propagationContext'; import { timestampInSeconds } from '../utils/time'; import { generateSentryTraceHeader } from '../utils/tracing'; +import { consoleSandbox } from './debug-logger'; import { _getSpanForScope } from './spanOnScope'; // These are aligned with OpenTelemetry trace flags diff --git a/packages/core/src/utils/stacktrace.ts b/packages/core/src/utils/stacktrace.ts index 2c95280d0407..1aa1fa5ab92d 100644 --- a/packages/core/src/utils/stacktrace.ts +++ b/packages/core/src/utils/stacktrace.ts @@ -133,7 +133,7 @@ export function getFunctionName(fn: unknown): string { return defaultFunctionName; } return fn.name || defaultFunctionName; - } catch (e) { + } catch { // Just accessing custom props in some Selenium environments // can cause a "Permission denied" exception (see raven-js#495). return defaultFunctionName; @@ -158,7 +158,7 @@ export function getFramesFromEvent(event: Event): StackFrame[] | undefined { } }); return frames; - } catch (_oO) { + } catch { return undefined; } } diff --git a/packages/core/src/utils/string.ts b/packages/core/src/utils/string.ts index ab98c794f681..34a1abd4eb46 100644 --- a/packages/core/src/utils/string.ts +++ b/packages/core/src/utils/string.ts @@ -85,7 +85,7 @@ export function safeJoin(input: unknown[], delimiter?: string): string { } else { output.push(String(value)); } - } catch (e) { + } catch { output.push('[value cannot be serialized]'); } } diff --git a/packages/core/src/utils/supports.ts b/packages/core/src/utils/supports.ts index 51f839ad8918..5c7f173c3d6e 100644 --- a/packages/core/src/utils/supports.ts +++ b/packages/core/src/utils/supports.ts @@ -1,5 +1,5 @@ import { DEBUG_BUILD } from '../debug-build'; -import { debug } from './logger'; +import { debug } from './debug-logger'; import { GLOBAL_OBJ } from './worldwide'; const WINDOW = GLOBAL_OBJ as unknown as Window; @@ -16,7 +16,7 @@ export function supportsErrorEvent(): boolean { try { new ErrorEvent(''); return true; - } catch (e) { + } catch { return false; } } @@ -34,7 +34,7 @@ export function supportsDOMError(): boolean { // @ts-expect-error It really needs 1 argument, not 0. new DOMError(''); return true; - } catch (e) { + } catch { return false; } } @@ -49,7 +49,7 @@ export function supportsDOMException(): boolean { try { new DOMException(''); return true; - } catch (e) { + } catch { return false; } } @@ -83,7 +83,7 @@ function _isFetchSupported(): boolean { new Request('http://www.example.com'); new Response(); return true; - } catch (e) { + } catch { return false; } } @@ -172,7 +172,7 @@ export function supportsReferrerPolicy(): boolean { referrerPolicy: 'origin' as ReferrerPolicy, }); return true; - } catch (e) { + } catch { return false; } } diff --git a/packages/core/src/utils/traceData.ts b/packages/core/src/utils/traceData.ts index 2a11ed56f24a..1c1912d147a5 100644 --- a/packages/core/src/utils/traceData.ts +++ b/packages/core/src/utils/traceData.ts @@ -8,7 +8,7 @@ import { getDynamicSamplingContextFromScope, getDynamicSamplingContextFromSpan } import type { Span } from '../types-hoist/span'; import type { SerializedTraceData } from '../types-hoist/tracing'; import { dynamicSamplingContextToSentryBaggageHeader } from './baggage'; -import { debug } from './logger'; +import { debug } from './debug-logger'; import { getActiveSpan, spanToTraceHeader } from './spanUtils'; import { generateSentryTraceHeader, TRACEPARENT_REGEXP } from './tracing'; diff --git a/packages/core/src/utils/vercel-ai-attributes.ts b/packages/core/src/utils/vercel-ai-attributes.ts index 8d7b6913a636..ac6774b08a02 100644 --- a/packages/core/src/utils/vercel-ai-attributes.ts +++ b/packages/core/src/utils/vercel-ai-attributes.ts @@ -1,3 +1,4 @@ +/* eslint-disable max-lines */ /** * AI SDK Telemetry Attributes * Based on https://ai-sdk.dev/docs/ai-sdk-core/telemetry#collected-data @@ -269,6 +270,15 @@ export const AI_MODEL_PROVIDER_ATTRIBUTE = 'ai.model.provider'; */ export const AI_REQUEST_HEADERS_ATTRIBUTE = 'ai.request.headers'; +/** + * Basic LLM span information + * Multiple spans + * + * Provider specific metadata returned with the generation response + * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#basic-llm-span-information + */ +export const AI_RESPONSE_PROVIDER_METADATA_ATTRIBUTE = 'ai.response.providerMetadata'; + /** * Basic LLM span information * Multiple spans @@ -792,3 +802,225 @@ export const AI_TOOL_CALL_SPAN_ATTRIBUTES = { AI_TELEMETRY_FUNCTION_ID: AI_TELEMETRY_FUNCTION_ID_ATTRIBUTE, AI_TELEMETRY_METADATA: AI_TELEMETRY_METADATA_ATTRIBUTE, } as const; + +// ============================================================================= +// PROVIDER METADATA +// ============================================================================= + +/** + * OpenAI Provider Metadata + * @see https://ai-sdk.dev/providers/ai-sdk-providers/openai + * @see https://github.com/vercel/ai/blob/65e042afde6aad4da9d7a62526ece839eb34f9a5/packages/openai/src/openai-chat-language-model.ts#L397-L416 + * @see https://github.com/vercel/ai/blob/65e042afde6aad4da9d7a62526ece839eb34f9a5/packages/openai/src/responses/openai-responses-language-model.ts#L377C7-L384 + */ +interface OpenAiProviderMetadata { + /** + * The number of predicted output tokens that were accepted. + * @see https://ai-sdk.dev/providers/ai-sdk-providers/openai#predicted-outputs + */ + acceptedPredictionTokens?: number; + + /** + * The number of predicted output tokens that were rejected. + * @see https://ai-sdk.dev/providers/ai-sdk-providers/openai#predicted-outputs + */ + rejectedPredictionTokens?: number; + + /** + * The number of reasoning tokens that the model generated. + * @see https://ai-sdk.dev/providers/ai-sdk-providers/openai#responses-models + */ + reasoningTokens?: number; + + /** + * The number of prompt tokens that were a cache hit. + * @see https://ai-sdk.dev/providers/ai-sdk-providers/openai#responses-models + */ + cachedPromptTokens?: number; + + /** + * @see https://ai-sdk.dev/providers/ai-sdk-providers/openai#responses-models + * + * The ID of the response. Can be used to continue a conversation. + */ + responseId?: string; +} + +/** + * Anthropic Provider Metadata + * @see https://ai-sdk.dev/providers/ai-sdk-providers/anthropic + * @see https://github.com/vercel/ai/blob/65e042afde6aad4da9d7a62526ece839eb34f9a5/packages/anthropic/src/anthropic-messages-language-model.ts#L346-L352 + */ +interface AnthropicProviderMetadata { + /** + * The number of tokens that were used to create the cache. + * @see https://ai-sdk.dev/providers/ai-sdk-providers/anthropic#cache-control + */ + cacheCreationInputTokens?: number; + + /** + * The number of tokens that were read from the cache. + * @see https://ai-sdk.dev/providers/ai-sdk-providers/anthropic#cache-control + */ + cacheReadInputTokens?: number; +} + +/** + * Amazon Bedrock Provider Metadata + * @see https://ai-sdk.dev/providers/ai-sdk-providers/amazon-bedrock + * @see https://github.com/vercel/ai/blob/65e042afde6aad4da9d7a62526ece839eb34f9a5/packages/amazon-bedrock/src/bedrock-chat-language-model.ts#L263-L280 + */ +interface AmazonBedrockProviderMetadata { + /** + * @see https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ConverseTrace.html + */ + trace?: { + /** + * The guardrail trace object. + * @see https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_GuardrailTraceAssessment.html + * + * This was purposely left as unknown as it's a complex object. This can be typed in the future + * if the SDK decides to support bedrock in a more advanced way. + */ + guardrail?: unknown; + /** + * The request's prompt router. + * @see https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_PromptRouterTrace.html + */ + promptRouter?: { + /** + * The ID of the invoked model. + */ + invokedModelId?: string; + }; + }; + usage?: { + /** + * The number of tokens that were read from the cache. + * @see https://ai-sdk.dev/providers/ai-sdk-providers/amazon-bedrock#cache-points + */ + cacheReadInputTokens?: number; + + /** + * The number of tokens that were written to the cache. + * @see https://ai-sdk.dev/providers/ai-sdk-providers/amazon-bedrock#cache-points + */ + cacheWriteInputTokens?: number; + }; +} + +/** + * Google Generative AI Provider Metadata + * @see https://ai-sdk.dev/providers/ai-sdk-providers/google-generative-ai + */ +export interface GoogleGenerativeAIProviderMetadata { + /** + * @see https://github.com/vercel/ai/blob/65e042afde6aad4da9d7a62526ece839eb34f9a5/packages/google/src/google-generative-ai-prompt.ts#L28-L30 + */ + groundingMetadata: null | { + /** + * Array of search queries used to retrieve information + * @example ["What's the weather in Chicago this weekend?"] + * + * @see https://ai-sdk.dev/providers/ai-sdk-providers/google-generative-ai#search-grounding + */ + webSearchQueries: string[] | null; + /** + * Contains the main search result content used as an entry point + * The `renderedContent` field contains the formatted content + * @see https://ai-sdk.dev/providers/ai-sdk-providers/google-generative-ai#search-grounding + */ + searchEntryPoint?: { + renderedContent: string; + } | null; + /** + * Contains details about how specific response parts are supported by search results + * @see https://ai-sdk.dev/providers/ai-sdk-providers/google-generative-ai#search-grounding + */ + groundingSupports: Array<{ + /** + * Information about the grounded text segment. + */ + segment: { + /** + * The start index of the text segment. + */ + startIndex?: number | null; + /** + * The end index of the text segment. + */ + endIndex?: number | null; + /** + * The actual text segment. + */ + text?: string | null; + }; + /** + * References to supporting search result chunks. + */ + groundingChunkIndices?: number[] | null; + /** + * Confidence scores (0-1) for each supporting chunk. + */ + confidenceScores?: number[] | null; + }> | null; + }; + /** + * @see https://github.com/vercel/ai/blob/65e042afde6aad4da9d7a62526ece839eb34f9a5/packages/google/src/google-generative-ai-language-model.ts#L620-L627 + * @see https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/configure-safety-filters + */ + safetyRatings?: null | unknown; +} + +/** + * DeepSeek Provider Metadata + * @see https://ai-sdk.dev/providers/ai-sdk-providers/deepseek + */ +interface DeepSeekProviderMetadata { + /** + * The number of tokens that were cache hits. + * @see https://ai-sdk.dev/providers/ai-sdk-providers/deepseek#cache-token-usage + */ + promptCacheHitTokens?: number; + + /** + * The number of tokens that were cache misses. + * @see https://ai-sdk.dev/providers/ai-sdk-providers/deepseek#cache-token-usage + */ + promptCacheMissTokens?: number; +} + +/** + * Perplexity Provider Metadata + * @see https://ai-sdk.dev/providers/ai-sdk-providers/perplexity + */ +interface PerplexityProviderMetadata { + /** + * Object containing citationTokens and numSearchQueries metrics + */ + usage?: { + citationTokens?: number; + numSearchQueries?: number; + }; + /** + * Array of image URLs when return_images is enabled. + * + * You can enable image responses by setting return_images: true in the provider options. + * This feature is only available to Perplexity Tier-2 users and above. + */ + images?: Array<{ + imageUrl?: string; + originUrl?: string; + height?: number; + width?: number; + }>; +} + +export interface ProviderMetadata { + openai?: OpenAiProviderMetadata; + anthropic?: AnthropicProviderMetadata; + bedrock?: AmazonBedrockProviderMetadata; + google?: GoogleGenerativeAIProviderMetadata; + deepseek?: DeepSeekProviderMetadata; + perplexity?: PerplexityProviderMetadata; +} diff --git a/packages/core/src/utils/vercel-ai.ts b/packages/core/src/utils/vercel-ai.ts index b1a2feedb454..c5491376d7c4 100644 --- a/packages/core/src/utils/vercel-ai.ts +++ b/packages/core/src/utils/vercel-ai.ts @@ -1,14 +1,16 @@ import type { Client } from '../client'; import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../semanticAttributes'; import type { Event } from '../types-hoist/event'; -import type { Span, SpanAttributes, SpanJSON, SpanOrigin } from '../types-hoist/span'; +import type { Span, SpanAttributes, SpanAttributeValue, SpanJSON, SpanOrigin } from '../types-hoist/span'; import { spanToJSON } from './spanUtils'; +import type { ProviderMetadata } from './vercel-ai-attributes'; import { AI_MODEL_ID_ATTRIBUTE, AI_MODEL_PROVIDER_ATTRIBUTE, AI_PROMPT_ATTRIBUTE, AI_PROMPT_MESSAGES_ATTRIBUTE, AI_PROMPT_TOOLS_ATTRIBUTE, + AI_RESPONSE_PROVIDER_METADATA_ATTRIBUTE, AI_RESPONSE_TEXT_ATTRIBUTE, AI_RESPONSE_TOOL_CALLS_ATTRIBUTE, AI_TELEMETRY_FUNCTION_ID_ATTRIBUTE, @@ -96,6 +98,8 @@ function processEndedVercelAiSpan(span: SpanJSON): void { renameAttributeKey(attributes, AI_TOOL_CALL_ARGS_ATTRIBUTE, 'gen_ai.tool.input'); renameAttributeKey(attributes, AI_TOOL_CALL_RESULT_ATTRIBUTE, 'gen_ai.tool.output'); + addProviderMetadataToAttributes(attributes); + // Change attributes namespaced with `ai.X` to `vercel.ai.X` for (const key of Object.keys(attributes)) { if (key.startsWith('ai.')) { @@ -234,3 +238,85 @@ export function addVercelAiProcessors(client: Client): void { // Note: We cannot do this on `spanEnd`, because the span cannot be mutated anymore at this point client.addEventProcessor(Object.assign(vercelAiEventProcessor, { id: 'VercelAiEventProcessor' })); } + +function addProviderMetadataToAttributes(attributes: SpanAttributes): void { + const providerMetadata = attributes[AI_RESPONSE_PROVIDER_METADATA_ATTRIBUTE] as string | undefined; + if (providerMetadata) { + try { + const providerMetadataObject = JSON.parse(providerMetadata) as ProviderMetadata; + if (providerMetadataObject.openai) { + setAttributeIfDefined( + attributes, + 'gen_ai.usage.input_tokens.cached', + providerMetadataObject.openai.cachedPromptTokens, + ); + setAttributeIfDefined( + attributes, + 'gen_ai.usage.output_tokens.reasoning', + providerMetadataObject.openai.reasoningTokens, + ); + setAttributeIfDefined( + attributes, + 'gen_ai.usage.output_tokens.prediction_accepted', + providerMetadataObject.openai.acceptedPredictionTokens, + ); + setAttributeIfDefined( + attributes, + 'gen_ai.usage.output_tokens.prediction_rejected', + providerMetadataObject.openai.rejectedPredictionTokens, + ); + setAttributeIfDefined(attributes, 'gen_ai.conversation.id', providerMetadataObject.openai.responseId); + } + + if (providerMetadataObject.anthropic) { + setAttributeIfDefined( + attributes, + 'gen_ai.usage.input_tokens.cached', + providerMetadataObject.anthropic.cacheReadInputTokens, + ); + setAttributeIfDefined( + attributes, + 'gen_ai.usage.input_tokens.cache_write', + providerMetadataObject.anthropic.cacheCreationInputTokens, + ); + } + + if (providerMetadataObject.bedrock?.usage) { + setAttributeIfDefined( + attributes, + 'gen_ai.usage.input_tokens.cached', + providerMetadataObject.bedrock.usage.cacheReadInputTokens, + ); + setAttributeIfDefined( + attributes, + 'gen_ai.usage.input_tokens.cache_write', + providerMetadataObject.bedrock.usage.cacheWriteInputTokens, + ); + } + + if (providerMetadataObject.deepseek) { + setAttributeIfDefined( + attributes, + 'gen_ai.usage.input_tokens.cached', + providerMetadataObject.deepseek.promptCacheHitTokens, + ); + setAttributeIfDefined( + attributes, + 'gen_ai.usage.input_tokens.cache_miss', + providerMetadataObject.deepseek.promptCacheMissTokens, + ); + } + } catch { + // Ignore + } + } +} + +/** + * Sets an attribute only if the value is not null or undefined. + */ +function setAttributeIfDefined(attributes: SpanAttributes, key: string, value: SpanAttributeValue | undefined): void { + if (value != null) { + attributes[key] = value; + } +} diff --git a/packages/core/src/utils/vercelWaitUntil.ts b/packages/core/src/utils/vercelWaitUntil.ts index f9bae863ce9a..bfcaa6b4b832 100644 --- a/packages/core/src/utils/vercelWaitUntil.ts +++ b/packages/core/src/utils/vercelWaitUntil.ts @@ -18,8 +18,7 @@ export function vercelWaitUntil(task: Promise): void { // @ts-expect-error This is not typed GLOBAL_OBJ[Symbol.for('@vercel/request-context')]; - const ctx = - vercelRequestContextGlobal?.get && vercelRequestContextGlobal.get() ? vercelRequestContextGlobal.get() : {}; + const ctx = vercelRequestContextGlobal?.get?.(); if (ctx?.waitUntil) { ctx.waitUntil(task); diff --git a/packages/core/test/lib/client.test.ts b/packages/core/test/lib/client.test.ts index 7962b43cf757..662d9b6adf67 100644 --- a/packages/core/test/lib/client.test.ts +++ b/packages/core/test/lib/client.test.ts @@ -16,7 +16,7 @@ import * as integrationModule from '../../src/integration'; import type { Envelope } from '../../src/types-hoist/envelope'; import type { ErrorEvent, Event, TransactionEvent } from '../../src/types-hoist/event'; import type { SpanJSON } from '../../src/types-hoist/span'; -import * as loggerModule from '../../src/utils/logger'; +import * as debugLoggerModule from '../../src/utils/debug-logger'; import * as miscModule from '../../src/utils/misc'; import * as stringModule from '../../src/utils/string'; import * as timeModule from '../../src/utils/time'; @@ -33,7 +33,7 @@ const clientEventFromException = vi.spyOn(TestClient.prototype, 'eventFromExcept const clientProcess = vi.spyOn(TestClient.prototype as any, '_process'); vi.spyOn(miscModule, 'uuid4').mockImplementation(() => '12312012123120121231201212312012'); -vi.spyOn(loggerModule, 'consoleSandbox').mockImplementation(cb => cb()); +vi.spyOn(debugLoggerModule, 'consoleSandbox').mockImplementation(cb => cb()); vi.spyOn(stringModule, 'truncate').mockImplementation(str => str); vi.spyOn(timeModule, 'dateTimestampInSeconds').mockImplementation(() => 2020); @@ -349,7 +349,7 @@ describe('Client', () => { }); test('captures debug message', () => { - const logSpy = vi.spyOn(loggerModule.debug, 'log').mockImplementation(() => undefined); + const logSpy = vi.spyOn(debugLoggerModule.debug, 'log').mockImplementation(() => undefined); const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); const client = new TestClient(options); @@ -441,7 +441,7 @@ describe('Client', () => { }); test('captures debug message', () => { - const logSpy = vi.spyOn(loggerModule.debug, 'log').mockImplementation(() => undefined); + const logSpy = vi.spyOn(debugLoggerModule.debug, 'log').mockImplementation(() => undefined); const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); const client = new TestClient(options); @@ -1207,7 +1207,7 @@ describe('Client', () => { const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, beforeSend }); const client = new TestClient(options); const captureExceptionSpy = vi.spyOn(client, 'captureException'); - const loggerLogSpy = vi.spyOn(loggerModule.debug, 'log'); + const loggerLogSpy = vi.spyOn(debugLoggerModule.debug, 'log'); client.captureEvent({ message: 'hello' }); @@ -1226,7 +1226,7 @@ describe('Client', () => { const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, beforeSendTransaction }); const client = new TestClient(options); const captureExceptionSpy = vi.spyOn(client, 'captureException'); - const loggerLogSpy = vi.spyOn(loggerModule.debug, 'log'); + const loggerLogSpy = vi.spyOn(debugLoggerModule.debug, 'log'); client.captureEvent({ transaction: '/dogs/are/great', type: 'transaction' }); @@ -1288,7 +1288,7 @@ describe('Client', () => { // @ts-expect-error we need to test regular-js behavior const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, beforeSend }); const client = new TestClient(options); - const loggerWarnSpy = vi.spyOn(loggerModule.debug, 'warn'); + const loggerWarnSpy = vi.spyOn(debugLoggerModule.debug, 'warn'); client.captureEvent({ message: 'hello' }); @@ -1307,7 +1307,7 @@ describe('Client', () => { // @ts-expect-error we need to test regular-js behavior const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, beforeSendTransaction }); const client = new TestClient(options); - const loggerWarnSpy = vi.spyOn(loggerModule.debug, 'warn'); + const loggerWarnSpy = vi.spyOn(debugLoggerModule.debug, 'warn'); client.captureEvent({ transaction: '/dogs/are/great', type: 'transaction' }); @@ -1551,7 +1551,7 @@ describe('Client', () => { const client = new TestClient(getDefaultTestClientOptions({ dsn: PUBLIC_DSN })); const captureExceptionSpy = vi.spyOn(client, 'captureException'); - const loggerLogSpy = vi.spyOn(loggerModule.debug, 'log'); + const loggerLogSpy = vi.spyOn(debugLoggerModule.debug, 'log'); const scope = new Scope(); scope.addEventProcessor(() => null); @@ -1569,7 +1569,7 @@ describe('Client', () => { const client = new TestClient(getDefaultTestClientOptions({ dsn: PUBLIC_DSN })); const captureExceptionSpy = vi.spyOn(client, 'captureException'); - const loggerLogSpy = vi.spyOn(loggerModule.debug, 'log'); + const loggerLogSpy = vi.spyOn(debugLoggerModule.debug, 'log'); const scope = new Scope(); scope.addEventProcessor(() => null); @@ -1668,7 +1668,7 @@ describe('Client', () => { const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); const client = new TestClient(options); const captureExceptionSpy = vi.spyOn(client, 'captureException'); - const loggerWarnSpy = vi.spyOn(loggerModule.debug, 'warn'); + const loggerWarnSpy = vi.spyOn(debugLoggerModule.debug, 'warn'); const scope = new Scope(); const exception = new Error('sorry'); scope.addEventProcessor(() => { @@ -1702,7 +1702,7 @@ describe('Client', () => { }); test('captures debug message', () => { - const logSpy = vi.spyOn(loggerModule.debug, 'log').mockImplementation(() => undefined); + const logSpy = vi.spyOn(debugLoggerModule.debug, 'log').mockImplementation(() => undefined); const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); const client = new TestClient(options); diff --git a/packages/core/test/lib/integration.test.ts b/packages/core/test/lib/integration.test.ts index e0e7ff3e5740..5b7554f261b1 100644 --- a/packages/core/test/lib/integration.test.ts +++ b/packages/core/test/lib/integration.test.ts @@ -4,7 +4,7 @@ import { addIntegration, getIntegrationsToSetup, installedIntegrations, setupInt import { setCurrentClient } from '../../src/sdk'; import type { Integration } from '../../src/types-hoist/integration'; import type { Options } from '../../src/types-hoist/options'; -import { debug } from '../../src/utils/logger'; +import { debug } from '../../src/utils/debug-logger'; import { getDefaultTestClientOptions, TestClient } from '../mocks/client'; function getTestClient(): TestClient { diff --git a/packages/core/test/lib/integrations/captureconsole.test.ts b/packages/core/test/lib/integrations/captureconsole.test.ts index bbc3af2722e9..2fe971710a07 100644 --- a/packages/core/test/lib/integrations/captureconsole.test.ts +++ b/packages/core/test/lib/integrations/captureconsole.test.ts @@ -9,7 +9,7 @@ import { resetInstrumentationHandlers } from '../../../src/instrument/handlers'; import { captureConsoleIntegration } from '../../../src/integrations/captureconsole'; import type { Event } from '../../../src/types-hoist/event'; import type { ConsoleLevel } from '../../../src/types-hoist/instrument'; -import { CONSOLE_LEVELS, originalConsoleMethods } from '../../../src/utils/logger'; +import { CONSOLE_LEVELS, originalConsoleMethods } from '../../../src/utils/debug-logger'; import { GLOBAL_OBJ } from '../../../src/utils/worldwide'; const mockConsole: { [key in ConsoleLevel]: Mock } = { diff --git a/packages/core/test/lib/logs/exports.test.ts b/packages/core/test/lib/logs/exports.test.ts index 1f93e3609f2f..536ae9a2adfd 100644 --- a/packages/core/test/lib/logs/exports.test.ts +++ b/packages/core/test/lib/logs/exports.test.ts @@ -7,7 +7,7 @@ import { logAttributeToSerializedLogAttribute, } from '../../../src/logs/exports'; import type { Log } from '../../../src/types-hoist/log'; -import * as loggerModule from '../../../src/utils/logger'; +import * as loggerModule from '../../../src/utils/debug-logger'; import { getDefaultTestClientOptions, TestClient } from '../../mocks/client'; const PUBLIC_DSN = 'https://username@domain/123'; diff --git a/packages/core/test/lib/tracing/trace.test.ts b/packages/core/test/lib/tracing/trace.test.ts index eccbb57f1610..31d04f0f971d 100644 --- a/packages/core/test/lib/tracing/trace.test.ts +++ b/packages/core/test/lib/tracing/trace.test.ts @@ -94,7 +94,7 @@ describe('startSpan', () => { await startSpan({ name: 'GET users/[id]' }, () => { return callback(); }); - } catch (e) { + } catch { // } expect(_span).toBeDefined(); @@ -113,7 +113,7 @@ describe('startSpan', () => { span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'http.server'); return callback(); }); - } catch (e) { + } catch { // } @@ -133,7 +133,7 @@ describe('startSpan', () => { return callback(); }); }); - } catch (e) { + } catch { // } @@ -160,7 +160,7 @@ describe('startSpan', () => { return callback(); }); }); - } catch (e) { + } catch { // } @@ -186,7 +186,7 @@ describe('startSpan', () => { return callback(); }, ); - } catch (e) { + } catch { // } diff --git a/packages/core/test/lib/utils/dsn.test.ts b/packages/core/test/lib/utils/dsn.test.ts index 81c4f9f9ead2..5d1cfbe2b538 100644 --- a/packages/core/test/lib/utils/dsn.test.ts +++ b/packages/core/test/lib/utils/dsn.test.ts @@ -1,7 +1,7 @@ import { beforeEach, describe, expect, it, test, vi } from 'vitest'; import { DEBUG_BUILD } from '../../../src/debug-build'; +import { debug } from '../../../src/utils/debug-logger'; import { dsnToString, extractOrgIdFromDsnHost, makeDsn } from '../../../src/utils/dsn'; -import { debug } from '../../../src/utils/logger'; function testIf(condition: boolean) { return condition ? test : test.skip; diff --git a/packages/core/test/lib/utils/featureFlags.test.ts b/packages/core/test/lib/utils/featureFlags.test.ts index fe76eb8da1ff..9775ff7ccb0b 100644 --- a/packages/core/test/lib/utils/featureFlags.test.ts +++ b/packages/core/test/lib/utils/featureFlags.test.ts @@ -1,11 +1,11 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; import { getCurrentScope } from '../../../src/currentScopes'; +import { debug } from '../../../src/utils/debug-logger'; import { type FeatureFlag, _INTERNAL_insertFlagToScope, _INTERNAL_insertToFlagBuffer, } from '../../../src/utils/featureFlags'; -import { debug } from '../../../src/utils/logger'; describe('flags', () => { describe('insertFlagToScope()', () => { diff --git a/packages/core/test/lib/utils/logger.test.ts b/packages/core/test/lib/utils/logger.test.ts index 4bac1b2b64e4..ad8d5780491b 100644 --- a/packages/core/test/lib/utils/logger.test.ts +++ b/packages/core/test/lib/utils/logger.test.ts @@ -1,61 +1,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { debug, logger } from '../../../src'; +import { debug } from '../../../src'; import { getMainCarrier, getSentryCarrier } from '../../../src/carrier'; -describe('logger', () => { - beforeEach(() => { - vi.clearAllMocks(); - getSentryCarrier(getMainCarrier()).loggerSettings = undefined; - }); - - it('works with defaults', () => { - const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); - logger.log('test'); - expect(consoleLogSpy).not.toHaveBeenCalled(); - expect(logger.isEnabled()).toBe(false); - }); - - it('allows to enable and disable logging', () => { - const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); - - logger.log('test'); - expect(logger.isEnabled()).toBe(false); - expect(consoleLogSpy).not.toHaveBeenCalled(); - - logger.enable(); - logger.log('test'); - expect(logger.isEnabled()).toBe(true); - expect(consoleLogSpy).toHaveBeenCalledTimes(1); - - logger.log('test2'); - expect(consoleLogSpy).toHaveBeenCalledTimes(2); - - logger.disable(); - - logger.log('test3'); - expect(logger.isEnabled()).toBe(false); - expect(consoleLogSpy).toHaveBeenCalledTimes(2); - }); - - it('picks up enabled logger settings from carrier', () => { - getSentryCarrier(getMainCarrier()).loggerSettings = { enabled: true }; - - const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); - logger.log('test'); - expect(consoleLogSpy).toHaveBeenCalledTimes(1); - expect(logger.isEnabled()).toBe(true); - }); - - it('picks up disabled logger settings from carrier', () => { - getSentryCarrier(getMainCarrier()).loggerSettings = { enabled: false }; - - const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); - logger.log('test'); - expect(consoleLogSpy).toHaveBeenCalledTimes(0); - expect(logger.isEnabled()).toBe(false); - }); -}); - describe('debug', () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/packages/core/test/lib/utils/openai-utils.test.ts b/packages/core/test/lib/utils/openai-utils.test.ts new file mode 100644 index 000000000000..bcff545627ed --- /dev/null +++ b/packages/core/test/lib/utils/openai-utils.test.ts @@ -0,0 +1,104 @@ +import { describe, expect, it } from 'vitest'; +import { + buildMethodPath, + getOperationName, + getSpanOperation, + isChatCompletionResponse, + isResponsesApiResponse, + shouldInstrument, +} from '../../../src/utils/openai/utils'; + +describe('openai-utils', () => { + describe('getOperationName', () => { + it('should return chat for chat.completions methods', () => { + expect(getOperationName('chat.completions.create')).toBe('chat'); + expect(getOperationName('some.path.chat.completions.method')).toBe('chat'); + }); + + it('should return chat for responses methods', () => { + expect(getOperationName('responses.create')).toBe('chat'); + expect(getOperationName('some.path.responses.method')).toBe('chat'); + }); + + it('should return the last part of path for unknown methods', () => { + expect(getOperationName('some.unknown.method')).toBe('method'); + expect(getOperationName('create')).toBe('create'); + }); + + it('should return unknown for empty path', () => { + expect(getOperationName('')).toBe('unknown'); + }); + }); + + describe('getSpanOperation', () => { + it('should prefix operation with gen_ai', () => { + expect(getSpanOperation('chat.completions.create')).toBe('gen_ai.chat'); + expect(getSpanOperation('responses.create')).toBe('gen_ai.chat'); + expect(getSpanOperation('some.custom.operation')).toBe('gen_ai.operation'); + }); + }); + + describe('shouldInstrument', () => { + it('should return true for instrumented methods', () => { + expect(shouldInstrument('responses.create')).toBe(true); + expect(shouldInstrument('chat.completions.create')).toBe(true); + }); + + it('should return false for non-instrumented methods', () => { + expect(shouldInstrument('unknown.method')).toBe(false); + expect(shouldInstrument('')).toBe(false); + }); + }); + + describe('buildMethodPath', () => { + it('should build method path correctly', () => { + expect(buildMethodPath('', 'chat')).toBe('chat'); + expect(buildMethodPath('chat', 'completions')).toBe('chat.completions'); + expect(buildMethodPath('chat.completions', 'create')).toBe('chat.completions.create'); + }); + }); + + describe('isChatCompletionResponse', () => { + it('should return true for valid chat completion responses', () => { + const validResponse = { + object: 'chat.completion', + id: 'chatcmpl-123', + model: 'gpt-4', + choices: [], + }; + expect(isChatCompletionResponse(validResponse)).toBe(true); + }); + + it('should return false for invalid responses', () => { + expect(isChatCompletionResponse(null)).toBe(false); + expect(isChatCompletionResponse(undefined)).toBe(false); + expect(isChatCompletionResponse('string')).toBe(false); + expect(isChatCompletionResponse(123)).toBe(false); + expect(isChatCompletionResponse({})).toBe(false); + expect(isChatCompletionResponse({ object: 'different' })).toBe(false); + expect(isChatCompletionResponse({ object: null })).toBe(false); + }); + }); + + describe('isResponsesApiResponse', () => { + it('should return true for valid responses API responses', () => { + const validResponse = { + object: 'response', + id: 'resp_123', + model: 'gpt-4', + choices: [], + }; + expect(isResponsesApiResponse(validResponse)).toBe(true); + }); + + it('should return false for invalid responses', () => { + expect(isResponsesApiResponse(null)).toBe(false); + expect(isResponsesApiResponse(undefined)).toBe(false); + expect(isResponsesApiResponse('string')).toBe(false); + expect(isResponsesApiResponse(123)).toBe(false); + expect(isResponsesApiResponse({})).toBe(false); + expect(isResponsesApiResponse({ object: 'different' })).toBe(false); + expect(isResponsesApiResponse({ object: null })).toBe(false); + }); + }); +}); diff --git a/packages/core/test/lib/utils/promisebuffer.test.ts b/packages/core/test/lib/utils/promisebuffer.test.ts index 1f0011dd6e50..618de06322a0 100644 --- a/packages/core/test/lib/utils/promisebuffer.test.ts +++ b/packages/core/test/lib/utils/promisebuffer.test.ts @@ -74,7 +74,7 @@ describe('PromiseBuffer', () => { expect(buffer.$.length).toEqual(1); try { await task; - } catch (_) { + } catch { // no-empty } expect(buffer.$.length).toEqual(0); diff --git a/packages/core/test/testutils.ts b/packages/core/test/testutils.ts index c00ec2b878b7..3fc56afa7f75 100644 --- a/packages/core/test/testutils.ts +++ b/packages/core/test/testutils.ts @@ -10,7 +10,7 @@ export const testOnlyIfNodeVersionAtLeast = (minVersion: number): Function => { if (Number(currentNodeVersion?.split('.')[0]) < minVersion) { return it.skip; } - } catch (oO) { + } catch { // we can't tell, so err on the side of running the test } diff --git a/packages/deno/scripts/install-deno.mjs b/packages/deno/scripts/install-deno.mjs index aa7235a278ff..ad962778af88 100644 --- a/packages/deno/scripts/install-deno.mjs +++ b/packages/deno/scripts/install-deno.mjs @@ -4,7 +4,7 @@ import { download } from './download.mjs'; try { execSync('deno --version', { stdio: 'inherit' }); -} catch (_) { +} catch { // eslint-disable-next-line no-console console.error('Deno is not installed. Installing...'); if (process.platform === 'win32') { diff --git a/packages/deno/src/integrations/contextlines.ts b/packages/deno/src/integrations/contextlines.ts index 8b068a17f8cb..cf13252052b8 100644 --- a/packages/deno/src/integrations/contextlines.ts +++ b/packages/deno/src/integrations/contextlines.ts @@ -28,7 +28,7 @@ async function readSourceFile(filename: string): Promise { let content: string | null = null; try { content = await Deno.readTextFile(filename); - } catch (_) { + } catch { // } @@ -102,7 +102,7 @@ async function addSourceContextToFrames(frames: StackFrame[], contextLines: numb try { const lines = sourceFile.split('\n'); addContextToFrame(lines, frame, contextLines); - } catch (_) { + } catch { // anomaly, being defensive in case // unlikely to ever happen in practice but can definitely happen in theory } diff --git a/packages/deno/src/integrations/globalhandlers.ts b/packages/deno/src/integrations/globalhandlers.ts index cb799b0be132..0e3639a7472d 100644 --- a/packages/deno/src/integrations/globalhandlers.ts +++ b/packages/deno/src/integrations/globalhandlers.ts @@ -96,7 +96,7 @@ function installGlobalUnhandledRejectionHandler(client: Client): void { if ('reason' in e) { error = e.reason; } - } catch (_oO) { + } catch { // no-empty } diff --git a/packages/deno/src/integrations/normalizepaths.ts b/packages/deno/src/integrations/normalizepaths.ts index 417fd139b545..4e7e599fb4d3 100644 --- a/packages/deno/src/integrations/normalizepaths.ts +++ b/packages/deno/src/integrations/normalizepaths.ts @@ -54,7 +54,7 @@ function getCwd(): string | undefined { if (permission.state == 'granted') { return Deno.cwd(); } - } catch (_) { + } catch { // } diff --git a/packages/deno/src/transports/index.ts b/packages/deno/src/transports/index.ts index 9a83dadfff63..f6c0ed8d1c52 100644 --- a/packages/deno/src/transports/index.ts +++ b/packages/deno/src/transports/index.ts @@ -1,5 +1,5 @@ import type { BaseTransportOptions, Transport, TransportMakeRequestResponse, TransportRequest } from '@sentry/core'; -import { consoleSandbox, createTransport, logger, rejectedSyncPromise, suppressTracing } from '@sentry/core'; +import { consoleSandbox, createTransport, debug, rejectedSyncPromise, suppressTracing } from '@sentry/core'; export interface DenoTransportOptions extends BaseTransportOptions { /** Custom headers for the transport. Used by the XHRTransport and FetchTransport */ @@ -24,7 +24,7 @@ export function makeFetchTransport(options: DenoTransportOptions): Transport { } }) .catch(() => { - logger.warn('Failed to read the "net" permission.'); + debug.warn('Failed to read the "net" permission.'); }); function makeRequest(request: TransportRequest): PromiseLike { diff --git a/packages/ember/tests/dummy/app/controllers/index.ts b/packages/ember/tests/dummy/app/controllers/index.ts index c49ff8d94147..418b51ccee07 100644 --- a/packages/ember/tests/dummy/app/controllers/index.ts +++ b/packages/ember/tests/dummy/app/controllers/index.ts @@ -22,7 +22,7 @@ export default class IndexController extends Controller { public createCaughtEmberError(): void { try { throw new Error('Looks like you have a caught EmberError'); - } catch (e) { + } catch { // do nothing } } diff --git a/packages/eslint-config-sdk/src/base.js b/packages/eslint-config-sdk/src/base.js index 10e410aaa592..fade0633fc98 100644 --- a/packages/eslint-config-sdk/src/base.js +++ b/packages/eslint-config-sdk/src/base.js @@ -11,6 +11,10 @@ module.exports = { rules: { 'no-unused-vars': ['error', { argsIgnorePattern: '^_' }], }, + parserOptions: { + sourceType: 'module', + ecmaVersion: 2020, + }, }, { // Configuration for typescript files @@ -185,7 +189,7 @@ module.exports = { files: ['*.config.js', '*.config.mjs'], parserOptions: { sourceType: 'module', - ecmaVersion: 2018, + ecmaVersion: 2020, }, }, { diff --git a/packages/feedback/src/core/integration.ts b/packages/feedback/src/core/integration.ts index e5f1092856f1..d70f563b6136 100644 --- a/packages/feedback/src/core/integration.ts +++ b/packages/feedback/src/core/integration.ts @@ -5,7 +5,7 @@ import type { Integration, IntegrationFn, } from '@sentry/core'; -import { addIntegration, isBrowser, logger } from '@sentry/core'; +import { addIntegration, debug, isBrowser } from '@sentry/core'; import { ADD_SCREENSHOT_LABEL, CANCEL_BUTTON_LABEL, @@ -194,7 +194,7 @@ export const buildFeedbackIntegration = ({ addIntegration(modalIntegration); } catch { DEBUG_BUILD && - logger.error( + debug.error( '[Feedback] Error when trying to load feedback integrations. Try using `feedbackSyncIntegration` in your `Sentry.init`.', ); throw new Error('[Feedback] Missing feedback modal integration!'); @@ -213,7 +213,7 @@ export const buildFeedbackIntegration = ({ } } catch { DEBUG_BUILD && - logger.error('[Feedback] Missing feedback screenshot integration. Proceeding without screenshots.'); + debug.error('[Feedback] Missing feedback screenshot integration. Proceeding without screenshots.'); } const dialog = modalIntegration.createDialog({ @@ -243,7 +243,7 @@ export const buildFeedbackIntegration = ({ typeof el === 'string' ? DOCUMENT.querySelector(el) : typeof el.addEventListener === 'function' ? el : null; if (!targetEl) { - DEBUG_BUILD && logger.error('[Feedback] Unable to attach to target element'); + DEBUG_BUILD && debug.error('[Feedback] Unable to attach to target element'); throw new Error('Unable to attach to target element'); } diff --git a/packages/feedback/src/modal/components/Form.tsx b/packages/feedback/src/modal/components/Form.tsx index b0242f9738a6..0f09e969fee5 100644 --- a/packages/feedback/src/modal/components/Form.tsx +++ b/packages/feedback/src/modal/components/Form.tsx @@ -4,7 +4,7 @@ import type { FeedbackScreenshotIntegration, SendFeedback, } from '@sentry/core'; -import { logger } from '@sentry/core'; +import { debug } from '@sentry/core'; import type { JSX, VNode } from 'preact'; import { h } from 'preact'; // eslint-disable-line @typescript-eslint/no-unused-vars import { useCallback, useState } from 'preact/hooks'; @@ -130,7 +130,7 @@ export function Form({ ); onSubmitSuccess(data, eventId); } catch (error) { - DEBUG_BUILD && logger.error(error); + DEBUG_BUILD && debug.error(error); setError(error as string); onSubmitError(error as Error); } diff --git a/packages/feedback/src/modal/integration.tsx b/packages/feedback/src/modal/integration.tsx index c01152b910ef..dd2c341760a7 100644 --- a/packages/feedback/src/modal/integration.tsx +++ b/packages/feedback/src/modal/integration.tsx @@ -70,8 +70,8 @@ export const feedbackModalIntegration = ((): FeedbackModalIntegration => { screenshotInput={screenshotInput} showName={options.showName || options.isNameRequired} showEmail={options.showEmail || options.isEmailRequired} - defaultName={(userKey && user && user[userKey.name]) || ''} - defaultEmail={(userKey && user && user[userKey.email]) || ''} + defaultName={(userKey && user?.[userKey.name]) || ''} + defaultEmail={(userKey && user?.[userKey.email]) || ''} onFormClose={() => { renderContent(false); options.onFormClose?.(); diff --git a/packages/gatsby/gatsby-node.js b/packages/gatsby/gatsby-node.js index 3b40481cd89d..7b96ab049d26 100644 --- a/packages/gatsby/gatsby-node.js +++ b/packages/gatsby/gatsby-node.js @@ -68,7 +68,7 @@ exports.onCreateWebpackConfig = ({ getConfig, actions }, options) => { let configFile = null; try { configFile = SENTRY_USER_CONFIG.find(file => fs.existsSync(file)); - } catch (error) { + } catch { // Some node versions (like v11) throw an exception on `existsSync` instead of // returning false. See https://github.com/tschaub/mock-fs/issues/256 } diff --git a/packages/google-cloud-serverless/src/gcpfunction/cloud_events.ts b/packages/google-cloud-serverless/src/gcpfunction/cloud_events.ts index 8e8bfca26d70..a5e9f886165d 100644 --- a/packages/google-cloud-serverless/src/gcpfunction/cloud_events.ts +++ b/packages/google-cloud-serverless/src/gcpfunction/cloud_events.ts @@ -1,6 +1,6 @@ import { + debug, handleCallbackErrors, - logger, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, } from '@sentry/core'; @@ -56,7 +56,7 @@ function _wrapCloudEventFunction( // eslint-disable-next-line @typescript-eslint/no-floating-promises flush(options.flushTimeout) .then(null, e => { - DEBUG_BUILD && logger.error(e); + DEBUG_BUILD && debug.error(e); }) .then(() => { if (typeof callback === 'function') { diff --git a/packages/google-cloud-serverless/src/gcpfunction/events.ts b/packages/google-cloud-serverless/src/gcpfunction/events.ts index 674e9033d7c0..2fece298ea05 100644 --- a/packages/google-cloud-serverless/src/gcpfunction/events.ts +++ b/packages/google-cloud-serverless/src/gcpfunction/events.ts @@ -1,6 +1,6 @@ import { + debug, handleCallbackErrors, - logger, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, } from '@sentry/core'; @@ -59,7 +59,7 @@ function _wrapEventFunction // eslint-disable-next-line @typescript-eslint/no-floating-promises flush(options.flushTimeout) .then(null, e => { - DEBUG_BUILD && logger.error(e); + DEBUG_BUILD && debug.error(e); }) .then(() => { if (typeof callback === 'function') { diff --git a/packages/google-cloud-serverless/src/gcpfunction/http.ts b/packages/google-cloud-serverless/src/gcpfunction/http.ts index 18ad8e4cefd2..46683372fa53 100644 --- a/packages/google-cloud-serverless/src/gcpfunction/http.ts +++ b/packages/google-cloud-serverless/src/gcpfunction/http.ts @@ -1,8 +1,8 @@ import { + debug, handleCallbackErrors, httpRequestToRequestData, isString, - logger, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, setHttpStatus, @@ -68,7 +68,7 @@ function _wrapHttpFunction(fn: HttpFunction, options: Partial): // eslint-disable-next-line @typescript-eslint/no-floating-promises flush(flushTimeout) .then(null, e => { - DEBUG_BUILD && logger.error(e); + DEBUG_BUILD && debug.error(e); }) .then(() => { _end.call(this, chunk, encoding, cb); diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts index 11547ba933a1..ba6d9640a8b5 100644 --- a/packages/google-cloud-serverless/src/index.ts +++ b/packages/google-cloud-serverless/src/index.ts @@ -51,6 +51,7 @@ export { nativeNodeFetchIntegration, onUncaughtExceptionIntegration, onUnhandledRejectionIntegration, + openAIIntegration, modulesIntegration, contextLinesIntegration, nodeContextIntegration, diff --git a/packages/nestjs/src/setup.ts b/packages/nestjs/src/setup.ts index 13d44e74a204..c1af5938e3c2 100644 --- a/packages/nestjs/src/setup.ts +++ b/packages/nestjs/src/setup.ts @@ -8,7 +8,7 @@ import type { } from '@nestjs/common'; import { Catch, Global, HttpException, Injectable, Logger, Module } from '@nestjs/common'; import { APP_INTERCEPTOR, BaseExceptionFilter } from '@nestjs/core'; -import { captureException, getDefaultIsolationScope, getIsolationScope, logger } from '@sentry/core'; +import { captureException, debug, getDefaultIsolationScope, getIsolationScope } from '@sentry/core'; import type { Observable } from 'rxjs'; import { isExpectedError } from './helpers'; @@ -49,7 +49,7 @@ class SentryTracingInterceptor implements NestInterceptor { */ public intercept(context: ExecutionContext, next: CallHandler): Observable { if (getIsolationScope() === getDefaultIsolationScope()) { - logger.warn('Isolation scope is still the default isolation scope, skipping setting transactionName.'); + debug.warn('Isolation scope is still the default isolation scope, skipping setting transactionName.'); return next.handle(); } diff --git a/packages/nextjs/src/client/clientNormalizationIntegration.ts b/packages/nextjs/src/client/clientNormalizationIntegration.ts index 1d80ac3736c4..c92147c82bbe 100644 --- a/packages/nextjs/src/client/clientNormalizationIntegration.ts +++ b/packages/nextjs/src/client/clientNormalizationIntegration.ts @@ -39,7 +39,7 @@ export const nextjsClientStackFrameNormalizationIntegration = defineIntegration( if (frameOrigin === windowOrigin) { frame.filename = frame.filename?.replace(frameOrigin, 'app://').replace(basePath, ''); } - } catch (err) { + } catch { // Filename wasn't a properly formed URL, so there's nothing we can do } } @@ -47,7 +47,7 @@ export const nextjsClientStackFrameNormalizationIntegration = defineIntegration( try { const { origin } = new URL(frame.filename as string); frame.filename = frame.filename?.replace(origin, 'app://').replace(rewriteFramesAssetPrefixPath, ''); - } catch (err) { + } catch { // Filename wasn't a properly formed URL, so there's nothing we can do } } diff --git a/packages/nextjs/src/client/index.ts b/packages/nextjs/src/client/index.ts index e650c4e23a10..4d09e7e2d170 100644 --- a/packages/nextjs/src/client/index.ts +++ b/packages/nextjs/src/client/index.ts @@ -81,7 +81,7 @@ export function init(options: BrowserOptions): Client | undefined { if (process.turbopack) { getGlobalScope().setTag('turbopack', true); } - } catch (e) { + } catch { // Noop // The statement above can throw because process is not defined on the client } diff --git a/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts b/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts index 75147930528b..e9317709f4e7 100644 --- a/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts +++ b/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts @@ -1,7 +1,7 @@ import type { Client, TransactionSource } from '@sentry/core'; import { browserPerformanceTimeOrigin, - logger, + debug, parseBaggageHeader, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, @@ -69,8 +69,8 @@ function extractNextDataTagInformation(): NextDataTagInfo { if (nextDataTag?.innerHTML) { try { nextData = JSON.parse(nextDataTag.innerHTML); - } catch (e) { - DEBUG_BUILD && logger.warn('Could not extract __NEXT_DATA__'); + } catch { + DEBUG_BUILD && debug.warn('Could not extract __NEXT_DATA__'); } } diff --git a/packages/nextjs/src/client/routing/parameterization.ts b/packages/nextjs/src/client/routing/parameterization.ts index 8ce98044a588..c20d71614234 100644 --- a/packages/nextjs/src/client/routing/parameterization.ts +++ b/packages/nextjs/src/client/routing/parameterization.ts @@ -1,4 +1,4 @@ -import { GLOBAL_OBJ, logger } from '@sentry/core'; +import { debug, GLOBAL_OBJ } from '@sentry/core'; import { DEBUG_BUILD } from '../../common/debug-build'; import type { RouteManifest } from '../../config/manifest/types'; @@ -54,7 +54,7 @@ function getCompiledRegex(regexString: string): RegExp | null { compiledRegexCache.set(regexString, regex); return regex; } catch (error) { - DEBUG_BUILD && logger.warn('Could not compile regex', { regexString, error }); + DEBUG_BUILD && debug.warn('Could not compile regex', { regexString, error }); // Cache the failure to avoid repeated attempts by storing undefined return null; } @@ -98,9 +98,9 @@ function getManifest(): RouteManifest | null { cachedManifest = manifest; cachedManifestString = currentManifestString; return manifest; - } catch (error) { + } catch { // Something went wrong while parsing the manifest, so we'll fallback to no parameterization - DEBUG_BUILD && logger.warn('Could not extract route manifest'); + DEBUG_BUILD && debug.warn('Could not extract route manifest'); return null; } } diff --git a/packages/nextjs/src/client/tunnelRoute.ts b/packages/nextjs/src/client/tunnelRoute.ts index b9cd43c35b7d..64025fdd5f9d 100644 --- a/packages/nextjs/src/client/tunnelRoute.ts +++ b/packages/nextjs/src/client/tunnelRoute.ts @@ -1,4 +1,4 @@ -import { dsnFromString, GLOBAL_OBJ, logger } from '@sentry/core'; +import { debug, dsnFromString, GLOBAL_OBJ } from '@sentry/core'; import type { BrowserOptions } from '@sentry/react'; import { DEBUG_BUILD } from '../common/debug-build'; @@ -25,9 +25,9 @@ export function applyTunnelRouteOption(options: BrowserOptions): void { tunnelPath += `&r=${regionCode}`; } options.tunnel = tunnelPath; - DEBUG_BUILD && logger.info(`Tunneling events to "${tunnelPath}"`); + DEBUG_BUILD && debug.log(`Tunneling events to "${tunnelPath}"`); } else { - DEBUG_BUILD && logger.warn('Provided DSN is not a Sentry SaaS DSN. Will not tunnel events.'); + DEBUG_BUILD && debug.warn('Provided DSN is not a Sentry SaaS DSN. Will not tunnel events.'); } } } diff --git a/packages/nextjs/src/common/devErrorSymbolicationEventProcessor.ts b/packages/nextjs/src/common/devErrorSymbolicationEventProcessor.ts index 9cda8013ed4a..2a1781481536 100644 --- a/packages/nextjs/src/common/devErrorSymbolicationEventProcessor.ts +++ b/packages/nextjs/src/common/devErrorSymbolicationEventProcessor.ts @@ -1,5 +1,5 @@ import type { Event, EventHint } from '@sentry/core'; -import { GLOBAL_OBJ, logger, parseSemver, suppressTracing } from '@sentry/core'; +import { debug, GLOBAL_OBJ, parseSemver, suppressTracing } from '@sentry/core'; import type { StackFrame } from 'stacktrace-parser'; import * as stackTraceParser from 'stacktrace-parser'; import { DEBUG_BUILD } from './debug-build'; @@ -91,7 +91,7 @@ export async function devErrorSymbolicationEventProcessor(event: Event, hint: Ev ); } } - } catch (e) { + } catch { return event; } @@ -150,7 +150,7 @@ async function resolveStackFrame( originalStackFrame: body.originalStackFrame, }; } catch (e) { - DEBUG_BUILD && logger.error('Failed to symbolicate event with Next.js dev server', e); + DEBUG_BUILD && debug.error('Failed to symbolicate event with Next.js dev server', e); return null; } } @@ -224,7 +224,7 @@ async function resolveStackFrames( }; }); } catch (e) { - DEBUG_BUILD && logger.error('Failed to symbolicate event with Next.js dev server', e); + DEBUG_BUILD && debug.error('Failed to symbolicate event with Next.js dev server', e); return null; } } diff --git a/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts b/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts index 78de31b78e11..ba50778d30ad 100644 --- a/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts +++ b/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts @@ -1,10 +1,10 @@ import { captureException, continueTrace, + debug, getActiveSpan, httpRequestToRequestData, isString, - logger, objectify, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, @@ -42,12 +42,12 @@ export function wrapApiHandlerWithSentry(apiHandler: NextApiHandler, parameteriz const [req, res] = args; if (!req) { - logger.debug( + debug.log( `Wrapped API handler on route "${parameterizedRoute}" was not passed a request object. Will not instrument.`, ); return wrappingTarget.apply(thisArg, args); } else if (!res) { - logger.debug( + debug.log( `Wrapped API handler on route "${parameterizedRoute}" was not passed a response object. Will not instrument.`, ); return wrappingTarget.apply(thisArg, args); diff --git a/packages/nextjs/src/common/utils/responseEnd.ts b/packages/nextjs/src/common/utils/responseEnd.ts index 9d81c54e040b..745908c2bb61 100644 --- a/packages/nextjs/src/common/utils/responseEnd.ts +++ b/packages/nextjs/src/common/utils/responseEnd.ts @@ -1,5 +1,5 @@ import type { Span } from '@sentry/core'; -import { fill, flush, logger, setHttpStatus } from '@sentry/core'; +import { debug, fill, flush, setHttpStatus } from '@sentry/core'; import type { ServerResponse } from 'http'; import { DEBUG_BUILD } from '../debug-build'; import type { ResponseEndMethod, WrappedResponseEndMethod } from '../types'; @@ -47,10 +47,10 @@ export function finishSpan(span: Span, res: ServerResponse): void { */ export async function flushSafelyWithTimeout(): Promise { try { - DEBUG_BUILD && logger.log('Flushing events...'); + DEBUG_BUILD && debug.log('Flushing events...'); await flush(2000); - DEBUG_BUILD && logger.log('Done flushing events'); + DEBUG_BUILD && debug.log('Done flushing events'); } catch (e) { - DEBUG_BUILD && logger.log('Error while flushing events:\n', e); + DEBUG_BUILD && debug.log('Error while flushing events:\n', e); } } diff --git a/packages/nextjs/src/common/utils/tracingUtils.ts b/packages/nextjs/src/common/utils/tracingUtils.ts index 65d031046423..bda3049fbc78 100644 --- a/packages/nextjs/src/common/utils/tracingUtils.ts +++ b/packages/nextjs/src/common/utils/tracingUtils.ts @@ -1,5 +1,5 @@ import type { PropagationContext } from '@sentry/core'; -import { getActiveSpan, getRootSpan, GLOBAL_OBJ, logger, Scope, spanToJSON, startNewTrace } from '@sentry/core'; +import { debug, getActiveSpan, getRootSpan, GLOBAL_OBJ, Scope, spanToJSON, startNewTrace } from '@sentry/core'; import { DEBUG_BUILD } from '../debug-build'; import { TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION } from '../span-attributes-with-logic-attached'; @@ -72,7 +72,7 @@ export function escapeNextjsTracing(cb: () => T): T { if (!MaybeGlobalAsyncLocalStorage) { DEBUG_BUILD && - logger.warn( + debug.warn( "Tried to register AsyncLocalStorage async context strategy in a runtime that doesn't support AsyncLocalStorage.", ); return cb(); diff --git a/packages/nextjs/src/common/utils/urls.ts b/packages/nextjs/src/common/utils/urls.ts index d1274e1c35d9..1d02bce01973 100644 --- a/packages/nextjs/src/common/utils/urls.ts +++ b/packages/nextjs/src/common/utils/urls.ts @@ -91,7 +91,7 @@ export function extractSanitizedUrlFromRefererHeader(headersDict?: HeadersDict): try { const refererUrl = new URL(referer); return getSanitizedUrlStringFromUrlObject(refererUrl); - } catch (error) { + } catch { return undefined; } } diff --git a/packages/nextjs/src/common/withServerActionInstrumentation.ts b/packages/nextjs/src/common/withServerActionInstrumentation.ts index 20d9c6937b12..9f8673a2fab8 100644 --- a/packages/nextjs/src/common/withServerActionInstrumentation.ts +++ b/packages/nextjs/src/common/withServerActionInstrumentation.ts @@ -2,11 +2,11 @@ import type { RequestEventData } from '@sentry/core'; import { captureException, continueTrace, + debug, getActiveSpan, getClient, getIsolationScope, handleCallbackErrors, - logger, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, SPAN_STATUS_ERROR, startSpan, @@ -82,9 +82,9 @@ async function withServerActionInstrumentationImplementation { fullHeadersObject[key] = value; }); - } catch (e) { + } catch { DEBUG_BUILD && - logger.warn( + debug.warn( "Sentry wasn't able to extract the tracing headers for a server action. Will not trace this request.", ); } diff --git a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts index 79af67475b06..aad64e0f4ea4 100644 --- a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts +++ b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts @@ -40,7 +40,7 @@ export function wrapGenerationFunctionWithSentry a // We try-catch here just in case anything goes wrong with the async storage here goes wrong since it is Next.js internal API try { headers = requestAsyncStorage?.getStore()?.headers; - } catch (e) { + } catch { /** empty */ } diff --git a/packages/nextjs/src/config/templates/routeHandlerWrapperTemplate.ts b/packages/nextjs/src/config/templates/routeHandlerWrapperTemplate.ts index a2aed7543c39..d8e931e06451 100644 --- a/packages/nextjs/src/config/templates/routeHandlerWrapperTemplate.ts +++ b/packages/nextjs/src/config/templates/routeHandlerWrapperTemplate.ts @@ -43,7 +43,7 @@ function wrapHandler(handler: T, method: 'GET' | 'POST' | 'PUT' | 'PATCH' | ' try { const requestAsyncStore = requestAsyncStorage?.getStore() as ReturnType; headers = requestAsyncStore?.headers; - } catch (e) { + } catch { /** empty */ } diff --git a/packages/nextjs/src/config/templates/serverComponentWrapperTemplate.ts b/packages/nextjs/src/config/templates/serverComponentWrapperTemplate.ts index 11a18cdb5044..f29df45d3542 100644 --- a/packages/nextjs/src/config/templates/serverComponentWrapperTemplate.ts +++ b/packages/nextjs/src/config/templates/serverComponentWrapperTemplate.ts @@ -50,7 +50,7 @@ if (typeof serverComponent === 'function') { sentryTraceHeader = requestAsyncStore?.headers.get('sentry-trace') ?? undefined; baggageHeader = requestAsyncStore?.headers.get('baggage') ?? undefined; headers = requestAsyncStore?.headers; - } catch (e) { + } catch { /** empty */ } diff --git a/packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts b/packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts index 4e5c404ec56c..50dd1a14588a 100644 --- a/packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts +++ b/packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts @@ -1,4 +1,4 @@ -import { logger } from '@sentry/core'; +import { debug } from '@sentry/core'; import * as chalk from 'chalk'; import * as path from 'path'; import type { RouteManifest } from '../manifest/types'; @@ -63,7 +63,7 @@ export function safelyAddTurbopackRule( // If the rule already exists, we don't want to mess with it. if (existingRules[matcher]) { - logger.info( + debug.log( `${chalk.cyan( 'info', )} - Turbopack rule already exists for ${matcher}. Please remove it from your Next.js config in order for Sentry to work properly.`, diff --git a/packages/nextjs/src/config/webpack.ts b/packages/nextjs/src/config/webpack.ts index 77db5120cda3..9c9c479cd724 100644 --- a/packages/nextjs/src/config/webpack.ts +++ b/packages/nextjs/src/config/webpack.ts @@ -1,7 +1,7 @@ /* eslint-disable complexity */ /* eslint-disable max-lines */ -import { escapeStringForRegex, loadModule, logger, parseSemver } from '@sentry/core'; +import { debug, escapeStringForRegex, loadModule, parseSemver } from '@sentry/core'; import * as chalk from 'chalk'; import * as fs from 'fs'; import * as path from 'path'; @@ -219,7 +219,7 @@ export function constructWebpackConfigFunction( // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access vercelCronsConfig = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'vercel.json'), 'utf8')).crons; if (vercelCronsConfig) { - logger.info( + debug.log( `${chalk.cyan( 'info', )} - Creating Sentry cron monitors for your Vercel Cron Jobs. You can disable this feature by setting the ${chalk.bold.cyan( @@ -233,7 +233,7 @@ export function constructWebpackConfigFunction( // noop if file does not exist } else { // log but noop - logger.error( + debug.error( `${chalk.red( 'error', )} - Sentry failed to read vercel.json for automatic cron job monitoring instrumentation`, @@ -368,7 +368,7 @@ export function constructWebpackConfigFunction( // We only update this if no explicit value is set // (Next.js defaults to `false`: https://github.com/vercel/next.js/blob/5f4f96c133bd6b10954812cc2fef6af085b82aa5/packages/next/src/build/webpack/config/blocks/base.ts#L61) if (!newConfig.devtool) { - logger.info(`[@sentry/nextjs] Automatically enabling source map generation for ${runtime} build.`); + debug.log(`[@sentry/nextjs] Automatically enabling source map generation for ${runtime} build.`); // `hidden-source-map` produces the same sourcemaps as `source-map`, but doesn't include the `sourceMappingURL` // comment at the bottom. For folks who aren't publicly hosting their sourcemaps, this is helpful because then // the browser won't look for them and throw errors into the console when it can't find them. Because this is a @@ -383,7 +383,7 @@ export function constructWebpackConfigFunction( // enable source map deletion if not explicitly disabled if (!isServer && userSentryOptions.sourcemaps?.deleteSourcemapsAfterUpload === undefined) { - logger.warn( + debug.warn( '[@sentry/nextjs] Source maps will be automatically deleted after being uploaded to Sentry. If you want to keep the source maps, set the `sourcemaps.deleteSourcemapsAfterUpload` option to false in `withSentryConfig()`. If you do not want to generate and upload sourcemaps at all, set the `sourcemaps.disable` option to true.', ); userSentryOptions.sourcemaps = { @@ -487,7 +487,7 @@ function getInstrumentationFile(projectDir: string, dotPrefixedExtensions: strin for (const pathSegments of paths) { try { return fs.readFileSync(path.resolve(projectDir, ...pathSegments), { encoding: 'utf-8' }); - } catch (e) { + } catch { // no-op } } @@ -643,7 +643,7 @@ function addFilesToWebpackEntryPoint( import: newImportValue, }; } - // malformed entry point (use `console.error` rather than `logger.error` because it will always be printed, regardless + // malformed entry point (use `console.error` rather than `debug.error` because it will always be printed, regardless // of SDK settings) else { // eslint-disable-next-line no-console diff --git a/packages/nextjs/src/config/withSentryConfig.ts b/packages/nextjs/src/config/withSentryConfig.ts index 85ade8e682de..57fff867f64a 100644 --- a/packages/nextjs/src/config/withSentryConfig.ts +++ b/packages/nextjs/src/config/withSentryConfig.ts @@ -470,7 +470,7 @@ function getGitRevision(): string | undefined { .execSync('git rev-parse HEAD', { stdio: ['ignore', 'pipe', 'ignore'] }) .toString() .trim(); - } catch (e) { + } catch { // noop } return gitRevision; diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index a6594e7fae1e..82d475a719c6 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -9,6 +9,7 @@ import { import type { EventProcessor } from '@sentry/core'; import { applySdkMetadata, + debug, extractTraceparentData, getCapturedScopesOnSpan, getClient, @@ -17,7 +18,6 @@ import { getIsolationScope, getRootSpan, GLOBAL_OBJ, - logger, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, @@ -125,13 +125,13 @@ export function init(options: NodeOptions): NodeClient | undefined { }; if (DEBUG_BUILD && opts.debug) { - logger.enable(); + debug.enable(); } - DEBUG_BUILD && logger.log('Initializing SDK...'); + DEBUG_BUILD && debug.log('Initializing SDK...'); if (sdkAlreadyInitialized()) { - DEBUG_BUILD && logger.log('SDK already initialized'); + DEBUG_BUILD && debug.log('SDK already initialized'); return; } @@ -377,7 +377,7 @@ export function init(options: NodeOptions): NodeClient | undefined { // The statement above can throw because process is not defined on the client } - DEBUG_BUILD && logger.log('SDK successfully initialized'); + DEBUG_BUILD && debug.log('SDK successfully initialized'); return client; } diff --git a/packages/node-core/package.json b/packages/node-core/package.json index ad7b99a82461..838a50f11ae5 100644 --- a/packages/node-core/package.json +++ b/packages/node-core/package.json @@ -60,7 +60,7 @@ "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.0.0", "@opentelemetry/core": "^1.30.1 || ^2.0.0", - "@opentelemetry/instrumentation": "^0.57.1 || ^0.202.0", + "@opentelemetry/instrumentation": ">=0.57.1 <1", "@opentelemetry/resources": "^1.30.1 || ^2.0.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.0.0", "@opentelemetry/semantic-conventions": "^1.34.0" diff --git a/packages/node-core/src/integrations/anr/index.ts b/packages/node-core/src/integrations/anr/index.ts index 9615ca241198..0d8c4f14a53d 100644 --- a/packages/node-core/src/integrations/anr/index.ts +++ b/packages/node-core/src/integrations/anr/index.ts @@ -2,6 +2,7 @@ import { types } from 'node:util'; import { Worker } from 'node:worker_threads'; import type { Contexts, Event, EventHint, Integration, IntegrationFn, ScopeData } from '@sentry/core'; import { + debug, defineIntegration, getClient, getCurrentScope, @@ -9,7 +10,6 @@ import { getGlobalScope, getIsolationScope, GLOBAL_OBJ, - logger, mergeScopeData, } from '@sentry/core'; import { NODE_VERSION } from '../../nodeVersion'; @@ -26,7 +26,7 @@ const DEFAULT_INTERVAL = 50; const DEFAULT_HANG_THRESHOLD = 5000; function log(message: string, ...args: unknown[]): void { - logger.log(`[ANR] ${message}`, ...args); + debug.log(`[ANR] ${message}`, ...args); } function globalWithScopeFetchFn(): typeof GLOBAL_OBJ & { __SENTRY_GET_SCOPES__?: () => ScopeData } { @@ -104,7 +104,7 @@ const _anrIntegration = ((options: Partial = {}) => { client = initClient; if (options.captureStackTrace && (await isDebuggerEnabled())) { - logger.warn('ANR captureStackTrace has been disabled because the debugger was already enabled'); + debug.warn('ANR captureStackTrace has been disabled because the debugger was already enabled'); options.captureStackTrace = false; } @@ -188,7 +188,7 @@ async function _startWorker( } const options: WorkerStartData = { - debug: logger.isEnabled(), + debug: debug.isEnabled(), dsn, tunnel: initOptions.tunnel, environment: initOptions.environment || 'production', @@ -231,7 +231,7 @@ async function _startWorker( const session = currentSession ? { ...currentSession, toJSON: undefined } : undefined; // message the worker to tell it the main event loop is still running worker.postMessage({ session, debugImages: getFilenameToDebugIdMap(initOptions.stackParser) }); - } catch (_) { + } catch { // } }, options.pollInterval); diff --git a/packages/node-core/src/integrations/anr/worker.ts b/packages/node-core/src/integrations/anr/worker.ts index dae062b4df7c..7c2ac91f30af 100644 --- a/packages/node-core/src/integrations/anr/worker.ts +++ b/packages/node-core/src/integrations/anr/worker.ts @@ -62,7 +62,7 @@ async function sendAbnormalSession(): Promise { try { // Notify the main process that the session has ended so the session can be cleared from the scope parentPort?.postMessage('session-ended'); - } catch (_) { + } catch { // ignore } } @@ -280,7 +280,7 @@ if (options.captureStackTrace) { session.post('Debugger.enable', () => { session.post('Debugger.pause'); }); - } catch (_) { + } catch { // } }; diff --git a/packages/node-core/src/integrations/context.ts b/packages/node-core/src/integrations/context.ts index b8376ab0ada8..f30361afd77b 100644 --- a/packages/node-core/src/integrations/context.ts +++ b/packages/node-core/src/integrations/context.ts @@ -192,7 +192,7 @@ function getCultureContext(): CultureContext | undefined { timezone: options.timeZone, }; } - } catch (err) { + } catch { // } @@ -228,7 +228,7 @@ export function getDeviceContext(deviceOpt: DeviceContextOptions | true): Device let uptime; try { uptime = os.uptime(); - } catch (e) { + } catch { // noop } @@ -347,7 +347,7 @@ async function getDarwinInfo(): Promise { darwinInfo.name = matchFirst(/^ProductName:\s+(.*)$/m, output); darwinInfo.version = matchFirst(/^ProductVersion:\s+(.*)$/m, output); darwinInfo.build = matchFirst(/^BuildVersion:\s+(.*)$/m, output); - } catch (e) { + } catch { // ignore } @@ -402,7 +402,7 @@ async function getLinuxInfo(): Promise { // are computed in `LINUX_VERSIONS`. const id = getLinuxDistroId(linuxInfo.name); linuxInfo.version = LINUX_VERSIONS[id]?.(contents); - } catch (e) { + } catch { // ignore } diff --git a/packages/node-core/src/integrations/contextlines.ts b/packages/node-core/src/integrations/contextlines.ts index 6667bed80e28..5c99166d0d54 100644 --- a/packages/node-core/src/integrations/contextlines.ts +++ b/packages/node-core/src/integrations/contextlines.ts @@ -1,7 +1,7 @@ import { createReadStream } from 'node:fs'; import { createInterface } from 'node:readline'; import type { Event, IntegrationFn, StackFrame } from '@sentry/core'; -import { defineIntegration, logger, LRUMap, snipLine } from '@sentry/core'; +import { debug, defineIntegration, LRUMap, snipLine } from '@sentry/core'; import { DEBUG_BUILD } from '../debug-build'; const LRU_FILE_CONTENTS_CACHE = new LRUMap>(10); @@ -167,7 +167,7 @@ function getContextLinesFromFile(path: string, ranges: ReadlineRange[], output: function onStreamError(e: Error): void { // Mark file path as failed to read and prevent multiple read attempts. LRU_FILE_CONTENTS_FS_READ_FAILED.set(path, 1); - DEBUG_BUILD && logger.error(`Failed to read file: ${path}. Error: ${e}`); + DEBUG_BUILD && debug.error(`Failed to read file: ${path}. Error: ${e}`); lineReaded.close(); lineReaded.removeAllListeners(); destroyStreamAndResolve(); @@ -281,7 +281,7 @@ async function addSourceContext(event: Event, contextLines: number): Promise { - DEBUG_BUILD && logger.log('Failed to read one or more source files and resolve context lines'); + DEBUG_BUILD && debug.log('Failed to read one or more source files and resolve context lines'); }); // Perform the same loop as above, but this time we can assume all files are in the cache @@ -339,7 +339,7 @@ export function addContextToFrame( // When there is no line number in the frame, attaching context is nonsensical and will even break grouping. // We already check for lineno before calling this, but since StackFrame lineno ism optional, we check it again. if (frame.lineno === undefined || contents === undefined) { - DEBUG_BUILD && logger.error('Cannot resolve context for frame with no lineno or file contents'); + DEBUG_BUILD && debug.error('Cannot resolve context for frame with no lineno or file contents'); return; } @@ -350,7 +350,7 @@ export function addContextToFrame( const line = contents[i]; if (line === undefined) { clearLineContext(frame); - DEBUG_BUILD && logger.error(`Could not find line ${i} in file ${frame.filename}`); + DEBUG_BUILD && debug.error(`Could not find line ${i} in file ${frame.filename}`); return; } @@ -361,7 +361,7 @@ export function addContextToFrame( // without adding any linecontext. if (contents[lineno] === undefined) { clearLineContext(frame); - DEBUG_BUILD && logger.error(`Could not find line ${lineno} in file ${frame.filename}`); + DEBUG_BUILD && debug.error(`Could not find line ${lineno} in file ${frame.filename}`); return; } diff --git a/packages/node-core/src/integrations/http/SentryHttpInstrumentation.ts b/packages/node-core/src/integrations/http/SentryHttpInstrumentation.ts index d1cfc3b1ea0c..daee4440e40c 100644 --- a/packages/node-core/src/integrations/http/SentryHttpInstrumentation.ts +++ b/packages/node-core/src/integrations/http/SentryHttpInstrumentation.ts @@ -12,6 +12,7 @@ import type { AggregationCounts, Client, SanitizedRequestData, Scope } from '@se import { addBreadcrumb, addNonEnumerableProperty, + debug, generateSpanId, getBreadcrumbLogLevelFromHttpStatusCode, getClient, @@ -21,7 +22,6 @@ import { getTraceData, httpRequestToRequestData, isError, - logger, LRUMap, parseUrl, SDK_VERSION, @@ -219,7 +219,7 @@ export class SentryHttpInstrumentation extends InstrumentationBase) => { @@ -503,13 +503,13 @@ function patchRequestToCaptureBody( chunks.push(bufferifiedChunk); bodyByteLength += bufferifiedChunk.byteLength; } else if (DEBUG_BUILD) { - logger.log( + debug.log( INSTRUMENTATION_NAME, `Dropping request body chunk because maximum body length of ${maxBodySize}b is exceeded.`, ); } } catch (err) { - DEBUG_BUILD && logger.error(INSTRUMENTATION_NAME, 'Encountered error while storing body chunk.'); + DEBUG_BUILD && debug.error(INSTRUMENTATION_NAME, 'Encountered error while storing body chunk.'); } return Reflect.apply(target, thisArg, args); @@ -561,13 +561,13 @@ function patchRequestToCaptureBody( } } catch (error) { if (DEBUG_BUILD) { - logger.error(INSTRUMENTATION_NAME, 'Error building captured request body', error); + debug.error(INSTRUMENTATION_NAME, 'Error building captured request body', error); } } }); } catch (error) { if (DEBUG_BUILD) { - logger.error(INSTRUMENTATION_NAME, 'Error patching request to capture body', error); + debug.error(INSTRUMENTATION_NAME, 'Error patching request to capture body', error); } } } @@ -611,7 +611,7 @@ export function recordRequestSession({ const requestSession = requestIsolationScope.getScopeData().sdkProcessingMetadata.requestSession; if (client && requestSession) { - DEBUG_BUILD && logger.debug(`Recorded request session with status: ${requestSession.status}`); + DEBUG_BUILD && debug.log(`Recorded request session with status: ${requestSession.status}`); const roundedDate = new Date(); roundedDate.setSeconds(0, 0); @@ -624,7 +624,7 @@ export function recordRequestSession({ if (existingClientAggregate) { existingClientAggregate[dateBucketKey] = bucket; } else { - DEBUG_BUILD && logger.debug('Opened new request session aggregate.'); + DEBUG_BUILD && debug.log('Opened new request session aggregate.'); const newClientAggregate = { [dateBucketKey]: bucket }; clientToRequestSessionAggregatesMap.set(client, newClientAggregate); @@ -645,11 +645,11 @@ export function recordRequestSession({ }; const unregisterClientFlushHook = client.on('flush', () => { - DEBUG_BUILD && logger.debug('Sending request session aggregate due to client flush'); + DEBUG_BUILD && debug.log('Sending request session aggregate due to client flush'); flushPendingClientAggregates(); }); const timeout = setTimeout(() => { - DEBUG_BUILD && logger.debug('Sending request session aggregate due to flushing schedule'); + DEBUG_BUILD && debug.log('Sending request session aggregate due to flushing schedule'); flushPendingClientAggregates(); }, sessionFlushingDelayMS).unref(); } diff --git a/packages/node-core/src/integrations/local-variables/local-variables-async.ts b/packages/node-core/src/integrations/local-variables/local-variables-async.ts index 6465760b5435..32fff66bab4e 100644 --- a/packages/node-core/src/integrations/local-variables/local-variables-async.ts +++ b/packages/node-core/src/integrations/local-variables/local-variables-async.ts @@ -1,6 +1,6 @@ import { Worker } from 'node:worker_threads'; import type { Event, EventHint, Exception, IntegrationFn } from '@sentry/core'; -import { defineIntegration, logger } from '@sentry/core'; +import { debug, defineIntegration } from '@sentry/core'; import type { NodeClient } from '../../sdk/client'; import { isDebuggerEnabled } from '../../utils/debug'; import type { FrameVariables, LocalVariablesIntegrationOptions, LocalVariablesWorkerArgs } from './common'; @@ -10,7 +10,7 @@ import { functionNamesMatch, LOCAL_VARIABLES_KEY } from './common'; export const base64WorkerScript = '###LocalVariablesWorkerScript###'; function log(...args: unknown[]): void { - logger.log('[LocalVariables]', ...args); + debug.log('[LocalVariables]', ...args); } /** @@ -111,13 +111,13 @@ export const localVariablesAsyncIntegration = defineIntegration((( } if (await isDebuggerEnabled()) { - logger.warn('Local variables capture has been disabled because the debugger was already enabled'); + debug.warn('Local variables capture has been disabled because the debugger was already enabled'); return; } const options: LocalVariablesWorkerArgs = { ...integrationOptions, - debug: logger.isEnabled(), + debug: debug.isEnabled(), }; startInspector().then( @@ -125,11 +125,11 @@ export const localVariablesAsyncIntegration = defineIntegration((( try { startWorker(options); } catch (e) { - logger.error('Failed to start worker', e); + debug.error('Failed to start worker', e); } }, e => { - logger.error('Failed to start inspector', e); + debug.error('Failed to start inspector', e); }, ); }, diff --git a/packages/node-core/src/integrations/local-variables/local-variables-sync.ts b/packages/node-core/src/integrations/local-variables/local-variables-sync.ts index 495a0712eb80..7de91a54276e 100644 --- a/packages/node-core/src/integrations/local-variables/local-variables-sync.ts +++ b/packages/node-core/src/integrations/local-variables/local-variables-sync.ts @@ -1,6 +1,6 @@ import type { Debugger, InspectorNotification, Runtime, Session } from 'node:inspector'; import type { Event, Exception, IntegrationFn, StackFrame, StackParser } from '@sentry/core'; -import { defineIntegration, getClient, logger, LRUMap } from '@sentry/core'; +import { debug, defineIntegration, getClient, LRUMap } from '@sentry/core'; import { NODE_MAJOR } from '../../nodeVersion'; import type { NodeClient } from '../../sdk/client'; import { isDebuggerEnabled } from '../../utils/debug'; @@ -76,7 +76,7 @@ export function createCallbackList(complete: Next): CallbackWrapper { try { popped(result); - } catch (_) { + } catch { // If there is an error, we still want to call the complete callback checkedComplete(result); } @@ -303,12 +303,12 @@ const _localVariablesSyncIntegration = (( const unsupportedNodeVersion = NODE_MAJOR < 18; if (unsupportedNodeVersion) { - logger.log('The `LocalVariables` integration is only supported on Node >= v18.'); + debug.log('The `LocalVariables` integration is only supported on Node >= v18.'); return; } if (await isDebuggerEnabled()) { - logger.warn('Local variables capture has been disabled because the debugger was already enabled'); + debug.warn('Local variables capture has been disabled because the debugger was already enabled'); return; } @@ -384,11 +384,11 @@ const _localVariablesSyncIntegration = (( rateLimiter = createRateLimiter( max, () => { - logger.log('Local variables rate-limit lifted.'); + debug.log('Local variables rate-limit lifted.'); session.setPauseOnExceptions(true); }, seconds => { - logger.log( + debug.log( `Local variables rate-limit exceeded. Disabling capturing of caught exceptions for ${seconds} seconds.`, ); session.setPauseOnExceptions(false); @@ -399,7 +399,7 @@ const _localVariablesSyncIntegration = (( shouldProcessEvent = true; }, error => { - logger.log('The `LocalVariables` integration failed to start.', error); + debug.log('The `LocalVariables` integration failed to start.', error); }, ); }, diff --git a/packages/node-core/src/integrations/modules.ts b/packages/node-core/src/integrations/modules.ts index 6adee9e46744..6724a473b5bb 100644 --- a/packages/node-core/src/integrations/modules.ts +++ b/packages/node-core/src/integrations/modules.ts @@ -44,7 +44,7 @@ export const modulesIntegration = _modulesIntegration; function getRequireCachePaths(): string[] { try { return require.cache ? Object.keys(require.cache as Record) : []; - } catch (e) { + } catch { return []; } } @@ -96,7 +96,7 @@ function collectRequireModules(): ModuleInfo { version: string; }; infos[info.name] = info.version; - } catch (_oO) { + } catch { // no-empty } }; @@ -126,7 +126,7 @@ function getPackageJson(): PackageJson { const packageJson = JSON.parse(readFileSync(filePath, 'utf8')) as PackageJson; return packageJson; - } catch (e) { + } catch { return {}; } } diff --git a/packages/node-core/src/integrations/onuncaughtexception.ts b/packages/node-core/src/integrations/onuncaughtexception.ts index 0634159338a6..045e2e9f7736 100644 --- a/packages/node-core/src/integrations/onuncaughtexception.ts +++ b/packages/node-core/src/integrations/onuncaughtexception.ts @@ -1,4 +1,4 @@ -import { captureException, defineIntegration, getClient, logger } from '@sentry/core'; +import { captureException, debug, defineIntegration, getClient } from '@sentry/core'; import { DEBUG_BUILD } from '../debug-build'; import type { NodeClient } from '../sdk/client'; import { logAndExitProcess } from '../utils/errorhandling'; @@ -122,7 +122,7 @@ export function makeErrorHandler(client: NodeClient, options: OnUncaughtExceptio if (calledFatalError) { // we hit an error *after* calling onFatalError - pretty boned at this point, just shut it down DEBUG_BUILD && - logger.warn( + debug.warn( 'uncaught exception after calling fatal error shutdown callback - this is bad! forcing shutdown', ); logAndExitProcess(error); diff --git a/packages/node-core/src/integrations/spotlight.ts b/packages/node-core/src/integrations/spotlight.ts index 4e36f3692fb0..47d73b956628 100644 --- a/packages/node-core/src/integrations/spotlight.ts +++ b/packages/node-core/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, suppressTracing } from '@sentry/core'; +import { debug, defineIntegration, serializeEnvelope, suppressTracing } from '@sentry/core'; type SpotlightConnectionOptions = { /** @@ -20,13 +20,12 @@ const _spotlightIntegration = ((options: Partial = { return { name: INTEGRATION_NAME, setup(client) { - 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?"); + try { + if (process.env.NODE_ENV && process.env.NODE_ENV !== 'development') { + debug.warn("[Spotlight] It seems you're not in dev mode. Do you really want to have Spotlight enabled?"); + } + } catch { + // ignore } connectToSpotlight(client, _options); }, @@ -52,7 +51,7 @@ function connectToSpotlight(client: Client, options: Required { if (failedRequests > 3) { - logger.warn('[Spotlight] Disabled Sentry -> Spotlight integration due to too many failed requests'); + debug.warn('[Spotlight] Disabled Sentry -> Spotlight integration due to too many failed requests'); return; } @@ -86,7 +85,7 @@ function connectToSpotlight(client: Client, options: Required { failedRequests++; - logger.warn('[Spotlight] Failed to send envelope to Spotlight Sidecar'); + debug.warn('[Spotlight] Failed to send envelope to Spotlight Sidecar'); }); req.write(serializedEnvelope); req.end(); @@ -98,7 +97,7 @@ function parseSidecarUrl(url: string): URL | undefined { try { return new URL(`${url}`); } catch { - logger.warn(`[Spotlight] Invalid sidecar URL: ${url}`); + debug.warn(`[Spotlight] Invalid sidecar URL: ${url}`); return undefined; } } diff --git a/packages/node-core/src/otel/logger.ts b/packages/node-core/src/otel/logger.ts index 53cbdc63c3ee..7b4ecb6104ba 100644 --- a/packages/node-core/src/otel/logger.ts +++ b/packages/node-core/src/otel/logger.ts @@ -1,18 +1,20 @@ import { diag, DiagLogLevel } from '@opentelemetry/api'; -import { logger } from '@sentry/core'; +import { debug } from '@sentry/core'; /** - * Setup the OTEL logger to use our own logger. + * Setup the OTEL logger to use our own debug logger. */ export function setupOpenTelemetryLogger(): void { - const otelLogger = new Proxy(logger as typeof logger & { verbose: (typeof logger)['debug'] }, { - get(target, prop, receiver) { - const actualProp = prop === 'verbose' ? 'debug' : prop; - return Reflect.get(target, actualProp, receiver); - }, - }); - // Disable diag, to ensure this works even if called multiple times diag.disable(); - diag.setLogger(otelLogger, DiagLogLevel.DEBUG); + diag.setLogger( + { + error: debug.error, + warn: debug.warn, + info: debug.log, + debug: debug.log, + verbose: debug.log, + }, + DiagLogLevel.DEBUG, + ); } diff --git a/packages/node-core/src/proxy/index.ts b/packages/node-core/src/proxy/index.ts index 8ff6741623b2..21eccb157ae6 100644 --- a/packages/node-core/src/proxy/index.ts +++ b/packages/node-core/src/proxy/index.ts @@ -34,13 +34,13 @@ import type * as http from 'node:http'; import type { OutgoingHttpHeaders } from 'node:http'; import * as net from 'node:net'; import * as tls from 'node:tls'; -import { logger } from '@sentry/core'; +import { debug } from '@sentry/core'; import type { AgentConnectOpts } from './base'; import { Agent } from './base'; import { parseProxyResponse } from './parse-proxy-response'; -function debug(...args: unknown[]): void { - logger.log('[https-proxy-agent]', ...args); +function debugLog(...args: unknown[]): void { + debug.log('[https-proxy-agent]', ...args); } type Protocol = T extends `${infer Protocol}:${infer _}` ? Protocol : never; @@ -83,7 +83,7 @@ export class HttpsProxyAgent extends Agent { this.options = {}; this.proxy = typeof proxy === 'string' ? new URL(proxy) : proxy; this.proxyHeaders = opts?.headers ?? {}; - debug('Creating new HttpsProxyAgent instance: %o', this.proxy.href); + debugLog('Creating new HttpsProxyAgent instance: %o', this.proxy.href); // Trim off the brackets from IPv6 addresses const host = (this.proxy.hostname || this.proxy.host).replace(/^\[|\]$/g, ''); @@ -111,14 +111,14 @@ export class HttpsProxyAgent extends Agent { // Create a socket connection to the proxy server. let socket: net.Socket; if (proxy.protocol === 'https:') { - debug('Creating `tls.Socket`: %o', this.connectOpts); + debugLog('Creating `tls.Socket`: %o', this.connectOpts); const servername = this.connectOpts.servername || this.connectOpts.host; socket = tls.connect({ ...this.connectOpts, servername: servername && net.isIP(servername) ? undefined : servername, }); } else { - debug('Creating `net.Socket`: %o', this.connectOpts); + debugLog('Creating `net.Socket`: %o', this.connectOpts); socket = net.connect(this.connectOpts); } @@ -158,7 +158,7 @@ export class HttpsProxyAgent extends Agent { if (opts.secureEndpoint) { // The proxy is connecting to a TLS server, so upgrade // this socket connection to a TLS connection. - debug('Upgrading socket connection to TLS'); + debugLog('Upgrading socket connection to TLS'); const servername = opts.servername || opts.host; return tls.connect({ ...omit(opts, 'host', 'path', 'port'), @@ -188,7 +188,7 @@ export class HttpsProxyAgent extends Agent { // Need to wait for the "socket" event to re-play the "data" events. req.once('socket', (s: net.Socket) => { - debug('Replaying proxy buffer for failed request'); + debugLog('Replaying proxy buffer for failed request'); // Replay the "buffered" Buffer onto the fake `socket`, since at // this point the HTTP module machinery has been hooked up for // the user. diff --git a/packages/node-core/src/proxy/parse-proxy-response.ts b/packages/node-core/src/proxy/parse-proxy-response.ts index afad0d2435f4..0d7481b82621 100644 --- a/packages/node-core/src/proxy/parse-proxy-response.ts +++ b/packages/node-core/src/proxy/parse-proxy-response.ts @@ -30,10 +30,10 @@ /* eslint-disable jsdoc/require-jsdoc */ import type { IncomingHttpHeaders } from 'node:http'; import type { Readable } from 'node:stream'; -import { logger } from '@sentry/core'; +import { debug } from '@sentry/core'; -function debug(...args: unknown[]): void { - logger.log('[https-proxy-agent:parse-proxy-response]', ...args); +function debugLog(...args: unknown[]): void { + debug.log('[https-proxy-agent:parse-proxy-response]', ...args); } export interface ConnectResponse { @@ -65,13 +65,13 @@ export function parseProxyResponse(socket: Readable): Promise<{ connect: Connect function onend() { cleanup(); - debug('onend'); + debugLog('onend'); reject(new Error('Proxy connection ended before receiving CONNECT response')); } function onerror(err: Error) { cleanup(); - debug('onerror %o', err); + debugLog('onerror %o', err); reject(err); } @@ -84,7 +84,7 @@ export function parseProxyResponse(socket: Readable): Promise<{ connect: Connect if (endOfHeaders === -1) { // keep buffering - debug('have not received end of HTTP headers yet...'); + debugLog('have not received end of HTTP headers yet...'); read(); return; } @@ -117,7 +117,7 @@ export function parseProxyResponse(socket: Readable): Promise<{ connect: Connect headers[key] = value; } } - debug('got proxy server response: %o %o', firstLine, headers); + debugLog('got proxy server response: %o %o', firstLine, headers); cleanup(); resolve({ connect: { diff --git a/packages/node-core/src/sdk/client.ts b/packages/node-core/src/sdk/client.ts index 022e3cb1ac33..50b0d4d92b6e 100644 --- a/packages/node-core/src/sdk/client.ts +++ b/packages/node-core/src/sdk/client.ts @@ -4,7 +4,7 @@ import { trace } from '@opentelemetry/api'; import { registerInstrumentations } from '@opentelemetry/instrumentation'; import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; import type { DynamicSamplingContext, Scope, ServerRuntimeClientOptions, TraceContext } from '@sentry/core'; -import { _INTERNAL_flushLogsBuffer, applySdkMetadata, logger, SDK_VERSION, ServerRuntimeClient } from '@sentry/core'; +import { _INTERNAL_flushLogsBuffer, applySdkMetadata, debug, SDK_VERSION, ServerRuntimeClient } from '@sentry/core'; import { getTraceContextForScope } from '@sentry/opentelemetry'; import { isMainThread, threadId } from 'worker_threads'; import { DEBUG_BUILD } from '../debug-build'; @@ -41,9 +41,7 @@ export class NodeClient extends ServerRuntimeClient { applySdkMetadata(clientOptions, 'node'); - logger.log( - `Initializing Sentry: process: ${process.pid}, thread: ${isMainThread ? 'main' : `worker-${threadId}`}.`, - ); + debug.log(`Initializing Sentry: process: ${process.pid}, thread: ${isMainThread ? 'main' : `worker-${threadId}`}.`); super(clientOptions); @@ -134,7 +132,7 @@ export class NodeClient extends ServerRuntimeClient { }; this._clientReportInterval = setInterval(() => { - DEBUG_BUILD && logger.log('Flushing client reports based on interval.'); + DEBUG_BUILD && debug.log('Flushing client reports based on interval.'); this._flushOutcomes(); }, clientOptions.clientReportFlushInterval ?? DEFAULT_CLIENT_REPORT_FLUSH_INTERVAL_MS) // Unref is critical for not preventing the process from exiting because the interval is active. diff --git a/packages/node-core/src/sdk/esmLoader.ts b/packages/node-core/src/sdk/esmLoader.ts index 487e2ce0c613..2f0d8b405333 100644 --- a/packages/node-core/src/sdk/esmLoader.ts +++ b/packages/node-core/src/sdk/esmLoader.ts @@ -1,4 +1,4 @@ -import { consoleSandbox, GLOBAL_OBJ, logger } from '@sentry/core'; +import { consoleSandbox, debug, GLOBAL_OBJ } from '@sentry/core'; import { createAddHookMessageChannel } from 'import-in-the-middle'; import moduleModule from 'module'; @@ -17,7 +17,7 @@ export function maybeInitializeEsmLoader(): void { transferList: [addHookMessagePort], }); } catch (error) { - logger.warn('Failed to register ESM hook', error); + debug.warn('Failed to register ESM hook', error); } } } else { diff --git a/packages/node-core/src/sdk/index.ts b/packages/node-core/src/sdk/index.ts index 9ec30a892927..7d9bf6e90fd7 100644 --- a/packages/node-core/src/sdk/index.ts +++ b/packages/node-core/src/sdk/index.ts @@ -3,13 +3,13 @@ import { applySdkMetadata, consoleIntegration, consoleSandbox, + debug, functionToStringIntegration, getCurrentScope, getIntegrationsToSetup, hasSpansEnabled, inboundFiltersIntegration, linkedErrorsIntegration, - logger, propagationContextFromHeaders, requestDataIntegration, stackParserFromStackParserOptions, @@ -94,9 +94,9 @@ function _init( if (options.debug === true) { if (DEBUG_BUILD) { - logger.enable(); + debug.enable(); } else { - // use `console.warn` rather than `logger.warn` since by non-debug bundles have all `logger.x` statements stripped + // use `console.warn` rather than `debug.warn` since by non-debug bundles have all `debug.x` statements stripped consoleSandbox(() => { // eslint-disable-next-line no-console console.warn('[Sentry] Cannot initialize SDK with `debug` option using a non-debug bundle.'); @@ -129,7 +129,7 @@ function _init( client.init(); - logger.log(`Running in ${isCjs() ? 'CommonJS' : 'ESM'} mode.`); + debug.log(`Running in ${isCjs() ? 'CommonJS' : 'ESM'} mode.`); client.startClientReportTracking(); @@ -159,14 +159,14 @@ export function validateOpenTelemetrySetup(): void { for (const k of required) { if (!setup.includes(k)) { - logger.error( + debug.error( `You have to set up the ${k}. Without this, the OpenTelemetry & Sentry integration will not work properly.`, ); } } if (!setup.includes('SentrySampler')) { - logger.warn( + debug.warn( 'You have to set up the SentrySampler. Without this, the OpenTelemetry & Sentry integration may still work, but sample rates set for the Sentry SDK will not be respected. If you use a custom sampler, make sure to use `wrapSamplingDecision`.', ); } diff --git a/packages/node-core/src/utils/debug.ts b/packages/node-core/src/utils/debug.ts index 71df5e761230..634592dfa780 100644 --- a/packages/node-core/src/utils/debug.ts +++ b/packages/node-core/src/utils/debug.ts @@ -9,7 +9,7 @@ export async function isDebuggerEnabled(): Promise { // Node can be built without inspector support const inspector = await import('node:inspector'); cachedDebuggerEnabled = !!inspector.url(); - } catch (_) { + } catch { cachedDebuggerEnabled = false; } } diff --git a/packages/node-core/src/utils/errorhandling.ts b/packages/node-core/src/utils/errorhandling.ts index bac86d09ccb5..bb22766b5155 100644 --- a/packages/node-core/src/utils/errorhandling.ts +++ b/packages/node-core/src/utils/errorhandling.ts @@ -1,4 +1,4 @@ -import { consoleSandbox, getClient, logger } from '@sentry/core'; +import { consoleSandbox, debug, getClient } from '@sentry/core'; import { DEBUG_BUILD } from '../debug-build'; import type { NodeClient } from '../sdk/client'; @@ -16,7 +16,7 @@ export function logAndExitProcess(error: unknown): void { const client = getClient(); if (client === undefined) { - DEBUG_BUILD && logger.warn('No NodeClient was defined, we are exiting the process now.'); + DEBUG_BUILD && debug.warn('No NodeClient was defined, we are exiting the process now.'); global.process.exit(1); return; } @@ -27,12 +27,12 @@ export function logAndExitProcess(error: unknown): void { client.close(timeout).then( (result: boolean) => { if (!result) { - DEBUG_BUILD && logger.warn('We reached the timeout for emptying the request buffer, still exiting now!'); + DEBUG_BUILD && debug.warn('We reached the timeout for emptying the request buffer, still exiting now!'); } global.process.exit(1); }, error => { - DEBUG_BUILD && logger.error(error); + DEBUG_BUILD && debug.error(error); }, ); } diff --git a/packages/node-core/test/helpers/mockSdkInit.ts b/packages/node-core/test/helpers/mockSdkInit.ts index f627e9999946..ce82f92de3d8 100644 --- a/packages/node-core/test/helpers/mockSdkInit.ts +++ b/packages/node-core/test/helpers/mockSdkInit.ts @@ -8,11 +8,11 @@ import { } from '@opentelemetry/semantic-conventions'; import { createTransport, + debug, getClient, getCurrentScope, getGlobalScope, getIsolationScope, - logger, resolvedSyncPromise, SDK_VERSION, } from '@sentry/core'; @@ -36,10 +36,10 @@ function clampSpanProcessorTimeout(maxSpanWaitDuration: number | undefined): num // We guard for a max. value here, because we create an array with this length // So if this value is too large, this would fail if (maxSpanWaitDuration > MAX_MAX_SPAN_WAIT_DURATION) { - logger.warn(`\`maxSpanWaitDuration\` is too high, using the maximum value of ${MAX_MAX_SPAN_WAIT_DURATION}`); + debug.warn(`\`maxSpanWaitDuration\` is too high, using the maximum value of ${MAX_MAX_SPAN_WAIT_DURATION}`); return MAX_MAX_SPAN_WAIT_DURATION; } else if (maxSpanWaitDuration <= 0 || Number.isNaN(maxSpanWaitDuration)) { - logger.warn('`maxSpanWaitDuration` must be a positive number, using default value instead.'); + debug.warn('`maxSpanWaitDuration` must be a positive number, using default value instead.'); return undefined; } diff --git a/packages/node-core/test/integration/transactions.test.ts b/packages/node-core/test/integration/transactions.test.ts index db499cd368df..0ce3f7c99984 100644 --- a/packages/node-core/test/integration/transactions.test.ts +++ b/packages/node-core/test/integration/transactions.test.ts @@ -1,7 +1,7 @@ import { context, trace, TraceFlags } from '@opentelemetry/api'; import type { SpanProcessor } from '@opentelemetry/sdk-trace-base'; import type { TransactionEvent } from '@sentry/core'; -import { logger, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; +import { debug, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import { SentrySpanProcessor } from '@sentry/opentelemetry'; import { afterEach, describe, expect, it, vi } from 'vitest'; import * as Sentry from '../../src'; @@ -558,7 +558,7 @@ describe('Integration | Transactions', () => { vi.setSystemTime(now); const logs: unknown[] = []; - vi.spyOn(logger, 'log').mockImplementation(msg => logs.push(msg)); + vi.spyOn(debug, 'log').mockImplementation(msg => logs.push(msg)); mockSdkInit({ tracesSampleRate: 1, beforeSendTransaction }); @@ -636,7 +636,7 @@ describe('Integration | Transactions', () => { vi.setSystemTime(now); const logs: unknown[] = []; - vi.spyOn(logger, 'log').mockImplementation(msg => logs.push(msg)); + vi.spyOn(debug, 'log').mockImplementation(msg => logs.push(msg)); mockSdkInit({ tracesSampleRate: 1, diff --git a/packages/node-core/test/integrations/spotlight.test.ts b/packages/node-core/test/integrations/spotlight.test.ts index 2bd10080fd31..19d52ebcdb21 100644 --- a/packages/node-core/test/integrations/spotlight.test.ts +++ b/packages/node-core/test/integrations/spotlight.test.ts @@ -1,6 +1,6 @@ import * as http from 'node:http'; import type { Envelope, EventEnvelope } from '@sentry/core'; -import { createEnvelope, logger } from '@sentry/core'; +import { createEnvelope, debug } from '@sentry/core'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { spotlightIntegration } from '../../src/integrations/spotlight'; import { NodeClient } from '../../src/sdk/client'; @@ -16,10 +16,10 @@ vi.mock('node:http', async () => { }); describe('Spotlight', () => { - const loggerSpy = vi.spyOn(logger, 'warn'); + const debugSpy = vi.spyOn(debug, 'warn'); afterEach(() => { - loggerSpy.mockClear(); + debugSpy.mockClear(); vi.clearAllMocks(); }); @@ -124,7 +124,7 @@ describe('Spotlight', () => { it('an invalid URL is passed', () => { const integration = spotlightIntegration({ sidecarUrl: 'invalid-url' }); integration.setup!(client); - expect(loggerSpy).toHaveBeenCalledWith(expect.stringContaining('Invalid sidecar URL: invalid-url')); + expect(debugSpy).toHaveBeenCalledWith(expect.stringContaining('Invalid sidecar URL: invalid-url')); }); }); @@ -135,7 +135,7 @@ describe('Spotlight', () => { const integration = spotlightIntegration({ sidecarUrl: 'http://localhost:8969' }); integration.setup!(client); - expect(loggerSpy).toHaveBeenCalledWith( + expect(debugSpy).toHaveBeenCalledWith( expect.stringContaining("It seems you're not in dev mode. Do you really want to have Spotlight enabled?"), ); @@ -149,7 +149,7 @@ describe('Spotlight', () => { const integration = spotlightIntegration({ sidecarUrl: 'http://localhost:8969' }); integration.setup!(client); - expect(loggerSpy).not.toHaveBeenCalledWith( + expect(debugSpy).not.toHaveBeenCalledWith( expect.stringContaining("It seems you're not in dev mode. Do you really want to have Spotlight enabled?"), ); @@ -165,7 +165,7 @@ describe('Spotlight', () => { const integration = spotlightIntegration({ sidecarUrl: 'http://localhost:8969' }); integration.setup!(client); - expect(loggerSpy).not.toHaveBeenCalledWith( + expect(debugSpy).not.toHaveBeenCalledWith( expect.stringContaining("It seems you're not in dev mode. Do you really want to have Spotlight enabled?"), ); @@ -181,7 +181,7 @@ describe('Spotlight', () => { const integration = spotlightIntegration({ sidecarUrl: 'http://localhost:8969' }); integration.setup!(client); - expect(loggerSpy).not.toHaveBeenCalledWith( + expect(debugSpy).not.toHaveBeenCalledWith( expect.stringContaining("It seems you're not in dev mode. Do you really want to have Spotlight enabled?"), ); diff --git a/packages/node-core/test/sdk/init.test.ts b/packages/node-core/test/sdk/init.test.ts index 02115fa26ff7..c0c574ab2b35 100644 --- a/packages/node-core/test/sdk/init.test.ts +++ b/packages/node-core/test/sdk/init.test.ts @@ -1,5 +1,5 @@ import type { Integration } from '@sentry/core'; -import { logger, SDK_VERSION } from '@sentry/core'; +import { debug, SDK_VERSION } from '@sentry/core'; import * as SentryOpentelemetry from '@sentry/opentelemetry'; import { type Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { getClient } from '../../src/'; @@ -227,8 +227,8 @@ describe('validateOpenTelemetrySetup', () => { }); it('works with correct setup', () => { - const errorSpy = vi.spyOn(logger, 'error').mockImplementation(() => {}); - const warnSpy = vi.spyOn(logger, 'warn').mockImplementation(() => {}); + const errorSpy = vi.spyOn(debug, 'error').mockImplementation(() => {}); + const warnSpy = vi.spyOn(debug, 'warn').mockImplementation(() => {}); vi.spyOn(SentryOpentelemetry, 'openTelemetrySetupCheck').mockImplementation(() => { return ['SentryContextManager', 'SentryPropagator', 'SentrySampler']; @@ -241,8 +241,8 @@ describe('validateOpenTelemetrySetup', () => { }); it('works with missing setup, without tracing', () => { - const errorSpy = vi.spyOn(logger, 'error').mockImplementation(() => {}); - const warnSpy = vi.spyOn(logger, 'warn').mockImplementation(() => {}); + const errorSpy = vi.spyOn(debug, 'error').mockImplementation(() => {}); + const warnSpy = vi.spyOn(debug, 'warn').mockImplementation(() => {}); vi.spyOn(SentryOpentelemetry, 'openTelemetrySetupCheck').mockImplementation(() => { return []; @@ -260,8 +260,8 @@ describe('validateOpenTelemetrySetup', () => { }); it('works with missing setup, with tracing', () => { - const errorSpy = vi.spyOn(logger, 'error').mockImplementation(() => {}); - const warnSpy = vi.spyOn(logger, 'warn').mockImplementation(() => {}); + const errorSpy = vi.spyOn(debug, 'error').mockImplementation(() => {}); + const warnSpy = vi.spyOn(debug, 'warn').mockImplementation(() => {}); vi.spyOn(SentryOpentelemetry, 'openTelemetrySetupCheck').mockImplementation(() => { return []; diff --git a/packages/node-native/src/event-loop-block-integration.ts b/packages/node-native/src/event-loop-block-integration.ts index 14e60fbf0bc0..713093f77961 100644 --- a/packages/node-native/src/event-loop-block-integration.ts +++ b/packages/node-native/src/event-loop-block-integration.ts @@ -48,7 +48,7 @@ function poll(enabled: boolean, clientOptions: ClientOptions): void { const session = currentSession ? { ...currentSession, toJSON: undefined } : undefined; // message the worker to tell it the main event loop is still running threadPoll({ session, debugImages: getFilenameToDebugIdMap(clientOptions.stackParser) }, !enabled); - } catch (_) { + } catch { // we ignore all errors } } diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index afcc42f16e84..4e7a8482c474 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -23,6 +23,7 @@ export { genericPoolIntegration } from './integrations/tracing/genericPool'; export { dataloaderIntegration } from './integrations/tracing/dataloader'; export { amqplibIntegration } from './integrations/tracing/amqplib'; export { vercelAIIntegration } from './integrations/tracing/vercelai'; +export { openAIIntegration } from './integrations/tracing/openai'; export { launchDarklyIntegration, buildLaunchDarklyFlagUsedHandler, diff --git a/packages/node/src/integrations/tracing/express-v5/utils.ts b/packages/node/src/integrations/tracing/express-v5/utils.ts index 45ef61ed7eb6..85bf42958bdd 100644 --- a/packages/node/src/integrations/tracing/express-v5/utils.ts +++ b/packages/node/src/integrations/tracing/express-v5/utils.ts @@ -146,7 +146,7 @@ export const isLayerIgnored = ( return true; } } - } catch (e) { + } catch { /* catch block */ } diff --git a/packages/node/src/integrations/tracing/express.ts b/packages/node/src/integrations/tracing/express.ts index 52dfc373d470..24155e2f5452 100644 --- a/packages/node/src/integrations/tracing/express.ts +++ b/packages/node/src/integrations/tracing/express.ts @@ -5,11 +5,11 @@ import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express'; import type { IntegrationFn } from '@sentry/core'; import { captureException, + debug, defineIntegration, getDefaultIsolationScope, getIsolationScope, httpRequestToRequestData, - logger, SEMANTIC_ATTRIBUTE_SENTRY_OP, spanToJSON, } from '@sentry/core'; @@ -40,7 +40,7 @@ function requestHook(span: Span): void { function spanNameHook(info: ExpressRequestInfo, defaultName: string): string { if (getIsolationScope() === getDefaultIsolationScope()) { - DEBUG_BUILD && logger.warn('Isolation scope is still default isolation scope - skipping setting transactionName'); + DEBUG_BUILD && debug.warn('Isolation scope is still default isolation scope - skipping setting transactionName'); return defaultName; } if (info.layerType === 'request_handler') { diff --git a/packages/node/src/integrations/tracing/fastify/index.ts b/packages/node/src/integrations/tracing/fastify/index.ts index b2202a915a47..0aaf7814e9e3 100644 --- a/packages/node/src/integrations/tracing/fastify/index.ts +++ b/packages/node/src/integrations/tracing/fastify/index.ts @@ -3,10 +3,10 @@ import type { Instrumentation, InstrumentationConfig } from '@opentelemetry/inst import type { IntegrationFn, Span } from '@sentry/core'; import { captureException, + debug, defineIntegration, getClient, getIsolationScope, - logger, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanToJSON, @@ -74,7 +74,7 @@ function handleFastifyError( if (this.diagnosticsChannelExists && handlerOrigin === 'onError-hook') { DEBUG_BUILD && - logger.warn( + debug.warn( 'Fastify error handler was already registered via diagnostics channel.', 'You can safely remove `setupFastifyErrorHandler` call.', ); @@ -100,7 +100,7 @@ export const instrumentFastify = generateInstrumentOnce(INTEGRATION_NAME, () => fastifyInstance?.register(plugin).after(err => { if (err) { - DEBUG_BUILD && logger.error('Failed to setup Fastify instrumentation', err); + DEBUG_BUILD && debug.error('Failed to setup Fastify instrumentation', err); } else { instrumentClient(); diff --git a/packages/node/src/integrations/tracing/hapi/index.ts b/packages/node/src/integrations/tracing/hapi/index.ts index f06bfeb18478..eaa8d9c56ad9 100644 --- a/packages/node/src/integrations/tracing/hapi/index.ts +++ b/packages/node/src/integrations/tracing/hapi/index.ts @@ -2,11 +2,11 @@ import { HapiInstrumentation } from '@opentelemetry/instrumentation-hapi'; import type { IntegrationFn, Span } from '@sentry/core'; import { captureException, + debug, defineIntegration, getClient, getDefaultIsolationScope, getIsolationScope, - logger, SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, @@ -78,7 +78,7 @@ export const hapiErrorPlugin = { } } else { DEBUG_BUILD && - logger.warn('Isolation scope is still the default isolation scope - skipping setting transactionName'); + debug.warn('Isolation scope is still the default isolation scope - skipping setting transactionName'); } if (isErrorEvent(event)) { diff --git a/packages/node/src/integrations/tracing/index.ts b/packages/node/src/integrations/tracing/index.ts index e7122562d619..54fb4c72be2d 100644 --- a/packages/node/src/integrations/tracing/index.ts +++ b/packages/node/src/integrations/tracing/index.ts @@ -14,6 +14,7 @@ import { instrumentMongo, mongoIntegration } from './mongo'; import { instrumentMongoose, mongooseIntegration } from './mongoose'; import { instrumentMysql, mysqlIntegration } from './mysql'; import { instrumentMysql2, mysql2Integration } from './mysql2'; +import { instrumentOpenAi, openAIIntegration } from './openai'; import { instrumentPostgres, postgresIntegration } from './postgres'; import { instrumentPostgresJs, postgresJsIntegration } from './postgresjs'; import { prismaIntegration } from './prisma'; @@ -45,6 +46,7 @@ export function getAutoPerformanceIntegrations(): Integration[] { amqplibIntegration(), lruMemoizerIntegration(), vercelAIIntegration(), + openAIIntegration(), postgresJsIntegration(), ]; } @@ -77,6 +79,7 @@ export function getOpenTelemetryInstrumentationToPreload(): (((options?: any) => instrumentGenericPool, instrumentAmqplib, instrumentVercelAi, + instrumentOpenAi, instrumentPostgresJs, ]; } diff --git a/packages/node/src/integrations/tracing/koa.ts b/packages/node/src/integrations/tracing/koa.ts index 049cc9064f9a..487f471a9a12 100644 --- a/packages/node/src/integrations/tracing/koa.ts +++ b/packages/node/src/integrations/tracing/koa.ts @@ -4,10 +4,10 @@ import { ATTR_HTTP_ROUTE } from '@opentelemetry/semantic-conventions'; import type { IntegrationFn } from '@sentry/core'; import { captureException, + debug, defineIntegration, getDefaultIsolationScope, getIsolationScope, - logger, SEMANTIC_ATTRIBUTE_SENTRY_OP, spanToJSON, } from '@sentry/core'; @@ -49,7 +49,7 @@ export const instrumentKoa = generateInstrumentOnce( } if (getIsolationScope() === getDefaultIsolationScope()) { - DEBUG_BUILD && logger.warn('Isolation scope is default isolation scope - skipping setting transactionName'); + DEBUG_BUILD && debug.warn('Isolation scope is default isolation scope - skipping setting transactionName'); return; } const route = attributes[ATTR_HTTP_ROUTE]; diff --git a/packages/node/src/integrations/tracing/openai/index.ts b/packages/node/src/integrations/tracing/openai/index.ts new file mode 100644 index 000000000000..0e88d2b315cc --- /dev/null +++ b/packages/node/src/integrations/tracing/openai/index.ts @@ -0,0 +1,74 @@ +import type { IntegrationFn, OpenAiOptions } from '@sentry/core'; +import { defineIntegration, OPENAI_INTEGRATION_NAME } from '@sentry/core'; +import { generateInstrumentOnce } from '@sentry/node-core'; +import { SentryOpenAiInstrumentation } from './instrumentation'; + +export const instrumentOpenAi = generateInstrumentOnce( + OPENAI_INTEGRATION_NAME, + () => new SentryOpenAiInstrumentation({}), +); + +const _openAiIntegration = ((options: OpenAiOptions = {}) => { + return { + name: OPENAI_INTEGRATION_NAME, + options, + setupOnce() { + instrumentOpenAi(); + }, + }; +}) satisfies IntegrationFn; + +/** + * Adds Sentry tracing instrumentation for the OpenAI SDK. + * + * This integration is enabled by default. + * + * When configured, this integration automatically instruments OpenAI SDK client instances + * to capture telemetry data following OpenTelemetry Semantic Conventions for Generative AI. + * + * @example + * ```javascript + * import * as Sentry from '@sentry/node'; + * + * Sentry.init({ + * integrations: [Sentry.openAIIntegration()], + * }); + * ``` + * + * ## Options + * + * - `recordInputs`: Whether to record prompt messages (default: respects `sendDefaultPii` client option) + * - `recordOutputs`: Whether to record response text (default: respects `sendDefaultPii` client option) + * + * ### Default Behavior + * + * By default, the integration will: + * - Record inputs and outputs ONLY if `sendDefaultPii` is set to `true` in your Sentry client options + * - Otherwise, inputs and outputs are NOT recorded unless explicitly enabled + * + * @example + * ```javascript + * // Record inputs and outputs when sendDefaultPii is false + * Sentry.init({ + * integrations: [ + * Sentry.openAIIntegration({ + * recordInputs: true, + * recordOutputs: true + * }) + * ], + * }); + * + * // Never record inputs/outputs regardless of sendDefaultPii + * Sentry.init({ + * sendDefaultPii: true, + * integrations: [ + * Sentry.openAIIntegration({ + * recordInputs: false, + * recordOutputs: false + * }) + * ], + * }); + * ``` + * + */ +export const openAIIntegration = defineIntegration(_openAiIntegration); diff --git a/packages/node/src/integrations/tracing/openai/instrumentation.ts b/packages/node/src/integrations/tracing/openai/instrumentation.ts new file mode 100644 index 000000000000..2cce987db182 --- /dev/null +++ b/packages/node/src/integrations/tracing/openai/instrumentation.ts @@ -0,0 +1,94 @@ +import { + type InstrumentationConfig, + type InstrumentationModuleDefinition, + InstrumentationBase, + InstrumentationNodeModuleDefinition, +} from '@opentelemetry/instrumentation'; +import type { Integration, OpenAiClient, OpenAiOptions } from '@sentry/core'; +import { getCurrentScope, instrumentOpenAiClient, OPENAI_INTEGRATION_NAME, SDK_VERSION } from '@sentry/core'; + +const supportedVersions = ['>=4.0.0 <6']; + +export interface OpenAiIntegration extends Integration { + options: OpenAiOptions; +} + +/** + * Represents the patched shape of the OpenAI module export. + */ +interface PatchedModuleExports { + [key: string]: unknown; + OpenAI: abstract new (...args: unknown[]) => OpenAiClient; +} + +/** + * Determines telemetry recording settings. + */ +function determineRecordingSettings( + integrationOptions: OpenAiOptions | undefined, + defaultEnabled: boolean, +): { recordInputs: boolean; recordOutputs: boolean } { + const recordInputs = integrationOptions?.recordInputs ?? defaultEnabled; + const recordOutputs = integrationOptions?.recordOutputs ?? defaultEnabled; + return { recordInputs, recordOutputs }; +} + +/** + * Sentry OpenAI instrumentation using OpenTelemetry. + */ +export class SentryOpenAiInstrumentation extends InstrumentationBase { + public constructor(config: InstrumentationConfig = {}) { + super('@sentry/instrumentation-openai', SDK_VERSION, config); + } + + /** + * Initializes the instrumentation by defining the modules to be patched. + */ + public init(): InstrumentationModuleDefinition { + const module = new InstrumentationNodeModuleDefinition('openai', supportedVersions, this._patch.bind(this)); + return module; + } + + /** + * Core patch logic applying instrumentation to the OpenAI client constructor. + */ + private _patch(exports: PatchedModuleExports): PatchedModuleExports | void { + const Original = exports.OpenAI; + + const WrappedOpenAI = function (this: unknown, ...args: unknown[]) { + const instance = Reflect.construct(Original, args); + const scopeClient = getCurrentScope().getClient(); + const integration = scopeClient?.getIntegrationByName(OPENAI_INTEGRATION_NAME); + const integrationOpts = integration?.options; + const defaultPii = Boolean(scopeClient?.getOptions().sendDefaultPii); + + const { recordInputs, recordOutputs } = determineRecordingSettings(integrationOpts, defaultPii); + + return instrumentOpenAiClient(instance as OpenAiClient, { + recordInputs, + recordOutputs, + }); + } as unknown as abstract new (...args: unknown[]) => OpenAiClient; + + // Preserve static and prototype chains + Object.setPrototypeOf(WrappedOpenAI, Original); + Object.setPrototypeOf(WrappedOpenAI.prototype, Original.prototype); + + for (const key of Object.getOwnPropertyNames(Original)) { + if (!['length', 'name', 'prototype'].includes(key)) { + const descriptor = Object.getOwnPropertyDescriptor(Original, key); + if (descriptor) { + Object.defineProperty(WrappedOpenAI, key, descriptor); + } + } + } + + const isESM = Object.prototype.toString.call(exports) === '[object Module]'; + if (isESM) { + exports.OpenAI = WrappedOpenAI; + return exports; + } + + return { ...exports, OpenAI: WrappedOpenAI }; + } +} diff --git a/packages/node/src/integrations/tracing/postgresjs.ts b/packages/node/src/integrations/tracing/postgresjs.ts index 45d581edc45f..1a0eae973bc6 100644 --- a/packages/node/src/integrations/tracing/postgresjs.ts +++ b/packages/node/src/integrations/tracing/postgresjs.ts @@ -19,9 +19,9 @@ import { } from '@opentelemetry/semantic-conventions'; import type { IntegrationFn, Span } from '@sentry/core'; import { + debug, defineIntegration, getCurrentScope, - logger, SDK_VERSION, SPAN_STATUS_ERROR, startSpanManual, @@ -211,7 +211,7 @@ export class PostgresJsInstrumentation extends InstrumentationBase requestHook(span, sanitizedSqlQuery, postgresConnectionContext), error => { if (error) { - logger.error(`Error in requestHook for ${INTEGRATION_NAME} integration:`, error); + debug.error(`Error in requestHook for ${INTEGRATION_NAME} integration:`, error); } }, ); diff --git a/packages/node/src/sdk/initOtel.ts b/packages/node/src/sdk/initOtel.ts index 3c35a1fecc3a..4e58414f347a 100644 --- a/packages/node/src/sdk/initOtel.ts +++ b/packages/node/src/sdk/initOtel.ts @@ -7,7 +7,7 @@ import { ATTR_SERVICE_VERSION, SEMRESATTRS_SERVICE_NAMESPACE, } from '@opentelemetry/semantic-conventions'; -import { consoleSandbox, GLOBAL_OBJ, logger, SDK_VERSION } from '@sentry/core'; +import { consoleSandbox, debug as coreDebug, GLOBAL_OBJ, SDK_VERSION } from '@sentry/core'; import { type NodeClient, isCjs, SentryContextManager, setupOpenTelemetryLogger } from '@sentry/node-core'; import { SentryPropagator, SentrySampler, SentrySpanProcessor } from '@sentry/opentelemetry'; import { createAddHookMessageChannel } from 'import-in-the-middle'; @@ -50,7 +50,7 @@ export function maybeInitializeEsmLoader(): void { transferList: [addHookMessagePort], }); } catch (error) { - logger.warn('Failed to register ESM hook', error); + coreDebug.warn('Failed to register ESM hook', error); } } } else { @@ -77,7 +77,7 @@ export function preloadOpenTelemetry(options: NodePreloadOptions = {}): void { const { debug } = options; if (debug) { - logger.enable(); + coreDebug.enable(); } if (!isCjs()) { @@ -89,7 +89,7 @@ export function preloadOpenTelemetry(options: NodePreloadOptions = {}): void { fn(); if (debug) { - logger.log(`[Sentry] Preloaded ${fn.id} instrumentation`); + coreDebug.log(`[Sentry] Preloaded ${fn.id} instrumentation`); } }); } @@ -142,10 +142,10 @@ export function _clampSpanProcessorTimeout(maxSpanWaitDuration: number | undefin // So if this value is too large, this would fail if (maxSpanWaitDuration > MAX_MAX_SPAN_WAIT_DURATION) { DEBUG_BUILD && - logger.warn(`\`maxSpanWaitDuration\` is too high, using the maximum value of ${MAX_MAX_SPAN_WAIT_DURATION}`); + coreDebug.warn(`\`maxSpanWaitDuration\` is too high, using the maximum value of ${MAX_MAX_SPAN_WAIT_DURATION}`); return MAX_MAX_SPAN_WAIT_DURATION; } else if (maxSpanWaitDuration <= 0 || Number.isNaN(maxSpanWaitDuration)) { - DEBUG_BUILD && logger.warn('`maxSpanWaitDuration` must be a positive number, using default value instead.'); + DEBUG_BUILD && coreDebug.warn('`maxSpanWaitDuration` must be a positive number, using default value instead.'); return undefined; } diff --git a/packages/node/src/utils/redisCache.ts b/packages/node/src/utils/redisCache.ts index 20cca873c55a..b9d6cb814856 100644 --- a/packages/node/src/utils/redisCache.ts +++ b/packages/node/src/utils/redisCache.ts @@ -53,7 +53,7 @@ export function getCacheKeySafely(redisCommand: string, cmdArgs: IORedisCommandA } return flatten(cmdArgs.map(arg => processArg(arg))); - } catch (e) { + } catch { return undefined; } } @@ -82,7 +82,7 @@ export function calculateCacheItemSize(response: unknown): number | undefined { else if (typeof value === 'number') return value.toString().length; else if (value === null || value === undefined) return 0; return JSON.stringify(value).length; - } catch (e) { + } catch { return undefined; } }; diff --git a/packages/node/test/integration/transactions.test.ts b/packages/node/test/integration/transactions.test.ts index db499cd368df..0ce3f7c99984 100644 --- a/packages/node/test/integration/transactions.test.ts +++ b/packages/node/test/integration/transactions.test.ts @@ -1,7 +1,7 @@ import { context, trace, TraceFlags } from '@opentelemetry/api'; import type { SpanProcessor } from '@opentelemetry/sdk-trace-base'; import type { TransactionEvent } from '@sentry/core'; -import { logger, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; +import { debug, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import { SentrySpanProcessor } from '@sentry/opentelemetry'; import { afterEach, describe, expect, it, vi } from 'vitest'; import * as Sentry from '../../src'; @@ -558,7 +558,7 @@ describe('Integration | Transactions', () => { vi.setSystemTime(now); const logs: unknown[] = []; - vi.spyOn(logger, 'log').mockImplementation(msg => logs.push(msg)); + vi.spyOn(debug, 'log').mockImplementation(msg => logs.push(msg)); mockSdkInit({ tracesSampleRate: 1, beforeSendTransaction }); @@ -636,7 +636,7 @@ describe('Integration | Transactions', () => { vi.setSystemTime(now); const logs: unknown[] = []; - vi.spyOn(logger, 'log').mockImplementation(msg => logs.push(msg)); + vi.spyOn(debug, 'log').mockImplementation(msg => logs.push(msg)); mockSdkInit({ tracesSampleRate: 1, diff --git a/packages/node/test/sdk/init.test.ts b/packages/node/test/sdk/init.test.ts index 1aa11387f5c3..422bbdc924f4 100644 --- a/packages/node/test/sdk/init.test.ts +++ b/packages/node/test/sdk/init.test.ts @@ -1,5 +1,5 @@ import type { Integration } from '@sentry/core'; -import { logger, SDK_VERSION } from '@sentry/core'; +import { debug, SDK_VERSION } from '@sentry/core'; import * as SentryOpentelemetry from '@sentry/opentelemetry'; import { type Mock, type MockInstance, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { getClient, NodeClient, validateOpenTelemetrySetup } from '../../src/'; @@ -26,8 +26,8 @@ describe('init()', () => { beforeEach(() => { global.__SENTRY__ = {}; - // prevent the logger from being enabled, resulting in console.log calls - vi.spyOn(logger, 'enable').mockImplementation(() => {}); + // prevent the debug from being enabled, resulting in console.log calls + vi.spyOn(debug, 'enable').mockImplementation(() => {}); mockAutoPerformanceIntegrations = vi.spyOn(auto, 'getAutoPerformanceIntegrations').mockImplementation(() => []); }); @@ -285,8 +285,8 @@ describe('validateOpenTelemetrySetup', () => { }); it('works with correct setup', () => { - const errorSpy = vi.spyOn(logger, 'error').mockImplementation(() => {}); - const warnSpy = vi.spyOn(logger, 'warn').mockImplementation(() => {}); + const errorSpy = vi.spyOn(debug, 'error').mockImplementation(() => {}); + const warnSpy = vi.spyOn(debug, 'warn').mockImplementation(() => {}); vi.spyOn(SentryOpentelemetry, 'openTelemetrySetupCheck').mockImplementation(() => { return ['SentryContextManager', 'SentryPropagator', 'SentrySampler']; @@ -299,8 +299,8 @@ describe('validateOpenTelemetrySetup', () => { }); it('works with missing setup, without tracing', () => { - const errorSpy = vi.spyOn(logger, 'error').mockImplementation(() => {}); - const warnSpy = vi.spyOn(logger, 'warn').mockImplementation(() => {}); + const errorSpy = vi.spyOn(debug, 'error').mockImplementation(() => {}); + const warnSpy = vi.spyOn(debug, 'warn').mockImplementation(() => {}); vi.spyOn(SentryOpentelemetry, 'openTelemetrySetupCheck').mockImplementation(() => { return []; @@ -318,8 +318,8 @@ describe('validateOpenTelemetrySetup', () => { }); it('works with missing setup, with tracing', () => { - const errorSpy = vi.spyOn(logger, 'error').mockImplementation(() => {}); - const warnSpy = vi.spyOn(logger, 'warn').mockImplementation(() => {}); + const errorSpy = vi.spyOn(debug, 'error').mockImplementation(() => {}); + const warnSpy = vi.spyOn(debug, 'warn').mockImplementation(() => {}); vi.spyOn(SentryOpentelemetry, 'openTelemetrySetupCheck').mockImplementation(() => { return []; diff --git a/packages/node/test/sdk/initOtel.test.ts b/packages/node/test/sdk/initOtel.test.ts index 69c1da3fc258..a373d733457b 100644 --- a/packages/node/test/sdk/initOtel.test.ts +++ b/packages/node/test/sdk/initOtel.test.ts @@ -1,4 +1,4 @@ -import { logger } from '@sentry/core'; +import { debug } from '@sentry/core'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { _clampSpanProcessorTimeout } from '../../src/sdk/initOtel'; @@ -8,71 +8,71 @@ describe('_clampSpanProcessorTimeout', () => { }); it('works with undefined', () => { - const loggerWarnSpy = vi.spyOn(logger, 'warn').mockImplementation(() => {}); + const debugWarnSpy = vi.spyOn(debug, 'warn').mockImplementation(() => {}); const timeout = _clampSpanProcessorTimeout(undefined); expect(timeout).toBe(undefined); - expect(loggerWarnSpy).not.toHaveBeenCalled(); + expect(debugWarnSpy).not.toHaveBeenCalled(); }); it('works with positive number', () => { - const loggerWarnSpy = vi.spyOn(logger, 'warn').mockImplementation(() => {}); + const debugWarnSpy = vi.spyOn(debug, 'warn').mockImplementation(() => {}); const timeout = _clampSpanProcessorTimeout(10); expect(timeout).toBe(10); - expect(loggerWarnSpy).not.toHaveBeenCalled(); + expect(debugWarnSpy).not.toHaveBeenCalled(); }); it('works with 0', () => { - const loggerWarnSpy = vi.spyOn(logger, 'warn').mockImplementation(() => {}); + const debugWarnSpy = vi.spyOn(debug, 'warn').mockImplementation(() => {}); const timeout = _clampSpanProcessorTimeout(0); expect(timeout).toBe(undefined); - expect(loggerWarnSpy).toHaveBeenCalledTimes(1); - expect(loggerWarnSpy).toHaveBeenCalledWith( + expect(debugWarnSpy).toHaveBeenCalledTimes(1); + expect(debugWarnSpy).toHaveBeenCalledWith( '`maxSpanWaitDuration` must be a positive number, using default value instead.', ); }); it('works with negative number', () => { - const loggerWarnSpy = vi.spyOn(logger, 'warn').mockImplementation(() => {}); + const debugWarnSpy = vi.spyOn(debug, 'warn').mockImplementation(() => {}); const timeout = _clampSpanProcessorTimeout(-10); expect(timeout).toBe(undefined); - expect(loggerWarnSpy).toHaveBeenCalledTimes(1); - expect(loggerWarnSpy).toHaveBeenCalledWith( + expect(debugWarnSpy).toHaveBeenCalledTimes(1); + expect(debugWarnSpy).toHaveBeenCalledWith( '`maxSpanWaitDuration` must be a positive number, using default value instead.', ); }); it('works with -Infinity', () => { - const loggerWarnSpy = vi.spyOn(logger, 'warn').mockImplementation(() => {}); + const debugWarnSpy = vi.spyOn(debug, 'warn').mockImplementation(() => {}); const timeout = _clampSpanProcessorTimeout(-Infinity); expect(timeout).toBe(undefined); - expect(loggerWarnSpy).toHaveBeenCalledTimes(1); - expect(loggerWarnSpy).toHaveBeenCalledWith( + expect(debugWarnSpy).toHaveBeenCalledTimes(1); + expect(debugWarnSpy).toHaveBeenCalledWith( '`maxSpanWaitDuration` must be a positive number, using default value instead.', ); }); it('works with Infinity', () => { - const loggerWarnSpy = vi.spyOn(logger, 'warn').mockImplementation(() => {}); + const debugWarnSpy = vi.spyOn(debug, 'warn').mockImplementation(() => {}); const timeout = _clampSpanProcessorTimeout(Infinity); expect(timeout).toBe(1_000_000); - expect(loggerWarnSpy).toHaveBeenCalledTimes(1); - expect(loggerWarnSpy).toHaveBeenCalledWith('`maxSpanWaitDuration` is too high, using the maximum value of 1000000'); + expect(debugWarnSpy).toHaveBeenCalledTimes(1); + expect(debugWarnSpy).toHaveBeenCalledWith('`maxSpanWaitDuration` is too high, using the maximum value of 1000000'); }); it('works with large number', () => { - const loggerWarnSpy = vi.spyOn(logger, 'warn').mockImplementation(() => {}); + const debugWarnSpy = vi.spyOn(debug, 'warn').mockImplementation(() => {}); const timeout = _clampSpanProcessorTimeout(1_000_000_000); expect(timeout).toBe(1_000_000); - expect(loggerWarnSpy).toHaveBeenCalledTimes(1); - expect(loggerWarnSpy).toHaveBeenCalledWith('`maxSpanWaitDuration` is too high, using the maximum value of 1000000'); + expect(debugWarnSpy).toHaveBeenCalledTimes(1); + expect(debugWarnSpy).toHaveBeenCalledWith('`maxSpanWaitDuration` is too high, using the maximum value of 1000000'); }); it('works with NaN', () => { - const loggerWarnSpy = vi.spyOn(logger, 'warn').mockImplementation(() => {}); + const debugWarnSpy = vi.spyOn(debug, 'warn').mockImplementation(() => {}); const timeout = _clampSpanProcessorTimeout(NaN); expect(timeout).toBe(undefined); - expect(loggerWarnSpy).toHaveBeenCalledTimes(1); - expect(loggerWarnSpy).toHaveBeenCalledWith( + expect(debugWarnSpy).toHaveBeenCalledTimes(1); + expect(debugWarnSpy).toHaveBeenCalledWith( '`maxSpanWaitDuration` must be a positive number, using default value instead.', ); }); diff --git a/packages/node/test/sdk/preload.test.ts b/packages/node/test/sdk/preload.test.ts index 8daf8ada5c4b..97badc28c9eb 100644 --- a/packages/node/test/sdk/preload.test.ts +++ b/packages/node/test/sdk/preload.test.ts @@ -1,10 +1,10 @@ -import { logger } from '@sentry/core'; +import { debug } from '@sentry/core'; import { afterEach, describe, expect, it, vi } from 'vitest'; describe('preload', () => { afterEach(() => { vi.resetAllMocks(); - logger.disable(); + debug.disable(); delete process.env.SENTRY_DEBUG; delete process.env.SENTRY_PRELOAD_INTEGRATIONS; diff --git a/packages/opentelemetry/package.json b/packages/opentelemetry/package.json index 18543419747c..3989ce16f685 100644 --- a/packages/opentelemetry/package.json +++ b/packages/opentelemetry/package.json @@ -45,7 +45,6 @@ "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.0.0", "@opentelemetry/core": "^1.30.1 || ^2.0.0", - "@opentelemetry/instrumentation": "^0.57.1 || ^0.202.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.0.0", "@opentelemetry/semantic-conventions": "^1.34.0" }, @@ -53,7 +52,6 @@ "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1", "@opentelemetry/core": "^1.30.1", - "@opentelemetry/instrumentation": "^0.57.2", "@opentelemetry/sdk-trace-base": "^1.30.1", "@opentelemetry/semantic-conventions": "^1.34.0" }, diff --git a/packages/opentelemetry/src/propagator.ts b/packages/opentelemetry/src/propagator.ts index 30429a490b14..40afc10120df 100644 --- a/packages/opentelemetry/src/propagator.ts +++ b/packages/opentelemetry/src/propagator.ts @@ -4,13 +4,13 @@ import { isTracingSuppressed, W3CBaggagePropagator } from '@opentelemetry/core'; import { ATTR_URL_FULL, SEMATTRS_HTTP_URL } from '@opentelemetry/semantic-conventions'; import type { Client, continueTrace, DynamicSamplingContext, Options, Scope } from '@sentry/core'; import { + debug, generateSentryTraceHeader, getClient, getCurrentScope, getDynamicSamplingContextFromScope, getDynamicSamplingContextFromSpan, getIsolationScope, - logger, LRUMap, parseBaggageHeader, propagationContextFromHeaders, @@ -45,7 +45,7 @@ export class SentryPropagator extends W3CBaggagePropagator { */ public inject(context: Context, carrier: unknown, setter: TextMapSetter): void { if (isTracingSuppressed(context)) { - DEBUG_BUILD && logger.log('[Tracing] Not injecting trace data for url because tracing is suppressed.'); + DEBUG_BUILD && debug.log('[Tracing] Not injecting trace data for url because tracing is suppressed.'); return; } @@ -55,10 +55,7 @@ export class SentryPropagator extends W3CBaggagePropagator { const tracePropagationTargets = getClient()?.getOptions()?.tracePropagationTargets; if (!shouldPropagateTraceForUrl(url, tracePropagationTargets, this._urlMatchesTargetsMap)) { DEBUG_BUILD && - logger.log( - '[Tracing] Not injecting trace data for url because it does not match tracePropagationTargets:', - url, - ); + debug.log('[Tracing] Not injecting trace data for url because it does not match tracePropagationTargets:', url); return; } @@ -139,14 +136,14 @@ export function shouldPropagateTraceForUrl( const cachedDecision = decisionMap?.get(url); if (cachedDecision !== undefined) { - DEBUG_BUILD && !cachedDecision && logger.log(NOT_PROPAGATED_MESSAGE, url); + DEBUG_BUILD && !cachedDecision && debug.log(NOT_PROPAGATED_MESSAGE, url); return cachedDecision; } const decision = stringMatchesSomePattern(url, tracePropagationTargets); decisionMap?.set(url, decision); - DEBUG_BUILD && !decision && logger.log(NOT_PROPAGATED_MESSAGE, url); + DEBUG_BUILD && !decision && debug.log(NOT_PROPAGATED_MESSAGE, url); return decision; } diff --git a/packages/opentelemetry/src/sampler.ts b/packages/opentelemetry/src/sampler.ts index 73c568152b97..e06fe51bfd2a 100644 --- a/packages/opentelemetry/src/sampler.ts +++ b/packages/opentelemetry/src/sampler.ts @@ -13,8 +13,8 @@ import { import type { Client, SpanAttributes } from '@sentry/core'; import { baggageHeaderToDynamicSamplingContext, + debug, hasSpansEnabled, - logger, parseSampleRate, sampleSpan, SEMANTIC_ATTRIBUTE_SENTRY_OP, @@ -137,7 +137,7 @@ export class SentrySampler implements Sampler { const method = `${maybeSpanHttpMethod}`.toUpperCase(); if (method === 'OPTIONS' || method === 'HEAD') { - DEBUG_BUILD && logger.log(`[Tracing] Not sampling span because HTTP method is '${method}' for ${spanName}`); + DEBUG_BUILD && debug.log(`[Tracing] Not sampling span because HTTP method is '${method}' for ${spanName}`); return wrapSamplingDecision({ decision: SamplingDecision.NOT_RECORD, @@ -153,7 +153,7 @@ export class SentrySampler implements Sampler { // We check for `parentSampled === undefined` because we only want to record client reports for spans that are trace roots (ie. when there was incoming trace) parentSampled === undefined ) { - DEBUG_BUILD && logger.log('[Tracing] Discarding root span because its trace was not chosen to be sampled.'); + DEBUG_BUILD && debug.log('[Tracing] Discarding root span because its trace was not chosen to be sampled.'); this._client.recordDroppedEvent('sample_rate', 'transaction'); } @@ -187,12 +187,12 @@ function getParentSampled(parentSpan: Span, traceId: string, spanName: string): if (parentContext.isRemote) { const parentSampled = getSamplingDecision(parentSpan.spanContext()); DEBUG_BUILD && - logger.log(`[Tracing] Inheriting remote parent's sampled decision for ${spanName}: ${parentSampled}`); + debug.log(`[Tracing] Inheriting remote parent's sampled decision for ${spanName}: ${parentSampled}`); return parentSampled; } const parentSampled = getSamplingDecision(parentContext); - DEBUG_BUILD && logger.log(`[Tracing] Inheriting parent's sampled decision for ${spanName}: ${parentSampled}`); + DEBUG_BUILD && debug.log(`[Tracing] Inheriting parent's sampled decision for ${spanName}: ${parentSampled}`); return parentSampled; } diff --git a/packages/opentelemetry/src/spanExporter.ts b/packages/opentelemetry/src/spanExporter.ts index 3328b64c8230..ea85641387a5 100644 --- a/packages/opentelemetry/src/spanExporter.ts +++ b/packages/opentelemetry/src/spanExporter.ts @@ -15,10 +15,10 @@ import { captureEvent, convertSpanLinksForEnvelope, debounce, + debug, getCapturedScopesOnSpan, getDynamicSamplingContextFromSpan, getStatusMessage, - logger, SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, @@ -105,7 +105,7 @@ export class SentrySpanExporter { }); if (droppedSpanCount > 0) { DEBUG_BUILD && - logger.log( + debug.log( `SpanExporter dropped ${droppedSpanCount} spans because they were pending for more than ${this._finishedSpanBucketSize} seconds.`, ); } @@ -142,7 +142,7 @@ export class SentrySpanExporter { const sentSpanCount = sentSpans.size; const remainingOpenSpanCount = finishedSpans.length - sentSpanCount; DEBUG_BUILD && - logger.log( + debug.log( `SpanExporter exported ${sentSpanCount} spans, ${remainingOpenSpanCount} spans are waiting for their parent spans to finish`, ); diff --git a/packages/opentelemetry/test/helpers/initOtel.ts b/packages/opentelemetry/test/helpers/initOtel.ts index dd797f44ffb2..fd7a33884b5c 100644 --- a/packages/opentelemetry/test/helpers/initOtel.ts +++ b/packages/opentelemetry/test/helpers/initOtel.ts @@ -7,7 +7,7 @@ import { ATTR_SERVICE_VERSION, SEMRESATTRS_SERVICE_NAMESPACE, } from '@opentelemetry/semantic-conventions'; -import { getClient, logger, SDK_VERSION } from '@sentry/core'; +import { debug, getClient, SDK_VERSION } from '@sentry/core'; import { wrapContextManagerClass } from '../../src/contextManager'; import { DEBUG_BUILD } from '../../src/debug-build'; import { SentryPropagator } from '../../src/propagator'; @@ -25,21 +25,25 @@ export function initOtel(): void { if (!client) { DEBUG_BUILD && - logger.warn( + debug.warn( 'No client available, skipping OpenTelemetry setup. This probably means that `Sentry.init()` was not called before `initOtel()`.', ); return; } if (client.getOptions().debug) { - const otelLogger = new Proxy(logger as typeof logger & { verbose: (typeof logger)['debug'] }, { - get(target, prop, receiver) { - const actualProp = prop === 'verbose' ? 'debug' : prop; - return Reflect.get(target, actualProp, receiver); + // Disable diag, to ensure this works even if called multiple times + diag.disable(); + diag.setLogger( + { + error: debug.error, + warn: debug.warn, + info: debug.log, + debug: debug.log, + verbose: debug.log, }, - }); - - diag.setLogger(otelLogger, DiagLogLevel.DEBUG); + DiagLogLevel.DEBUG, + ); } setupEventContextTrace(client); diff --git a/packages/opentelemetry/test/integration/transactions.test.ts b/packages/opentelemetry/test/integration/transactions.test.ts index c3cc9b0e8b7b..b476d7536e5e 100644 --- a/packages/opentelemetry/test/integration/transactions.test.ts +++ b/packages/opentelemetry/test/integration/transactions.test.ts @@ -5,8 +5,8 @@ import type { SpanProcessor } from '@opentelemetry/sdk-trace-base'; import type { Event, TransactionEvent } from '@sentry/core'; import { addBreadcrumb, + debug, getClient, - logger, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, setTag, @@ -440,7 +440,7 @@ describe('Integration | Transactions', () => { vi.setSystemTime(now); const logs: unknown[] = []; - vi.spyOn(logger, 'log').mockImplementation(msg => logs.push(msg)); + vi.spyOn(debug, 'log').mockImplementation(msg => logs.push(msg)); mockSdkInit({ tracesSampleRate: 1, beforeSendTransaction }); @@ -510,7 +510,7 @@ describe('Integration | Transactions', () => { vi.setSystemTime(now); const logs: unknown[] = []; - vi.spyOn(logger, 'log').mockImplementation(msg => logs.push(msg)); + vi.spyOn(debug, 'log').mockImplementation(msg => logs.push(msg)); const transactions: Event[] = []; @@ -568,7 +568,7 @@ describe('Integration | Transactions', () => { vi.setSystemTime(now); const logs: unknown[] = []; - vi.spyOn(logger, 'log').mockImplementation(msg => logs.push(msg)); + vi.spyOn(debug, 'log').mockImplementation(msg => logs.push(msg)); const transactions: Event[] = []; @@ -639,7 +639,7 @@ describe('Integration | Transactions', () => { vi.setSystemTime(now); const logs: unknown[] = []; - vi.spyOn(logger, 'log').mockImplementation(msg => logs.push(msg)); + vi.spyOn(debug, 'log').mockImplementation(msg => logs.push(msg)); const transactions: Event[] = []; @@ -701,7 +701,7 @@ describe('Integration | Transactions', () => { vi.setSystemTime(now); const logs: unknown[] = []; - vi.spyOn(logger, 'log').mockImplementation(msg => logs.push(msg)); + vi.spyOn(debug, 'log').mockImplementation(msg => logs.push(msg)); const transactions: Event[] = []; diff --git a/packages/react-router/src/server/instrumentation/reactRouter.ts b/packages/react-router/src/server/instrumentation/reactRouter.ts index f369e22ce66e..5b7b001e4de5 100644 --- a/packages/react-router/src/server/instrumentation/reactRouter.ts +++ b/packages/react-router/src/server/instrumentation/reactRouter.ts @@ -2,9 +2,9 @@ import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; import { InstrumentationBase, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation'; import { SEMATTRS_HTTP_TARGET } from '@opentelemetry/semantic-conventions'; import { + debug, getActiveSpan, getRootSpan, - logger, SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, @@ -67,7 +67,7 @@ export class ReactRouterInstrumentation extends InstrumentationBase transaction.match(regex))) { - options.debug && logger.log('[ReactRouter] Filtered node_modules transaction:', event.transaction); + options.debug && debug.log('[ReactRouter] Filtered node_modules transaction:', event.transaction); return null; } diff --git a/packages/react-router/src/server/sdk.ts b/packages/react-router/src/server/sdk.ts index 9d8a22862a11..8c3954e4a418 100644 --- a/packages/react-router/src/server/sdk.ts +++ b/packages/react-router/src/server/sdk.ts @@ -1,5 +1,5 @@ import type { Integration } from '@sentry/core'; -import { applySdkMetadata, logger, setTag } from '@sentry/core'; +import { applySdkMetadata, debug, setTag } from '@sentry/core'; import type { NodeClient, NodeOptions } from '@sentry/node'; import { getDefaultIntegrations as getNodeDefaultIntegrations, init as initNodeSdk } from '@sentry/node'; import { DEBUG_BUILD } from '../common/debug-build'; @@ -27,7 +27,7 @@ export function init(options: NodeOptions): NodeClient | undefined { defaultIntegrations: getDefaultReactRouterServerIntegrations(options), }; - DEBUG_BUILD && logger.log('Initializing SDK...'); + DEBUG_BUILD && debug.log('Initializing SDK...'); applySdkMetadata(opts, 'react-router', ['react-router', 'node']); @@ -35,7 +35,7 @@ export function init(options: NodeOptions): NodeClient | undefined { setTag('runtime', 'node'); - DEBUG_BUILD && logger.log('SDK successfully initialized'); + DEBUG_BUILD && debug.log('SDK successfully initialized'); return client; } diff --git a/packages/react-router/test/server/instrumentation/reactRouterServer.test.ts b/packages/react-router/test/server/instrumentation/reactRouterServer.test.ts index d89fdf624294..473ad1272ca4 100644 --- a/packages/react-router/test/server/instrumentation/reactRouterServer.test.ts +++ b/packages/react-router/test/server/instrumentation/reactRouterServer.test.ts @@ -10,8 +10,8 @@ vi.mock('@sentry/core', async () => { getRootSpan: vi.fn(), spanToJSON: vi.fn(), updateSpanName: vi.fn(), - logger: { - debug: vi.fn(), + debug: { + log: vi.fn(), }, SDK_VERSION: '1.0.0', SEMANTIC_ATTRIBUTE_SENTRY_OP: 'sentry.op', @@ -82,9 +82,7 @@ describe('ReactRouterInstrumentation', () => { const req = createRequest('https://test.com/data'); await wrappedHandler(req); - expect(SentryCore.logger.debug).toHaveBeenCalledWith( - 'No active root span found, skipping tracing for data request', - ); + expect(SentryCore.debug.log).toHaveBeenCalledWith('No active root span found, skipping tracing for data request'); expect(originalHandler).toHaveBeenCalledWith(req, undefined); }); diff --git a/packages/react-router/test/server/lowQualityTransactionsFilterIntegration.test.ts b/packages/react-router/test/server/lowQualityTransactionsFilterIntegration.test.ts index 3aac16d0d05d..4f43b48dcad9 100644 --- a/packages/react-router/test/server/lowQualityTransactionsFilterIntegration.test.ts +++ b/packages/react-router/test/server/lowQualityTransactionsFilterIntegration.test.ts @@ -4,7 +4,7 @@ import * as SentryNode from '@sentry/node'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { lowQualityTransactionsFilterIntegration } from '../../src/server/integration/lowQualityTransactionsFilterIntegration'; -const loggerLog = vi.spyOn(SentryCore.logger, 'log').mockImplementation(() => {}); +const debugLoggerLogSpy = vi.spyOn(SentryCore.debug, 'log').mockImplementation(() => {}); describe('Low Quality Transactions Filter Integration', () => { afterEach(() => { @@ -30,7 +30,7 @@ describe('Low Quality Transactions Filter Integration', () => { expect(result).toBeNull(); - expect(loggerLog).toHaveBeenCalledWith('[ReactRouter] Filtered node_modules transaction:', transaction); + expect(debugLoggerLogSpy).toHaveBeenCalledWith('[ReactRouter] Filtered node_modules transaction:', transaction); }); }); diff --git a/packages/react/src/redux.ts b/packages/react/src/redux.ts index 3a713701c676..1e0be6f7d190 100644 --- a/packages/react/src/redux.ts +++ b/packages/react/src/redux.ts @@ -106,7 +106,7 @@ function createReduxEnhancer(enhancerOptions?: Partial): { filename: 'redux_state.json', data: JSON.stringify(event.contexts.state.state.value) }, ]; } - } catch (_) { + } catch { // empty } return event; diff --git a/packages/remix/src/server/errors.ts b/packages/remix/src/server/errors.ts index d21e9d57d42c..def6092717c8 100644 --- a/packages/remix/src/server/errors.ts +++ b/packages/remix/src/server/errors.ts @@ -54,7 +54,7 @@ export async function captureRemixServerException(err: unknown, name: string, re try { normalizedRequest = winterCGRequestToRequestData(request); - } catch (e) { + } catch { DEBUG_BUILD && debug.warn('Failed to normalize Remix request'); } diff --git a/packages/remix/src/server/instrumentServer.ts b/packages/remix/src/server/instrumentServer.ts index f93c195df355..db8322a0c828 100644 --- a/packages/remix/src/server/instrumentServer.ts +++ b/packages/remix/src/server/instrumentServer.ts @@ -288,7 +288,7 @@ function wrapRequestHandler ServerBuild | Promise try { normalizedRequest = winterCGRequestToRequestData(request); - } catch (e) { + } catch { DEBUG_BUILD && debug.warn('Failed to normalize Remix request'); } diff --git a/packages/remix/test/integration/test/client/utils/helpers.ts b/packages/remix/test/integration/test/client/utils/helpers.ts index 97007673703b..a0e68d1ed42e 100644 --- a/packages/remix/test/integration/test/client/utils/helpers.ts +++ b/packages/remix/test/integration/test/client/utils/helpers.ts @@ -172,7 +172,7 @@ export async function runScriptInSandbox( ): Promise { try { await page.addScriptTag({ path: impl.path, content: impl.content }); - } catch (e) { + } catch { // no-op } } diff --git a/packages/replay-canvas/src/canvas.ts b/packages/replay-canvas/src/canvas.ts index 968e6d586688..d026567e01b1 100644 --- a/packages/replay-canvas/src/canvas.ts +++ b/packages/replay-canvas/src/canvas.ts @@ -94,7 +94,7 @@ export const _replayCanvasIntegration = ((options: Partial if (typeof err === 'object') { (err as Error & { __rrweb__?: boolean }).__rrweb__ = true; } - } catch (error) { + } catch { // ignore errors here // this can happen if the error is frozen or does not allow mutation for other reasons } diff --git a/packages/replay-internal/src/coreHandlers/handleDom.ts b/packages/replay-internal/src/coreHandlers/handleDom.ts index f84a3938125c..ffe5dbc9096f 100644 --- a/packages/replay-internal/src/coreHandlers/handleDom.ts +++ b/packages/replay-internal/src/coreHandlers/handleDom.ts @@ -30,8 +30,7 @@ export const handleDomListener: (replay: ReplayContainer) => (handlerData: Handl if ( isClick && replay.clickDetector && - event && - event.target && + event?.target && !event.altKey && !event.metaKey && !event.ctrlKey && @@ -98,7 +97,7 @@ function getDomTarget(handlerData: HandlerDataDom): { target: Node | null; messa try { target = isClick ? getClickTargetNode(handlerData.event as Event) : getTargetNode(handlerData.event as Event); message = htmlTreeAsString(target, { maxStringLength: 200 }) || ''; - } catch (e) { + } catch { message = ''; } diff --git a/packages/replay-internal/src/coreHandlers/util/onWindowOpen.ts b/packages/replay-internal/src/coreHandlers/util/onWindowOpen.ts index f63e0b2ec1fa..16dc8c1aab73 100644 --- a/packages/replay-internal/src/coreHandlers/util/onWindowOpen.ts +++ b/packages/replay-internal/src/coreHandlers/util/onWindowOpen.ts @@ -32,7 +32,7 @@ function monkeyPatchWindowOpen(): void { if (handlers) { try { handlers.forEach(handler => handler()); - } catch (e) { + } catch { // ignore errors in here } } diff --git a/packages/replay-internal/src/integration.ts b/packages/replay-internal/src/integration.ts index 6db78dced270..795562c7f6ce 100644 --- a/packages/replay-internal/src/integration.ts +++ b/packages/replay-internal/src/integration.ts @@ -145,7 +145,7 @@ export class Replay implements Integration { errorHandler: (err: Error & { __rrweb__?: boolean }) => { try { err.__rrweb__ = true; - } catch (error) { + } catch { // ignore errors here // this can happen if the error is frozen or does not allow mutation for other reasons } diff --git a/packages/replay-internal/src/util/addMemoryEntry.ts b/packages/replay-internal/src/util/addMemoryEntry.ts index 01c87daa7ead..b87f19521cbc 100644 --- a/packages/replay-internal/src/util/addMemoryEntry.ts +++ b/packages/replay-internal/src/util/addMemoryEntry.ts @@ -23,7 +23,7 @@ export async function addMemoryEntry(replay: ReplayContainer): Promise) => void; + }; + }; + + // Cloudflare workers have a `waitUntil` method that we can use to flush the event queue + // We already call this in `wrapRequestHandler` from `sentryHandleInitCloudflare` + // However, `handleError` can be invoked when wrapRequestHandler already finished + // (e.g. when responses are streamed / returning promises from load functions) + const cloudflareWaitUntil = platform?.context?.waitUntil; + if (typeof cloudflareWaitUntil === 'function') { + const waitUntil = cloudflareWaitUntil.bind(platform.context); + waitUntil(flush(2000)); + } else { + await flushIfServerless(); + } // We're extra cautious with SafeHandleServerErrorInput - this type is not compatible with HandleServerErrorInput // @ts-expect-error - we're still passing the same object, just with a different (backwards-compatible) type diff --git a/packages/sveltekit/src/vite/sourceMaps.ts b/packages/sveltekit/src/vite/sourceMaps.ts index 721587371425..5559f5be4592 100644 --- a/packages/sveltekit/src/vite/sourceMaps.ts +++ b/packages/sveltekit/src/vite/sourceMaps.ts @@ -330,7 +330,7 @@ export async function makeCustomSentryVitePlugins(options?: CustomSentryVitePlug // Not pretty but my testing shows that it works. // @ts-expect-error - this hook exists on the plugin! await sentryViteDebugIdUploadPlugin.writeBundle({ dir: outDir }); - } catch (_) { + } catch { // eslint-disable-next-line no-console console.warn('[Source Maps Plugin] Failed to upload source maps!'); // eslint-disable-next-line no-console @@ -464,7 +464,7 @@ function detectSentryRelease(): string { .execSync('git rev-parse HEAD', { stdio: ['ignore', 'pipe', 'ignore'] }) .toString() .trim(); - } catch (_) { + } catch { // the command can throw for various reasons. Most importantly: // - git is not installed // - there is no git repo or no commit yet diff --git a/packages/sveltekit/src/vite/svelteConfig.ts b/packages/sveltekit/src/vite/svelteConfig.ts index 5e88a310726e..ed0c9ec6f801 100644 --- a/packages/sveltekit/src/vite/svelteConfig.ts +++ b/packages/sveltekit/src/vite/svelteConfig.ts @@ -107,7 +107,7 @@ async function getNodeAdapterOutputDir(svelteConfig: Config): Promise { try { await nodeAdapter.adapt(adapterBuilder); - } catch (_) { + } catch { // We expect the adapter to throw in writeClient! } diff --git a/packages/sveltekit/test/server-common/handle.test.ts b/packages/sveltekit/test/server-common/handle.test.ts index 98c483996f11..79c0f88e0b5d 100644 --- a/packages/sveltekit/test/server-common/handle.test.ts +++ b/packages/sveltekit/test/server-common/handle.test.ts @@ -133,7 +133,7 @@ describe('sentryHandle', () => { try { await sentryHandle()({ event: mockEvent(), resolve: resolve(type, isError) }); - } catch (e) { + } catch { // } @@ -172,7 +172,7 @@ describe('sentryHandle', () => { return mockResponse; }, }); - } catch (e) { + } catch { // } @@ -226,7 +226,7 @@ describe('sentryHandle', () => { try { await sentryHandle()({ event, resolve: resolve(type, isError) }); - } catch (e) { + } catch { // } @@ -275,7 +275,7 @@ describe('sentryHandle', () => { try { await sentryHandle()({ event, resolve: resolve(type, isError) }); - } catch (e) { + } catch { // } @@ -305,7 +305,7 @@ describe('sentryHandle', () => { it("doesn't send redirects in a request handler to Sentry", async () => { try { await sentryHandle()({ event: mockEvent(), resolve: resolve(type, false, 'redirect') }); - } catch (e) { + } catch { expect(mockCaptureException).toBeCalledTimes(0); } }); @@ -313,7 +313,7 @@ describe('sentryHandle', () => { it("doesn't send Http 4xx errors in a request handler to Sentry", async () => { try { await sentryHandle()({ event: mockEvent(), resolve: resolve(type, false, 'http') }); - } catch (e) { + } catch { expect(mockCaptureException).toBeCalledTimes(0); } }); diff --git a/packages/sveltekit/test/server-common/handleError.test.ts b/packages/sveltekit/test/server-common/handleError.test.ts index b24c1c78b747..287e8f2c88e8 100644 --- a/packages/sveltekit/test/server-common/handleError.test.ts +++ b/packages/sveltekit/test/server-common/handleError.test.ts @@ -91,5 +91,29 @@ describe('handleError', () => { // Check that the default handler wasn't invoked expect(consoleErrorSpy).toHaveBeenCalledTimes(0); }); + + it('calls waitUntil if available', async () => { + const wrappedHandleError = handleErrorWithSentry(); + const mockError = new Error('test'); + const waitUntilSpy = vi.fn(); + + await wrappedHandleError({ + error: mockError, + event: { + ...requestEvent, + platform: { + context: { + waitUntil: waitUntilSpy, + }, + }, + }, + status: 500, + message: 'Internal Error', + }); + + expect(waitUntilSpy).toHaveBeenCalledTimes(1); + // flush() returns a promise, this is what we expect here + expect(waitUntilSpy).toHaveBeenCalledWith(expect.any(Promise)); + }); }); }); diff --git a/packages/vue/src/pinia.ts b/packages/vue/src/pinia.ts index 9d576a461cef..f55be4d6602c 100644 --- a/packages/vue/src/pinia.ts +++ b/packages/vue/src/pinia.ts @@ -70,7 +70,7 @@ export const createSentryPiniaPlugin: ( }, ]; } - } catch (_) { + } catch { // empty } diff --git a/scripts/normalize-e2e-test-dump-transaction-events.js b/scripts/normalize-e2e-test-dump-transaction-events.js index 9b775e62a381..e3459d52621d 100644 --- a/scripts/normalize-e2e-test-dump-transaction-events.js +++ b/scripts/normalize-e2e-test-dump-transaction-events.js @@ -25,7 +25,7 @@ glob.glob( let envelope; try { envelope = JSON.parse(serializedEnvelope); - } catch (e) { + } catch { return; // noop } diff --git a/yarn.lock b/yarn.lock index 333cac9f5d06..16bf8fe6d899 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5824,10 +5824,10 @@ dependencies: "@octokit/openapi-types" "^18.0.0" -"@opentelemetry/api-logs@0.200.0": - version "0.200.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/api-logs/-/api-logs-0.200.0.tgz#f9015fd844920c13968715b3cdccf5a4d4ff907e" - integrity sha512-IKJBQxh91qJ+3ssRly5hYEJ8NDHu9oY/B1PXVSCWf7zytmYO9RNLB0Ox9XQ/fJ8m6gY6Q6NtBWlmXfaXt5Uc4Q== +"@opentelemetry/api-logs@0.203.0": + version "0.203.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz#3309a76c51a848ea820cd7f00ee62daf36b06380" + integrity sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ== dependencies: "@opentelemetry/api" "^1.3.0" @@ -6097,16 +6097,14 @@ semver "^7.5.2" shimmer "^1.2.1" -"@opentelemetry/instrumentation@^0.200.0": - version "0.200.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.200.0.tgz#29d1d4f70cbf0cb1ca9f2f78966379b0be96bddc" - integrity sha512-pmPlzfJd+vvgaZd/reMsC8RWgTXn2WY1OWT5RT42m3aOn5532TozwXNDhg1vzqJ+jnvmkREcdLr27ebJEQt0Jg== +"@opentelemetry/instrumentation@^0.203.0": + version "0.203.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.203.0.tgz#5c74a41cd6868f7ba47b346ff5a58ea7b18cf381" + integrity sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ== dependencies: - "@opentelemetry/api-logs" "0.200.0" - "@types/shimmer" "^1.2.0" + "@opentelemetry/api-logs" "0.203.0" import-in-the-middle "^1.8.1" require-in-the-middle "^7.1.1" - shimmer "^1.2.1" "@opentelemetry/propagation-utils@^0.30.16": version "0.30.16"