diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 000000000000..a651eb363952 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,14 @@ +{ + "permissions": { + "allow": [ + "Bash(find:*)", + "Bash(ls:*)", + "Bash(git:*)", + "Bash(yarn:*)", + "WebFetch(domain:github.com)", + "Bash(grep:*)", + "Bash(mv:*)" + ], + "deny": [] + } +} \ No newline at end of file diff --git a/.cursor/rules/publishing_release.mdc b/.cursor/rules/publishing_release.mdc new file mode 100644 index 000000000000..bf801c7a7ff3 --- /dev/null +++ b/.cursor/rules/publishing_release.mdc @@ -0,0 +1,32 @@ +--- +description: Use this rule if you are looking to publish a release for the Sentry JavaScript SDKs +globs: +alwaysApply: false +--- +# Publishing a Release + +Use these guidelines when publishing a new Sentry JavaScript SDK release. + +## Standard Release Process (from develop to master) + +The release process is outlined in [publishing-a-release.md](mdc:docs/publishing-a-release.md). + +1. Make sure you are on the latest version of the `develop` branch. To confirm this, run `git pull origin develop` to get the latest changes from the repo. +2. Run `yarn changelog` on the `develop` branch and copy the output. You can use `yarn changelog | pbcopy` to copy the output of `yarn changelog` into your clipboard. +3. Decide on a version for the release based on [semver](mdc:https://semver.org). The version should be decided based on what is in included in the release. For example, if the release includes a new feature, we should increment the minor version. If it includes only bug fixes, we should increment the patch version. +4. Create a branch `prepare-release/VERSION`, eg. `prepare-release/8.1.0`, off `develop`. +5. Update [CHANGELOG.md](mdc:CHANGELOG.md) to add an entry for the next release number and a list of changes since the last release from the output of `yarn changelog`. See the `Updating the Changelog` section in [publishing-a-release.md](mdc:docs/publishing-a-release.md) for more details. If you remove changelog entries because they are not applicable, please let the user know. +6. Commit the changes to [CHANGELOG.md](mdc:CHANGELOG.md) with `meta(changelog): Update changelog for VERSION` where `VERSION` is the version of the release, e.g. `meta(changelog): Update changelog for 8.1.0` +7. Push the `prepare-release/VERSION` branch to origin and remind the user that the release PR needs to be opened from the `master` branch. + +## Key Commands + +- `yarn changelog` - Generate changelog entries +- `yarn lint` - Ensure code quality +- `yarn test` - Run test suite +- `yarn build:dev` - Verify build + +## Important Notes + +- This repository uses **Git Flow** - target `develop` for feature PRs, not `master`. See [gitflow.md](mdc:docs/gitflow.md) for more details. +- For first-time SDK releases, follow the New SDK Release Checklist [new-sdk-release-checklist.md](mdc:docs/new-sdk-release-checklist.md). If you notice there is something not following the new SDK release checklist, please remind the user. diff --git a/.cursor/rules/sdk_development.mdc b/.cursor/rules/sdk_development.mdc new file mode 100644 index 000000000000..988703ddac81 --- /dev/null +++ b/.cursor/rules/sdk_development.mdc @@ -0,0 +1,128 @@ +--- +description: Guidelines for working on the Sentry JavaScript SDK monorepo +alwaysApply: true +--- + +# 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) +- `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 + +### 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 + +## 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 + +### Branch Naming +- Features: `feat/descriptive-name` +- Releases: `release/X.Y.Z` + +## Repository Architecture + +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/browser-utils/` - Browser-specific utilities and instrumentation + +### Platform SDKs +- `packages/browser/` - Browser SDK with bundled variants +- `packages/node/` - Node.js SDK with server-side integrations +- `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 +- `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 (`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 +- `tsconfig.json`, `tsconfig.test.json`, `tsconfig.types.json` +- `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 + +## 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 +- Never introduce code that exposes secrets or keys +- 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 diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 4a2d16e13598..ba0bdd8df4c3 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -50,6 +50,7 @@ body: - '@sentry/nestjs' - '@sentry/nextjs' - '@sentry/nuxt' + - '@sentry/pino-transport' - '@sentry/react' - '@sentry/react-router' - '@sentry/remix' diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 94b1b007c912..980a1be55b61 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -415,8 +415,6 @@ jobs: if: needs.job_build.outputs.changed_bun == 'true' || github.event_name != 'pull_request' timeout-minutes: 10 runs-on: ubuntu-24.04 - strategy: - fail-fast: false steps: - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) uses: actions/checkout@v4 @@ -442,8 +440,6 @@ jobs: if: needs.job_build.outputs.changed_deno == 'true' || github.event_name != 'pull_request' timeout-minutes: 10 runs-on: ubuntu-24.04 - strategy: - fail-fast: false steps: - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) uses: actions/checkout@v4 @@ -475,7 +471,7 @@ jobs: strategy: fail-fast: false matrix: - node: [18, 20, 22, '^24.0.1'] + node: [18, 20, 22, 24] steps: - name: Check out base commit (${{ github.event.pull_request.base.sha }}) uses: actions/checkout@v4 @@ -885,18 +881,11 @@ jobs: ref: ${{ env.HEAD_COMMIT }} - uses: pnpm/action-setup@v4 with: - version: 9.4.0 - # TODO: Remove this once the repo is bumped to 20.19.2 or higher - - name: Set up Node for Angular 20 - if: matrix.test-application == 'angular-20' - uses: actions/setup-node@v4 - with: - node-version: '20.19.2' + version: 9.15.9 - name: Set up Node - if: matrix.test-application != 'angular-20' uses: actions/setup-node@v4 with: - node-version-file: 'dev-packages/e2e-tests/package.json' + node-version-file: 'dev-packages/e2e-tests/test-applications/${{ matrix.test-application }}/package.json' - name: Set up Bun if: matrix.test-application == 'node-exports-test-app' uses: oven-sh/setup-bun@v2 @@ -1012,11 +1001,11 @@ jobs: ref: ${{ env.HEAD_COMMIT }} - uses: pnpm/action-setup@v4 with: - version: 9.4.0 + version: 9.15.9 - name: Set up Node uses: actions/setup-node@v4 with: - node-version-file: 'dev-packages/e2e-tests/package.json' + node-version-file: 'dev-packages/e2e-tests/test-applications/${{ matrix.test-application }}/package.json' - name: Restore caches uses: ./.github/actions/restore-cache with: diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index 27abae270d73..49f603e8235c 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -125,18 +125,12 @@ jobs: ref: ${{ env.HEAD_COMMIT }} - uses: pnpm/action-setup@v4 with: - version: 9.4.0 - # TODO: Remove this once the repo is bumped to 20.19.2 or higher - - name: Set up Node for Angular 20 - if: matrix.test-application == 'angular-20' - uses: actions/setup-node@v4 - with: - node-version: '20.19.2' + version: 9.15.9 - name: Set up Node if: matrix.test-application != 'angular-20' uses: actions/setup-node@v4 with: - node-version-file: 'dev-packages/e2e-tests/package.json' + node-version-file: 'dev-packages/e2e-tests/test-applications/${{ matrix.test-application }}/package.json' - name: Restore canary cache uses: actions/cache/restore@v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index baf79461528f..349cd0b3ce71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,33 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 9.31.0 + +### Important Changes + +- feat(nextjs): Add option for auto-generated random tunnel route ([#16626](https://github.com/getsentry/sentry-javascript/pull/16626)) + +Adds an option to automatically generate a random tunnel route for the Next.js SDK. This helps prevent ad blockers and other tools from blocking Sentry requests by using a randomized path instead of the predictable `/monitoring` endpoint. + +- feat(core): Allow to pass `scope` & `client` to `getTraceData` ([#16633](https://github.com/getsentry/sentry-javascript/pull/16633)) + +Adds the ability to pass custom `scope` and `client` parameters to the `getTraceData` function, providing more flexibility when generating trace data for distributed tracing. + +### Other Changes + +- deps: Remove unused `@sentry/opentelemetry` dependency ([#16677](https://github.com/getsentry/sentry-javascript/pull/16677)) +- deps: Update all bundler plugin instances to latest & allow caret ranges ([#16641](https://github.com/getsentry/sentry-javascript/pull/16641)) +- feat(deps): Bump @prisma/instrumentation from 6.8.2 to 6.9.0 ([#16608](https://github.com/getsentry/sentry-javascript/pull/16608)) +- feat(flags): add node support for generic featureFlagsIntegration and move utils to core ([#16585](https://github.com/getsentry/sentry-javascript/pull/16585)) +- feat(flags): capture feature flag evaluations on spans ([#16485](https://github.com/getsentry/sentry-javascript/pull/16485)) +- feat(pino): Add initial package for `@sentry/pino-transport` ([#16652](https://github.com/getsentry/sentry-javascript/pull/16652)) +- fix: Wait for the correct clientWidth/clientHeight when showing Feedback Screenshot previews ([#16648](https://github.com/getsentry/sentry-javascript/pull/16648)) +- fix(browser): Remove usage of Array.at() method ([#16647](https://github.com/getsentry/sentry-javascript/pull/16647)) +- fix(core): Improve `safeJoin` usage in console logging integration ([#16658](https://github.com/getsentry/sentry-javascript/pull/16658)) +- fix(google-cloud-serverless): Make `CloudEvent` type compatible ([#16661](https://github.com/getsentry/sentry-javascript/pull/16661)) +- fix(nextjs): Fix lookup of `instrumentation-client.js` file ([#16637](https://github.com/getsentry/sentry-javascript/pull/16637)) +- fix(node): Ensure graphql errors result in errored spans ([#16678](https://github.com/getsentry/sentry-javascript/pull/16678)) + ## 9.30.0 - feat(nextjs): Add URL to tags of server components and generation functions issues ([#16500](https://github.com/getsentry/sentry-javascript/pull/16500)) diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000000..263b9eef97a4 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,147 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## 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: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 + +### 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 + +### Package Management + +- `yarn clean` - Clean build artifacts and caches +- `yarn clean:deps` - Clean and reinstall all dependencies + +## Repository Architecture + +This is a Lerna monorepo containing 40+ packages in the `@sentry/*` namespace. Key architectural components: + +### Core Packages + +- `packages/core/` - Base SDK with interfaces, type definitions, and core functionality +- `packages/types/` - Shared TypeScript type definitions (active) +- `packages/browser-utils/` - Browser-specific utilities and instrumentation + +### Platform SDKs + +- `packages/browser/` - Browser SDK with bundled variants +- `packages/node/` - Node.js SDK with server-side integrations +- `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) + +### User Experience Packages + +- `packages/replay-internal/` - Session replay functionality +- `packages/replay-canvas/` - Canvas recording support for replay +- `packages/replay-worker/` - Web worker support for replay +- `packages/feedback/` - User feedback integration + +### Build System + +- Uses Rollup for bundling with config files: `rollup.*.config.mjs` +- TypeScript with multiple tsconfig files per package (main, test, types) +- 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 +- `tsconfig.json`, `tsconfig.test.json`, `tsconfig.types.json` - TypeScript configs +- `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 + +## Git Flow Branching Strategy + +This repository uses **Git Flow** branching model. See [detailed documentation](docs/gitflow.md). + +### 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 + +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 + +### CI/CD Integration + +- All PRs automatically run full lint/test/build pipeline +- Failed checks block merging +- Use `yarn test:pr` for testing only affected changes + +## Testing Single Packages + +To test a specific package: `cd packages/{package-name} && yarn test` +To build a specific package: `yarn build:dev:filter @sentry/{package-name}` + +## Cursor IDE Integration + +For Cursor IDE users, see [.cursor/rules/sdk_development.mdc](.cursor/rules/sdk_development.mdc) for complementary development rules. diff --git a/README.md b/README.md index 8b22dafb0c63..f92fd89ec716 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,8 @@ below: Provides the integration for Session Replay. - [`@sentry/core`](https://github.com/getsentry/sentry-javascript/tree/master/packages/core): The base for all JavaScript SDKs with interfaces, type definitions and base classes. +- [`@sentry/pino-transport`](https://github.com/getsentry/sentry-javascript/tree/master/packages/pino-transport): Pino + transport for automatically sending log messages to Sentry. ## Bug Bounty Program diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/constants.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/constants.ts deleted file mode 100644 index 680105d242e5..000000000000 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/constants.ts +++ /dev/null @@ -1 +0,0 @@ -export const FLAG_BUFFER_SIZE = 100; // Corresponds to constant in featureFlags.ts, in browser utils. diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/basic/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/basic/test.ts similarity index 84% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/basic/test.ts rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/basic/test.ts index b63583906cc4..3233c9047649 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/basic/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/basic/test.ts @@ -1,7 +1,11 @@ import { expect } from '@playwright/test'; -import { sentryTest } from '../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers'; -import { FLAG_BUFFER_SIZE } from '../../constants'; +import { _INTERNAL_FLAG_BUFFER_SIZE as FLAG_BUFFER_SIZE } from '@sentry/core'; +import { sentryTest } from '../../../../../../utils/fixtures'; +import { + envelopeRequestParser, + shouldSkipFeatureFlagsTest, + waitForErrorRequest, +} from '../../../../../../utils/helpers'; sentryTest('Basic test with eviction, update, and no async tasks', async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest()) { diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/init.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/init.js rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/init.js diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/subject.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/subject.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/subject.js rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/subject.js diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/template.html b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/template.html similarity index 100% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/template.html rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/template.html diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/withScope/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/withScope/test.ts similarity index 91% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/withScope/test.ts rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/withScope/test.ts index 41418122b526..fecc762d4c99 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/withScope/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/withScope/test.ts @@ -1,7 +1,11 @@ import { expect } from '@playwright/test'; import type { Scope } from '@sentry/browser'; -import { sentryTest } from '../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers'; +import { sentryTest } from '../../../../../../utils/fixtures'; +import { + envelopeRequestParser, + shouldSkipFeatureFlagsTest, + waitForErrorRequest, +} from '../../../../../../utils/helpers'; sentryTest('Flag evaluations in forked scopes are stored separately.', async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest()) { diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/init.js new file mode 100644 index 000000000000..fa6a67ec3711 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/init.js @@ -0,0 +1,13 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 1.0, + tracesSampleRate: 1.0, + integrations: [ + Sentry.browserTracingIntegration({ instrumentNavigation: false, instrumentPageLoad: false }), + Sentry.featureFlagsIntegration(), + ], +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/subject.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/subject.js new file mode 100644 index 000000000000..ad874b2bd697 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/subject.js @@ -0,0 +1,16 @@ +const btnStartSpan = document.getElementById('btnStartSpan'); +const btnEndSpan = document.getElementById('btnEndSpan'); +const btnStartNestedSpan = document.getElementById('btnStartNestedSpan'); +const btnEndNestedSpan = document.getElementById('btnEndNestedSpan'); + +window.withNestedSpans = callback => { + window.Sentry.startSpan({ name: 'test-root-span' }, rootSpan => { + window.traceId = rootSpan.spanContext().traceId; + + window.Sentry.startSpan({ name: 'test-span' }, _span => { + window.Sentry.startSpan({ name: 'test-nested-span' }, _nestedSpan => { + callback(); + }); + }); + }); +}; diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/template.html b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/template.html new file mode 100644 index 000000000000..59340f55667c --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/template.html @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/test.ts new file mode 100644 index 000000000000..6516d8e36abb --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/test.ts @@ -0,0 +1,66 @@ +import { expect } from '@playwright/test'; +import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core'; +import { sentryTest } from '../../../../../utils/fixtures'; +import { + type EventAndTraceHeader, + eventAndTraceHeaderRequestParser, + getMultipleSentryEnvelopeRequests, + shouldSkipFeatureFlagsTest, + shouldSkipTracingTest, +} from '../../../../../utils/helpers'; + +sentryTest("Feature flags are added to active span's attributes on span end.", async ({ getLocalTestUrl, page }) => { + if (shouldSkipFeatureFlagsTest() || shouldSkipTracingTest()) { + sentryTest.skip(); + } + + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({}), + }); + }); + + const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true }); + await page.goto(url); + + const envelopeRequestPromise = getMultipleSentryEnvelopeRequests( + page, + 1, + {}, + eventAndTraceHeaderRequestParser, + ); + + // withNestedSpans is a util used to start 3 nested spans: root-span (not recorded in transaction_event.spans), span, and nested-span. + await page.evaluate(maxFlags => { + (window as any).withNestedSpans(() => { + const flagsIntegration = (window as any).Sentry.getClient().getIntegrationByName('FeatureFlags'); + for (let i = 1; i <= maxFlags; i++) { + flagsIntegration.addFeatureFlag(`feat${i}`, false); + } + flagsIntegration.addFeatureFlag(`feat${maxFlags + 1}`, true); // dropped flag + flagsIntegration.addFeatureFlag('feat3', true); // update + }); + return true; + }, MAX_FLAGS_PER_SPAN); + + const event = (await envelopeRequestPromise)[0][0]; + const innerSpan = event.spans?.[0]; + const outerSpan = event.spans?.[1]; + const outerSpanFlags = Object.entries(outerSpan?.data ?? {}).filter(([key, _val]) => + key.startsWith('flag.evaluation'), + ); + const innerSpanFlags = Object.entries(innerSpan?.data ?? {}).filter(([key, _val]) => + key.startsWith('flag.evaluation'), + ); + + expect(innerSpanFlags).toEqual([]); + + const expectedOuterSpanFlags = []; + for (let i = 1; i <= MAX_FLAGS_PER_SPAN; i++) { + expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, i === 3]); + } + // Order agnostic (attribute dict is unordered). + expect(outerSpanFlags.sort()).toEqual(expectedOuterSpanFlags.sort()); +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/basic/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/basic/test.ts similarity index 84% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/basic/test.ts rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/basic/test.ts index 5d7f58bdb27b..bc3e0afdc292 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/basic/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/basic/test.ts @@ -1,7 +1,11 @@ import { expect } from '@playwright/test'; -import { sentryTest } from '../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers'; -import { FLAG_BUFFER_SIZE } from '../../constants'; +import { _INTERNAL_FLAG_BUFFER_SIZE as FLAG_BUFFER_SIZE } from '@sentry/core'; +import { sentryTest } from '../../../../../../utils/fixtures'; +import { + envelopeRequestParser, + shouldSkipFeatureFlagsTest, + waitForErrorRequest, +} from '../../../../../../utils/helpers'; sentryTest('Basic test with eviction, update, and no async tasks', async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest()) { diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/init.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/init.js rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/init.js diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/subject.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/subject.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/subject.js rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/subject.js diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/template.html b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/template.html similarity index 100% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/template.html rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/template.html diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/withScope/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/withScope/test.ts similarity index 90% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/withScope/test.ts rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/withScope/test.ts index 78703e4e5389..2efb3fdc9ad0 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/withScope/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/withScope/test.ts @@ -1,7 +1,11 @@ import { expect } from '@playwright/test'; import type { Scope } from '@sentry/browser'; -import { sentryTest } from '../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers'; +import { sentryTest } from '../../../../../../utils/fixtures'; +import { + envelopeRequestParser, + shouldSkipFeatureFlagsTest, + waitForErrorRequest, +} from '../../../../../../utils/helpers'; sentryTest('Flag evaluations in forked scopes are stored separately.', async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest()) { diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/init.js new file mode 100644 index 000000000000..9e4b802f28f3 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/init.js @@ -0,0 +1,39 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; +window.sentryLDIntegration = Sentry.launchDarklyIntegration(); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 1.0, + tracesSampleRate: 1.0, + integrations: [ + Sentry.browserTracingIntegration({ instrumentNavigation: false, instrumentPageLoad: false }), + window.sentryLDIntegration, + ], +}); + +// Manually mocking this because LD only has mock test utils for the React SDK. +// Also, no SDK has mock utils for FlagUsedHandler's. +const MockLaunchDarkly = { + initialize(_clientId, context, options) { + const flagUsedHandler = options.inspectors ? options.inspectors[0].method : undefined; + + return { + variation(key, defaultValue) { + if (flagUsedHandler) { + flagUsedHandler(key, { value: defaultValue }, context); + } + return defaultValue; + }, + }; + }, +}; + +window.initializeLD = () => { + return MockLaunchDarkly.initialize( + 'example-client-id', + { kind: 'user', key: 'example-context-key' }, + { inspectors: [Sentry.buildLaunchDarklyFlagUsedHandler()] }, + ); +}; diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/subject.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/subject.js new file mode 100644 index 000000000000..ad874b2bd697 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/subject.js @@ -0,0 +1,16 @@ +const btnStartSpan = document.getElementById('btnStartSpan'); +const btnEndSpan = document.getElementById('btnEndSpan'); +const btnStartNestedSpan = document.getElementById('btnStartNestedSpan'); +const btnEndNestedSpan = document.getElementById('btnEndNestedSpan'); + +window.withNestedSpans = callback => { + window.Sentry.startSpan({ name: 'test-root-span' }, rootSpan => { + window.traceId = rootSpan.spanContext().traceId; + + window.Sentry.startSpan({ name: 'test-span' }, _span => { + window.Sentry.startSpan({ name: 'test-nested-span' }, _nestedSpan => { + callback(); + }); + }); + }); +}; diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/template.html b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/template.html new file mode 100644 index 000000000000..59340f55667c --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/template.html @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/test.ts new file mode 100644 index 000000000000..eb7eb003c838 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/test.ts @@ -0,0 +1,66 @@ +import { expect } from '@playwright/test'; +import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core'; +import { sentryTest } from '../../../../../utils/fixtures'; +import { + type EventAndTraceHeader, + eventAndTraceHeaderRequestParser, + getMultipleSentryEnvelopeRequests, + shouldSkipFeatureFlagsTest, + shouldSkipTracingTest, +} from '../../../../../utils/helpers'; + +sentryTest("Feature flags are added to active span's attributes on span end.", async ({ getLocalTestUrl, page }) => { + if (shouldSkipFeatureFlagsTest() || shouldSkipTracingTest()) { + sentryTest.skip(); + } + + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({}), + }); + }); + + const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true }); + await page.goto(url); + + const envelopeRequestPromise = getMultipleSentryEnvelopeRequests( + page, + 1, + {}, + eventAndTraceHeaderRequestParser, + ); + + // withNestedSpans is a util used to start 3 nested spans: root-span (not recorded in transaction_event.spans), span, and nested-span. + await page.evaluate(maxFlags => { + (window as any).withNestedSpans(() => { + const ldClient = (window as any).initializeLD(); + for (let i = 1; i <= maxFlags; i++) { + ldClient.variation(`feat${i}`, false); + } + ldClient.variation(`feat${maxFlags + 1}`, true); // dropped + ldClient.variation('feat3', true); // update + }); + return true; + }, MAX_FLAGS_PER_SPAN); + + const event = (await envelopeRequestPromise)[0][0]; + const innerSpan = event.spans?.[0]; + const outerSpan = event.spans?.[1]; + const outerSpanFlags = Object.entries(outerSpan?.data ?? {}).filter(([key, _val]) => + key.startsWith('flag.evaluation'), + ); + const innerSpanFlags = Object.entries(innerSpan?.data ?? {}).filter(([key, _val]) => + key.startsWith('flag.evaluation'), + ); + + expect(innerSpanFlags).toEqual([]); + + const expectedOuterSpanFlags = []; + for (let i = 1; i <= MAX_FLAGS_PER_SPAN; i++) { + expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, i === 3]); + } + // Order agnostic (attribute dict is unordered). + expect(outerSpanFlags.sort()).toEqual(expectedOuterSpanFlags.sort()); +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/basic/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/basic/test.ts similarity index 83% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/basic/test.ts rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/basic/test.ts index 77112ee82658..5953f1e0b087 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/basic/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/basic/test.ts @@ -1,7 +1,11 @@ import { expect } from '@playwright/test'; -import { sentryTest } from '../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers'; -import { FLAG_BUFFER_SIZE } from '../../constants'; +import { _INTERNAL_FLAG_BUFFER_SIZE as FLAG_BUFFER_SIZE } from '@sentry/core'; +import { sentryTest } from '../../../../../../utils/fixtures'; +import { + envelopeRequestParser, + shouldSkipFeatureFlagsTest, + waitForErrorRequest, +} from '../../../../../../utils/helpers'; sentryTest('Basic test with eviction, update, and no async tasks', async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest()) { diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/errorHook/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/errorHook/init.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/errorHook/init.js rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/errorHook/init.js diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/errorHook/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/errorHook/test.ts similarity index 84% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/errorHook/test.ts rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/errorHook/test.ts index d8f1e1311dfa..89654c82eda1 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/errorHook/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/errorHook/test.ts @@ -1,7 +1,11 @@ import { expect } from '@playwright/test'; -import { sentryTest } from '../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers'; -import { FLAG_BUFFER_SIZE } from '../../constants'; +import { _INTERNAL_FLAG_BUFFER_SIZE as FLAG_BUFFER_SIZE } from '@sentry/core'; +import { sentryTest } from '../../../../../../utils/fixtures'; +import { + envelopeRequestParser, + shouldSkipFeatureFlagsTest, + waitForErrorRequest, +} from '../../../../../../utils/helpers'; sentryTest('Flag evaluation error hook', async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest()) { diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/init.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/init.js rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/init.js diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/subject.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/subject.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/subject.js rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/subject.js diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/template.html b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/template.html similarity index 100% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/template.html rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/template.html diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/withScope/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/withScope/test.ts similarity index 90% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/withScope/test.ts rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/withScope/test.ts index 67e68becb104..14cc072af30d 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/withScope/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/withScope/test.ts @@ -1,7 +1,11 @@ import { expect } from '@playwright/test'; import type { Scope } from '@sentry/browser'; -import { sentryTest } from '../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers'; +import { sentryTest } from '../../../../../../utils/fixtures'; +import { + envelopeRequestParser, + shouldSkipFeatureFlagsTest, + waitForErrorRequest, +} from '../../../../../../utils/helpers'; sentryTest('Flag evaluations in forked scopes are stored separately.', async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest()) { diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/init.js new file mode 100644 index 000000000000..4fc1cace150c --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/init.js @@ -0,0 +1,24 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; +window.sentryOpenFeatureIntegration = Sentry.openFeatureIntegration(); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 1.0, + tracesSampleRate: 1.0, + integrations: [ + window.sentryOpenFeatureIntegration, + Sentry.browserTracingIntegration({ instrumentNavigation: false, instrumentPageLoad: false }), + ], +}); + +window.initialize = () => { + return { + getBooleanValue(flag, value) { + let hook = new Sentry.OpenFeatureIntegrationHook(); + hook.after(null, { flagKey: flag, value: value }); + return value; + }, + }; +}; diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/subject.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/subject.js new file mode 100644 index 000000000000..ad874b2bd697 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/subject.js @@ -0,0 +1,16 @@ +const btnStartSpan = document.getElementById('btnStartSpan'); +const btnEndSpan = document.getElementById('btnEndSpan'); +const btnStartNestedSpan = document.getElementById('btnStartNestedSpan'); +const btnEndNestedSpan = document.getElementById('btnEndNestedSpan'); + +window.withNestedSpans = callback => { + window.Sentry.startSpan({ name: 'test-root-span' }, rootSpan => { + window.traceId = rootSpan.spanContext().traceId; + + window.Sentry.startSpan({ name: 'test-span' }, _span => { + window.Sentry.startSpan({ name: 'test-nested-span' }, _nestedSpan => { + callback(); + }); + }); + }); +}; diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/template.html b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/template.html new file mode 100644 index 000000000000..59340f55667c --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/template.html @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/test.ts new file mode 100644 index 000000000000..5ade5d01b3d5 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/test.ts @@ -0,0 +1,66 @@ +import { expect } from '@playwright/test'; +import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core'; +import { sentryTest } from '../../../../../utils/fixtures'; +import { + type EventAndTraceHeader, + eventAndTraceHeaderRequestParser, + getMultipleSentryEnvelopeRequests, + shouldSkipFeatureFlagsTest, + shouldSkipTracingTest, +} from '../../../../../utils/helpers'; + +sentryTest("Feature flags are added to active span's attributes on span end.", async ({ getLocalTestUrl, page }) => { + if (shouldSkipFeatureFlagsTest() || shouldSkipTracingTest()) { + sentryTest.skip(); + } + + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({}), + }); + }); + + const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true }); + await page.goto(url); + + const envelopeRequestPromise = getMultipleSentryEnvelopeRequests( + page, + 1, + {}, + eventAndTraceHeaderRequestParser, + ); + + // withNestedSpans is a util used to start 3 nested spans: root-span (not recorded in transaction_event.spans), span, and nested-span. + await page.evaluate(maxFlags => { + (window as any).withNestedSpans(() => { + const client = (window as any).initialize(); + for (let i = 1; i <= maxFlags; i++) { + client.getBooleanValue(`feat${i}`, false); + } + client.getBooleanValue(`feat${maxFlags + 1}`, true); // drop + client.getBooleanValue('feat3', true); // update + }); + return true; + }, MAX_FLAGS_PER_SPAN); + + const event = (await envelopeRequestPromise)[0][0]; + const innerSpan = event.spans?.[0]; + const outerSpan = event.spans?.[1]; + const outerSpanFlags = Object.entries(outerSpan?.data ?? {}).filter(([key, _val]) => + key.startsWith('flag.evaluation'), + ); + const innerSpanFlags = Object.entries(innerSpan?.data ?? {}).filter(([key, _val]) => + key.startsWith('flag.evaluation'), + ); + + expect(innerSpanFlags).toEqual([]); + + const expectedOuterSpanFlags = []; + for (let i = 1; i <= MAX_FLAGS_PER_SPAN; i++) { + expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, i === 3]); + } + // Order agnostic (attribute dict is unordered). + expect(outerSpanFlags.sort()).toEqual(expectedOuterSpanFlags.sort()); +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/basic/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/basic/test.ts similarity index 84% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/basic/test.ts rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/basic/test.ts index cb434e49e86e..134b29417d53 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/basic/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/basic/test.ts @@ -1,7 +1,11 @@ import { expect } from '@playwright/test'; -import { sentryTest } from '../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers'; -import { FLAG_BUFFER_SIZE } from '../../constants'; +import { _INTERNAL_FLAG_BUFFER_SIZE as FLAG_BUFFER_SIZE } from '@sentry/core'; +import { sentryTest } from '../../../../../../utils/fixtures'; +import { + envelopeRequestParser, + shouldSkipFeatureFlagsTest, + waitForErrorRequest, +} from '../../../../../../utils/helpers'; sentryTest('Basic test with eviction, update, and no async tasks', async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest()) { diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/init.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/init.js rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/init.js diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/subject.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/subject.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/subject.js rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/subject.js diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/template.html b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/template.html similarity index 100% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/template.html rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/template.html diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/withScope/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/withScope/test.ts similarity index 91% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/withScope/test.ts rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/withScope/test.ts index 42ee35e4604d..e80c6dbfc5fa 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/withScope/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/withScope/test.ts @@ -1,7 +1,11 @@ import { expect } from '@playwright/test'; import type { Scope } from '@sentry/browser'; -import { sentryTest } from '../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers'; +import { sentryTest } from '../../../../../../utils/fixtures'; +import { + envelopeRequestParser, + shouldSkipFeatureFlagsTest, + waitForErrorRequest, +} from '../../../../../../utils/helpers'; sentryTest('Flag evaluations in forked scopes are stored separately.', async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest()) { diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/init.js new file mode 100644 index 000000000000..22f74d2ebd7c --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/init.js @@ -0,0 +1,39 @@ +import * as Sentry from '@sentry/browser'; + +class MockStatsigClient { + constructor() { + this._gateEvaluationListeners = []; + this._mockGateValues = {}; + } + + on(event, listener) { + this._gateEvaluationListeners.push(listener); + } + + checkGate(name) { + const value = this._mockGateValues[name] || false; // unknown features default to false. + this._gateEvaluationListeners.forEach(listener => { + listener({ gate: { name, value } }); + }); + return value; + } + + setMockGateValue(name, value) { + this._mockGateValues[name] = value; + } +} + +window.statsigClient = new MockStatsigClient(); + +window.Sentry = Sentry; +window.sentryStatsigIntegration = Sentry.statsigIntegration({ featureFlagClient: window.statsigClient }); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 1.0, + tracesSampleRate: 1.0, + integrations: [ + window.sentryStatsigIntegration, + Sentry.browserTracingIntegration({ instrumentNavigation: false, instrumentPageLoad: false }), + ], +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/subject.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/subject.js new file mode 100644 index 000000000000..ad874b2bd697 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/subject.js @@ -0,0 +1,16 @@ +const btnStartSpan = document.getElementById('btnStartSpan'); +const btnEndSpan = document.getElementById('btnEndSpan'); +const btnStartNestedSpan = document.getElementById('btnStartNestedSpan'); +const btnEndNestedSpan = document.getElementById('btnEndNestedSpan'); + +window.withNestedSpans = callback => { + window.Sentry.startSpan({ name: 'test-root-span' }, rootSpan => { + window.traceId = rootSpan.spanContext().traceId; + + window.Sentry.startSpan({ name: 'test-span' }, _span => { + window.Sentry.startSpan({ name: 'test-nested-span' }, _nestedSpan => { + callback(); + }); + }); + }); +}; diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/template.html b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/template.html new file mode 100644 index 000000000000..59340f55667c --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/template.html @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/test.ts new file mode 100644 index 000000000000..1ea192f98850 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/test.ts @@ -0,0 +1,70 @@ +import { expect } from '@playwright/test'; +import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core'; +import { sentryTest } from '../../../../../utils/fixtures'; +import { + type EventAndTraceHeader, + eventAndTraceHeaderRequestParser, + getMultipleSentryEnvelopeRequests, + shouldSkipFeatureFlagsTest, + shouldSkipTracingTest, +} from '../../../../../utils/helpers'; + +sentryTest("Feature flags are added to active span's attributes on span end.", async ({ getLocalTestUrl, page }) => { + if (shouldSkipFeatureFlagsTest() || shouldSkipTracingTest()) { + sentryTest.skip(); + } + + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({}), + }); + }); + + const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true }); + await page.goto(url); + + const envelopeRequestPromise = getMultipleSentryEnvelopeRequests( + page, + 1, + {}, + eventAndTraceHeaderRequestParser, + ); + + // withNestedSpans is a util used to start 3 nested spans: root-span (not recorded in transaction_event.spans), span, and nested-span. + await page.evaluate(maxFlags => { + (window as any).withNestedSpans(() => { + const client = (window as any).statsigClient; + for (let i = 1; i <= maxFlags; i++) { + client.checkGate(`feat${i}`); // values default to false + } + + client.setMockGateValue(`feat${maxFlags + 1}`, true); + client.checkGate(`feat${maxFlags + 1}`); // dropped + + client.setMockGateValue('feat3', true); + client.checkGate('feat3'); // update + }); + return true; + }, MAX_FLAGS_PER_SPAN); + + const event = (await envelopeRequestPromise)[0][0]; + const innerSpan = event.spans?.[0]; + const outerSpan = event.spans?.[1]; + const outerSpanFlags = Object.entries(outerSpan?.data ?? {}).filter(([key, _val]) => + key.startsWith('flag.evaluation'), + ); + const innerSpanFlags = Object.entries(innerSpan?.data ?? {}).filter(([key, _val]) => + key.startsWith('flag.evaluation'), + ); + + expect(innerSpanFlags).toEqual([]); + + const expectedOuterSpanFlags = []; + for (let i = 1; i <= MAX_FLAGS_PER_SPAN; i++) { + expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, i === 3]); + } + // Order agnostic (attribute dict is unordered). + expect(outerSpanFlags.sort()).toEqual(expectedOuterSpanFlags.sort()); +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/basic/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/basic/test.ts similarity index 87% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/basic/test.ts rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/basic/test.ts index b2e522fc78f4..6e2760b69600 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/basic/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/basic/test.ts @@ -1,7 +1,11 @@ import { expect } from '@playwright/test'; -import { sentryTest } from '../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers'; -import { FLAG_BUFFER_SIZE } from '../../constants'; +import { _INTERNAL_FLAG_BUFFER_SIZE as FLAG_BUFFER_SIZE } from '@sentry/core'; +import { sentryTest } from '../../../../../../utils/fixtures'; +import { + envelopeRequestParser, + shouldSkipFeatureFlagsTest, + waitForErrorRequest, +} from '../../../../../../utils/helpers'; sentryTest('Basic test with eviction, update, and no async tasks', async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest()) { diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/init.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/init.js rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/init.js diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/subject.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/subject.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/subject.js rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/subject.js diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/template.html b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/template.html similarity index 100% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/template.html rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/template.html diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/withScope/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/withScope/test.ts similarity index 90% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/withScope/test.ts rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/withScope/test.ts index a512882b568a..fe3aec3ff188 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/withScope/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/withScope/test.ts @@ -1,7 +1,11 @@ import { expect } from '@playwright/test'; import type { Scope } from '@sentry/browser'; -import { sentryTest } from '../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers'; +import { sentryTest } from '../../../../../../utils/fixtures'; +import { + envelopeRequestParser, + shouldSkipFeatureFlagsTest, + waitForErrorRequest, +} from '../../../../../../utils/helpers'; sentryTest('Flag evaluations in forked scopes are stored separately.', async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest()) { diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/init.js new file mode 100644 index 000000000000..93993d8f6188 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/init.js @@ -0,0 +1,59 @@ +import * as Sentry from '@sentry/browser'; + +window.UnleashClient = class { + constructor() { + this._featureToVariant = { + strFeat: { name: 'variant1', enabled: true, feature_enabled: true, payload: { type: 'string', value: 'test' } }, + noPayloadFeat: { name: 'eu-west', enabled: true, feature_enabled: true }, + jsonFeat: { + name: 'paid-orgs', + enabled: true, + feature_enabled: true, + payload: { + type: 'json', + value: '{"foo": {"bar": "baz"}, "hello": [1, 2, 3]}', + }, + }, + + // Enabled feature with no configured variants. + noVariantFeat: { name: 'disabled', enabled: false, feature_enabled: true }, + + // Disabled feature. + disabledFeat: { name: 'disabled', enabled: false, feature_enabled: false }, + }; + + // Variant returned for features that don't exist. + // `feature_enabled` may be defined in prod, but we want to test the undefined case. + this._fallbackVariant = { + name: 'disabled', + enabled: false, + }; + } + + isEnabled(toggleName) { + const variant = this._featureToVariant[toggleName] || this._fallbackVariant; + return variant.feature_enabled || false; + } + + getVariant(toggleName) { + return this._featureToVariant[toggleName] || this._fallbackVariant; + } +}; + +// Not a mock UnleashClient class method since it needs to match the signature of the actual UnleashClient. +window.setVariant = (client, featureName, variantName, isEnabled) => { + client._featureToVariant[featureName] = { name: variantName, enabled: isEnabled, feature_enabled: isEnabled }; +}; + +window.Sentry = Sentry; +window.sentryUnleashIntegration = Sentry.unleashIntegration({ featureFlagClientClass: window.UnleashClient }); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 1.0, + tracesSampleRate: 1.0, + integrations: [ + window.sentryUnleashIntegration, + Sentry.browserTracingIntegration({ instrumentNavigation: false, instrumentPageLoad: false }), + ], +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/subject.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/subject.js new file mode 100644 index 000000000000..ad874b2bd697 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/subject.js @@ -0,0 +1,16 @@ +const btnStartSpan = document.getElementById('btnStartSpan'); +const btnEndSpan = document.getElementById('btnEndSpan'); +const btnStartNestedSpan = document.getElementById('btnStartNestedSpan'); +const btnEndNestedSpan = document.getElementById('btnEndNestedSpan'); + +window.withNestedSpans = callback => { + window.Sentry.startSpan({ name: 'test-root-span' }, rootSpan => { + window.traceId = rootSpan.spanContext().traceId; + + window.Sentry.startSpan({ name: 'test-span' }, _span => { + window.Sentry.startSpan({ name: 'test-nested-span' }, _nestedSpan => { + callback(); + }); + }); + }); +}; diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/template.html b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/template.html new file mode 100644 index 000000000000..59340f55667c --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/template.html @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/test.ts new file mode 100644 index 000000000000..984bba3bc0e3 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/test.ts @@ -0,0 +1,68 @@ +import { expect } from '@playwright/test'; +import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core'; +import { sentryTest } from '../../../../../utils/fixtures'; +import { + type EventAndTraceHeader, + eventAndTraceHeaderRequestParser, + getMultipleSentryEnvelopeRequests, + shouldSkipFeatureFlagsTest, + shouldSkipTracingTest, +} from '../../../../../utils/helpers'; + +sentryTest("Feature flags are added to active span's attributes on span end.", async ({ getLocalTestUrl, page }) => { + if (shouldSkipFeatureFlagsTest() || shouldSkipTracingTest()) { + sentryTest.skip(); + } + + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({}), + }); + }); + + const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true }); + await page.goto(url); + + const envelopeRequestPromise = getMultipleSentryEnvelopeRequests( + page, + 1, + {}, + eventAndTraceHeaderRequestParser, + ); + + // withNestedSpans is a util used to start 3 nested spans: root-span (not recorded in transaction_event.spans), span, and nested-span. + await page.evaluate(maxFlags => { + (window as any).withNestedSpans(() => { + const client = new (window as any).UnleashClient(); + for (let i = 1; i <= maxFlags; i++) { + client.isEnabled(`feat${i}`); + } + client.isEnabled(`feat${maxFlags + 1}`); // dropped + + (window as any).setVariant(client, 'feat3', 'var1', true); + client.isEnabled('feat3'); // update + }); + return true; + }, MAX_FLAGS_PER_SPAN); + + const event = (await envelopeRequestPromise)[0][0]; + const innerSpan = event.spans?.[0]; + const outerSpan = event.spans?.[1]; + const outerSpanFlags = Object.entries(outerSpan?.data ?? {}).filter(([key, _val]) => + key.startsWith('flag.evaluation'), + ); + const innerSpanFlags = Object.entries(innerSpan?.data ?? {}).filter(([key, _val]) => + key.startsWith('flag.evaluation'), + ); + + expect(innerSpanFlags).toEqual([]); + + const expectedOuterSpanFlags = []; + for (let i = 1; i <= MAX_FLAGS_PER_SPAN; i++) { + expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, i === 3]); + } + // Order agnostic (attribute dict is unordered). + expect(outerSpanFlags.sort()).toEqual(expectedOuterSpanFlags.sort()); +}); diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureException/builtInClassInstance/subject.js b/dev-packages/browser-integration-tests/suites/public-api/captureException/builtInClassInstance/subject.js new file mode 100644 index 000000000000..556cd5babb09 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/captureException/builtInClassInstance/subject.js @@ -0,0 +1 @@ +Sentry.captureException(new Response('test body')); diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureException/builtInClassInstance/test.ts b/dev-packages/browser-integration-tests/suites/public-api/captureException/builtInClassInstance/test.ts new file mode 100644 index 000000000000..4f93b7b87d44 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/captureException/builtInClassInstance/test.ts @@ -0,0 +1,20 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/core'; +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; + +sentryTest('should capture a class instance of a built-in class', async ({ getLocalTestUrl, page }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + + expect(eventData.exception?.values).toHaveLength(1); + expect(eventData.exception?.values?.[0]).toMatchObject({ + type: 'Error', + value: '[object Response]', + mechanism: { + type: 'generic', + handled: true, + }, + }); +}); diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureException/classInstance/test.ts b/dev-packages/browser-integration-tests/suites/public-api/captureException/classInstance/test.ts index 5cdd446a1510..19349532282e 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/captureException/classInstance/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/captureException/classInstance/test.ts @@ -3,7 +3,7 @@ import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; -sentryTest('should capture an POJO', async ({ getLocalTestUrl, page }) => { +sentryTest('should capture an class instance', async ({ getLocalTestUrl, page }) => { const url = await getLocalTestUrl({ testDir: __dirname }); const eventData = await getFirstSentryEnvelopeRequest(page, url); diff --git a/dev-packages/browser-integration-tests/suites/public-api/logger/integration/subject.js b/dev-packages/browser-integration-tests/suites/public-api/logger/integration/subject.js index 6c2e9cfdde7a..6974f191b76b 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/logger/integration/subject.js +++ b/dev-packages/browser-integration-tests/suites/public-api/logger/integration/subject.js @@ -6,6 +6,11 @@ console.warn('console.warn', 123, false); console.error('console.error', 123, false); console.assert(false, 'console.assert', 123, false); +// Test object and array truncation +console.log('Object:', { key: 'value', nested: { prop: 123 } }); +console.log('Array:', [1, 2, 3, 'string']); +console.log('Mixed:', 'prefix', { obj: true }, [4, 5, 6], 'suffix'); + console.log(''); Sentry.flush(); diff --git a/dev-packages/browser-integration-tests/suites/public-api/logger/integration/test.ts b/dev-packages/browser-integration-tests/suites/public-api/logger/integration/test.ts index e3aabc0d2fe6..7561b76e8b72 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/logger/integration/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/logger/integration/test.ts @@ -18,7 +18,7 @@ sentryTest('should capture console object calls', async ({ getLocalTestUrl, page expect(envelopeItems[0]).toEqual([ { type: 'log', - item_count: 8, + item_count: 11, content_type: 'application/vnd.sentry.items.log+json', }, { @@ -107,6 +107,42 @@ sentryTest('should capture console object calls', async ({ getLocalTestUrl, page 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, }, }, + { + timestamp: expect.any(Number), + level: 'info', + severity_number: 10, + trace_id: expect.any(String), + body: 'Object: {"key":"value","nested":{"prop":123}}', + attributes: { + 'sentry.origin': { value: 'auto.console.logging', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + }, + }, + { + timestamp: expect.any(Number), + level: 'info', + severity_number: 10, + trace_id: expect.any(String), + body: 'Array: [1,2,3,"string"]', + attributes: { + 'sentry.origin': { value: 'auto.console.logging', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + }, + }, + { + timestamp: expect.any(Number), + level: 'info', + severity_number: 10, + trace_id: expect.any(String), + body: 'Mixed: prefix {"obj":true} [4,5,6] suffix', + attributes: { + 'sentry.origin': { value: 'auto.console.logging', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + }, + }, { timestamp: expect.any(Number), level: 'info', diff --git a/dev-packages/e2e-tests/test-applications/angular-20/package.json b/dev-packages/e2e-tests/test-applications/angular-20/package.json index 34ce69c6ea44..bfd14c64c93f 100644 --- a/dev-packages/e2e-tests/test-applications/angular-20/package.json +++ b/dev-packages/e2e-tests/test-applications/angular-20/package.json @@ -46,6 +46,7 @@ "typescript": "~5.8.3" }, "volta": { - "extends": "../../package.json" + "extends": "../../package.json", + "node": "20.19.2" } } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/package.json b/dev-packages/e2e-tests/test-applications/nextjs-14/package.json index a1e33dbb10ec..a7365cc3fb10 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-14/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-14/package.json @@ -17,7 +17,7 @@ "@types/node": "^18.19.1", "@types/react": "18.0.26", "@types/react-dom": "18.0.9", - "next": "14.1.3", + "next": "14.2.30", "react": "18.2.0", "react-dom": "18.2.0", "typescript": "~5.0.0" diff --git a/dev-packages/e2e-tests/verdaccio-config/config.yaml b/dev-packages/e2e-tests/verdaccio-config/config.yaml index 8535c5898175..c5400118d12c 100644 --- a/dev-packages/e2e-tests/verdaccio-config/config.yaml +++ b/dev-packages/e2e-tests/verdaccio-config/config.yaml @@ -110,6 +110,12 @@ packages: unpublish: $all # proxy: npmjs # Don't proxy for E2E tests! + '@sentry/pino-transport': + access: $all + publish: $all + unpublish: $all + # proxy: npmjs # Don't proxy for E2E tests! + '@sentry/profiling-node': access: $all publish: $all diff --git a/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/basic/scenario.ts b/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/basic/scenario.ts new file mode 100644 index 000000000000..6d1b88137b87 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/basic/scenario.ts @@ -0,0 +1,19 @@ +import { _INTERNAL_FLAG_BUFFER_SIZE as FLAG_BUFFER_SIZE } from '@sentry/core'; +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 1.0, + transport: loggingTransport, + integrations: [Sentry.featureFlagsIntegration()], +}); + +const flagsIntegration = Sentry.getClient()?.getIntegrationByName('FeatureFlags'); +for (let i = 1; i <= FLAG_BUFFER_SIZE; i++) { + flagsIntegration?.addFeatureFlag(`feat${i}`, false); +} +flagsIntegration?.addFeatureFlag(`feat${FLAG_BUFFER_SIZE + 1}`, true); // eviction +flagsIntegration?.addFeatureFlag('feat3', true); // update + +throw new Error('Test error'); diff --git a/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/basic/test.ts b/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/basic/test.ts new file mode 100644 index 000000000000..74ff1c125b45 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/basic/test.ts @@ -0,0 +1,31 @@ +import { _INTERNAL_FLAG_BUFFER_SIZE as FLAG_BUFFER_SIZE } from '@sentry/core'; +import { afterAll, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('Flags captured on error with eviction, update, and no async tasks', async () => { + // Based on scenario.ts. + const expectedFlags = [{ flag: 'feat2', result: false }]; + for (let i = 4; i <= FLAG_BUFFER_SIZE; i++) { + expectedFlags.push({ flag: `feat${i}`, result: false }); + } + expectedFlags.push({ flag: `feat${FLAG_BUFFER_SIZE + 1}`, result: true }); + expectedFlags.push({ flag: 'feat3', result: true }); + + await createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + exception: { values: [{ type: 'Error', value: 'Test error' }] }, + contexts: { + flags: { + values: expectedFlags, + }, + }, + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/withScope/scenario.ts b/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/withScope/scenario.ts new file mode 100644 index 000000000000..4ba9d63b9733 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/withScope/scenario.ts @@ -0,0 +1,30 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +const flagsIntegration = Sentry.featureFlagsIntegration(); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 1.0, + transport: loggingTransport, + integrations: [flagsIntegration], +}); + +async function run(): Promise { + flagsIntegration.addFeatureFlag('shared', true); + + Sentry.withScope(() => { + flagsIntegration.addFeatureFlag('forked', true); + flagsIntegration.addFeatureFlag('shared', false); + Sentry.captureException(new Error('Error in forked scope')); + }); + + await Sentry.flush(); + + flagsIntegration.addFeatureFlag('main', true); + + throw new Error('Error in main scope'); +} + +// eslint-disable-next-line @typescript-eslint/no-floating-promises +run(); diff --git a/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/withScope/test.ts b/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/withScope/test.ts new file mode 100644 index 000000000000..947b299923e7 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/withScope/test.ts @@ -0,0 +1,38 @@ +import { afterAll, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('Flags captured on error are isolated by current scope', async () => { + await createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + exception: { values: [{ type: 'Error', value: 'Error in forked scope' }] }, + contexts: { + flags: { + values: [ + { flag: 'forked', result: true }, + { flag: 'shared', result: false }, + ], + }, + }, + }, + }) + .expect({ + event: { + exception: { values: [{ type: 'Error', value: 'Error in main scope' }] }, + contexts: { + flags: { + values: [ + { flag: 'shared', result: true }, + { flag: 'main', result: true }, + ], + }, + }, + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/scenario.ts b/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/scenario.ts new file mode 100644 index 000000000000..2c07e46b40ed --- /dev/null +++ b/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/scenario.ts @@ -0,0 +1,25 @@ +import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core'; +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 1.0, + tracesSampleRate: 1.0, + transport: loggingTransport, + integrations: [Sentry.featureFlagsIntegration()], +}); + +const flagsIntegration = Sentry.getClient()?.getIntegrationByName('FeatureFlags'); + +Sentry.startSpan({ name: 'test-root-span' }, () => { + Sentry.startSpan({ name: 'test-span' }, () => { + Sentry.startSpan({ name: 'test-nested-span' }, () => { + for (let i = 1; i <= MAX_FLAGS_PER_SPAN; i++) { + flagsIntegration?.addFeatureFlag(`feat${i}`, false); + } + flagsIntegration?.addFeatureFlag(`feat${MAX_FLAGS_PER_SPAN + 1}`, true); // dropped flag + flagsIntegration?.addFeatureFlag('feat3', true); // update + }); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/test.ts b/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/test.ts new file mode 100644 index 000000000000..4a417a3c3959 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/test.ts @@ -0,0 +1,33 @@ +import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core'; +import { afterAll, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('Flags captured on span attributes with max limit', async () => { + // Based on scenario.ts. + const expectedFlags: Record = {}; + for (let i = 1; i <= MAX_FLAGS_PER_SPAN; i++) { + expectedFlags[`flag.evaluation.feat${i}`] = i === 3; + } + + await createRunner(__dirname, 'scenario.ts') + .expect({ + transaction: { + spans: [ + expect.objectContaining({ + description: 'test-span', + data: expect.objectContaining({}), + }), + expect.objectContaining({ + description: 'test-nested-span', + data: expect.objectContaining(expectedFlags), + }), + ], + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/scenario-error.js b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/scenario-error.js new file mode 100644 index 000000000000..99d091325720 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/scenario-error.js @@ -0,0 +1,43 @@ +const Sentry = require('@sentry/node'); +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +// Stop the process from exiting before the transaction is sent +setInterval(() => {}, 1000); + +async function run() { + const { gql } = require('apollo-server'); + const server = require('./apollo-server')(); + + await Sentry.startSpan( + { + name: 'Test Transaction', + op: 'transaction', + }, + async span => { + // Ref: https://www.apollographql.com/docs/apollo-server/testing/testing/#testing-using-executeoperation + await server.executeOperation({ + query: gql` + mutation Mutation($email: String) { + login(email: $email) + } + `, + // We want to trigger an error by passing an invalid variable type + variables: { email: 123 }, + }); + + setTimeout(() => { + span.end(); + server.stop(); + }, 500); + }, + ); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/test.ts b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/test.ts index 2abe2932ece2..4d6661101dc0 100644 --- a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/test.ts @@ -55,4 +55,29 @@ describe('GraphQL/Apollo Tests', () => { .start() .completed(); }); + + test('should handle GraphQL errors.', async () => { + const EXPECTED_TRANSACTION = { + transaction: 'Test Transaction (mutation Mutation)', + spans: expect.arrayContaining([ + expect.objectContaining({ + data: { + 'graphql.operation.name': 'Mutation', + 'graphql.operation.type': 'mutation', + 'graphql.source': 'mutation Mutation($email: String) {\n login(email: $email)\n}', + 'sentry.origin': 'auto.graphql.otel.graphql', + }, + description: 'mutation Mutation', + status: 'unknown_error', + origin: 'auto.graphql.otel.graphql', + }), + ]), + }; + + await createRunner(__dirname, 'scenario-error.js') + .expect({ transaction: EXPECTED_START_SERVER_TRANSACTION }) + .expect({ transaction: EXPECTED_TRANSACTION }) + .start() + .completed(); + }); }); diff --git a/docs/publishing-a-release.md b/docs/publishing-a-release.md index 1ee26a12de77..3b952672e4e9 100644 --- a/docs/publishing-a-release.md +++ b/docs/publishing-a-release.md @@ -2,11 +2,13 @@ _These steps are only relevant to Sentry employees when preparing and publishing a new SDK release._ +These have also been documented via [Cursor Rules](../.cursor/rules/publishing-release.mdc). + **If you want to release a new SDK for the first time, be sure to follow the [New SDK Release Checklist](./new-sdk-release-checklist.md)** 1. Run `yarn changelog` on the `develop` branch and determine what version will be released (we use - [semver](https://semver.org)) + [semver](https://semver.org)). The semver version should be decided based on what is in included in the release. For example, if the release includes a new feature, we should increment the minor version. If it includes only bug fixes, we should increment the patch version. 2. Create a branch `prepare-release/VERSION`, eg. `prepare-release/8.1.0`, off develop 3. Update [`CHANGELOG.md`](https://github.com/getsentry/sentry-javascript/edit/master/CHANGELOG.md) to add an entry for the next release number and a list of changes since the last release. (See details below.) @@ -43,8 +45,9 @@ _These steps are only relevant to Sentry employees when preparing and publishing 2. Create a new section in the changelog with the previously determined version number. 3. Paste in the logs you copied earlier. 4. Delete any which aren't user-facing changes (such as docs or tests). -5. Highlight any important changes with subheadings. -6. If any of the PRs are from external contributors, include underneath the commits +5. If there are any important features or fixes, highlight them under the `Important Changes` subheading. If there are no important changes, don't include this section. If the `Important Changes` subheading is used, put all other changes under the `Other Changes` subheading. +6. Make sure the changelog entries are ordered alphabetically. +7. If any of the PRs are from external contributors, include underneath the commits `Work in this release contributed by . Thank you for your contributions!`. If there's only one external PR, don't forget to remove the final `s`. If there are three or more, use an Oxford comma. (It's in the Sentry styleguide!) @@ -52,4 +55,28 @@ _These steps are only relevant to Sentry employees when preparing and publishing "Unreleased". The GitHub Action creates a PR with this change every time a PR of an external contributor is merged. You can safely cut and paste this line to the new release section of the changelog (but a sanity check is never wrong). -7. Commit, push, and continue with step 4 from the previous section with the general instructions (above). +8. Commit, push, and continue with step 4 from the previous section with the general instructions (above). + +### Example Changelog Entry + +This is an example of a changelog entry for a release. + +```md +## 9.28.0 + +### Important Changes + +- **feat(nestjs): Stop creating spans for `TracingInterceptor` ([#16501](https://github.com/getsentry/sentry-javascript/pull/16501))** + +With this change we stop creating spans for `TracingInterceptor` as this interceptor only serves as an internal helper and adds noise for the user. + +- **feat(node): Update vercel ai spans as per new conventions ([#16497](https://github.com/getsentry/sentry-javascript/pull/16497))** + +This feature ships updates to the span names and ops to better match OpenTelemetry. This should make them more easily accessible to the new agents module view we are building. + +### Other Changes + +- fix(sveltekit): Export `vercelAIIntegration` from `@sentry/node` ([#16496](https://github.com/getsentry/sentry-javascript/pull/16496)) + +Work in this release was contributed by @agrattan0820. Thank you for your contribution! +``` diff --git a/package.json b/package.json index 13e1a600e83d..75bd3b8f8380 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "volta": { "node": "20.18.2", "yarn": "1.22.22", - "pnpm": "9.15.0" + "pnpm": "9.15.9" }, "workspaces": [ "packages/angular", @@ -70,6 +70,7 @@ "packages/node", "packages/nuxt", "packages/opentelemetry", + "packages/pino-transport", "packages/profiling-node", "packages/react", "packages/react-router", diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index ac222eca825b..750eb05d8b10 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -135,6 +135,8 @@ export { consoleLoggingIntegration, wrapMcpServerWithSentry, NODE_VERSION, + featureFlagsIntegration, + type FeatureFlagsIntegration, } from '@sentry/node'; export { init } from './server/sdk'; diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts index 24513a325188..b13f69a9b6ce 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -121,6 +121,8 @@ export { consoleLoggingIntegration, wrapMcpServerWithSentry, NODE_VERSION, + featureFlagsIntegration, + type FeatureFlagsIntegration, } from '@sentry/node'; export { diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/LayoutShiftManager.ts b/packages/browser-utils/src/metrics/web-vitals/lib/LayoutShiftManager.ts index 76de0eb8290c..c9171b56ef0c 100644 --- a/packages/browser-utils/src/metrics/web-vitals/lib/LayoutShiftManager.ts +++ b/packages/browser-utils/src/metrics/web-vitals/lib/LayoutShiftManager.ts @@ -30,7 +30,8 @@ export class LayoutShiftManager { if (entry.hadRecentInput) return; const firstSessionEntry = this._sessionEntries[0]; - const lastSessionEntry = this._sessionEntries.at(-1); + // This previously used `this._sessionEntries.at(-1)` but that is ES2022. We support ES2021 and earlier. + const lastSessionEntry = this._sessionEntries[this._sessionEntries.length - 1]; // If the entry occurred less than 1 second after the previous entry // and less than 5 seconds after the first entry in the session, diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index 2b2279c099b3..963d8ab38546 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -61,13 +61,13 @@ export { instrumentSupabaseClient, zodErrorsIntegration, thirdPartyErrorFilterIntegration, + featureFlagsIntegration, } from '@sentry/core'; -export type { Span } from '@sentry/core'; +export type { Span, FeatureFlagsIntegration } from '@sentry/core'; export { makeBrowserOfflineTransport } from './transports/offline'; export { browserProfilingIntegration } from './profiling/integration'; export { spotlightBrowserIntegration } from './integrations/spotlight'; export { browserSessionIntegration } from './integrations/browsersession'; -export { featureFlagsIntegration, type FeatureFlagsIntegration } from './integrations/featureFlags'; export { launchDarklyIntegration, buildLaunchDarklyFlagUsedHandler } from './integrations/featureFlags/launchdarkly'; export { openFeatureIntegration, OpenFeatureIntegrationHook } from './integrations/featureFlags/openfeature'; export { unleashIntegration } from './integrations/featureFlags/unleash'; diff --git a/packages/browser/src/integrations/featureFlags/launchdarkly/integration.ts b/packages/browser/src/integrations/featureFlags/launchdarkly/integration.ts index f96b8deb8fa0..822e4b1d7f80 100644 --- a/packages/browser/src/integrations/featureFlags/launchdarkly/integration.ts +++ b/packages/browser/src/integrations/featureFlags/launchdarkly/integration.ts @@ -1,10 +1,14 @@ import type { Client, Event, EventHint, IntegrationFn } from '@sentry/core'; -import { defineIntegration } from '@sentry/core'; -import { copyFlagsFromScopeToEvent, insertFlagToScope } from '../../../utils/featureFlags'; +import { + _INTERNAL_addFeatureFlagToActiveSpan, + _INTERNAL_copyFlagsFromScopeToEvent, + _INTERNAL_insertFlagToScope, + defineIntegration, +} from '@sentry/core'; import type { LDContext, LDEvaluationDetail, LDInspectionFlagUsedHandler } from './types'; /** - * Sentry integration for capturing feature flags from LaunchDarkly. + * Sentry integration for capturing feature flag evaluations from LaunchDarkly. * * See the [feature flag documentation](https://develop.sentry.dev/sdk/expected-features/#feature-flags) for more information. * @@ -23,16 +27,16 @@ export const launchDarklyIntegration = defineIntegration(() => { name: 'LaunchDarkly', processEvent(event: Event, _hint: EventHint, _client: Client): Event { - return copyFlagsFromScopeToEvent(event); + return _INTERNAL_copyFlagsFromScopeToEvent(event); }, }; }) satisfies IntegrationFn; /** - * LaunchDarkly hook that listens for flag evaluations and updates the `flags` - * context in our Sentry scope. This needs to be registered as an - * 'inspector' in LaunchDarkly initialize() options, separately from - * `launchDarklyIntegration`. Both are needed to collect feature flags on error. + * LaunchDarkly hook to listen for and buffer flag evaluations. This needs to + * be registered as an 'inspector' in LaunchDarkly initialize() options, + * separately from `launchDarklyIntegration`. Both the hook and the integration + * are needed to capture LaunchDarkly flags. */ export function buildLaunchDarklyFlagUsedHandler(): LDInspectionFlagUsedHandler { return { @@ -45,7 +49,8 @@ export function buildLaunchDarklyFlagUsedHandler(): LDInspectionFlagUsedHandler * Handle a flag evaluation by storing its name and value on the current scope. */ method: (flagKey: string, flagDetail: LDEvaluationDetail, _context: LDContext) => { - insertFlagToScope(flagKey, flagDetail.value); + _INTERNAL_insertFlagToScope(flagKey, flagDetail.value); + _INTERNAL_addFeatureFlagToActiveSpan(flagKey, flagDetail.value); }, }; } diff --git a/packages/browser/src/integrations/featureFlags/openfeature/integration.ts b/packages/browser/src/integrations/featureFlags/openfeature/integration.ts index b1963e9964e6..85aedbf779f9 100644 --- a/packages/browser/src/integrations/featureFlags/openfeature/integration.ts +++ b/packages/browser/src/integrations/featureFlags/openfeature/integration.ts @@ -1,13 +1,25 @@ /** - * OpenFeature integration. + * Sentry integration for capturing OpenFeature feature flag evaluations. * - * Add the openFeatureIntegration() function call to your integration lists. - * Add the integration hook to your OpenFeature object. - * - OpenFeature.getClient().addHooks(new OpenFeatureIntegrationHook()); + * See the [feature flag documentation](https://develop.sentry.dev/sdk/expected-features/#feature-flags) for more information. + * + * @example + * ``` + * import * as Sentry from "@sentry/browser"; + * import { OpenFeature } from "@openfeature/web-sdk"; + * + * Sentry.init(..., integrations: [Sentry.openFeatureIntegration()]); + * OpenFeature.setProvider(new MyProviderOfChoice()); + * OpenFeature.addHooks(new Sentry.OpenFeatureIntegrationHook()); + * ``` */ import type { Client, Event, EventHint, IntegrationFn } from '@sentry/core'; -import { defineIntegration } from '@sentry/core'; -import { copyFlagsFromScopeToEvent, insertFlagToScope } from '../../../utils/featureFlags'; +import { + _INTERNAL_addFeatureFlagToActiveSpan, + _INTERNAL_copyFlagsFromScopeToEvent, + _INTERNAL_insertFlagToScope, + defineIntegration, +} from '@sentry/core'; import type { EvaluationDetails, HookContext, HookHints, JsonValue, OpenFeatureHook } from './types'; export const openFeatureIntegration = defineIntegration(() => { @@ -15,7 +27,7 @@ export const openFeatureIntegration = defineIntegration(() => { name: 'OpenFeature', processEvent(event: Event, _hint: EventHint, _client: Client): Event { - return copyFlagsFromScopeToEvent(event); + return _INTERNAL_copyFlagsFromScopeToEvent(event); }, }; }) satisfies IntegrationFn; @@ -28,13 +40,15 @@ export class OpenFeatureIntegrationHook implements OpenFeatureHook { * Successful evaluation result. */ public after(_hookContext: Readonly>, evaluationDetails: EvaluationDetails): void { - insertFlagToScope(evaluationDetails.flagKey, evaluationDetails.value); + _INTERNAL_insertFlagToScope(evaluationDetails.flagKey, evaluationDetails.value); + _INTERNAL_addFeatureFlagToActiveSpan(evaluationDetails.flagKey, evaluationDetails.value); } /** * On error evaluation result. */ public error(hookContext: Readonly>, _error: unknown, _hookHints?: HookHints): void { - insertFlagToScope(hookContext.flagKey, hookContext.defaultValue); + _INTERNAL_insertFlagToScope(hookContext.flagKey, hookContext.defaultValue); + _INTERNAL_addFeatureFlagToActiveSpan(hookContext.flagKey, hookContext.defaultValue); } } diff --git a/packages/browser/src/integrations/featureFlags/statsig/integration.ts b/packages/browser/src/integrations/featureFlags/statsig/integration.ts index 54600458cfb9..9aef234045b5 100644 --- a/packages/browser/src/integrations/featureFlags/statsig/integration.ts +++ b/packages/browser/src/integrations/featureFlags/statsig/integration.ts @@ -1,6 +1,10 @@ import type { Client, Event, EventHint, IntegrationFn } from '@sentry/core'; -import { defineIntegration } from '@sentry/core'; -import { copyFlagsFromScopeToEvent, insertFlagToScope } from '../../../utils/featureFlags'; +import { + _INTERNAL_addFeatureFlagToActiveSpan, + _INTERNAL_copyFlagsFromScopeToEvent, + _INTERNAL_insertFlagToScope, + defineIntegration, +} from '@sentry/core'; import type { FeatureGate, StatsigClient } from './types'; /** @@ -31,15 +35,16 @@ export const statsigIntegration = defineIntegration( return { name: 'Statsig', - processEvent(event: Event, _hint: EventHint, _client: Client): Event { - return copyFlagsFromScopeToEvent(event); - }, - - setup() { + setup(_client: Client) { statsigClient.on('gate_evaluation', (event: { gate: FeatureGate }) => { - insertFlagToScope(event.gate.name, event.gate.value); + _INTERNAL_insertFlagToScope(event.gate.name, event.gate.value); + _INTERNAL_addFeatureFlagToActiveSpan(event.gate.name, event.gate.value); }); }, + + processEvent(event: Event, _hint: EventHint, _client: Client): Event { + return _INTERNAL_copyFlagsFromScopeToEvent(event); + }, }; }, ) satisfies IntegrationFn; diff --git a/packages/browser/src/integrations/featureFlags/unleash/integration.ts b/packages/browser/src/integrations/featureFlags/unleash/integration.ts index 21d945dfcaae..699c797edecf 100644 --- a/packages/browser/src/integrations/featureFlags/unleash/integration.ts +++ b/packages/browser/src/integrations/featureFlags/unleash/integration.ts @@ -1,7 +1,13 @@ import type { Client, Event, EventHint, IntegrationFn } from '@sentry/core'; -import { defineIntegration, fill, logger } from '@sentry/core'; +import { + _INTERNAL_addFeatureFlagToActiveSpan, + _INTERNAL_copyFlagsFromScopeToEvent, + _INTERNAL_insertFlagToScope, + defineIntegration, + fill, + logger, +} from '@sentry/core'; import { DEBUG_BUILD } from '../../../debug-build'; -import { copyFlagsFromScopeToEvent, insertFlagToScope } from '../../../utils/featureFlags'; import type { UnleashClient, UnleashClientClass } from './types'; type UnleashIntegrationOptions = { @@ -35,14 +41,14 @@ export const unleashIntegration = defineIntegration( return { name: 'Unleash', - processEvent(event: Event, _hint: EventHint, _client: Client): Event { - return copyFlagsFromScopeToEvent(event); - }, - setupOnce() { const unleashClientPrototype = unleashClientClass.prototype as UnleashClient; fill(unleashClientPrototype, 'isEnabled', _wrappedIsEnabled); }, + + processEvent(event: Event, _hint: EventHint, _client: Client): Event { + return _INTERNAL_copyFlagsFromScopeToEvent(event); + }, }; }, ) satisfies IntegrationFn; @@ -64,7 +70,8 @@ function _wrappedIsEnabled( const result = original.apply(this, args); if (typeof toggleName === 'string' && typeof result === 'boolean') { - insertFlagToScope(toggleName, result); + _INTERNAL_insertFlagToScope(toggleName, result); + _INTERNAL_addFeatureFlagToActiveSpan(toggleName, result); } else if (DEBUG_BUILD) { logger.error( `[Feature Flags] UnleashClient.isEnabled does not match expected signature. arg0: ${toggleName} (${typeof toggleName}), result: ${result} (${typeof result})`, diff --git a/packages/browser/src/transports/fetch.ts b/packages/browser/src/transports/fetch.ts index 05ec0f2e211a..d21ae82486ec 100644 --- a/packages/browser/src/transports/fetch.ts +++ b/packages/browser/src/transports/fetch.ts @@ -45,7 +45,7 @@ export function makeFetchTransport( } try { - // TODO: This may need a `suppressTracing` call in the future when we switch the browser SDK to OTEL + // Note: We do not need to suppress tracing here, becasue we are using the native fetch, instead of our wrapped one. return nativeFetch(options.url, requestOptions).then(response => { pendingBodySize -= requestSize; pendingCount--; diff --git a/packages/browser/src/utils/featureFlags.ts b/packages/browser/src/utils/featureFlags.ts deleted file mode 100644 index a71e7233fe75..000000000000 --- a/packages/browser/src/utils/featureFlags.ts +++ /dev/null @@ -1,89 +0,0 @@ -import type { Event, FeatureFlag } from '@sentry/core'; -import { getCurrentScope, logger } from '@sentry/core'; -import { DEBUG_BUILD } from '../debug-build'; - -/** - * Ordered LRU cache for storing feature flags in the scope context. The name - * of each flag in the buffer is unique, and the output of getAll() is ordered - * from oldest to newest. - */ - -/** - * Max size of the LRU flag buffer stored in Sentry scope and event contexts. - */ -export const FLAG_BUFFER_SIZE = 100; - -/** - * Copies feature flags that are in current scope context to the event context - */ -export function copyFlagsFromScopeToEvent(event: Event): Event { - const scope = getCurrentScope(); - const flagContext = scope.getScopeData().contexts.flags; - const flagBuffer = flagContext ? flagContext.values : []; - - if (!flagBuffer.length) { - return event; - } - - if (event.contexts === undefined) { - event.contexts = {}; - } - event.contexts.flags = { values: [...flagBuffer] }; - return event; -} - -/** - * Creates a feature flags values array in current context if it does not exist - * and inserts the flag into a FeatureFlag array while maintaining ordered LRU - * properties. Not thread-safe. After inserting: - * - `flags` is sorted in order of recency, with the newest flag at the end. - * - No other flags with the same name exist in `flags`. - * - The length of `flags` does not exceed `maxSize`. The oldest flag is evicted - * as needed. - * - * @param name Name of the feature flag to insert. - * @param value Value of the feature flag. - * @param maxSize Max number of flags the buffer should store. It's recommended - * to keep this consistent across insertions. Default is FLAG_BUFFER_SIZE - */ -export function insertFlagToScope(name: string, value: unknown, maxSize: number = FLAG_BUFFER_SIZE): void { - const scopeContexts = getCurrentScope().getScopeData().contexts; - if (!scopeContexts.flags) { - scopeContexts.flags = { values: [] }; - } - const flags = scopeContexts.flags.values as FeatureFlag[]; - insertToFlagBuffer(flags, name, value, maxSize); -} - -/** - * Exported for tests. Currently only accepts boolean values (otherwise no-op). - */ -export function insertToFlagBuffer(flags: FeatureFlag[], name: string, value: unknown, maxSize: number): void { - if (typeof value !== 'boolean') { - return; - } - - if (flags.length > maxSize) { - DEBUG_BUILD && logger.error(`[Feature Flags] insertToFlagBuffer called on a buffer larger than maxSize=${maxSize}`); - return; - } - - // Check if the flag is already in the buffer - O(n) - const index = flags.findIndex(f => f.flag === name); - - if (index !== -1) { - // The flag was found, remove it from its current position - O(n) - flags.splice(index, 1); - } - - if (flags.length === maxSize) { - // If at capacity, pop the earliest flag - O(n) - flags.shift(); - } - - // Push the flag to the end - O(1) - flags.push({ - flag: name, - result: value, - }); -} diff --git a/packages/bun/package.json b/packages/bun/package.json index 1beafadb1baf..80048fb579f9 100644 --- a/packages/bun/package.json +++ b/packages/bun/package.json @@ -40,8 +40,7 @@ }, "dependencies": { "@sentry/core": "9.30.0", - "@sentry/node": "9.30.0", - "@sentry/opentelemetry": "9.30.0" + "@sentry/node": "9.30.0" }, "devDependencies": { "bun-types": "^1.2.9" diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index 70104de6d7c3..14a44e2d38fc 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -15,6 +15,7 @@ export type { Stacktrace, Thread, User, + FeatureFlagsIntegration, } from '@sentry/core'; export { @@ -139,6 +140,7 @@ export { consoleLoggingIntegration, createSentryWinstonTransport, wrapMcpServerWithSentry, + featureFlagsIntegration, } from '@sentry/node'; export { diff --git a/packages/cloudflare/src/index.ts b/packages/cloudflare/src/index.ts index 6754ffd04f7c..1a366aeb5dd0 100644 --- a/packages/cloudflare/src/index.ts +++ b/packages/cloudflare/src/index.ts @@ -8,6 +8,7 @@ export type { EventHint, ErrorEvent, Exception, + FeatureFlagsIntegration, Session, SeverityLevel, Span, @@ -91,6 +92,7 @@ export { updateSpanName, wrapMcpServerWithSentry, consoleLoggingIntegration, + featureFlagsIntegration, } from '@sentry/core'; export * as logger from './logs/exports'; diff --git a/packages/core/.eslintrc.js b/packages/core/.eslintrc.js index 5c0b9f20f693..5a021c016763 100644 --- a/packages/core/.eslintrc.js +++ b/packages/core/.eslintrc.js @@ -1,4 +1,4 @@ module.exports = { extends: ['../../.eslintrc.js'], - ignorePatterns: ['rollup.npm.config.mjs', 'test/utils-hoist/buildPolyfills/originals.js'], + ignorePatterns: ['rollup.npm.config.mjs'], }; diff --git a/packages/core/src/api.ts b/packages/core/src/api.ts index 9a83e0667476..924c6a8e28ad 100644 --- a/packages/core/src/api.ts +++ b/packages/core/src/api.ts @@ -1,7 +1,7 @@ import type { ReportDialogOptions } from './report-dialog'; import type { DsnComponents, DsnLike } from './types-hoist/dsn'; import type { SdkInfo } from './types-hoist/sdkinfo'; -import { dsnToString, makeDsn } from './utils-hoist/dsn'; +import { dsnToString, makeDsn } from './utils/dsn'; const SENTRY_API_VERSION = '7'; diff --git a/packages/core/src/asyncContext/stackStrategy.ts b/packages/core/src/asyncContext/stackStrategy.ts index cb0bf878b39c..845605be731d 100644 --- a/packages/core/src/asyncContext/stackStrategy.ts +++ b/packages/core/src/asyncContext/stackStrategy.ts @@ -1,7 +1,7 @@ import type { Client } from '../client'; import { getDefaultCurrentScope, getDefaultIsolationScope } from '../defaultScopes'; import { Scope } from '../scope'; -import { isThenable } from '../utils-hoist/is'; +import { isThenable } from '../utils/is'; import { getMainCarrier, getSentryCarrier } from './../carrier'; import type { AsyncContextStrategy } from './types'; diff --git a/packages/core/src/breadcrumbs.ts b/packages/core/src/breadcrumbs.ts index 5724de09be0a..78157a40e2e3 100644 --- a/packages/core/src/breadcrumbs.ts +++ b/packages/core/src/breadcrumbs.ts @@ -1,7 +1,7 @@ import { getClient, getIsolationScope } from './currentScopes'; import type { Breadcrumb, BreadcrumbHint } from './types-hoist/breadcrumb'; -import { consoleSandbox } from './utils-hoist/logger'; -import { dateTimestampInSeconds } from './utils-hoist/time'; +import { consoleSandbox } from './utils/logger'; +import { dateTimestampInSeconds } from './utils/time'; /** * Default maximum number of breadcrumbs added to an event. Can be overwritten diff --git a/packages/core/src/carrier.ts b/packages/core/src/carrier.ts index 5136121cb8ae..d11ef2940d0c 100644 --- a/packages/core/src/carrier.ts +++ b/packages/core/src/carrier.ts @@ -1,9 +1,9 @@ import type { AsyncContextStack } from './asyncContext/stackStrategy'; import type { AsyncContextStrategy } from './asyncContext/types'; import type { Scope } from './scope'; -import type { Logger } from './utils-hoist/logger'; -import { SDK_VERSION } from './utils-hoist/version'; -import { GLOBAL_OBJ } from './utils-hoist/worldwide'; +import type { Logger } from './utils/logger'; +import { SDK_VERSION } from './utils/version'; +import { GLOBAL_OBJ } from './utils/worldwide'; /** * An object that contains globally accessible properties and maintains a scope stack. diff --git a/packages/core/src/checkin.ts b/packages/core/src/checkin.ts index 424dfb2badda..7037c9ebadb2 100644 --- a/packages/core/src/checkin.ts +++ b/packages/core/src/checkin.ts @@ -2,8 +2,8 @@ import type { SerializedCheckIn } from './types-hoist/checkin'; import type { DsnComponents } from './types-hoist/dsn'; import type { CheckInEnvelope, CheckInItem, DynamicSamplingContext } from './types-hoist/envelope'; import type { SdkMetadata } from './types-hoist/sdkmetadata'; -import { dsnToString } from './utils-hoist/dsn'; -import { createEnvelope } from './utils-hoist/envelope'; +import { dsnToString } from './utils/dsn'; +import { createEnvelope } from './utils/envelope'; /** * Create envelope from check in item. diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index d9abd8f5d0d2..3dd8bd66d023 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -32,19 +32,19 @@ import type { SeverityLevel } from './types-hoist/severity'; import type { Span, SpanAttributes, SpanContextData, SpanJSON } from './types-hoist/span'; import type { StartSpanOptions } from './types-hoist/startSpanOptions'; import type { Transport, TransportMakeRequestResponse } from './types-hoist/transport'; +import { createClientReportEnvelope } from './utils/clientreport'; +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 { logger } from './utils/logger'; import { merge } from './utils/merge'; +import { checkOrSetAlreadyCaught, uuid4 } from './utils/misc'; import { parseSampleRate } from './utils/parseSampleRate'; import { prepareEvent } from './utils/prepareEvent'; import { getActiveSpan, showSpanDropWarning, spanToTraceContext } from './utils/spanUtils'; +import { rejectedSyncPromise, resolvedSyncPromise, SyncPromise } from './utils/syncpromise'; import { convertSpanJsonToTransactionEvent, convertTransactionEventToSpanJson } from './utils/transactionEvent'; -import { createClientReportEnvelope } from './utils-hoist/clientreport'; -import { dsnToString, makeDsn } from './utils-hoist/dsn'; -import { addItemToEnvelope, createAttachmentEnvelopeItem } from './utils-hoist/envelope'; -import { isParameterizedString, isPlainObject, isPrimitive, isThenable } from './utils-hoist/is'; -import { logger } from './utils-hoist/logger'; -import { checkOrSetAlreadyCaught, uuid4 } from './utils-hoist/misc'; -import { rejectedSyncPromise, resolvedSyncPromise, SyncPromise } from './utils-hoist/syncpromise'; const ALREADY_SEEN_ERROR = "Not capturing exception because it's already been captured."; const MISSING_RELEASE_FOR_SESSION_ERROR = 'Discarded session because of missing or non-string release'; diff --git a/packages/core/src/currentScopes.ts b/packages/core/src/currentScopes.ts index b8d719098e49..fc40051e56d8 100644 --- a/packages/core/src/currentScopes.ts +++ b/packages/core/src/currentScopes.ts @@ -3,7 +3,7 @@ import { getGlobalSingleton, getMainCarrier } from './carrier'; import type { Client } from './client'; import { Scope } from './scope'; import type { TraceContext } from './types-hoist/context'; -import { generateSpanId } from './utils-hoist/propagationContext'; +import { generateSpanId } from './utils/propagationContext'; /** * Get the currently active scope. diff --git a/packages/core/src/envelope.ts b/packages/core/src/envelope.ts index efec02eb7323..dc094d218812 100644 --- a/packages/core/src/envelope.ts +++ b/packages/core/src/envelope.ts @@ -18,15 +18,15 @@ import type { Event } from './types-hoist/event'; import type { SdkInfo } from './types-hoist/sdkinfo'; import type { SdkMetadata } from './types-hoist/sdkmetadata'; import type { Session, SessionAggregates } from './types-hoist/session'; -import { showSpanDropWarning, spanToJSON } from './utils/spanUtils'; -import { dsnToString } from './utils-hoist/dsn'; +import { dsnToString } from './utils/dsn'; import { createEnvelope, createEventEnvelopeHeaders, createSpanEnvelopeItem, getSdkMetadataForEnvelopeHeader, -} from './utils-hoist/envelope'; -import { uuid4 } from './utils-hoist/misc'; +} from './utils/envelope'; +import { uuid4 } from './utils/misc'; +import { showSpanDropWarning, spanToJSON } from './utils/spanUtils'; /** * Apply SdkInfo (name, version, packages, integrations) to the corresponding event key. diff --git a/packages/core/src/eventProcessors.ts b/packages/core/src/eventProcessors.ts index d2405a90032b..436f3e3c79ed 100644 --- a/packages/core/src/eventProcessors.ts +++ b/packages/core/src/eventProcessors.ts @@ -1,9 +1,9 @@ import { DEBUG_BUILD } from './debug-build'; import type { Event, EventHint } from './types-hoist/event'; import type { EventProcessor } from './types-hoist/eventprocessor'; -import { isThenable } from './utils-hoist/is'; -import { logger } from './utils-hoist/logger'; -import { SyncPromise } from './utils-hoist/syncpromise'; +import { isThenable } from './utils/is'; +import { logger } from './utils/logger'; +import { SyncPromise } from './utils/syncpromise'; /** * Process an array of event processors, returning the processed event (or `null` if the event was dropped). diff --git a/packages/core/src/exports.ts b/packages/core/src/exports.ts index d7e678b16edd..92e4d09d4d81 100644 --- a/packages/core/src/exports.ts +++ b/packages/core/src/exports.ts @@ -10,13 +10,13 @@ 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 { isThenable } from './utils/is'; +import { logger } from './utils/logger'; +import { uuid4 } from './utils/misc'; import type { ExclusiveEventHintOrCaptureContext } from './utils/prepareEvent'; import { parseEventHintOrCaptureContext } from './utils/prepareEvent'; -import { isThenable } from './utils-hoist/is'; -import { logger } from './utils-hoist/logger'; -import { uuid4 } from './utils-hoist/misc'; -import { timestampInSeconds } from './utils-hoist/time'; -import { GLOBAL_OBJ } from './utils-hoist/worldwide'; +import { timestampInSeconds } from './utils/time'; +import { GLOBAL_OBJ } from './utils/worldwide'; /** * Captures an exception event and sends it to Sentry. diff --git a/packages/core/src/featureFlags.ts b/packages/core/src/featureFlags.ts deleted file mode 100644 index f80e17ef7f9d..000000000000 --- a/packages/core/src/featureFlags.ts +++ /dev/null @@ -1 +0,0 @@ -export type FeatureFlag = { readonly flag: string; readonly result: boolean }; diff --git a/packages/core/src/fetch.ts b/packages/core/src/fetch.ts index 65274d1e82a3..cf82eca1e6c1 100644 --- a/packages/core/src/fetch.ts +++ b/packages/core/src/fetch.ts @@ -5,12 +5,12 @@ import { SentryNonRecordingSpan } from './tracing/sentryNonRecordingSpan'; import type { FetchBreadcrumbHint } from './types-hoist/breadcrumb'; import type { HandlerDataFetch } from './types-hoist/instrument'; import type { Span, SpanAttributes, SpanOrigin } from './types-hoist/span'; +import { SENTRY_BAGGAGE_KEY_PREFIX } from './utils/baggage'; import { hasSpansEnabled } from './utils/hasSpansEnabled'; +import { isInstanceOf, isRequest } from './utils/is'; import { getActiveSpan } from './utils/spanUtils'; import { getTraceData } from './utils/traceData'; -import { SENTRY_BAGGAGE_KEY_PREFIX } from './utils-hoist/baggage'; -import { isInstanceOf, isRequest } from './utils-hoist/is'; -import { getSanitizedUrlStringFromUrlObject, isURLObjectRelative, parseStringToURLObject } from './utils-hoist/url'; +import { getSanitizedUrlStringFromUrlObject, isURLObjectRelative, parseStringToURLObject } from './utils/url'; type PolymorphicRequestHeaders = | Record diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 986d18a972d2..b4f09d89f381 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -88,6 +88,7 @@ export { parseSampleRate } from './utils/parseSampleRate'; export { applySdkMetadata } from './utils/sdkMetadata'; export { getTraceData } from './utils/traceData'; export { getTraceMetaTags } from './utils/meta'; +export { debounce } from './utils/debounce'; export { winterCGHeadersToDict, winterCGRequestToRequestData, @@ -112,6 +113,7 @@ export { supabaseIntegration, instrumentSupabaseClient } from './integrations/su export { zodErrorsIntegration } from './integrations/zoderrors'; export { thirdPartyErrorFilterIntegration } from './integrations/third-party-errors-filter'; export { consoleIntegration } from './integrations/console'; +export { featureFlagsIntegration, type FeatureFlagsIntegration } from './integrations/featureFlags'; export { profiler } from './profiling'; export { instrumentFetchRequest } from './fetch'; @@ -122,26 +124,28 @@ export type { ReportDialogOptions } from './report-dialog'; export { _INTERNAL_captureLog, _INTERNAL_flushLogsBuffer, _INTERNAL_captureSerializedLog } from './logs/exports'; export { consoleLoggingIntegration } from './logs/console-integration'; -export type { FeatureFlag } from './featureFlags'; +export type { FeatureFlag } from './utils/featureFlags'; +export { + _INTERNAL_copyFlagsFromScopeToEvent, + _INTERNAL_insertFlagToScope, + _INTERNAL_addFeatureFlagToActiveSpan, + _INTERNAL_FLAG_BUFFER_SIZE, + _INTERNAL_MAX_FLAGS_PER_SPAN, +} from './utils/featureFlags'; -export { applyAggregateErrorsToEvent } from './utils-hoist/aggregate-errors'; -export { getBreadcrumbLogLevelFromHttpStatusCode } from './utils-hoist/breadcrumb-log-level'; -export { getComponentName, getLocationHref, htmlTreeAsString } from './utils-hoist/browser'; -export { dsnFromString, dsnToString, makeDsn } from './utils-hoist/dsn'; +export { applyAggregateErrorsToEvent } from './utils/aggregate-errors'; +export { getBreadcrumbLogLevelFromHttpStatusCode } from './utils/breadcrumb-log-level'; +export { getComponentName, getLocationHref, htmlTreeAsString } from './utils/browser'; +export { dsnFromString, dsnToString, makeDsn } from './utils/dsn'; // eslint-disable-next-line deprecation/deprecation -export { SentryError } from './utils-hoist/error'; -export { GLOBAL_OBJ } from './utils-hoist/worldwide'; -export type { InternalGlobal } from './utils-hoist/worldwide'; -export { addConsoleInstrumentationHandler } from './utils-hoist/instrument/console'; -export { addFetchEndInstrumentationHandler, addFetchInstrumentationHandler } from './utils-hoist/instrument/fetch'; -export { addGlobalErrorInstrumentationHandler } from './utils-hoist/instrument/globalError'; -export { addGlobalUnhandledRejectionInstrumentationHandler } from './utils-hoist/instrument/globalUnhandledRejection'; -export { - addHandler, - maybeInstrument, - resetInstrumentationHandlers, - triggerHandlers, -} from './utils-hoist/instrument/handlers'; +export { SentryError } from './utils/error'; +export { GLOBAL_OBJ } from './utils/worldwide'; +export type { InternalGlobal } from './utils/worldwide'; +export { addConsoleInstrumentationHandler } from './instrument/console'; +export { addFetchEndInstrumentationHandler, addFetchInstrumentationHandler } from './instrument/fetch'; +export { addGlobalErrorInstrumentationHandler } from './instrument/globalError'; +export { addGlobalUnhandledRejectionInstrumentationHandler } from './instrument/globalUnhandledRejection'; +export { addHandler, maybeInstrument, resetInstrumentationHandlers, triggerHandlers } from './instrument/handlers'; export { isDOMError, isDOMException, @@ -158,10 +162,10 @@ export { isSyntheticEvent, isThenable, isVueViewModel, -} from './utils-hoist/is'; -export { isBrowser } from './utils-hoist/isBrowser'; -export { CONSOLE_LEVELS, consoleSandbox, logger, originalConsoleMethods } from './utils-hoist/logger'; -export type { Logger } from './utils-hoist/logger'; +} from './utils/is'; +export { isBrowser } from './utils/isBrowser'; +export { CONSOLE_LEVELS, consoleSandbox, logger, originalConsoleMethods } from './utils/logger'; +export type { Logger } from './utils/logger'; export { addContextToFrame, addExceptionMechanism, @@ -170,9 +174,9 @@ export { getEventDescription, parseSemver, uuid4, -} from './utils-hoist/misc'; -export { isNodeEnv, loadModule } from './utils-hoist/node'; -export { normalize, normalizeToSize, normalizeUrlToBase } from './utils-hoist/normalize'; +} from './utils/misc'; +export { isNodeEnv, loadModule } from './utils/node'; +export { normalize, normalizeToSize, normalizeUrlToBase } from './utils/normalize'; export { addNonEnumerableProperty, convertToPlainObject, @@ -183,11 +187,11 @@ export { getOriginalFunction, markFunctionWrapped, objectify, -} from './utils-hoist/object'; -export { basename, dirname, isAbsolute, join, normalizePath, relative, resolve } from './utils-hoist/path'; -export { makePromiseBuffer, SENTRY_BUFFER_FULL_ERROR } from './utils-hoist/promisebuffer'; -export type { PromiseBuffer } from './utils-hoist/promisebuffer'; -export { severityLevelFromString } from './utils-hoist/severity'; +} from './utils/object'; +export { basename, dirname, isAbsolute, join, normalizePath, relative, resolve } from './utils/path'; +export { makePromiseBuffer, SENTRY_BUFFER_FULL_ERROR } from './utils/promisebuffer'; +export type { PromiseBuffer } from './utils/promisebuffer'; +export { severityLevelFromString } from './utils/severity'; export { UNKNOWN_FUNCTION, createStackParser, @@ -195,9 +199,9 @@ export { getFunctionName, stackParserFromStackParserOptions, stripSentryFramesAndReverse, -} from './utils-hoist/stacktrace'; -export { filenameIsInApp, node, nodeStackLineParser } from './utils-hoist/node-stack-trace'; -export { isMatchingPattern, safeJoin, snipLine, stringMatchesSomePattern, truncate } from './utils-hoist/string'; +} from './utils/stacktrace'; +export { filenameIsInApp, node, nodeStackLineParser } from './utils/node-stack-trace'; +export { isMatchingPattern, safeJoin, snipLine, stringMatchesSomePattern, truncate } from './utils/string'; export { isNativeFunction, supportsDOMError, @@ -210,17 +214,17 @@ export { // eslint-disable-next-line deprecation/deprecation supportsReferrerPolicy, supportsReportingObserver, -} from './utils-hoist/supports'; -export { SyncPromise, rejectedSyncPromise, resolvedSyncPromise } from './utils-hoist/syncpromise'; -export { browserPerformanceTimeOrigin, dateTimestampInSeconds, timestampInSeconds } from './utils-hoist/time'; +} from './utils/supports'; +export { SyncPromise, rejectedSyncPromise, resolvedSyncPromise } from './utils/syncpromise'; +export { browserPerformanceTimeOrigin, dateTimestampInSeconds, timestampInSeconds } from './utils/time'; export { TRACEPARENT_REGEXP, extractTraceparentData, generateSentryTraceHeader, propagationContextFromHeaders, -} from './utils-hoist/tracing'; -export { getSDKSource, isBrowserBundle } from './utils-hoist/env'; -export type { SdkSource } from './utils-hoist/env'; +} from './utils/tracing'; +export { getSDKSource, isBrowserBundle } from './utils/env'; +export type { SdkSource } from './utils/env'; export { addItemToEnvelope, createAttachmentEnvelopeItem, @@ -233,16 +237,16 @@ export { getSdkMetadataForEnvelopeHeader, parseEnvelope, serializeEnvelope, -} from './utils-hoist/envelope'; -export { createClientReportEnvelope } from './utils-hoist/clientreport'; +} from './utils/envelope'; +export { createClientReportEnvelope } from './utils/clientreport'; export { DEFAULT_RETRY_AFTER, disabledUntil, isRateLimited, parseRetryAfterHeader, updateRateLimits, -} from './utils-hoist/ratelimit'; -export type { RateLimits } from './utils-hoist/ratelimit'; +} from './utils/ratelimit'; +export type { RateLimits } from './utils/ratelimit'; export { MAX_BAGGAGE_STRING_LENGTH, SENTRY_BAGGAGE_KEY_PREFIX, @@ -251,7 +255,7 @@ export { dynamicSamplingContextToSentryBaggageHeader, parseBaggageHeader, objectToBaggageHeader, -} from './utils-hoist/baggage'; +} from './utils/baggage'; export { getSanitizedUrlString, parseUrl, @@ -260,20 +264,15 @@ export { getHttpSpanDetailsFromUrlObject, isURLObjectRelative, getSanitizedUrlStringFromUrlObject, -} from './utils-hoist/url'; -export { - eventFromMessage, - eventFromUnknownInput, - exceptionFromError, - parseStackFrames, -} from './utils-hoist/eventbuilder'; -export { callFrameToStackFrame, watchdogTimer } from './utils-hoist/anr'; -export { LRUMap } from './utils-hoist/lru'; -export { generateTraceId, generateSpanId } from './utils-hoist/propagationContext'; -export { vercelWaitUntil } from './utils-hoist/vercelWaitUntil'; -export { SDK_VERSION } from './utils-hoist/version'; -export { getDebugImagesForResources, getFilenameToDebugIdMap } from './utils-hoist/debug-ids'; -export { escapeStringForRegex } from './utils-hoist/vendor/escapeStringForRegex'; +} from './utils/url'; +export { eventFromMessage, eventFromUnknownInput, exceptionFromError, parseStackFrames } from './utils/eventbuilder'; +export { callFrameToStackFrame, watchdogTimer } from './utils/anr'; +export { LRUMap } from './utils/lru'; +export { generateTraceId, generateSpanId } from './utils/propagationContext'; +export { vercelWaitUntil } from './utils/vercelWaitUntil'; +export { SDK_VERSION } from './utils/version'; +export { getDebugImagesForResources, getFilenameToDebugIdMap } from './utils/debug-ids'; +export { escapeStringForRegex } from './vendor/escapeStringForRegex'; export type { Attachment } from './types-hoist/attachment'; export type { diff --git a/packages/core/src/utils-hoist/instrument/console.ts b/packages/core/src/instrument/console.ts similarity index 83% rename from packages/core/src/utils-hoist/instrument/console.ts rename to packages/core/src/instrument/console.ts index 90fad9fbde43..141f3bcb33c9 100644 --- a/packages/core/src/utils-hoist/instrument/console.ts +++ b/packages/core/src/instrument/console.ts @@ -1,9 +1,9 @@ /* 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 '../logger'; -import { fill } from '../object'; -import { GLOBAL_OBJ } from '../worldwide'; +import type { ConsoleLevel, HandlerDataConsole } from '../types-hoist/instrument'; +import { CONSOLE_LEVELS, originalConsoleMethods } from '../utils/logger'; +import { fill } from '../utils/object'; +import { GLOBAL_OBJ } from '../utils/worldwide'; import { addHandler, maybeInstrument, triggerHandlers } from './handlers'; /** diff --git a/packages/core/src/utils-hoist/instrument/fetch.ts b/packages/core/src/instrument/fetch.ts similarity index 95% rename from packages/core/src/utils-hoist/instrument/fetch.ts rename to packages/core/src/instrument/fetch.ts index 270219f80efb..be079280215a 100644 --- a/packages/core/src/utils-hoist/instrument/fetch.ts +++ b/packages/core/src/instrument/fetch.ts @@ -1,11 +1,11 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import type { HandlerDataFetch } from '../../types-hoist/instrument'; -import type { WebFetchHeaders } from '../../types-hoist/webfetchapi'; -import { isError, isRequest } from '../is'; -import { addNonEnumerableProperty, fill } from '../object'; -import { supportsNativeFetch } from '../supports'; -import { timestampInSeconds } from '../time'; -import { GLOBAL_OBJ } from '../worldwide'; +import type { HandlerDataFetch } from '../types-hoist/instrument'; +import type { WebFetchHeaders } from '../types-hoist/webfetchapi'; +import { isError, isRequest } from '../utils/is'; +import { addNonEnumerableProperty, fill } from '../utils/object'; +import { supportsNativeFetch } from '../utils/supports'; +import { timestampInSeconds } from '../utils/time'; +import { GLOBAL_OBJ } from '../utils/worldwide'; import { addHandler, maybeInstrument, triggerHandlers } from './handlers'; type FetchResource = string | { toString(): string } | { url: string }; diff --git a/packages/core/src/utils-hoist/instrument/globalError.ts b/packages/core/src/instrument/globalError.ts similarity index 92% rename from packages/core/src/utils-hoist/instrument/globalError.ts rename to packages/core/src/instrument/globalError.ts index 6f4f23cb65b7..39e00ed816c4 100644 --- a/packages/core/src/utils-hoist/instrument/globalError.ts +++ b/packages/core/src/instrument/globalError.ts @@ -1,5 +1,5 @@ -import type { HandlerDataError } from '../../types-hoist/instrument'; -import { GLOBAL_OBJ } from '../worldwide'; +import type { HandlerDataError } from '../types-hoist/instrument'; +import { GLOBAL_OBJ } from '../utils/worldwide'; import { addHandler, maybeInstrument, triggerHandlers } from './handlers'; let _oldOnErrorHandler: (typeof GLOBAL_OBJ)['onerror'] | null = null; diff --git a/packages/core/src/utils-hoist/instrument/globalUnhandledRejection.ts b/packages/core/src/instrument/globalUnhandledRejection.ts similarity index 91% rename from packages/core/src/utils-hoist/instrument/globalUnhandledRejection.ts rename to packages/core/src/instrument/globalUnhandledRejection.ts index bbf43c949394..230146ae8646 100644 --- a/packages/core/src/utils-hoist/instrument/globalUnhandledRejection.ts +++ b/packages/core/src/instrument/globalUnhandledRejection.ts @@ -1,5 +1,5 @@ -import type { HandlerDataUnhandledRejection } from '../../types-hoist/instrument'; -import { GLOBAL_OBJ } from '../worldwide'; +import type { HandlerDataUnhandledRejection } from '../types-hoist/instrument'; +import { GLOBAL_OBJ } from '../utils/worldwide'; import { addHandler, maybeInstrument, triggerHandlers } from './handlers'; let _oldOnUnhandledRejectionHandler: (typeof GLOBAL_OBJ)['onunhandledrejection'] | null = null; diff --git a/packages/core/src/utils-hoist/instrument/handlers.ts b/packages/core/src/instrument/handlers.ts similarity index 93% rename from packages/core/src/utils-hoist/instrument/handlers.ts rename to packages/core/src/instrument/handlers.ts index 9d6222662912..2d88b3d8abe9 100644 --- a/packages/core/src/utils-hoist/instrument/handlers.ts +++ b/packages/core/src/instrument/handlers.ts @@ -1,6 +1,6 @@ -import { DEBUG_BUILD } from '../../debug-build'; -import { logger } from '../logger'; -import { getFunctionName } from '../stacktrace'; +import { DEBUG_BUILD } from '../debug-build'; +import { logger } from '../utils/logger'; +import { getFunctionName } from '../utils/stacktrace'; export type InstrumentHandlerType = | 'console' diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts index c5636d002cc1..d8d741183287 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 { logger } from './utils-hoist/logger'; +import { logger } from './utils/logger'; export const installedIntegrations: string[] = []; diff --git a/packages/core/src/integrations/captureconsole.ts b/packages/core/src/integrations/captureconsole.ts index e5dc43200dfe..83863349e14a 100644 --- a/packages/core/src/integrations/captureconsole.ts +++ b/packages/core/src/integrations/captureconsole.ts @@ -1,14 +1,14 @@ import { getClient, withScope } from '../currentScopes'; import { captureException, captureMessage } from '../exports'; +import { addConsoleInstrumentationHandler } from '../instrument/console'; import { defineIntegration } from '../integration'; import type { CaptureContext } from '../scope'; import type { IntegrationFn } from '../types-hoist/integration'; -import { addConsoleInstrumentationHandler } from '../utils-hoist/instrument/console'; -import { CONSOLE_LEVELS } from '../utils-hoist/logger'; -import { addExceptionMechanism } from '../utils-hoist/misc'; -import { severityLevelFromString } from '../utils-hoist/severity'; -import { safeJoin } from '../utils-hoist/string'; -import { GLOBAL_OBJ } from '../utils-hoist/worldwide'; +import { CONSOLE_LEVELS } from '../utils/logger'; +import { addExceptionMechanism } from '../utils/misc'; +import { severityLevelFromString } from '../utils/severity'; +import { safeJoin } from '../utils/string'; +import { GLOBAL_OBJ } from '../utils/worldwide'; interface CaptureConsoleOptions { levels?: string[]; diff --git a/packages/core/src/integrations/console.ts b/packages/core/src/integrations/console.ts index e995056a1f23..3aa1cd8d9f3e 100644 --- a/packages/core/src/integrations/console.ts +++ b/packages/core/src/integrations/console.ts @@ -1,12 +1,12 @@ import { addBreadcrumb } from '../breadcrumbs'; import { getClient } from '../currentScopes'; +import { addConsoleInstrumentationHandler } from '../instrument/console'; import { defineIntegration } from '../integration'; import type { ConsoleLevel } from '../types-hoist/instrument'; -import { addConsoleInstrumentationHandler } from '../utils-hoist/instrument/console'; -import { CONSOLE_LEVELS } from '../utils-hoist/logger'; -import { severityLevelFromString } from '../utils-hoist/severity'; -import { safeJoin } from '../utils-hoist/string'; -import { GLOBAL_OBJ } from '../utils-hoist/worldwide'; +import { CONSOLE_LEVELS } from '../utils/logger'; +import { severityLevelFromString } from '../utils/severity'; +import { safeJoin } from '../utils/string'; +import { GLOBAL_OBJ } from '../utils/worldwide'; interface ConsoleIntegrationOptions { levels: ConsoleLevel[]; diff --git a/packages/core/src/integrations/dedupe.ts b/packages/core/src/integrations/dedupe.ts index 9b2fae754e28..ab0e7eb41b25 100644 --- a/packages/core/src/integrations/dedupe.ts +++ b/packages/core/src/integrations/dedupe.ts @@ -4,8 +4,8 @@ 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 { logger } from '../utils-hoist/logger'; -import { getFramesFromEvent } from '../utils-hoist/stacktrace'; +import { logger } from '../utils/logger'; +import { getFramesFromEvent } from '../utils/stacktrace'; const INTEGRATION_NAME = 'Dedupe'; diff --git a/packages/core/src/integrations/eventFilters.ts b/packages/core/src/integrations/eventFilters.ts index 7e254c16f0fa..c1818c8a46f8 100644 --- a/packages/core/src/integrations/eventFilters.ts +++ b/packages/core/src/integrations/eventFilters.ts @@ -4,9 +4,9 @@ import type { Event } from '../types-hoist/event'; import type { IntegrationFn } from '../types-hoist/integration'; import type { StackFrame } from '../types-hoist/stackframe'; import { getPossibleEventMessages } from '../utils/eventUtils'; -import { logger } from '../utils-hoist/logger'; -import { getEventDescription } from '../utils-hoist/misc'; -import { stringMatchesSomePattern } from '../utils-hoist/string'; +import { logger } from '../utils/logger'; +import { getEventDescription } from '../utils/misc'; +import { stringMatchesSomePattern } from '../utils/string'; // "Script error." is hard coded into browsers for errors that it can't read. // this is the result of a script being pulled in from an external domain and CORS. diff --git a/packages/core/src/integrations/extraerrordata.ts b/packages/core/src/integrations/extraerrordata.ts index d58d116783f3..3856699c9f68 100644 --- a/packages/core/src/integrations/extraerrordata.ts +++ b/packages/core/src/integrations/extraerrordata.ts @@ -4,11 +4,11 @@ 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 { isError, isPlainObject } from '../utils-hoist/is'; -import { logger } from '../utils-hoist/logger'; -import { normalize } from '../utils-hoist/normalize'; -import { addNonEnumerableProperty } from '../utils-hoist/object'; -import { truncate } from '../utils-hoist/string'; +import { isError, isPlainObject } from '../utils/is'; +import { logger } from '../utils/logger'; +import { normalize } from '../utils/normalize'; +import { addNonEnumerableProperty } from '../utils/object'; +import { truncate } from '../utils/string'; const INTEGRATION_NAME = 'ExtraErrorData'; diff --git a/packages/browser/src/integrations/featureFlags/featureFlagsIntegration.ts b/packages/core/src/integrations/featureFlags/featureFlagsIntegration.ts similarity index 61% rename from packages/browser/src/integrations/featureFlags/featureFlagsIntegration.ts rename to packages/core/src/integrations/featureFlags/featureFlagsIntegration.ts index 54b5680cccd1..fddc1944ceab 100644 --- a/packages/browser/src/integrations/featureFlags/featureFlagsIntegration.ts +++ b/packages/core/src/integrations/featureFlags/featureFlagsIntegration.ts @@ -1,15 +1,20 @@ -import type { Client, Event, EventHint, Integration, IntegrationFn } from '@sentry/core'; -import { defineIntegration } from '@sentry/core'; -import { copyFlagsFromScopeToEvent, insertFlagToScope } from '../../utils/featureFlags'; +import { type Client } from '../../client'; +import { defineIntegration } from '../../integration'; +import { type Event, type EventHint } from '../../types-hoist/event'; +import { type Integration, type IntegrationFn } from '../../types-hoist/integration'; +import { + _INTERNAL_addFeatureFlagToActiveSpan, + _INTERNAL_copyFlagsFromScopeToEvent, + _INTERNAL_insertFlagToScope, +} from '../../utils/featureFlags'; export interface FeatureFlagsIntegration extends Integration { addFeatureFlag: (name: string, value: unknown) => void; } /** - * Sentry integration for buffering feature flags manually with an API, and - * capturing them on error events. We recommend you do this on each flag - * evaluation. Flags are buffered per Sentry scope and limited to 100 per event. + * Sentry integration for buffering feature flag evaluations manually with an API, and + * capturing them on error events and spans. * * See the [feature flag documentation](https://develop.sentry.dev/sdk/expected-features/#feature-flags) for more information. * @@ -36,11 +41,12 @@ export const featureFlagsIntegration = defineIntegration(() => { name: 'FeatureFlags', processEvent(event: Event, _hint: EventHint, _client: Client): Event { - return copyFlagsFromScopeToEvent(event); + return _INTERNAL_copyFlagsFromScopeToEvent(event); }, addFeatureFlag(name: string, value: unknown): void { - insertFlagToScope(name, value); + _INTERNAL_insertFlagToScope(name, value); + _INTERNAL_addFeatureFlagToActiveSpan(name, value); }, }; }) as IntegrationFn; diff --git a/packages/browser/src/integrations/featureFlags/index.ts b/packages/core/src/integrations/featureFlags/index.ts similarity index 100% rename from packages/browser/src/integrations/featureFlags/index.ts rename to packages/core/src/integrations/featureFlags/index.ts diff --git a/packages/core/src/integrations/functiontostring.ts b/packages/core/src/integrations/functiontostring.ts index aef1b95795cd..5426e8291cb6 100644 --- a/packages/core/src/integrations/functiontostring.ts +++ b/packages/core/src/integrations/functiontostring.ts @@ -3,7 +3,7 @@ import { getClient } from '../currentScopes'; import { defineIntegration } from '../integration'; import type { IntegrationFn } from '../types-hoist/integration'; import type { WrappedFunction } from '../types-hoist/wrappedfunction'; -import { getOriginalFunction } from '../utils-hoist/object'; +import { getOriginalFunction } from '../utils/object'; let originalFunctionToString: () => void; diff --git a/packages/core/src/integrations/linkederrors.ts b/packages/core/src/integrations/linkederrors.ts index c9263eba744d..a45837be7548 100644 --- a/packages/core/src/integrations/linkederrors.ts +++ b/packages/core/src/integrations/linkederrors.ts @@ -1,7 +1,7 @@ import { defineIntegration } from '../integration'; import type { IntegrationFn } from '../types-hoist/integration'; -import { applyAggregateErrorsToEvent } from '../utils-hoist/aggregate-errors'; -import { exceptionFromError } from '../utils-hoist/eventbuilder'; +import { applyAggregateErrorsToEvent } from '../utils/aggregate-errors'; +import { exceptionFromError } from '../utils/eventbuilder'; interface LinkedErrorsOptions { key?: string; diff --git a/packages/core/src/integrations/metadata.ts b/packages/core/src/integrations/metadata.ts index 2a00d4d48137..1bbbbdfb9624 100644 --- a/packages/core/src/integrations/metadata.ts +++ b/packages/core/src/integrations/metadata.ts @@ -1,7 +1,7 @@ import { defineIntegration } from '../integration'; import { addMetadataToStackFrames, stripMetadataFromStackFrames } from '../metadata'; import type { EventItem } from '../types-hoist/envelope'; -import { forEachEnvelopeItem } from '../utils-hoist/envelope'; +import { forEachEnvelopeItem } from '../utils/envelope'; /** * Adds module metadata to stack frames. diff --git a/packages/core/src/integrations/rewriteframes.ts b/packages/core/src/integrations/rewriteframes.ts index 8b7361e40d94..c903016b0531 100644 --- a/packages/core/src/integrations/rewriteframes.ts +++ b/packages/core/src/integrations/rewriteframes.ts @@ -2,8 +2,8 @@ import { defineIntegration } from '../integration'; import type { Event } from '../types-hoist/event'; import type { StackFrame } from '../types-hoist/stackframe'; import type { Stacktrace } from '../types-hoist/stacktrace'; -import { basename, relative } from '../utils-hoist/path'; -import { GLOBAL_OBJ } from '../utils-hoist/worldwide'; +import { basename, relative } from '../utils/path'; +import { GLOBAL_OBJ } from '../utils/worldwide'; type StackFrameIteratee = (frame: StackFrame) => StackFrame; diff --git a/packages/core/src/integrations/supabase.ts b/packages/core/src/integrations/supabase.ts index 084d50356a83..ac781e95ece6 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 { isPlainObject } from '../utils-hoist/is'; -import { logger } from '../utils-hoist/logger'; +import { isPlainObject } from '../utils/is'; +import { logger } from '../utils/logger'; const AUTH_OPERATIONS_TO_INSTRUMENT = [ 'reauthenticate', diff --git a/packages/core/src/integrations/third-party-errors-filter.ts b/packages/core/src/integrations/third-party-errors-filter.ts index b61e6ccffb45..1a0628359f5b 100644 --- a/packages/core/src/integrations/third-party-errors-filter.ts +++ b/packages/core/src/integrations/third-party-errors-filter.ts @@ -2,8 +2,8 @@ import { defineIntegration } from '../integration'; import { addMetadataToStackFrames, stripMetadataFromStackFrames } from '../metadata'; import type { EventItem } from '../types-hoist/envelope'; import type { Event } from '../types-hoist/event'; -import { forEachEnvelopeItem } from '../utils-hoist/envelope'; -import { getFramesFromEvent } from '../utils-hoist/stacktrace'; +import { forEachEnvelopeItem } from '../utils/envelope'; +import { getFramesFromEvent } from '../utils/stacktrace'; interface Options { /** diff --git a/packages/core/src/integrations/zoderrors.ts b/packages/core/src/integrations/zoderrors.ts index 315b24592096..50c9c9e0237e 100644 --- a/packages/core/src/integrations/zoderrors.ts +++ b/packages/core/src/integrations/zoderrors.ts @@ -1,8 +1,8 @@ import { defineIntegration } from '../integration'; import type { Event, EventHint } from '../types-hoist/event'; import type { IntegrationFn } from '../types-hoist/integration'; -import { isError } from '../utils-hoist/is'; -import { truncate } from '../utils-hoist/string'; +import { isError } from '../utils/is'; +import { truncate } from '../utils/string'; interface ZodErrorsOptions { key?: string; diff --git a/packages/core/src/logs/console-integration.ts b/packages/core/src/logs/console-integration.ts index 9d64439e1786..677532c36346 100644 --- a/packages/core/src/logs/console-integration.ts +++ b/packages/core/src/logs/console-integration.ts @@ -1,13 +1,14 @@ import { getClient } from '../currentScopes'; import { DEBUG_BUILD } from '../debug-build'; +import { addConsoleInstrumentationHandler } from '../instrument/console'; 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 { addConsoleInstrumentationHandler } from '../utils-hoist/instrument/console'; -import { CONSOLE_LEVELS, logger } from '../utils-hoist/logger'; -import { safeJoin } from '../utils-hoist/string'; -import { GLOBAL_OBJ } from '../utils-hoist/worldwide'; +import { isPrimitive } from '../utils/is'; +import { CONSOLE_LEVELS, logger } from '../utils/logger'; +import { normalize } from '../utils/normalize'; +import { GLOBAL_OBJ } from '../utils/worldwide'; import { _INTERNAL_captureLog } from './exports'; interface CaptureConsoleOptions { @@ -32,7 +33,8 @@ const _consoleLoggingIntegration = ((options: Partial = { return { name: INTEGRATION_NAME, setup(client) { - if (!client.getOptions()._experiments?.enableLogs) { + const { _experiments, normalizeDepth = 3, normalizeMaxBreadth = 1_000 } = client.getOptions(); + if (!_experiments?.enableLogs) { DEBUG_BUILD && logger.warn('`_experiments.enableLogs` is not enabled, ConsoleLogs integration disabled'); return; } @@ -45,9 +47,11 @@ const _consoleLoggingIntegration = ((options: Partial = { if (level === 'assert') { if (!args[0]) { const followingArgs = args.slice(1); - const message = - followingArgs.length > 0 ? `Assertion failed: ${formatConsoleArgs(followingArgs)}` : 'Assertion failed'; - _INTERNAL_captureLog({ level: 'error', message, attributes: DEFAULT_ATTRIBUTES }); + const assertionMessage = + followingArgs.length > 0 + ? `Assertion failed: ${formatConsoleArgs(followingArgs, normalizeDepth, normalizeMaxBreadth)}` + : 'Assertion failed'; + _INTERNAL_captureLog({ level: 'error', message: assertionMessage, attributes: DEFAULT_ATTRIBUTES }); } return; } @@ -55,7 +59,7 @@ const _consoleLoggingIntegration = ((options: Partial = { const isLevelLog = level === 'log'; _INTERNAL_captureLog({ level: isLevelLog ? 'info' : level, - message: formatConsoleArgs(args), + message: formatConsoleArgs(args, normalizeDepth, normalizeMaxBreadth), severityNumber: isLevelLog ? 10 : undefined, attributes: DEFAULT_ATTRIBUTES, }); @@ -85,8 +89,16 @@ const _consoleLoggingIntegration = ((options: Partial = { */ export const consoleLoggingIntegration = defineIntegration(_consoleLoggingIntegration); -function formatConsoleArgs(values: unknown[]): string { +function formatConsoleArgs(values: unknown[], normalizeDepth: number, normalizeMaxBreadth: number): string { return 'util' in GLOBAL_OBJ && typeof (GLOBAL_OBJ as GlobalObjectWithUtil).util.format === 'function' ? (GLOBAL_OBJ as GlobalObjectWithUtil).util.format(...values) - : safeJoin(values, ' '); + : safeJoinConsoleArgs(values, normalizeDepth, normalizeMaxBreadth); +} + +function safeJoinConsoleArgs(values: unknown[], normalizeDepth: number, normalizeMaxBreadth: number): string { + return values + .map(value => + isPrimitive(value) ? String(value) : JSON.stringify(normalize(value, normalizeDepth, normalizeMaxBreadth)), + ) + .join(' '); } diff --git a/packages/core/src/logs/envelope.ts b/packages/core/src/logs/envelope.ts index c909a9140de1..c1d5b23e1575 100644 --- a/packages/core/src/logs/envelope.ts +++ b/packages/core/src/logs/envelope.ts @@ -2,8 +2,8 @@ import type { DsnComponents } from '../types-hoist/dsn'; import type { LogContainerItem, LogEnvelope } from '../types-hoist/envelope'; import type { SerializedLog } from '../types-hoist/log'; import type { SdkMetadata } from '../types-hoist/sdkmetadata'; -import { dsnToString } from '../utils-hoist/dsn'; -import { createEnvelope } from '../utils-hoist/envelope'; +import { dsnToString } from '../utils/dsn'; +import { createEnvelope } from '../utils/envelope'; /** * Creates a log container envelope item for a list of logs. diff --git a/packages/core/src/logs/exports.ts b/packages/core/src/logs/exports.ts index 176cdcd657b0..0ec4d960c66d 100644 --- a/packages/core/src/logs/exports.ts +++ b/packages/core/src/logs/exports.ts @@ -5,11 +5,11 @@ 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 { isParameterizedString } from '../utils/is'; +import { logger } from '../utils/logger'; import { _getSpanForScope } from '../utils/spanOnScope'; -import { isParameterizedString } from '../utils-hoist/is'; -import { logger } from '../utils-hoist/logger'; -import { timestampInSeconds } from '../utils-hoist/time'; -import { GLOBAL_OBJ } from '../utils-hoist/worldwide'; +import { timestampInSeconds } from '../utils/time'; +import { GLOBAL_OBJ } from '../utils/worldwide'; import { SEVERITY_TEXT_TO_SEVERITY_NUMBER } from './constants'; import { createLogEnvelope } from './envelope'; diff --git a/packages/core/src/mcp-server.ts b/packages/core/src/mcp-server.ts index bd3d83795d1c..1a6f626a83f2 100644 --- a/packages/core/src/mcp-server.ts +++ b/packages/core/src/mcp-server.ts @@ -6,8 +6,8 @@ import { } from './semanticAttributes'; import { startSpan, withActiveSpan } from './tracing'; import type { Span } from './types-hoist/span'; +import { logger } from './utils/logger'; import { getActiveSpan } from './utils/spanUtils'; -import { logger } from './utils-hoist/logger'; interface MCPTransport { // The first argument is a JSON RPC message diff --git a/packages/core/src/metadata.ts b/packages/core/src/metadata.ts index 8eec6542a7d4..c88d7a36406d 100644 --- a/packages/core/src/metadata.ts +++ b/packages/core/src/metadata.ts @@ -1,6 +1,6 @@ import type { Event } from './types-hoist/event'; import type { StackParser } from './types-hoist/stacktrace'; -import { GLOBAL_OBJ } from './utils-hoist/worldwide'; +import { GLOBAL_OBJ } from './utils/worldwide'; /** Keys are source filename/url, values are metadata objects. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/core/src/profiling.ts b/packages/core/src/profiling.ts index 8e7da5d8e946..956f37992f81 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 { logger } from './utils-hoist/logger'; +import { logger } from './utils/logger'; function isProfilingIntegrationWithProfiler( integration: ProfilingIntegration | undefined, diff --git a/packages/core/src/scope.ts b/packages/core/src/scope.ts index 414f1e38a2df..6f9269faec78 100644 --- a/packages/core/src/scope.ts +++ b/packages/core/src/scope.ts @@ -15,14 +15,14 @@ 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 { isPlainObject } from './utils/is'; +import { logger } from './utils/logger'; import { merge } from './utils/merge'; +import { uuid4 } from './utils/misc'; +import { generateTraceId } from './utils/propagationContext'; import { _getSpanForScope, _setSpanForScope } from './utils/spanOnScope'; -import { isPlainObject } from './utils-hoist/is'; -import { logger } from './utils-hoist/logger'; -import { uuid4 } from './utils-hoist/misc'; -import { generateTraceId } from './utils-hoist/propagationContext'; -import { truncate } from './utils-hoist/string'; -import { dateTimestampInSeconds } from './utils-hoist/time'; +import { truncate } from './utils/string'; +import { dateTimestampInSeconds } from './utils/time'; /** * Default value for maximum number of breadcrumbs added to an event. diff --git a/packages/core/src/sdk.ts b/packages/core/src/sdk.ts index fa3194d1ebc5..3ebbcbd4b673 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, logger } from './utils-hoist/logger'; +import { consoleSandbox, logger } from './utils/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 261c5b582c23..9f41e6142c24 100644 --- a/packages/core/src/server-runtime-client.ts +++ b/packages/core/src/server-runtime-client.ts @@ -13,11 +13,11 @@ 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 { eventFromMessage, eventFromUnknownInput } from './utils-hoist/eventbuilder'; -import { isPrimitive } from './utils-hoist/is'; -import { logger } from './utils-hoist/logger'; -import { uuid4 } from './utils-hoist/misc'; -import { resolvedSyncPromise } from './utils-hoist/syncpromise'; +import { eventFromMessage, eventFromUnknownInput } from './utils/eventbuilder'; +import { isPrimitive } from './utils/is'; +import { logger } from './utils/logger'; +import { uuid4 } from './utils/misc'; +import { resolvedSyncPromise } from './utils/syncpromise'; // TODO: Make this configurable const DEFAULT_LOG_FLUSH_INTERVAL = 5000; diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index 116a00a9adbe..492378279288 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -1,6 +1,6 @@ import type { SerializedSession, Session, SessionContext, SessionStatus } from './types-hoist/session'; -import { uuid4 } from './utils-hoist/misc'; -import { timestampInSeconds } from './utils-hoist/time'; +import { uuid4 } from './utils/misc'; +import { timestampInSeconds } from './utils/time'; /** * Creates a new `Session` object by setting certain default parameters. If optional @param context diff --git a/packages/core/src/tracing/dynamicSamplingContext.ts b/packages/core/src/tracing/dynamicSamplingContext.ts index 5f10f11db19c..adbcf0ae032a 100644 --- a/packages/core/src/tracing/dynamicSamplingContext.ts +++ b/packages/core/src/tracing/dynamicSamplingContext.ts @@ -9,14 +9,11 @@ import { } from '../semanticAttributes'; import type { DynamicSamplingContext } from '../types-hoist/envelope'; import type { Span } from '../types-hoist/span'; +import { baggageHeaderToDynamicSamplingContext, dynamicSamplingContextToSentryBaggageHeader } from '../utils/baggage'; +import { extractOrgIdFromDsnHost } from '../utils/dsn'; import { hasSpansEnabled } from '../utils/hasSpansEnabled'; +import { addNonEnumerableProperty } from '../utils/object'; import { getRootSpan, spanIsSampled, spanToJSON } from '../utils/spanUtils'; -import { - baggageHeaderToDynamicSamplingContext, - dynamicSamplingContextToSentryBaggageHeader, -} from '../utils-hoist/baggage'; -import { extractOrgIdFromDsnHost } from '../utils-hoist/dsn'; -import { addNonEnumerableProperty } from '../utils-hoist/object'; import { getCapturedScopesOnSpan } from './utils'; /** diff --git a/packages/core/src/tracing/errors.ts b/packages/core/src/tracing/errors.ts index 34d2696da52d..28f582f32309 100644 --- a/packages/core/src/tracing/errors.ts +++ b/packages/core/src/tracing/errors.ts @@ -1,8 +1,8 @@ import { DEBUG_BUILD } from '../debug-build'; +import { addGlobalErrorInstrumentationHandler } from '../instrument/globalError'; +import { addGlobalUnhandledRejectionInstrumentationHandler } from '../instrument/globalUnhandledRejection'; +import { logger } from '../utils/logger'; import { getActiveSpan, getRootSpan } from '../utils/spanUtils'; -import { addGlobalErrorInstrumentationHandler } from '../utils-hoist/instrument/globalError'; -import { addGlobalUnhandledRejectionInstrumentationHandler } from '../utils-hoist/instrument/globalUnhandledRejection'; -import { logger } from '../utils-hoist/logger'; import { SPAN_STATUS_ERROR } from './spanstatus'; let errorsInstrumented = false; diff --git a/packages/core/src/tracing/idleSpan.ts b/packages/core/src/tracing/idleSpan.ts index e5667d27a5ec..be0f1b80fb47 100644 --- a/packages/core/src/tracing/idleSpan.ts +++ b/packages/core/src/tracing/idleSpan.ts @@ -5,6 +5,7 @@ import type { DynamicSamplingContext } from '../types-hoist/envelope'; import type { Span } from '../types-hoist/span'; import type { StartSpanOptions } from '../types-hoist/startSpanOptions'; import { hasSpansEnabled } from '../utils/hasSpansEnabled'; +import { logger } from '../utils/logger'; import { _setSpanForScope } from '../utils/spanOnScope'; import { getActiveSpan, @@ -13,8 +14,7 @@ import { spanTimeInputToSeconds, spanToJSON, } from '../utils/spanUtils'; -import { logger } from '../utils-hoist/logger'; -import { timestampInSeconds } from '../utils-hoist/time'; +import { timestampInSeconds } from '../utils/time'; import { freezeDscOnSpan, getDynamicSamplingContextFromSpan } from './dynamicSamplingContext'; import { SentryNonRecordingSpan } from './sentryNonRecordingSpan'; import { SPAN_STATUS_ERROR } from './spanstatus'; diff --git a/packages/core/src/tracing/logSpans.ts b/packages/core/src/tracing/logSpans.ts index c0bcb34266ae..9728ac6f3399 100644 --- a/packages/core/src/tracing/logSpans.ts +++ b/packages/core/src/tracing/logSpans.ts @@ -1,7 +1,7 @@ import { DEBUG_BUILD } from '../debug-build'; import type { Span } from '../types-hoist/span'; +import { logger } from '../utils/logger'; import { getRootSpan, spanIsSampled, spanToJSON } from '../utils/spanUtils'; -import { logger } from '../utils-hoist/logger'; /** * Print a log message for a started span. diff --git a/packages/core/src/tracing/measurement.ts b/packages/core/src/tracing/measurement.ts index b94cc6831009..5ee0397f8946 100644 --- a/packages/core/src/tracing/measurement.ts +++ b/packages/core/src/tracing/measurement.ts @@ -5,8 +5,8 @@ import { } from '../semanticAttributes'; import type { Measurements, MeasurementUnit } from '../types-hoist/measurement'; import type { TimedEvent } from '../types-hoist/timedEvent'; +import { logger } from '../utils/logger'; import { getActiveSpan, getRootSpan } from '../utils/spanUtils'; -import { logger } from '../utils-hoist/logger'; /** * Adds a measurement to the active transaction on the current global scope. You can optionally pass in a different span diff --git a/packages/core/src/tracing/sampling.ts b/packages/core/src/tracing/sampling.ts index 098a09baa4ad..8fec768c9d52 100644 --- a/packages/core/src/tracing/sampling.ts +++ b/packages/core/src/tracing/sampling.ts @@ -2,8 +2,8 @@ import { DEBUG_BUILD } from '../debug-build'; import type { Options } from '../types-hoist/options'; import type { SamplingContext } from '../types-hoist/samplingcontext'; import { hasSpansEnabled } from '../utils/hasSpansEnabled'; +import { logger } from '../utils/logger'; import { parseSampleRate } from '../utils/parseSampleRate'; -import { logger } from '../utils-hoist/logger'; /** * Makes a sampling decision for the given options. diff --git a/packages/core/src/tracing/sentryNonRecordingSpan.ts b/packages/core/src/tracing/sentryNonRecordingSpan.ts index 9ace30c4e70a..69d1aa2a85ba 100644 --- a/packages/core/src/tracing/sentryNonRecordingSpan.ts +++ b/packages/core/src/tracing/sentryNonRecordingSpan.ts @@ -7,8 +7,8 @@ import type { SpanTimeInput, } from '../types-hoist/span'; import type { SpanStatus } from '../types-hoist/spanStatus'; +import { generateSpanId, generateTraceId } from '../utils/propagationContext'; import { TRACE_FLAG_NONE } from '../utils/spanUtils'; -import { generateSpanId, generateTraceId } from '../utils-hoist/propagationContext'; /** * A Sentry Span that is non-recording, meaning it will not be sent to Sentry. diff --git a/packages/core/src/tracing/sentrySpan.ts b/packages/core/src/tracing/sentrySpan.ts index f0e2a6bb374a..74b59c655e83 100644 --- a/packages/core/src/tracing/sentrySpan.ts +++ b/packages/core/src/tracing/sentrySpan.ts @@ -25,6 +25,8 @@ import type { import type { SpanStatus } from '../types-hoist/spanStatus'; import type { TimedEvent } from '../types-hoist/timedEvent'; import type { TransactionSource } from '../types-hoist/transaction'; +import { logger } from '../utils/logger'; +import { generateSpanId, generateTraceId } from '../utils/propagationContext'; import { convertSpanLinksForEnvelope, getRootSpan, @@ -36,9 +38,7 @@ import { TRACE_FLAG_NONE, TRACE_FLAG_SAMPLED, } from '../utils/spanUtils'; -import { logger } from '../utils-hoist/logger'; -import { generateSpanId, generateTraceId } from '../utils-hoist/propagationContext'; -import { timestampInSeconds } from '../utils-hoist/time'; +import { timestampInSeconds } from '../utils/time'; import { getDynamicSamplingContextFromSpan } from './dynamicSamplingContext'; import { logSpanEnd } from './logSpans'; import { timedEventsToMeasurements } from './measurement'; diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index 427e4ebb0bf6..7a2229090e97 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -13,12 +13,12 @@ import type { SentrySpanArguments, Span, SpanTimeInput } from '../types-hoist/sp import type { StartSpanOptions } from '../types-hoist/startSpanOptions'; import { handleCallbackErrors } from '../utils/handleCallbackErrors'; import { hasSpansEnabled } from '../utils/hasSpansEnabled'; +import { logger } from '../utils/logger'; import { parseSampleRate } from '../utils/parseSampleRate'; +import { generateTraceId } from '../utils/propagationContext'; import { _getSpanForScope, _setSpanForScope } from '../utils/spanOnScope'; import { addChildSpanToSpan, getRootSpan, spanIsSampled, spanTimeInputToSeconds, spanToJSON } from '../utils/spanUtils'; -import { logger } from '../utils-hoist/logger'; -import { generateTraceId } from '../utils-hoist/propagationContext'; -import { propagationContextFromHeaders } from '../utils-hoist/tracing'; +import { propagationContextFromHeaders } from '../utils/tracing'; import { freezeDscOnSpan, getDynamicSamplingContextFromSpan } from './dynamicSamplingContext'; import { logSpanStart } from './logSpans'; import { sampleSpan } from './sampling'; diff --git a/packages/core/src/tracing/utils.ts b/packages/core/src/tracing/utils.ts index f7e3d4924b35..5fa0a4b34420 100644 --- a/packages/core/src/tracing/utils.ts +++ b/packages/core/src/tracing/utils.ts @@ -1,6 +1,6 @@ import type { Scope } from '../scope'; import type { Span } from '../types-hoist/span'; -import { addNonEnumerableProperty } from '../utils-hoist/object'; +import { addNonEnumerableProperty } from '../utils/object'; const SCOPE_ON_START_SPAN_FIELD = '_sentryScope'; const ISOLATION_SCOPE_ON_START_SPAN_FIELD = '_sentryIsolationScope'; diff --git a/packages/core/src/transports/base.ts b/packages/core/src/transports/base.ts index d5a4f83c1657..9c74bb4261db 100644 --- a/packages/core/src/transports/base.ts +++ b/packages/core/src/transports/base.ts @@ -12,11 +12,11 @@ import { envelopeItemTypeToDataCategory, forEachEnvelopeItem, serializeEnvelope, -} from '../utils-hoist/envelope'; -import { logger } from '../utils-hoist/logger'; -import { type PromiseBuffer, makePromiseBuffer, SENTRY_BUFFER_FULL_ERROR } from '../utils-hoist/promisebuffer'; -import { type RateLimits, isRateLimited, updateRateLimits } from '../utils-hoist/ratelimit'; -import { resolvedSyncPromise } from '../utils-hoist/syncpromise'; +} from '../utils/envelope'; +import { logger } 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'; export const DEFAULT_TRANSPORT_BUFFER_SIZE = 64; diff --git a/packages/core/src/transports/multiplexed.ts b/packages/core/src/transports/multiplexed.ts index 8e93f5f81b53..1b6d53ab0ae8 100644 --- a/packages/core/src/transports/multiplexed.ts +++ b/packages/core/src/transports/multiplexed.ts @@ -2,8 +2,8 @@ import { getEnvelopeEndpointWithUrlEncodedAuth } from '../api'; import type { Envelope, EnvelopeItemType, EventItem } from '../types-hoist/envelope'; import type { Event } from '../types-hoist/event'; import type { BaseTransportOptions, Transport, TransportMakeRequestResponse } from '../types-hoist/transport'; -import { dsnFromString } from '../utils-hoist/dsn'; -import { createEnvelope, forEachEnvelopeItem } from '../utils-hoist/envelope'; +import { dsnFromString } from '../utils/dsn'; +import { createEnvelope, forEachEnvelopeItem } from '../utils/envelope'; interface MatchParam { /** The envelope to be sent */ diff --git a/packages/core/src/transports/offline.ts b/packages/core/src/transports/offline.ts index b5e584039d1a..b7713b12bb2b 100644 --- a/packages/core/src/transports/offline.ts +++ b/packages/core/src/transports/offline.ts @@ -1,9 +1,9 @@ import { DEBUG_BUILD } from '../debug-build'; import type { Envelope } from '../types-hoist/envelope'; import type { InternalBaseTransportOptions, Transport, TransportMakeRequestResponse } from '../types-hoist/transport'; -import { envelopeContainsItemType } from '../utils-hoist/envelope'; -import { logger } from '../utils-hoist/logger'; -import { parseRetryAfterHeader } from '../utils-hoist/ratelimit'; +import { envelopeContainsItemType } from '../utils/envelope'; +import { logger } from '../utils/logger'; +import { parseRetryAfterHeader } from '../utils/ratelimit'; export const MIN_DELAY = 100; // 100 ms export const START_DELAY = 5_000; // 5 seconds diff --git a/packages/core/src/trpc.ts b/packages/core/src/trpc.ts index 7e29a69903a1..611dded043b6 100644 --- a/packages/core/src/trpc.ts +++ b/packages/core/src/trpc.ts @@ -2,8 +2,8 @@ import { getClient, withIsolationScope } from './currentScopes'; import { captureException } from './exports'; import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from './semanticAttributes'; import { startSpanManual } from './tracing'; -import { normalize } from './utils-hoist/normalize'; -import { addNonEnumerableProperty } from './utils-hoist/object'; +import { normalize } from './utils/normalize'; +import { addNonEnumerableProperty } from './utils/object'; interface SentryTrpcMiddlewareOptions { /** Whether to include procedure inputs in reported events. Defaults to `false`. */ diff --git a/packages/core/src/types-hoist/context.ts b/packages/core/src/types-hoist/context.ts index 0ad6eebf6ac3..a3673f84a968 100644 --- a/packages/core/src/types-hoist/context.ts +++ b/packages/core/src/types-hoist/context.ts @@ -1,4 +1,4 @@ -import type { FeatureFlag } from '../featureFlags'; +import type { FeatureFlag } from '../utils/featureFlags'; import type { SpanLinkJSON } from './link'; import type { Primitive } from './misc'; import type { SpanOrigin } from './span'; diff --git a/packages/core/src/utils-hoist/aggregate-errors.ts b/packages/core/src/utils/aggregate-errors.ts similarity index 100% rename from packages/core/src/utils-hoist/aggregate-errors.ts rename to packages/core/src/utils/aggregate-errors.ts diff --git a/packages/core/src/utils-hoist/anr.ts b/packages/core/src/utils/anr.ts similarity index 100% rename from packages/core/src/utils-hoist/anr.ts rename to packages/core/src/utils/anr.ts diff --git a/packages/core/src/utils-hoist/baggage.ts b/packages/core/src/utils/baggage.ts similarity index 99% rename from packages/core/src/utils-hoist/baggage.ts rename to packages/core/src/utils/baggage.ts index 4a44ee5c35b0..0e90457db97b 100644 --- a/packages/core/src/utils-hoist/baggage.ts +++ b/packages/core/src/utils/baggage.ts @@ -1,5 +1,5 @@ +import { DEBUG_BUILD } from '../debug-build'; import type { DynamicSamplingContext } from '../types-hoist/envelope'; -import { DEBUG_BUILD } from './../debug-build'; import { isString } from './is'; import { logger } from './logger'; diff --git a/packages/core/src/utils-hoist/breadcrumb-log-level.ts b/packages/core/src/utils/breadcrumb-log-level.ts similarity index 100% rename from packages/core/src/utils-hoist/breadcrumb-log-level.ts rename to packages/core/src/utils/breadcrumb-log-level.ts diff --git a/packages/core/src/utils-hoist/browser.ts b/packages/core/src/utils/browser.ts similarity index 100% rename from packages/core/src/utils-hoist/browser.ts rename to packages/core/src/utils/browser.ts diff --git a/packages/core/src/utils-hoist/clientreport.ts b/packages/core/src/utils/clientreport.ts similarity index 100% rename from packages/core/src/utils-hoist/clientreport.ts rename to packages/core/src/utils/clientreport.ts diff --git a/packages/core/src/utils/debounce.ts b/packages/core/src/utils/debounce.ts new file mode 100644 index 000000000000..5f936d1e14e2 --- /dev/null +++ b/packages/core/src/utils/debounce.ts @@ -0,0 +1,76 @@ +type DebouncedCallback = { + (): void | unknown; + flush: () => void | unknown; + cancel: () => void; +}; +type CallbackFunction = () => unknown; +type DebounceOptions = { + /** The max. time in ms to wait for the callback to be invoked. */ + maxWait?: number; + /** This can be overwritten to use a different setTimeout implementation, e.g. to avoid triggering change detection in Angular */ + setTimeoutImpl?: typeof setTimeout; +}; + +/** + * Heavily simplified debounce function based on lodash.debounce. + * + * This function takes a callback function (@param fun) and delays its invocation + * by @param wait milliseconds. Optionally, a maxWait can be specified in @param options, + * which ensures that the callback is invoked at least once after the specified max. wait time. + * + * @param func the function whose invocation is to be debounced + * @param wait the minimum time until the function is invoked after it was called once + * @param options the options object, which can contain the `maxWait` property + * + * @returns the debounced version of the function, which needs to be called at least once to start the + * debouncing process. Subsequent calls will reset the debouncing timer and, in case @paramfunc + * was already invoked in the meantime, return @param func's return value. + * The debounced function has two additional properties: + * - `flush`: Invokes the debounced function immediately and returns its return value + * - `cancel`: Cancels the debouncing process and resets the debouncing timer + */ +export function debounce(func: CallbackFunction, wait: number, options?: DebounceOptions): DebouncedCallback { + let callbackReturnValue: unknown; + + let timerId: ReturnType | undefined; + let maxTimerId: ReturnType | undefined; + + const maxWait = options?.maxWait ? Math.max(options.maxWait, wait) : 0; + const setTimeoutImpl = options?.setTimeoutImpl || setTimeout; + + function invokeFunc(): unknown { + cancelTimers(); + callbackReturnValue = func(); + return callbackReturnValue; + } + + function cancelTimers(): void { + timerId !== undefined && clearTimeout(timerId); + maxTimerId !== undefined && clearTimeout(maxTimerId); + timerId = maxTimerId = undefined; + } + + function flush(): unknown { + if (timerId !== undefined || maxTimerId !== undefined) { + return invokeFunc(); + } + return callbackReturnValue; + } + + function debounced(): unknown { + if (timerId) { + clearTimeout(timerId); + } + timerId = setTimeoutImpl(invokeFunc, wait); + + if (maxWait && maxTimerId === undefined) { + maxTimerId = setTimeoutImpl(invokeFunc, maxWait); + } + + return callbackReturnValue; + } + + debounced.cancel = cancelTimers; + debounced.flush = flush; + return debounced; +} diff --git a/packages/core/src/utils-hoist/debug-ids.ts b/packages/core/src/utils/debug-ids.ts similarity index 100% rename from packages/core/src/utils-hoist/debug-ids.ts rename to packages/core/src/utils/debug-ids.ts diff --git a/packages/core/src/utils-hoist/dsn.ts b/packages/core/src/utils/dsn.ts similarity index 98% rename from packages/core/src/utils-hoist/dsn.ts rename to packages/core/src/utils/dsn.ts index e26cbb4c8bf1..b22e2baed1f8 100644 --- a/packages/core/src/utils-hoist/dsn.ts +++ b/packages/core/src/utils/dsn.ts @@ -1,5 +1,5 @@ +import { DEBUG_BUILD } from '../debug-build'; import type { DsnComponents, DsnLike, DsnProtocol } from '../types-hoist/dsn'; -import { DEBUG_BUILD } from './../debug-build'; import { consoleSandbox, logger } from './logger'; /** Regular expression used to extract org ID from a DSN host. */ diff --git a/packages/core/src/utils-hoist/env.ts b/packages/core/src/utils/env.ts similarity index 100% rename from packages/core/src/utils-hoist/env.ts rename to packages/core/src/utils/env.ts diff --git a/packages/core/src/utils-hoist/envelope.ts b/packages/core/src/utils/envelope.ts similarity index 100% rename from packages/core/src/utils-hoist/envelope.ts rename to packages/core/src/utils/envelope.ts diff --git a/packages/core/src/utils-hoist/error.ts b/packages/core/src/utils/error.ts similarity index 100% rename from packages/core/src/utils-hoist/error.ts rename to packages/core/src/utils/error.ts diff --git a/packages/core/src/utils-hoist/eventbuilder.ts b/packages/core/src/utils/eventbuilder.ts similarity index 100% rename from packages/core/src/utils-hoist/eventbuilder.ts rename to packages/core/src/utils/eventbuilder.ts diff --git a/packages/core/src/utils/featureFlags.ts b/packages/core/src/utils/featureFlags.ts new file mode 100644 index 000000000000..a055e8875c06 --- /dev/null +++ b/packages/core/src/utils/featureFlags.ts @@ -0,0 +1,152 @@ +import { getCurrentScope } from '../currentScopes'; +import { DEBUG_BUILD } from '../debug-build'; +import { type Event } from '../types-hoist/event'; +import { type Span } from '../types-hoist/span'; +import { logger } from '../utils/logger'; +import { GLOBAL_OBJ } from '../utils/worldwide'; +import { getActiveSpan } from './spanUtils'; + +/** + * Ordered LRU cache for storing feature flags in the scope context. The name + * of each flag in the buffer is unique, and the output of getAll() is ordered + * from oldest to newest. + */ + +export type FeatureFlag = { readonly flag: string; readonly result: boolean }; + +/** + * Max size of the LRU flag buffer stored in Sentry scope and event contexts. + */ +export const _INTERNAL_FLAG_BUFFER_SIZE = 100; + +/** + * Max number of flag evaluations to record per span. + */ +export const _INTERNAL_MAX_FLAGS_PER_SPAN = 10; + +// Global map of spans to feature flag buffers. Populated by feature flag integrations. +GLOBAL_OBJ._spanToFlagBufferMap = new WeakMap>(); + +const SPAN_FLAG_ATTRIBUTE_PREFIX = 'flag.evaluation.'; + +/** + * Copies feature flags that are in current scope context to the event context + */ +export function _INTERNAL_copyFlagsFromScopeToEvent(event: Event): Event { + const scope = getCurrentScope(); + const flagContext = scope.getScopeData().contexts.flags; + const flagBuffer = flagContext ? flagContext.values : []; + + if (!flagBuffer.length) { + return event; + } + + if (event.contexts === undefined) { + event.contexts = {}; + } + event.contexts.flags = { values: [...flagBuffer] }; + return event; +} + +/** + * Inserts a flag into the current scope's context while maintaining ordered LRU properties. + * Not thread-safe. After inserting: + * - The flag buffer is sorted in order of recency, with the newest evaluation at the end. + * - The names in the buffer are always unique. + * - The length of the buffer never exceeds `maxSize`. + * + * @param name Name of the feature flag to insert. + * @param value Value of the feature flag. + * @param maxSize Max number of flags the buffer should store. Default value should always be used in production. + */ +export function _INTERNAL_insertFlagToScope( + name: string, + value: unknown, + maxSize: number = _INTERNAL_FLAG_BUFFER_SIZE, +): void { + const scopeContexts = getCurrentScope().getScopeData().contexts; + if (!scopeContexts.flags) { + scopeContexts.flags = { values: [] }; + } + const flags = scopeContexts.flags.values as FeatureFlag[]; + _INTERNAL_insertToFlagBuffer(flags, name, value, maxSize); +} + +/** + * Exported for tests only. Currently only accepts boolean values (otherwise no-op). + * Inserts a flag into a FeatureFlag array while maintaining the following properties: + * - Flags are sorted in order of recency, with the newest evaluation at the end. + * - The flag names are always unique. + * - The length of the array never exceeds `maxSize`. + * + * @param flags The buffer to insert the flag into. + * @param name Name of the feature flag to insert. + * @param value Value of the feature flag. + * @param maxSize Max number of flags the buffer should store. Default value should always be used in production. + */ +export function _INTERNAL_insertToFlagBuffer( + flags: FeatureFlag[], + name: string, + value: unknown, + maxSize: number, +): void { + if (typeof value !== 'boolean') { + return; + } + + if (flags.length > maxSize) { + DEBUG_BUILD && logger.error(`[Feature Flags] insertToFlagBuffer called on a buffer larger than maxSize=${maxSize}`); + return; + } + + // Check if the flag is already in the buffer - O(n) + const index = flags.findIndex(f => f.flag === name); + + if (index !== -1) { + // The flag was found, remove it from its current position - O(n) + flags.splice(index, 1); + } + + if (flags.length === maxSize) { + // If at capacity, pop the earliest flag - O(n) + flags.shift(); + } + + // Push the flag to the end - O(1) + flags.push({ + flag: name, + result: value, + }); +} + +/** + * Records a feature flag evaluation for the active span. This is a no-op for non-boolean values. + * The flag and its value is stored in span attributes with the `flag.evaluation` prefix. Once the + * unique flags for a span reaches maxFlagsPerSpan, subsequent flags are dropped. + * + * @param name Name of the feature flag. + * @param value Value of the feature flag. Non-boolean values are ignored. + * @param maxFlagsPerSpan Max number of flags a buffer should store. Default value should always be used in production. + */ +export function _INTERNAL_addFeatureFlagToActiveSpan( + name: string, + value: unknown, + maxFlagsPerSpan: number = _INTERNAL_MAX_FLAGS_PER_SPAN, +): void { + const spanFlagMap = GLOBAL_OBJ._spanToFlagBufferMap; + if (!spanFlagMap || typeof value !== 'boolean') { + return; + } + + const span = getActiveSpan(); + if (span) { + const flags = spanFlagMap.get(span) || new Set(); + if (flags.has(name)) { + span.setAttribute(`${SPAN_FLAG_ATTRIBUTE_PREFIX}${name}`, value); + } else if (flags.size < maxFlagsPerSpan) { + flags.add(name); + span.setAttribute(`${SPAN_FLAG_ATTRIBUTE_PREFIX}${name}`, value); + } + spanFlagMap.set(span, flags); + } +} diff --git a/packages/core/src/utils/handleCallbackErrors.ts b/packages/core/src/utils/handleCallbackErrors.ts index 7e281b6b7d1c..cf4d29766445 100644 --- a/packages/core/src/utils/handleCallbackErrors.ts +++ b/packages/core/src/utils/handleCallbackErrors.ts @@ -1,4 +1,4 @@ -import { isThenable } from '../utils-hoist/is'; +import { isThenable } from '../utils/is'; /** * Wrap a callback function with error handling. diff --git a/packages/core/src/utils-hoist/is.ts b/packages/core/src/utils/is.ts similarity index 100% rename from packages/core/src/utils-hoist/is.ts rename to packages/core/src/utils/is.ts diff --git a/packages/core/src/utils-hoist/isBrowser.ts b/packages/core/src/utils/isBrowser.ts similarity index 100% rename from packages/core/src/utils-hoist/isBrowser.ts rename to packages/core/src/utils/isBrowser.ts diff --git a/packages/core/src/utils-hoist/logger.ts b/packages/core/src/utils/logger.ts similarity index 98% rename from packages/core/src/utils-hoist/logger.ts rename to packages/core/src/utils/logger.ts index 0c1e8f4d169b..669917dfbadf 100644 --- a/packages/core/src/utils-hoist/logger.ts +++ b/packages/core/src/utils/logger.ts @@ -1,6 +1,6 @@ import { getGlobalSingleton } from '../carrier'; +import { DEBUG_BUILD } from '../debug-build'; import type { ConsoleLevel } from '../types-hoist/instrument'; -import { DEBUG_BUILD } from './../debug-build'; import { GLOBAL_OBJ } from './worldwide'; /** Prefix for logging strings */ diff --git a/packages/core/src/utils-hoist/lru.ts b/packages/core/src/utils/lru.ts similarity index 100% rename from packages/core/src/utils-hoist/lru.ts rename to packages/core/src/utils/lru.ts diff --git a/packages/core/src/utils-hoist/misc.ts b/packages/core/src/utils/misc.ts similarity index 100% rename from packages/core/src/utils-hoist/misc.ts rename to packages/core/src/utils/misc.ts diff --git a/packages/core/src/utils-hoist/node-stack-trace.ts b/packages/core/src/utils/node-stack-trace.ts similarity index 100% rename from packages/core/src/utils-hoist/node-stack-trace.ts rename to packages/core/src/utils/node-stack-trace.ts diff --git a/packages/core/src/utils-hoist/node.ts b/packages/core/src/utils/node.ts similarity index 100% rename from packages/core/src/utils-hoist/node.ts rename to packages/core/src/utils/node.ts diff --git a/packages/core/src/utils-hoist/normalize.ts b/packages/core/src/utils/normalize.ts similarity index 100% rename from packages/core/src/utils-hoist/normalize.ts rename to packages/core/src/utils/normalize.ts diff --git a/packages/core/src/utils-hoist/object.ts b/packages/core/src/utils/object.ts similarity index 99% rename from packages/core/src/utils-hoist/object.ts rename to packages/core/src/utils/object.ts index 366e3f2c5e98..c973ad056dee 100644 --- a/packages/core/src/utils-hoist/object.ts +++ b/packages/core/src/utils/object.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import { DEBUG_BUILD } from '../debug-build'; import type { WrappedFunction } from '../types-hoist/wrappedfunction'; -import { DEBUG_BUILD } from './../debug-build'; import { htmlTreeAsString } from './browser'; import { isElement, isError, isEvent, isInstanceOf, isPrimitive } from './is'; import { logger } from './logger'; diff --git a/packages/core/src/utils-hoist/path.ts b/packages/core/src/utils/path.ts similarity index 100% rename from packages/core/src/utils-hoist/path.ts rename to packages/core/src/utils/path.ts diff --git a/packages/core/src/utils/prepareEvent.ts b/packages/core/src/utils/prepareEvent.ts index 30ed787d4879..6bff4e8b3d50 100644 --- a/packages/core/src/utils/prepareEvent.ts +++ b/packages/core/src/utils/prepareEvent.ts @@ -7,12 +7,12 @@ import { Scope } from '../scope'; import type { Event, EventHint } from '../types-hoist/event'; import type { ClientOptions } from '../types-hoist/options'; import type { StackParser } from '../types-hoist/stacktrace'; -import { getFilenameToDebugIdMap } from '../utils-hoist/debug-ids'; -import { addExceptionMechanism, uuid4 } from '../utils-hoist/misc'; -import { normalize } from '../utils-hoist/normalize'; -import { truncate } from '../utils-hoist/string'; -import { dateTimestampInSeconds } from '../utils-hoist/time'; import { applyScopeDataToEvent, mergeScopeData } from './applyScopeDataToEvent'; +import { getFilenameToDebugIdMap } from './debug-ids'; +import { addExceptionMechanism, uuid4 } from './misc'; +import { normalize } from './normalize'; +import { truncate } from './string'; +import { dateTimestampInSeconds } from './time'; /** * This type makes sure that we get either a CaptureContext, OR an EventHint. diff --git a/packages/core/src/utils-hoist/promisebuffer.ts b/packages/core/src/utils/promisebuffer.ts similarity index 100% rename from packages/core/src/utils-hoist/promisebuffer.ts rename to packages/core/src/utils/promisebuffer.ts diff --git a/packages/core/src/utils-hoist/propagationContext.ts b/packages/core/src/utils/propagationContext.ts similarity index 100% rename from packages/core/src/utils-hoist/propagationContext.ts rename to packages/core/src/utils/propagationContext.ts diff --git a/packages/core/src/utils-hoist/ratelimit.ts b/packages/core/src/utils/ratelimit.ts similarity index 100% rename from packages/core/src/utils-hoist/ratelimit.ts rename to packages/core/src/utils/ratelimit.ts diff --git a/packages/core/src/utils/sdkMetadata.ts b/packages/core/src/utils/sdkMetadata.ts index 9235efbe8c0c..437668a9e7f4 100644 --- a/packages/core/src/utils/sdkMetadata.ts +++ b/packages/core/src/utils/sdkMetadata.ts @@ -1,5 +1,5 @@ import type { Options } from '../types-hoist/options'; -import { SDK_VERSION } from '../utils-hoist/version'; +import { SDK_VERSION } from '../utils/version'; /** * A builder for the SDK metadata in the options for the SDK initialization. diff --git a/packages/core/src/utils-hoist/severity.ts b/packages/core/src/utils/severity.ts similarity index 100% rename from packages/core/src/utils-hoist/severity.ts rename to packages/core/src/utils/severity.ts diff --git a/packages/core/src/utils/spanOnScope.ts b/packages/core/src/utils/spanOnScope.ts index 29eec0a2574b..ad18e1687746 100644 --- a/packages/core/src/utils/spanOnScope.ts +++ b/packages/core/src/utils/spanOnScope.ts @@ -1,6 +1,6 @@ import type { Scope } from '../scope'; import type { Span } from '../types-hoist/span'; -import { addNonEnumerableProperty } from '../utils-hoist/object'; +import { addNonEnumerableProperty } from '../utils/object'; const SCOPE_SPAN_FIELD = '_sentrySpan'; diff --git a/packages/core/src/utils/spanUtils.ts b/packages/core/src/utils/spanUtils.ts index 8018a62a20d4..361be95b6510 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-hoist/logger'; -import { addNonEnumerableProperty } from '../utils-hoist/object'; -import { generateSpanId } from '../utils-hoist/propagationContext'; -import { timestampInSeconds } from '../utils-hoist/time'; -import { generateSentryTraceHeader } from '../utils-hoist/tracing'; +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 { _getSpanForScope } from './spanOnScope'; // These are aligned with OpenTelemetry trace flags diff --git a/packages/core/src/utils-hoist/stacktrace.ts b/packages/core/src/utils/stacktrace.ts similarity index 100% rename from packages/core/src/utils-hoist/stacktrace.ts rename to packages/core/src/utils/stacktrace.ts diff --git a/packages/core/src/utils-hoist/string.ts b/packages/core/src/utils/string.ts similarity index 98% rename from packages/core/src/utils-hoist/string.ts rename to packages/core/src/utils/string.ts index 4771023fa2f5..ab98c794f681 100644 --- a/packages/core/src/utils-hoist/string.ts +++ b/packages/core/src/utils/string.ts @@ -1,6 +1,6 @@ import { isRegExp, isString, isVueViewModel } from './is'; -export { escapeStringForRegex } from './vendor/escapeStringForRegex'; +export { escapeStringForRegex } from '../vendor/escapeStringForRegex'; /** * Truncates given string to the maximum characters count diff --git a/packages/core/src/utils-hoist/supports.ts b/packages/core/src/utils/supports.ts similarity index 98% rename from packages/core/src/utils-hoist/supports.ts rename to packages/core/src/utils/supports.ts index 9cb6a71d8058..b5c96a7e50d8 100644 --- a/packages/core/src/utils-hoist/supports.ts +++ b/packages/core/src/utils/supports.ts @@ -1,4 +1,4 @@ -import { DEBUG_BUILD } from './../debug-build'; +import { DEBUG_BUILD } from '../debug-build'; import { logger } from './logger'; import { GLOBAL_OBJ } from './worldwide'; diff --git a/packages/core/src/utils-hoist/syncpromise.ts b/packages/core/src/utils/syncpromise.ts similarity index 100% rename from packages/core/src/utils-hoist/syncpromise.ts rename to packages/core/src/utils/syncpromise.ts diff --git a/packages/core/src/utils-hoist/time.ts b/packages/core/src/utils/time.ts similarity index 100% rename from packages/core/src/utils-hoist/time.ts rename to packages/core/src/utils/time.ts diff --git a/packages/core/src/utils/traceData.ts b/packages/core/src/utils/traceData.ts index e9d284e5a2c5..e65271cf5a23 100644 --- a/packages/core/src/utils/traceData.ts +++ b/packages/core/src/utils/traceData.ts @@ -1,15 +1,16 @@ import { getAsyncContextStrategy } from '../asyncContext'; import { getMainCarrier } from '../carrier'; +import type { Client } from '../client'; import { getClient, getCurrentScope } from '../currentScopes'; import { isEnabled } from '../exports'; import type { Scope } from '../scope'; import { getDynamicSamplingContextFromScope, getDynamicSamplingContextFromSpan } from '../tracing'; import type { Span } from '../types-hoist/span'; import type { SerializedTraceData } from '../types-hoist/tracing'; -import { dynamicSamplingContextToSentryBaggageHeader } from '../utils-hoist/baggage'; -import { logger } from '../utils-hoist/logger'; -import { generateSentryTraceHeader, TRACEPARENT_REGEXP } from '../utils-hoist/tracing'; +import { dynamicSamplingContextToSentryBaggageHeader } from './baggage'; +import { logger } from './logger'; import { getActiveSpan, spanToTraceHeader } from './spanUtils'; +import { generateSentryTraceHeader, TRACEPARENT_REGEXP } from './tracing'; /** * Extracts trace propagation data from the current span or from the client's scope (via transaction or propagation @@ -22,8 +23,8 @@ import { getActiveSpan, spanToTraceHeader } from './spanUtils'; * @returns an object with the tracing data values. The object keys are the name of the tracing key to be used as header * or meta tag name. */ -export function getTraceData(options: { span?: Span } = {}): SerializedTraceData { - const client = getClient(); +export function getTraceData(options: { span?: Span; scope?: Scope; client?: Client } = {}): SerializedTraceData { + const client = options.client || getClient(); if (!isEnabled() || !client) { return {}; } @@ -34,7 +35,7 @@ export function getTraceData(options: { span?: Span } = {}): SerializedTraceData return acs.getTraceData(options); } - const scope = getCurrentScope(); + const scope = options.scope || getCurrentScope(); const span = options.span || getActiveSpan(); const sentryTrace = span ? spanToTraceHeader(span) : scopeToTraceHeader(scope); const dsc = span ? getDynamicSamplingContextFromSpan(span) : getDynamicSamplingContextFromScope(client, scope); diff --git a/packages/core/src/utils-hoist/tracing.ts b/packages/core/src/utils/tracing.ts similarity index 98% rename from packages/core/src/utils-hoist/tracing.ts rename to packages/core/src/utils/tracing.ts index a3add28426f7..fe299d2d6338 100644 --- a/packages/core/src/utils-hoist/tracing.ts +++ b/packages/core/src/utils/tracing.ts @@ -1,8 +1,8 @@ import type { DynamicSamplingContext } from '../types-hoist/envelope'; import type { PropagationContext } from '../types-hoist/tracing'; import type { TraceparentData } from '../types-hoist/transaction'; -import { parseSampleRate } from '../utils/parseSampleRate'; import { baggageHeaderToDynamicSamplingContext } from './baggage'; +import { parseSampleRate } from './parseSampleRate'; import { generateSpanId, generateTraceId } from './propagationContext'; // eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- RegExp is used for readability here diff --git a/packages/core/src/utils-hoist/url.ts b/packages/core/src/utils/url.ts similarity index 100% rename from packages/core/src/utils-hoist/url.ts rename to packages/core/src/utils/url.ts diff --git a/packages/core/src/utils-hoist/vercelWaitUntil.ts b/packages/core/src/utils/vercelWaitUntil.ts similarity index 100% rename from packages/core/src/utils-hoist/vercelWaitUntil.ts rename to packages/core/src/utils/vercelWaitUntil.ts diff --git a/packages/core/src/utils-hoist/version.ts b/packages/core/src/utils/version.ts similarity index 100% rename from packages/core/src/utils-hoist/version.ts rename to packages/core/src/utils/version.ts diff --git a/packages/core/src/utils-hoist/worldwide.ts b/packages/core/src/utils/worldwide.ts similarity index 91% rename from packages/core/src/utils-hoist/worldwide.ts rename to packages/core/src/utils/worldwide.ts index 3a396d96f809..70196e4b0c8b 100644 --- a/packages/core/src/utils-hoist/worldwide.ts +++ b/packages/core/src/utils/worldwide.ts @@ -15,6 +15,7 @@ import type { Carrier } from '../carrier'; import type { Client } from '../client'; import type { SerializedLog } from '../types-hoist/log'; +import type { Span } from '../types-hoist/span'; import type { SdkSource } from './env'; /** Internal global with common properties and Sentry extensions */ @@ -56,6 +57,10 @@ export type InternalGlobal = { */ _sentryModuleMetadata?: Record; _sentryEsmLoaderHookRegistered?: boolean; + /** + * A map of spans to evaluated feature flags. Populated by feature flag integrations. + */ + _spanToFlagBufferMap?: WeakMap>; } & Carrier; /** Get's the global object for the current JavaScript runtime */ diff --git a/packages/core/src/utils-hoist/vendor/escapeStringForRegex.ts b/packages/core/src/vendor/escapeStringForRegex.ts similarity index 100% rename from packages/core/src/utils-hoist/vendor/escapeStringForRegex.ts rename to packages/core/src/vendor/escapeStringForRegex.ts diff --git a/packages/core/test/clear-global-scope.ts b/packages/core/test/clear-global-scope.ts new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/core/test/lib/api.test.ts b/packages/core/test/lib/api.test.ts index 647095a98842..be7dd70dd068 100644 --- a/packages/core/test/lib/api.test.ts +++ b/packages/core/test/lib/api.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it, test } from 'vitest'; import { getEnvelopeEndpointWithUrlEncodedAuth, getReportDialogEndpoint } from '../../src/api'; import type { DsnComponents } from '../../src/types-hoist/dsn'; import type { SdkInfo } from '../../src/types-hoist/sdkinfo'; -import { makeDsn } from '../../src/utils-hoist/dsn'; +import { makeDsn } from '../../src/utils/dsn'; const ingestDsn = 'https://abc@xxxx.ingest.sentry.io:1234/subpath/123'; const dsnPublic = 'https://abc@sentry.io:1234/subpath/123'; diff --git a/packages/core/test/lib/attachments.test.ts b/packages/core/test/lib/attachments.test.ts index eab0927068fe..edab989af717 100644 --- a/packages/core/test/lib/attachments.test.ts +++ b/packages/core/test/lib/attachments.test.ts @@ -1,6 +1,6 @@ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; import { createTransport } from '../../src/transports/base'; -import { parseEnvelope } from '../../src/utils-hoist/envelope'; +import { parseEnvelope } from '../../src/utils/envelope'; import { getDefaultTestClientOptions, TestClient } from '../mocks/client'; describe('Attachments', () => { diff --git a/packages/core/test/lib/carrier.test.ts b/packages/core/test/lib/carrier.test.ts index 144355dc1d3b..792121310a71 100644 --- a/packages/core/test/lib/carrier.test.ts +++ b/packages/core/test/lib/carrier.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; import { getSentryCarrier } from '../../src/carrier'; -import { SDK_VERSION } from '../../src/utils-hoist/version'; +import { SDK_VERSION } from '../../src/utils/version'; describe('getSentryCarrier', () => { describe('base case (one SDK)', () => { diff --git a/packages/core/test/lib/clear-global-scope.ts b/packages/core/test/lib/clear-global-scope.ts deleted file mode 100644 index 8fa0340a22bb..000000000000 --- a/packages/core/test/lib/clear-global-scope.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { getSentryCarrier } from '../../src/carrier'; -import { GLOBAL_OBJ } from '../../src/utils-hoist/worldwide'; - -export function clearGlobalScope() { - const carrier = getSentryCarrier(GLOBAL_OBJ); - carrier.globalScope = undefined; -} diff --git a/packages/core/test/lib/client.test.ts b/packages/core/test/lib/client.test.ts index 07ce87fca3e2..844b49e4b55f 100644 --- a/packages/core/test/lib/client.test.ts +++ b/packages/core/test/lib/client.test.ts @@ -16,14 +16,14 @@ 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-hoist/logger'; -import * as miscModule from '../../src/utils-hoist/misc'; -import * as stringModule from '../../src/utils-hoist/string'; -import * as timeModule from '../../src/utils-hoist/time'; +import * as loggerModule from '../../src/utils/logger'; +import * as miscModule from '../../src/utils/misc'; +import * as stringModule from '../../src/utils/string'; +import * as timeModule from '../../src/utils/time'; import { getDefaultTestClientOptions, TestClient } from '../mocks/client'; import { AdHocIntegration, TestIntegration } from '../mocks/integration'; import { makeFakeTransport } from '../mocks/transport'; -import { clearGlobalScope } from './clear-global-scope'; +import { clearGlobalScope } from '../testutils'; const PUBLIC_DSN = 'https://username@domain/123'; // eslint-disable-next-line no-var diff --git a/packages/core/test/utils-hoist/instrument/fetch.test.ts b/packages/core/test/lib/instrument/fetch.test.ts similarity index 93% rename from packages/core/test/utils-hoist/instrument/fetch.test.ts rename to packages/core/test/lib/instrument/fetch.test.ts index 0f08d9f03c6e..88d780a7dbad 100644 --- a/packages/core/test/utils-hoist/instrument/fetch.test.ts +++ b/packages/core/test/lib/instrument/fetch.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { parseFetchArgs } from '../../../src/utils-hoist/instrument/fetch'; +import { parseFetchArgs } from '../../../src/instrument/fetch'; describe('instrument > parseFetchArgs', () => { it.each([ diff --git a/packages/core/test/utils-hoist/instrument.test.ts b/packages/core/test/lib/instrument/handlers.test.ts similarity index 81% rename from packages/core/test/utils-hoist/instrument.test.ts rename to packages/core/test/lib/instrument/handlers.test.ts index b6917994c96b..87e227a99323 100644 --- a/packages/core/test/utils-hoist/instrument.test.ts +++ b/packages/core/test/lib/instrument/handlers.test.ts @@ -1,5 +1,5 @@ import { describe, test } from 'vitest'; -import { maybeInstrument } from '../../src/utils-hoist/instrument/handlers'; +import { maybeInstrument } from '../../../src/instrument/handlers'; describe('maybeInstrument', () => { test('does not throw when instrumenting fails', () => { diff --git a/packages/core/test/lib/integration.test.ts b/packages/core/test/lib/integration.test.ts index 3e4c26e0b959..58fabde15ef7 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 { logger } from '../../src/utils-hoist/logger'; +import { logger } from '../../src/utils/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 8c4e1e55ccce..faabc2590aac 100644 --- a/packages/core/test/lib/integrations/captureconsole.test.ts +++ b/packages/core/test/lib/integrations/captureconsole.test.ts @@ -4,13 +4,13 @@ import { type Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vite import type { Client } from '../../../src'; import * as CurrentScopes from '../../../src/currentScopes'; import * as SentryCore from '../../../src/exports'; +import { addConsoleInstrumentationHandler } from '../../../src/instrument/console'; +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 { addConsoleInstrumentationHandler } from '../../../src/utils-hoist/instrument/console'; -import { resetInstrumentationHandlers } from '../../../src/utils-hoist/instrument/handlers'; -import { CONSOLE_LEVELS, originalConsoleMethods } from '../../../src/utils-hoist/logger'; -import { GLOBAL_OBJ } from '../../../src/utils-hoist/worldwide'; +import { CONSOLE_LEVELS, originalConsoleMethods } from '../../../src/utils/logger'; +import { GLOBAL_OBJ } from '../../../src/utils/worldwide'; const mockConsole: { [key in ConsoleLevel]: Mock } = { debug: vi.fn(), diff --git a/packages/core/test/lib/integrations/third-party-errors-filter.test.ts b/packages/core/test/lib/integrations/third-party-errors-filter.test.ts index b9b705cc5cac..d68dbaeb5b56 100644 --- a/packages/core/test/lib/integrations/third-party-errors-filter.test.ts +++ b/packages/core/test/lib/integrations/third-party-errors-filter.test.ts @@ -3,9 +3,9 @@ import type { Client } from '../../../src/client'; import { thirdPartyErrorFilterIntegration } from '../../../src/integrations/third-party-errors-filter'; import { addMetadataToStackFrames } from '../../../src/metadata'; import type { Event } from '../../../src/types-hoist/event'; -import { nodeStackLineParser } from '../../../src/utils-hoist/node-stack-trace'; -import { createStackParser } from '../../../src/utils-hoist/stacktrace'; -import { GLOBAL_OBJ } from '../../../src/utils-hoist/worldwide'; +import { nodeStackLineParser } from '../../../src/utils/node-stack-trace'; +import { createStackParser } from '../../../src/utils/stacktrace'; +import { GLOBAL_OBJ } from '../../../src/utils/worldwide'; function clone(data: T): T { return JSON.parse(JSON.stringify(data)); diff --git a/packages/core/test/lib/logs/envelope.test.ts b/packages/core/test/lib/logs/envelope.test.ts index cd765cf018bc..7fbe1a439910 100644 --- a/packages/core/test/lib/logs/envelope.test.ts +++ b/packages/core/test/lib/logs/envelope.test.ts @@ -3,14 +3,14 @@ import { createLogContainerEnvelopeItem, createLogEnvelope } from '../../../src/ import type { DsnComponents } from '../../../src/types-hoist/dsn'; import type { SerializedLog } from '../../../src/types-hoist/log'; import type { SdkMetadata } from '../../../src/types-hoist/sdkmetadata'; -import * as utilsDsn from '../../../src/utils-hoist/dsn'; -import * as utilsEnvelope from '../../../src/utils-hoist/envelope'; +import * as utilsDsn from '../../../src/utils/dsn'; +import * as utilsEnvelope from '../../../src/utils/envelope'; -// Mock utils-hoist functions -vi.mock('../../../src/utils-hoist/dsn', () => ({ +// Mock utils functions +vi.mock('../../../src/utils/dsn', () => ({ dsnToString: vi.fn(dsn => `https://${dsn.publicKey}@${dsn.host}/`), })); -vi.mock('../../../src/utils-hoist/envelope', () => ({ +vi.mock('../../../src/utils/envelope', () => ({ createEnvelope: vi.fn((_headers, items) => [_headers, items]), })); diff --git a/packages/core/test/lib/logs/exports.test.ts b/packages/core/test/lib/logs/exports.test.ts index 3ba9f59b50d3..0dd73f064619 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-hoist/logger'; +import * as loggerModule from '../../../src/utils/logger'; import { getDefaultTestClientOptions, TestClient } from '../../mocks/client'; const PUBLIC_DSN = 'https://username@domain/123'; diff --git a/packages/core/test/lib/metadata.test.ts b/packages/core/test/lib/metadata.test.ts index 87ffcc5284e0..bedf4cdcf7e9 100644 --- a/packages/core/test/lib/metadata.test.ts +++ b/packages/core/test/lib/metadata.test.ts @@ -1,9 +1,9 @@ import { beforeEach, describe, expect, it } from 'vitest'; import { addMetadataToStackFrames, getMetadataForUrl, stripMetadataFromStackFrames } from '../../src/metadata'; import type { Event } from '../../src/types-hoist/event'; -import { nodeStackLineParser } from '../../src/utils-hoist/node-stack-trace'; -import { createStackParser } from '../../src/utils-hoist/stacktrace'; -import { GLOBAL_OBJ } from '../../src/utils-hoist/worldwide'; +import { nodeStackLineParser } from '../../src/utils/node-stack-trace'; +import { createStackParser } from '../../src/utils/stacktrace'; +import { GLOBAL_OBJ } from '../../src/utils/worldwide'; const parser = createStackParser(nodeStackLineParser()); diff --git a/packages/core/test/lib/prepareEvent.test.ts b/packages/core/test/lib/prepareEvent.test.ts index 42279371dc30..d0fd86ae63f8 100644 --- a/packages/core/test/lib/prepareEvent.test.ts +++ b/packages/core/test/lib/prepareEvent.test.ts @@ -14,7 +14,7 @@ import { parseEventHintOrCaptureContext, prepareEvent, } from '../../src/utils/prepareEvent'; -import { clearGlobalScope } from './clear-global-scope'; +import { clearGlobalScope } from '../testutils'; describe('applyDebugIds', () => { afterEach(() => { diff --git a/packages/core/test/lib/scope.test.ts b/packages/core/test/lib/scope.test.ts index bac55994c199..280ba4c651ff 100644 --- a/packages/core/test/lib/scope.test.ts +++ b/packages/core/test/lib/scope.test.ts @@ -12,7 +12,7 @@ import type { Breadcrumb } from '../../src/types-hoist/breadcrumb'; import type { Event } from '../../src/types-hoist/event'; import { applyScopeDataToEvent } from '../../src/utils/applyScopeDataToEvent'; import { getDefaultTestClientOptions, TestClient } from '../mocks/client'; -import { clearGlobalScope } from './clear-global-scope'; +import { clearGlobalScope } from '../testutils'; describe('Scope', () => { beforeEach(() => { diff --git a/packages/core/test/lib/session.test.ts b/packages/core/test/lib/session.test.ts index 6744b2107e7b..9d6cfe06e426 100644 --- a/packages/core/test/lib/session.test.ts +++ b/packages/core/test/lib/session.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, test } from 'vitest'; import { closeSession, makeSession, updateSession } from '../../src/session'; import type { SessionContext } from '../../src/types-hoist/session'; -import { timestampInSeconds } from '../../src/utils-hoist/time'; +import { timestampInSeconds } from '../../src/utils/time'; describe('Session', () => { it('initializes with the proper defaults', () => { diff --git a/packages/core/test/lib/tracing/errors.test.ts b/packages/core/test/lib/tracing/errors.test.ts index cadc8b79219b..128ee9180d54 100644 --- a/packages/core/test/lib/tracing/errors.test.ts +++ b/packages/core/test/lib/tracing/errors.test.ts @@ -1,9 +1,9 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { setCurrentClient, spanToJSON, startInactiveSpan, startSpan } from '../../../src'; +import * as globalErrorModule from '../../../src/instrument/globalError'; +import * as globalUnhandledRejectionModule from '../../../src/instrument/globalUnhandledRejection'; import { _resetErrorsInstrumented, registerSpanErrorInstrumentation } from '../../../src/tracing/errors'; import type { HandlerDataError, HandlerDataUnhandledRejection } from '../../../src/types-hoist/instrument'; -import * as globalErrorModule from '../../../src/utils-hoist/instrument/globalError'; -import * as globalUnhandledRejectionModule from '../../../src/utils-hoist/instrument/globalUnhandledRejection'; import { getDefaultTestClientOptions, TestClient } from '../../mocks/client'; let mockErrorCallback: (data: HandlerDataError) => void = () => {}; diff --git a/packages/core/test/lib/tracing/sentrySpan.test.ts b/packages/core/test/lib/tracing/sentrySpan.test.ts index a63e161e5ada..601e25be0d23 100644 --- a/packages/core/test/lib/tracing/sentrySpan.test.ts +++ b/packages/core/test/lib/tracing/sentrySpan.test.ts @@ -6,7 +6,7 @@ import { SentrySpan } from '../../../src/tracing/sentrySpan'; import { SPAN_STATUS_ERROR } from '../../../src/tracing/spanstatus'; import type { SpanJSON } from '../../../src/types-hoist/span'; import { spanToJSON, TRACE_FLAG_NONE, TRACE_FLAG_SAMPLED } from '../../../src/utils/spanUtils'; -import { timestampInSeconds } from '../../../src/utils-hoist/time'; +import { timestampInSeconds } from '../../../src/utils/time'; import { getDefaultTestClientOptions, TestClient } from '../../mocks/client'; describe('SentrySpan', () => { diff --git a/packages/core/test/lib/transports/base.test.ts b/packages/core/test/lib/transports/base.test.ts index af3655d89972..ef2220ac1f8b 100644 --- a/packages/core/test/lib/transports/base.test.ts +++ b/packages/core/test/lib/transports/base.test.ts @@ -2,9 +2,9 @@ import { describe, expect, it, vi } from 'vitest'; import { createTransport } from '../../../src/transports/base'; import type { AttachmentItem, EventEnvelope, EventItem } from '../../../src/types-hoist/envelope'; import type { TransportMakeRequestResponse } from '../../../src/types-hoist/transport'; -import { createEnvelope, serializeEnvelope } from '../../../src/utils-hoist/envelope'; -import type { PromiseBuffer } from '../../../src/utils-hoist/promisebuffer'; -import { resolvedSyncPromise } from '../../../src/utils-hoist/syncpromise'; +import { createEnvelope, serializeEnvelope } from '../../../src/utils/envelope'; +import type { PromiseBuffer } from '../../../src/utils/promisebuffer'; +import { resolvedSyncPromise } from '../../../src/utils/syncpromise'; const ERROR_ENVELOPE = createEnvelope({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' }, [ [{ type: 'event' }, { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' }] as EventItem, diff --git a/packages/core/test/utils-hoist/aggregate-errors.test.ts b/packages/core/test/lib/utils/aggregate-errors.test.ts similarity index 95% rename from packages/core/test/utils-hoist/aggregate-errors.test.ts rename to packages/core/test/lib/utils/aggregate-errors.test.ts index 007d81678082..01ede6d9186a 100644 --- a/packages/core/test/utils-hoist/aggregate-errors.test.ts +++ b/packages/core/test/lib/utils/aggregate-errors.test.ts @@ -1,10 +1,10 @@ import { describe, expect, test } from 'vitest'; -import type { ExtendedError } from '../../src/types-hoist/error'; -import type { Event, EventHint } from '../../src/types-hoist/event'; -import type { Exception } from '../../src/types-hoist/exception'; -import type { StackParser } from '../../src/types-hoist/stacktrace'; -import { applyAggregateErrorsToEvent } from '../../src/utils-hoist/aggregate-errors'; -import { createStackParser } from '../../src/utils-hoist/stacktrace'; +import type { ExtendedError } from '../../../src/types-hoist/error'; +import type { Event, EventHint } from '../../../src/types-hoist/event'; +import type { Exception } from '../../../src/types-hoist/exception'; +import type { StackParser } from '../../../src/types-hoist/stacktrace'; +import { applyAggregateErrorsToEvent } from '../../../src/utils/aggregate-errors'; +import { createStackParser } from '../../../src/utils/stacktrace'; const stackParser = createStackParser([0, line => ({ filename: line })]); const exceptionFromError = (_stackParser: StackParser, ex: Error): Exception => { diff --git a/packages/core/test/utils-hoist/baggage.test.ts b/packages/core/test/lib/utils/baggage.test.ts similarity index 98% rename from packages/core/test/utils-hoist/baggage.test.ts rename to packages/core/test/lib/utils/baggage.test.ts index c05ac0d5dd96..4816a3fbf079 100644 --- a/packages/core/test/utils-hoist/baggage.test.ts +++ b/packages/core/test/lib/utils/baggage.test.ts @@ -3,7 +3,7 @@ import { baggageHeaderToDynamicSamplingContext, dynamicSamplingContextToSentryBaggageHeader, parseBaggageHeader, -} from '../../src/utils-hoist/baggage'; +} from '../../../src/utils/baggage'; test.each([ ['', undefined], diff --git a/packages/core/test/utils-hoist/breadcrumb-log-level.test.ts b/packages/core/test/lib/utils/breadcrumb-log-level.test.ts similarity index 82% rename from packages/core/test/utils-hoist/breadcrumb-log-level.test.ts rename to packages/core/test/lib/utils/breadcrumb-log-level.test.ts index 86062e025f37..506567c72b8b 100644 --- a/packages/core/test/utils-hoist/breadcrumb-log-level.test.ts +++ b/packages/core/test/lib/utils/breadcrumb-log-level.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { getBreadcrumbLogLevelFromHttpStatusCode } from '../../src/utils-hoist/breadcrumb-log-level'; +import { getBreadcrumbLogLevelFromHttpStatusCode } from '../../../src/utils/breadcrumb-log-level'; describe('getBreadcrumbLogLevelFromHttpStatusCode()', () => { it.each([ diff --git a/packages/core/test/utils-hoist/browser.test.ts b/packages/core/test/lib/utils/browser.test.ts similarity index 97% rename from packages/core/test/utils-hoist/browser.test.ts rename to packages/core/test/lib/utils/browser.test.ts index d7fc8aab022c..0bcf71884482 100644 --- a/packages/core/test/utils-hoist/browser.test.ts +++ b/packages/core/test/lib/utils/browser.test.ts @@ -1,6 +1,6 @@ import { JSDOM } from 'jsdom'; import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; -import { htmlTreeAsString } from '../../src/utils-hoist/browser'; +import { htmlTreeAsString } from '../../../src/utils/browser'; beforeAll(() => { const dom = new JSDOM(); diff --git a/packages/core/test/utils-hoist/clientreport.test.ts b/packages/core/test/lib/utils/clientreport.test.ts similarity index 88% rename from packages/core/test/utils-hoist/clientreport.test.ts rename to packages/core/test/lib/utils/clientreport.test.ts index 49f6a67b35cb..8acc9c9d8dc0 100644 --- a/packages/core/test/utils-hoist/clientreport.test.ts +++ b/packages/core/test/lib/utils/clientreport.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest'; -import type { ClientReport } from '../../src/types-hoist/clientreport'; -import { createClientReportEnvelope } from '../../src/utils-hoist/clientreport'; -import { parseEnvelope, serializeEnvelope } from '../../src/utils-hoist/envelope'; +import type { ClientReport } from '../../../src/types-hoist/clientreport'; +import { createClientReportEnvelope } from '../../../src/utils/clientreport'; +import { parseEnvelope, serializeEnvelope } from '../../../src/utils/envelope'; const DEFAULT_DISCARDED_EVENTS: ClientReport['discarded_events'] = [ { diff --git a/packages/core/test/lib/utils/debounce.test.ts b/packages/core/test/lib/utils/debounce.test.ts new file mode 100644 index 000000000000..d44371e94b49 --- /dev/null +++ b/packages/core/test/lib/utils/debounce.test.ts @@ -0,0 +1,276 @@ +import { beforeAll, describe, expect, it, vi } from 'vitest'; +import { debounce } from '../../../src/utils/debounce'; + +describe('Unit | util | debounce', () => { + beforeAll(() => { + vi.useFakeTimers(); + }); + + it('delay the execution of the passed callback function by the passed minDelay', () => { + const callback = vi.fn(); + const debouncedCallback = debounce(callback, 100); + debouncedCallback(); + expect(callback).not.toHaveBeenCalled(); + + vi.advanceTimersByTime(99); + expect(callback).not.toHaveBeenCalled(); + + vi.advanceTimersByTime(1); + expect(callback).toHaveBeenCalled(); + }); + + it('should invoke the callback at latest by maxWait, if the option is specified', () => { + const callback = vi.fn(); + const debouncedCallback = debounce(callback, 100, { maxWait: 150 }); + debouncedCallback(); + expect(callback).not.toHaveBeenCalled(); + + vi.advanceTimersByTime(98); + expect(callback).not.toHaveBeenCalled(); + + debouncedCallback(); + + vi.advanceTimersByTime(1); + expect(callback).not.toHaveBeenCalled(); + + vi.advanceTimersByTime(49); + // at this time, the callback shouldn't be invoked and with a new call, it should be debounced further. + debouncedCallback(); + expect(callback).not.toHaveBeenCalled(); + + // But because the maxWait is reached, the callback should nevertheless be invoked. + vi.advanceTimersByTime(10); + expect(callback).toHaveBeenCalled(); + }); + + it('should not invoke the callback as long as it is debounced and no maxWait option is specified', () => { + const callback = vi.fn(); + const debouncedCallback = debounce(callback, 100); + debouncedCallback(); + expect(callback).not.toHaveBeenCalled(); + + vi.advanceTimersByTime(99); + expect(callback).not.toHaveBeenCalled(); + + debouncedCallback(); + + vi.advanceTimersByTime(1); + expect(callback).not.toHaveBeenCalled(); + + vi.advanceTimersByTime(98); + debouncedCallback(); + expect(callback).not.toHaveBeenCalled(); + + vi.advanceTimersByTime(99); + expect(callback).not.toHaveBeenCalled(); + debouncedCallback(); + + vi.advanceTimersByTime(100); + expect(callback).toHaveBeenCalled(); + }); + + it('should invoke the callback as soon as callback.flush() is called', () => { + const callback = vi.fn(); + const debouncedCallback = debounce(callback, 100, { maxWait: 200 }); + debouncedCallback(); + expect(callback).not.toHaveBeenCalled(); + + vi.advanceTimersByTime(10); + expect(callback).not.toHaveBeenCalled(); + + debouncedCallback.flush(); + expect(callback).toHaveBeenCalled(); + }); + + it('should not invoke the callback, if callback.cancel() is called', () => { + const callback = vi.fn(); + const debouncedCallback = debounce(callback, 100, { maxWait: 200 }); + debouncedCallback(); + expect(callback).not.toHaveBeenCalled(); + + vi.advanceTimersByTime(99); + expect(callback).not.toHaveBeenCalled(); + + // If the callback is canceled, it should not be invoked after the minwait + debouncedCallback.cancel(); + vi.advanceTimersByTime(1); + expect(callback).not.toHaveBeenCalled(); + + // And it should also not be invoked after the maxWait + vi.advanceTimersByTime(500); + expect(callback).not.toHaveBeenCalled(); + }); + + it("should return the callback's return value when calling callback.flush()", () => { + const callback = vi.fn().mockReturnValue('foo'); + const debouncedCallback = debounce(callback, 100); + + debouncedCallback(); + + const returnValue = debouncedCallback.flush(); + expect(returnValue).toBe('foo'); + }); + + it('should return the callbacks return value on subsequent calls of the debounced function', () => { + const callback = vi.fn().mockReturnValue('foo'); + const debouncedCallback = debounce(callback, 100); + + const returnValue1 = debouncedCallback(); + expect(returnValue1).toBe(undefined); + expect(callback).not.toHaveBeenCalled(); + + // now we expect the callback to have been invoked + vi.advanceTimersByTime(200); + expect(callback).toHaveBeenCalledTimes(1); + + // calling the debounced function now should return the return value of the callback execution + const returnValue2 = debouncedCallback(); + expect(returnValue2).toBe('foo'); + expect(callback).toHaveBeenCalledTimes(1); + + // and the callback should also be invoked again + vi.advanceTimersByTime(200); + expect(callback).toHaveBeenCalledTimes(2); + }); + + it('should handle return values of consecutive invocations without maxWait', () => { + let i = 0; + const callback = vi.fn().mockImplementation(() => { + return `foo-${++i}`; + }); + const debouncedCallback = debounce(callback, 100); + + const returnValue0 = debouncedCallback(); + expect(returnValue0).toBe(undefined); + expect(callback).not.toHaveBeenCalled(); + + // now we expect the callback to have been invoked + vi.advanceTimersByTime(200); + expect(callback).toHaveBeenCalledTimes(1); + + // calling the debounced function now should return the return value of the callback execution + const returnValue1 = debouncedCallback(); + expect(returnValue1).toBe('foo-1'); + expect(callback).toHaveBeenCalledTimes(1); + + vi.advanceTimersByTime(1); + const returnValue2 = debouncedCallback(); + expect(returnValue2).toBe('foo-1'); + expect(callback).toHaveBeenCalledTimes(1); + + // and the callback should also be invoked again + vi.advanceTimersByTime(200); + const returnValue3 = debouncedCallback(); + expect(returnValue3).toBe('foo-2'); + expect(callback).toHaveBeenCalledTimes(2); + }); + + it('should handle return values of consecutive invocations with maxWait', () => { + let i = 0; + const callback = vi.fn().mockImplementation(() => { + return `foo-${++i}`; + }); + const debouncedCallback = debounce(callback, 150, { maxWait: 200 }); + + const returnValue0 = debouncedCallback(); + expect(returnValue0).toBe(undefined); + expect(callback).not.toHaveBeenCalled(); + + // now we expect the callback to have been invoked + vi.advanceTimersByTime(149); + const returnValue1 = debouncedCallback(); + expect(returnValue1).toBe(undefined); + expect(callback).not.toHaveBeenCalled(); + + // calling the debounced function now should return the return value of the callback execution + // as it was executed because of maxWait + vi.advanceTimersByTime(51); + const returnValue2 = debouncedCallback(); + expect(returnValue2).toBe('foo-1'); + expect(callback).toHaveBeenCalledTimes(1); + + // at this point (100ms after the last debounce call), nothing should have happened + vi.advanceTimersByTime(100); + const returnValue3 = debouncedCallback(); + expect(returnValue3).toBe('foo-1'); + expect(callback).toHaveBeenCalledTimes(1); + + // and the callback should now have been invoked again + vi.advanceTimersByTime(150); + const returnValue4 = debouncedCallback(); + expect(returnValue4).toBe('foo-2'); + expect(callback).toHaveBeenCalledTimes(2); + }); + + it('should handle return values of consecutive invocations after a cancellation', () => { + let i = 0; + const callback = vi.fn().mockImplementation(() => { + return `foo-${++i}`; + }); + const debouncedCallback = debounce(callback, 150, { maxWait: 200 }); + + const returnValue0 = debouncedCallback(); + expect(returnValue0).toBe(undefined); + expect(callback).not.toHaveBeenCalled(); + + // now we expect the callback to have been invoked + vi.advanceTimersByTime(149); + const returnValue1 = debouncedCallback(); + expect(returnValue1).toBe(undefined); + expect(callback).not.toHaveBeenCalled(); + + debouncedCallback.cancel(); + + // calling the debounced function now still return undefined because we cancelled the invocation + vi.advanceTimersByTime(51); + const returnValue2 = debouncedCallback(); + expect(returnValue2).toBe(undefined); + expect(callback).not.toHaveBeenCalled(); + + // and the callback should also be invoked again + vi.advanceTimersByTime(150); + const returnValue3 = debouncedCallback(); + expect(returnValue3).toBe('foo-1'); + expect(callback).toHaveBeenCalledTimes(1); + }); + + it('should handle the return value of calling flush after cancelling', () => { + const callback = vi.fn().mockReturnValue('foo'); + const debouncedCallback = debounce(callback, 100); + + debouncedCallback(); + debouncedCallback.cancel(); + + const returnValue = debouncedCallback.flush(); + expect(returnValue).toBe(undefined); + }); + + it('should handle equal wait and maxWait values and only invoke func once', () => { + const callback = vi.fn().mockReturnValue('foo'); + const debouncedCallback = debounce(callback, 100, { maxWait: 100 }); + + debouncedCallback(); + vi.advanceTimersByTime(25); + debouncedCallback(); + vi.advanceTimersByTime(25); + debouncedCallback(); + vi.advanceTimersByTime(25); + debouncedCallback(); + vi.advanceTimersByTime(25); + + expect(callback).toHaveBeenCalledTimes(1); + + const retval = debouncedCallback(); + expect(retval).toBe('foo'); + + vi.advanceTimersByTime(25); + debouncedCallback(); + vi.advanceTimersByTime(25); + debouncedCallback(); + vi.advanceTimersByTime(25); + debouncedCallback(); + vi.advanceTimersByTime(25); + + expect(callback).toHaveBeenCalledTimes(2); + }); +}); diff --git a/packages/core/test/utils-hoist/dsn.test.ts b/packages/core/test/lib/utils/dsn.test.ts similarity index 98% rename from packages/core/test/utils-hoist/dsn.test.ts rename to packages/core/test/lib/utils/dsn.test.ts index b5d22130816b..3dc081866703 100644 --- a/packages/core/test/utils-hoist/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 { dsnToString, extractOrgIdFromDsnHost, makeDsn } from '../../src/utils-hoist/dsn'; -import { logger } from '../../src/utils-hoist/logger'; +import { DEBUG_BUILD } from '../../../src/debug-build'; +import { dsnToString, extractOrgIdFromDsnHost, makeDsn } from '../../../src/utils/dsn'; +import { logger } from '../../../src/utils/logger'; function testIf(condition: boolean) { return condition ? test : test.skip; diff --git a/packages/core/test/utils-hoist/envelope.test.ts b/packages/core/test/lib/utils/envelope.test.ts similarity index 94% rename from packages/core/test/utils-hoist/envelope.test.ts rename to packages/core/test/lib/utils/envelope.test.ts index 00a745807fcd..85bff6a662f2 100644 --- a/packages/core/test/utils-hoist/envelope.test.ts +++ b/packages/core/test/lib/utils/envelope.test.ts @@ -6,10 +6,10 @@ import { spanToJSON, } from '@sentry/core'; import { afterEach, describe, expect, it, test, vi } from 'vitest'; -import { getSentryCarrier } from '../../src/carrier'; -import type { EventEnvelope } from '../../src/types-hoist/envelope'; -import type { Event } from '../../src/types-hoist/event'; -import type { SpanAttributes } from '../../src/types-hoist/span'; +import { getSentryCarrier } from '../../../src/carrier'; +import type { EventEnvelope } from '../../../src/types-hoist/envelope'; +import type { Event } from '../../../src/types-hoist/event'; +import type { SpanAttributes } from '../../../src/types-hoist/span'; import { addItemToEnvelope, createEnvelope, @@ -17,9 +17,9 @@ import { forEachEnvelopeItem, parseEnvelope, serializeEnvelope, -} from '../../src/utils-hoist/envelope'; -import type { InternalGlobal } from '../../src/utils-hoist/worldwide'; -import { GLOBAL_OBJ } from '../../src/utils-hoist/worldwide'; +} from '../../../src/utils/envelope'; +import type { InternalGlobal } from '../../../src/utils/worldwide'; +import { GLOBAL_OBJ } from '../../../src/utils/worldwide'; describe('envelope', () => { describe('createSpanEnvelope()', () => { diff --git a/packages/core/test/utils-hoist/eventbuilder.test.ts b/packages/core/test/lib/utils/eventbuilder.test.ts similarity index 95% rename from packages/core/test/utils-hoist/eventbuilder.test.ts rename to packages/core/test/lib/utils/eventbuilder.test.ts index 877df25d8fed..77fa2ff93d96 100644 --- a/packages/core/test/utils-hoist/eventbuilder.test.ts +++ b/packages/core/test/lib/utils/eventbuilder.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it, test } from 'vitest'; -import type { Client } from '../../src/client'; -import { eventFromMessage, eventFromUnknownInput } from '../../src/utils-hoist/eventbuilder'; -import { nodeStackLineParser } from '../../src/utils-hoist/node-stack-trace'; -import { createStackParser } from '../../src/utils-hoist/stacktrace'; +import type { Client } from '../../../src/client'; +import { eventFromMessage, eventFromUnknownInput } from '../../../src/utils/eventbuilder'; +import { nodeStackLineParser } from '../../../src/utils/node-stack-trace'; +import { createStackParser } from '../../../src/utils/stacktrace'; const stackParser = createStackParser(nodeStackLineParser()); diff --git a/packages/browser/test/utils/featureFlags.test.ts b/packages/core/test/lib/utils/featureFlags.test.ts similarity index 62% rename from packages/browser/test/utils/featureFlags.test.ts rename to packages/core/test/lib/utils/featureFlags.test.ts index 1c0bed312590..8f6e512bca7b 100644 --- a/packages/browser/test/utils/featureFlags.test.ts +++ b/packages/core/test/lib/utils/featureFlags.test.ts @@ -1,16 +1,20 @@ -import type { FeatureFlag } from '@sentry/core'; -import { getCurrentScope, logger } from '@sentry/core'; import { afterEach, describe, expect, it, vi } from 'vitest'; -import { insertFlagToScope, insertToFlagBuffer } from '../../src/utils/featureFlags'; +import { getCurrentScope } from '../../../src/currentScopes'; +import { + type FeatureFlag, + _INTERNAL_insertFlagToScope, + _INTERNAL_insertToFlagBuffer, +} from '../../../src/utils/featureFlags'; +import { logger } from '../../../src/utils/logger'; describe('flags', () => { describe('insertFlagToScope()', () => { it('adds flags to the current scope context', () => { const maxSize = 3; - insertFlagToScope('feat1', true, maxSize); - insertFlagToScope('feat2', true, maxSize); - insertFlagToScope('feat3', true, maxSize); - insertFlagToScope('feat4', true, maxSize); + _INTERNAL_insertFlagToScope('feat1', true, maxSize); + _INTERNAL_insertFlagToScope('feat2', true, maxSize); + _INTERNAL_insertFlagToScope('feat3', true, maxSize); + _INTERNAL_insertFlagToScope('feat4', true, maxSize); const scope = getCurrentScope(); expect(scope.getScopeData().contexts.flags?.values).toEqual([ @@ -31,10 +35,10 @@ describe('flags', () => { it('maintains ordering and evicts the oldest entry', () => { const buffer: FeatureFlag[] = []; const maxSize = 3; - insertToFlagBuffer(buffer, 'feat1', true, maxSize); - insertToFlagBuffer(buffer, 'feat2', true, maxSize); - insertToFlagBuffer(buffer, 'feat3', true, maxSize); - insertToFlagBuffer(buffer, 'feat4', true, maxSize); + _INTERNAL_insertToFlagBuffer(buffer, 'feat1', true, maxSize); + _INTERNAL_insertToFlagBuffer(buffer, 'feat2', true, maxSize); + _INTERNAL_insertToFlagBuffer(buffer, 'feat3', true, maxSize); + _INTERNAL_insertToFlagBuffer(buffer, 'feat4', true, maxSize); expect(buffer).toEqual([ { flag: 'feat2', result: true }, @@ -46,11 +50,11 @@ describe('flags', () => { it('does not duplicate same-name flags and updates order and values', () => { const buffer: FeatureFlag[] = []; const maxSize = 3; - insertToFlagBuffer(buffer, 'feat1', true, maxSize); - insertToFlagBuffer(buffer, 'feat2', true, maxSize); - insertToFlagBuffer(buffer, 'feat3', true, maxSize); - insertToFlagBuffer(buffer, 'feat3', false, maxSize); - insertToFlagBuffer(buffer, 'feat1', false, maxSize); + _INTERNAL_insertToFlagBuffer(buffer, 'feat1', true, maxSize); + _INTERNAL_insertToFlagBuffer(buffer, 'feat2', true, maxSize); + _INTERNAL_insertToFlagBuffer(buffer, 'feat3', true, maxSize); + _INTERNAL_insertToFlagBuffer(buffer, 'feat3', false, maxSize); + _INTERNAL_insertToFlagBuffer(buffer, 'feat1', false, maxSize); expect(buffer).toEqual([ { flag: 'feat2', result: true }, @@ -62,8 +66,8 @@ describe('flags', () => { it('does not allocate unnecessary space', () => { const buffer: FeatureFlag[] = []; const maxSize = 1000; - insertToFlagBuffer(buffer, 'feat1', true, maxSize); - insertToFlagBuffer(buffer, 'feat2', true, maxSize); + _INTERNAL_insertToFlagBuffer(buffer, 'feat1', true, maxSize); + _INTERNAL_insertToFlagBuffer(buffer, 'feat2', true, maxSize); expect(buffer).toEqual([ { flag: 'feat1', result: true }, @@ -74,8 +78,8 @@ describe('flags', () => { it('does not accept non-boolean values', () => { const buffer: FeatureFlag[] = []; const maxSize = 1000; - insertToFlagBuffer(buffer, 'feat1', 1, maxSize); - insertToFlagBuffer(buffer, 'feat2', 'string', maxSize); + _INTERNAL_insertToFlagBuffer(buffer, 'feat1', 1, maxSize); + _INTERNAL_insertToFlagBuffer(buffer, 'feat2', 'string', maxSize); expect(buffer).toEqual([]); }); @@ -86,7 +90,7 @@ describe('flags', () => { { flag: 'feat2', result: true }, ]; - insertToFlagBuffer(buffer, 'feat1', true, 1); + _INTERNAL_insertToFlagBuffer(buffer, 'feat1', true, 1); expect(loggerSpy).toHaveBeenCalledWith( expect.stringContaining('[Feature Flags] insertToFlagBuffer called on a buffer larger than maxSize'), ); @@ -95,7 +99,7 @@ describe('flags', () => { { flag: 'feat2', result: true }, ]); - insertToFlagBuffer(buffer, 'feat1', true, -2); + _INTERNAL_insertToFlagBuffer(buffer, 'feat1', true, -2); expect(loggerSpy).toHaveBeenCalledWith( expect.stringContaining('[Feature Flags] insertToFlagBuffer called on a buffer larger than maxSize'), ); diff --git a/packages/core/test/utils-hoist/is.test.ts b/packages/core/test/lib/utils/is.test.ts similarity index 95% rename from packages/core/test/utils-hoist/is.test.ts rename to packages/core/test/lib/utils/is.test.ts index 70a83eee5efd..745cf275be06 100644 --- a/packages/core/test/utils-hoist/is.test.ts +++ b/packages/core/test/lib/utils/is.test.ts @@ -9,10 +9,10 @@ import { isPrimitive, isThenable, isVueViewModel, -} from '../../src/utils-hoist/is'; -import { supportsDOMError, supportsDOMException, supportsErrorEvent } from '../../src/utils-hoist/supports'; -import { resolvedSyncPromise } from '../../src/utils-hoist/syncpromise'; -import { testOnlyIfNodeVersionAtLeast } from './testutils'; +} from '../../../src/utils/is'; +import { supportsDOMError, supportsDOMException, supportsErrorEvent } from '../../../src/utils/supports'; +import { resolvedSyncPromise } from '../../../src/utils/syncpromise'; +import { testOnlyIfNodeVersionAtLeast } from '../../testutils'; if (supportsDOMError()) { describe('isDOMError()', () => { diff --git a/packages/core/test/utils-hoist/lru.test.ts b/packages/core/test/lib/utils/lru.test.ts similarity index 94% rename from packages/core/test/utils-hoist/lru.test.ts rename to packages/core/test/lib/utils/lru.test.ts index 28259a2700ad..5940f10684e1 100644 --- a/packages/core/test/utils-hoist/lru.test.ts +++ b/packages/core/test/lib/utils/lru.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from 'vitest'; -import { LRUMap } from '../../src/utils-hoist/lru'; +import { LRUMap } from '../../../src/utils/lru'; describe('LRUMap', () => { test('evicts older entries when reaching max size', () => { diff --git a/packages/core/test/utils-hoist/misc.test.ts b/packages/core/test/lib/utils/misc.test.ts similarity index 97% rename from packages/core/test/utils-hoist/misc.test.ts rename to packages/core/test/lib/utils/misc.test.ts index 6aece21ecb81..83e7f4c05b66 100644 --- a/packages/core/test/utils-hoist/misc.test.ts +++ b/packages/core/test/lib/utils/misc.test.ts @@ -1,14 +1,14 @@ import { describe, expect, it, test } from 'vitest'; -import type { Event } from '../../src/types-hoist/event'; -import type { Mechanism } from '../../src/types-hoist/mechanism'; -import type { StackFrame } from '../../src/types-hoist/stackframe'; +import type { Event } from '../../../src/types-hoist/event'; +import type { Mechanism } from '../../../src/types-hoist/mechanism'; +import type { StackFrame } from '../../../src/types-hoist/stackframe'; import { addContextToFrame, addExceptionMechanism, checkOrSetAlreadyCaught, getEventDescription, uuid4, -} from '../../src/utils-hoist/misc'; +} from '../../../src/utils/misc'; describe('getEventDescription()', () => { test('message event', () => { diff --git a/packages/core/test/utils-hoist/normalize-url.test.ts b/packages/core/test/lib/utils/normalize-url.test.ts similarity index 97% rename from packages/core/test/utils-hoist/normalize-url.test.ts rename to packages/core/test/lib/utils/normalize-url.test.ts index 0e8b5d787601..888383940a1a 100644 --- a/packages/core/test/utils-hoist/normalize-url.test.ts +++ b/packages/core/test/lib/utils/normalize-url.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { normalizeUrlToBase } from '../../src/utils-hoist/normalize'; +import { normalizeUrlToBase } from '../../../src/utils/normalize'; describe('normalizeUrlToBase()', () => { it('Example app on Windows', () => { diff --git a/packages/core/test/utils-hoist/normalize.test.ts b/packages/core/test/lib/utils/normalize.test.ts similarity index 99% rename from packages/core/test/utils-hoist/normalize.test.ts rename to packages/core/test/lib/utils/normalize.test.ts index e08c7187c839..17c0628e53df 100644 --- a/packages/core/test/utils-hoist/normalize.test.ts +++ b/packages/core/test/lib/utils/normalize.test.ts @@ -3,9 +3,9 @@ */ import { describe, expect, test, vi } from 'vitest'; -import { addNonEnumerableProperty, normalize } from '../../src'; -import * as isModule from '../../src/utils-hoist/is'; -import * as stacktraceModule from '../../src/utils-hoist/stacktrace'; +import { addNonEnumerableProperty, normalize } from '../../../src'; +import * as isModule from '../../../src/utils/is'; +import * as stacktraceModule from '../../../src/utils/stacktrace'; describe('normalize()', () => { describe('acts as a pass-through for simple-cases', () => { diff --git a/packages/core/test/utils-hoist/object.test.ts b/packages/core/test/lib/utils/object.test.ts similarity index 98% rename from packages/core/test/utils-hoist/object.test.ts rename to packages/core/test/lib/utils/object.test.ts index c6c5d05b6af0..bc8c7611abb8 100644 --- a/packages/core/test/utils-hoist/object.test.ts +++ b/packages/core/test/lib/utils/object.test.ts @@ -3,7 +3,7 @@ */ import { describe, expect, it, test, vi } from 'vitest'; -import type { WrappedFunction } from '../../src/types-hoist/wrappedfunction'; +import type { WrappedFunction } from '../../../src/types-hoist/wrappedfunction'; import { addNonEnumerableProperty, dropUndefinedKeys, @@ -11,8 +11,8 @@ import { fill, markFunctionWrapped, objectify, -} from '../../src/utils-hoist/object'; -import { testOnlyIfNodeVersionAtLeast } from './testutils'; +} from '../../../src/utils/object'; +import { testOnlyIfNodeVersionAtLeast } from '../../testutils'; describe('fill()', () => { test('wraps a method by calling a replacement function on it', () => { diff --git a/packages/core/test/utils-hoist/path.test.ts b/packages/core/test/lib/utils/path.test.ts similarity index 95% rename from packages/core/test/utils-hoist/path.test.ts rename to packages/core/test/lib/utils/path.test.ts index 6cd760ac84aa..7e17f99228d4 100644 --- a/packages/core/test/utils-hoist/path.test.ts +++ b/packages/core/test/lib/utils/path.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from 'vitest'; -import { basename, dirname } from '../../src/utils-hoist/path'; +import { basename, dirname } from '../../../src/utils/path'; describe('path', () => { describe('basename', () => { diff --git a/packages/core/test/utils-hoist/promisebuffer.test.ts b/packages/core/test/lib/utils/promisebuffer.test.ts similarity index 96% rename from packages/core/test/utils-hoist/promisebuffer.test.ts rename to packages/core/test/lib/utils/promisebuffer.test.ts index 6b0b81d1588e..1f0011dd6e50 100644 --- a/packages/core/test/utils-hoist/promisebuffer.test.ts +++ b/packages/core/test/lib/utils/promisebuffer.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test, vi } from 'vitest'; -import { makePromiseBuffer } from '../../src/utils-hoist/promisebuffer'; -import { SyncPromise } from '../../src/utils-hoist/syncpromise'; +import { makePromiseBuffer } from '../../../src/utils/promisebuffer'; +import { SyncPromise } from '../../../src/utils/syncpromise'; describe('PromiseBuffer', () => { describe('add()', () => { diff --git a/packages/core/test/utils-hoist/ratelimit.test.ts b/packages/core/test/lib/utils/ratelimit.test.ts similarity index 98% rename from packages/core/test/utils-hoist/ratelimit.test.ts rename to packages/core/test/lib/utils/ratelimit.test.ts index 556280b93ba4..772bd5e5c9d4 100644 --- a/packages/core/test/utils-hoist/ratelimit.test.ts +++ b/packages/core/test/lib/utils/ratelimit.test.ts @@ -1,12 +1,12 @@ import { describe, expect, test } from 'vitest'; -import type { RateLimits } from '../../src/utils-hoist/ratelimit'; +import type { RateLimits } from '../../../src/utils/ratelimit'; import { DEFAULT_RETRY_AFTER, disabledUntil, isRateLimited, parseRetryAfterHeader, updateRateLimits, -} from '../../src/utils-hoist/ratelimit'; +} from '../../../src/utils/ratelimit'; describe('parseRetryAfterHeader()', () => { test('should fallback to 60s when incorrect header provided', () => { diff --git a/packages/core/test/utils-hoist/severity.test.ts b/packages/core/test/lib/utils/severity.test.ts similarity index 87% rename from packages/core/test/utils-hoist/severity.test.ts rename to packages/core/test/lib/utils/severity.test.ts index 2f5595dfc9f0..6cc44a5bbeee 100644 --- a/packages/core/test/utils-hoist/severity.test.ts +++ b/packages/core/test/lib/utils/severity.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from 'vitest'; -import { severityLevelFromString } from '../../src/utils-hoist/severity'; +import { severityLevelFromString } from '../../../src/utils/severity'; describe('severityLevelFromString()', () => { test("converts 'warn' to 'warning'", () => { diff --git a/packages/core/test/utils-hoist/stacktrace.test.ts b/packages/core/test/lib/utils/stacktrace.test.ts similarity index 98% rename from packages/core/test/utils-hoist/stacktrace.test.ts rename to packages/core/test/lib/utils/stacktrace.test.ts index cfcc70819fa0..b0d74e2e9f75 100644 --- a/packages/core/test/utils-hoist/stacktrace.test.ts +++ b/packages/core/test/lib/utils/stacktrace.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { nodeStackLineParser } from '../../src/utils-hoist/node-stack-trace'; -import { stripSentryFramesAndReverse } from '../../src/utils-hoist/stacktrace'; +import { nodeStackLineParser } from '../../../src/utils/node-stack-trace'; +import { stripSentryFramesAndReverse } from '../../../src/utils/stacktrace'; describe('Stacktrace', () => { describe('stripSentryFramesAndReverse()', () => { diff --git a/packages/core/test/utils-hoist/string.test.ts b/packages/core/test/lib/utils/string.test.ts similarity index 99% rename from packages/core/test/utils-hoist/string.test.ts rename to packages/core/test/lib/utils/string.test.ts index 364d29223a66..b3d166568163 100644 --- a/packages/core/test/utils-hoist/string.test.ts +++ b/packages/core/test/lib/utils/string.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from 'vitest'; -import { isMatchingPattern, stringMatchesSomePattern, truncate } from '../../src/utils-hoist/string'; +import { isMatchingPattern, stringMatchesSomePattern, truncate } from '../../../src/utils/string'; describe('truncate()', () => { test('it works as expected', () => { diff --git a/packages/core/test/utils-hoist/supports.test.ts b/packages/core/test/lib/utils/supports.test.ts similarity index 93% rename from packages/core/test/utils-hoist/supports.test.ts rename to packages/core/test/lib/utils/supports.test.ts index fffc6be02e1e..97ad75d6c7c3 100644 --- a/packages/core/test/utils-hoist/supports.test.ts +++ b/packages/core/test/lib/utils/supports.test.ts @@ -1,6 +1,6 @@ import { afterEach } from 'node:test'; import { describe, expect, it } from 'vitest'; -import { supportsHistory } from '../../src/utils-hoist/supports'; +import { supportsHistory } from '../../../src/utils/supports'; describe('supportsHistory', () => { const originalHistory = globalThis.history; diff --git a/packages/core/test/utils-hoist/syncpromise.test.ts b/packages/core/test/lib/utils/syncpromise.test.ts similarity index 99% rename from packages/core/test/utils-hoist/syncpromise.test.ts rename to packages/core/test/lib/utils/syncpromise.test.ts index e332de2c2141..14bc5d7abbe0 100644 --- a/packages/core/test/utils-hoist/syncpromise.test.ts +++ b/packages/core/test/lib/utils/syncpromise.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test, vi } from 'vitest'; -import { rejectedSyncPromise, resolvedSyncPromise, SyncPromise } from '../../src/utils-hoist/syncpromise'; +import { rejectedSyncPromise, resolvedSyncPromise, SyncPromise } from '../../../src/utils/syncpromise'; describe('SyncPromise', () => { test('simple', async () => { diff --git a/packages/core/test/lib/utils/traceData.test.ts b/packages/core/test/lib/utils/traceData.test.ts index 8d38bdb062d4..d10bf1e3b592 100644 --- a/packages/core/test/lib/utils/traceData.test.ts +++ b/packages/core/test/lib/utils/traceData.test.ts @@ -6,6 +6,7 @@ import { getIsolationScope, getMainCarrier, getTraceData, + Scope, SentrySpan, setAsyncContextStrategy, setCurrentClient, @@ -158,6 +159,35 @@ describe('getTraceData', () => { }); }); + it('allows to pass a scope & client directly', () => { + // this default client & scope should not be used! + setupClient(); + getCurrentScope().setPropagationContext({ + traceId: '12345678901234567890123456789099', + sampleRand: 0.44, + }); + + const options = getDefaultTestClientOptions({ + dsn: 'https://567@sentry.io/42', + tracesSampleRate: 1, + }); + const customClient = new TestClient(options); + + const scope = new Scope(); + scope.setPropagationContext({ + traceId: '12345678901234567890123456789012', + sampleRand: 0.42, + }); + scope.setClient(customClient); + + const traceData = getTraceData({ client: customClient, scope }); + + expect(traceData['sentry-trace']).toMatch(/^12345678901234567890123456789012-[a-f0-9]{16}$/); + expect(traceData.baggage).toEqual( + 'sentry-environment=production,sentry-public_key=567,sentry-trace_id=12345678901234567890123456789012', + ); + }); + it('returns propagationContext DSC data if no span is available', () => { setupClient(); diff --git a/packages/core/test/utils-hoist/tracing.test.ts b/packages/core/test/lib/utils/tracing.test.ts similarity index 99% rename from packages/core/test/utils-hoist/tracing.test.ts rename to packages/core/test/lib/utils/tracing.test.ts index 851ee7b109c4..ea99555e70e1 100644 --- a/packages/core/test/utils-hoist/tracing.test.ts +++ b/packages/core/test/lib/utils/tracing.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, test } from 'vitest'; -import { extractTraceparentData, propagationContextFromHeaders } from '../../src/utils-hoist/tracing'; +import { extractTraceparentData, propagationContextFromHeaders } from '../../../src/utils/tracing'; const EXAMPLE_SENTRY_TRACE = '12312012123120121231201212312012-1121201211212012-1'; const EXAMPLE_BAGGAGE = 'sentry-release=1.2.3,sentry-foo=bar,other=baz,sentry-sample_rand=0.42'; diff --git a/packages/core/test/utils-hoist/url.test.ts b/packages/core/test/lib/utils/url.test.ts similarity index 99% rename from packages/core/test/utils-hoist/url.test.ts rename to packages/core/test/lib/utils/url.test.ts index 67ec8b31644f..33364d66daa5 100644 --- a/packages/core/test/utils-hoist/url.test.ts +++ b/packages/core/test/lib/utils/url.test.ts @@ -7,7 +7,7 @@ import { parseStringToURLObject, parseUrl, stripUrlQueryAndFragment, -} from '../../src/utils-hoist/url'; +} from '../../../src/utils/url'; describe('stripQueryStringAndFragment', () => { const urlString = 'http://dogs.are.great:1231/yay/'; diff --git a/packages/core/test/utils-hoist/vercelWaitUntil.test.ts b/packages/core/test/lib/utils/vercelWaitUntil.test.ts similarity index 90% rename from packages/core/test/utils-hoist/vercelWaitUntil.test.ts rename to packages/core/test/lib/utils/vercelWaitUntil.test.ts index c35b5f076cd4..78637cb3ef18 100644 --- a/packages/core/test/utils-hoist/vercelWaitUntil.test.ts +++ b/packages/core/test/lib/utils/vercelWaitUntil.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest'; -import { vercelWaitUntil } from '../../src/utils-hoist/vercelWaitUntil'; -import { GLOBAL_OBJ } from '../../src/utils-hoist/worldwide'; +import { vercelWaitUntil } from '../../../src/utils/vercelWaitUntil'; +import { GLOBAL_OBJ } from '../../../src/utils/worldwide'; describe('vercelWaitUntil', () => { it('should do nothing if GLOBAL_OBJ does not have the @vercel/request-context symbol', () => { diff --git a/packages/core/test/utils-hoist/worldwide.test.ts b/packages/core/test/lib/utils/worldwide.test.ts similarity index 85% rename from packages/core/test/utils-hoist/worldwide.test.ts rename to packages/core/test/lib/utils/worldwide.test.ts index efd8a152a0ab..f7c952a9930d 100644 --- a/packages/core/test/utils-hoist/worldwide.test.ts +++ b/packages/core/test/lib/utils/worldwide.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from 'vitest'; -import { GLOBAL_OBJ } from '../../src/utils-hoist/worldwide'; +import { GLOBAL_OBJ } from '../../../src/utils/worldwide'; describe('GLOBAL_OBJ', () => { test('should return the same object', () => { diff --git a/packages/core/test/mocks/client.ts b/packages/core/test/mocks/client.ts index 811046972675..2b7162a112b5 100644 --- a/packages/core/test/mocks/client.ts +++ b/packages/core/test/mocks/client.ts @@ -8,7 +8,7 @@ import type { ClientOptions } from '../../src/types-hoist/options'; import type { ParameterizedString } from '../../src/types-hoist/parameterize'; import type { Session } from '../../src/types-hoist/session'; import type { SeverityLevel } from '../../src/types-hoist/severity'; -import { resolvedSyncPromise } from '../../src/utils-hoist/syncpromise'; +import { resolvedSyncPromise } from '../../src/utils/syncpromise'; export function getDefaultTestClientOptions(options: Partial = {}): TestClientOptions { return { diff --git a/packages/core/test/mocks/transport.ts b/packages/core/test/mocks/transport.ts index ba49025ce5d7..c8ae84f80d64 100644 --- a/packages/core/test/mocks/transport.ts +++ b/packages/core/test/mocks/transport.ts @@ -1,6 +1,6 @@ import { createTransport } from '../../src/transports/base'; import type { Transport } from '../../src/types-hoist/transport'; -import { SyncPromise } from '../../src/utils-hoist/syncpromise'; +import { SyncPromise } from '../../src/utils/syncpromise'; async function sleep(delay: number): Promise { return new SyncPromise(resolve => setTimeout(resolve, delay)); diff --git a/packages/core/test/utils-hoist/testutils.ts b/packages/core/test/testutils.ts similarity index 65% rename from packages/core/test/utils-hoist/testutils.ts rename to packages/core/test/testutils.ts index b830095d3393..c00ec2b878b7 100644 --- a/packages/core/test/utils-hoist/testutils.ts +++ b/packages/core/test/testutils.ts @@ -1,4 +1,6 @@ import { it } from 'vitest'; +import { getSentryCarrier } from '../src/carrier'; +import { GLOBAL_OBJ } from '../src/utils/worldwide'; // eslint-disable-next-line @typescript-eslint/ban-types export const testOnlyIfNodeVersionAtLeast = (minVersion: number): Function => { @@ -14,3 +16,8 @@ export const testOnlyIfNodeVersionAtLeast = (minVersion: number): Function => { return it; }; + +export function clearGlobalScope() { + const carrier = getSentryCarrier(GLOBAL_OBJ); + carrier.globalScope = undefined; +} diff --git a/packages/core/test/utils-hoist/types/typedef.test.ts b/packages/core/test/types/typedef.test.ts similarity index 100% rename from packages/core/test/utils-hoist/types/typedef.test.ts rename to packages/core/test/types/typedef.test.ts diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts index f388de7cb5ee..12bcedc35270 100644 --- a/packages/deno/src/index.ts +++ b/packages/deno/src/index.ts @@ -8,6 +8,7 @@ export type { EventHint, ErrorEvent, Exception, + FeatureFlagsIntegration, Session, SeverityLevel, Span, @@ -87,6 +88,7 @@ export { spanToBaggageHeader, updateSpanName, wrapMcpServerWithSentry, + featureFlagsIntegration, } from '@sentry/core'; export { DenoClient } from './client'; diff --git a/packages/feedback/src/screenshot/components/ScreenshotEditor.tsx b/packages/feedback/src/screenshot/components/ScreenshotEditor.tsx index ae3d0f653dd8..083f5c16dec6 100644 --- a/packages/feedback/src/screenshot/components/ScreenshotEditor.tsx +++ b/packages/feedback/src/screenshot/components/ScreenshotEditor.tsx @@ -166,6 +166,11 @@ export function ScreenshotEditorFactory({ ); setScaleFactor(scale); }); + + // For Firefox, the canvas is not yet measured, so we need to wait for it to get the correct size + if (measurementDiv.clientHeight === 0 || measurementDiv.clientWidth === 0) { + setTimeout(handleResize, 0); + } }; handleResize(); diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json index 919b05974986..2b5606a75db7 100644 --- a/packages/gatsby/package.json +++ b/packages/gatsby/package.json @@ -47,7 +47,7 @@ "dependencies": { "@sentry/core": "9.30.0", "@sentry/react": "9.30.0", - "@sentry/webpack-plugin": "3.5.0" + "@sentry/webpack-plugin": "^3.5.0" }, "peerDependencies": { "gatsby": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0", diff --git a/packages/google-cloud-serverless/src/gcpfunction/general.ts b/packages/google-cloud-serverless/src/gcpfunction/general.ts index f819bd5aaaf3..cae27ee6c2ee 100644 --- a/packages/google-cloud-serverless/src/gcpfunction/general.ts +++ b/packages/google-cloud-serverless/src/gcpfunction/general.ts @@ -29,10 +29,10 @@ export interface CloudFunctionsContext { export interface CloudEventsContext { [key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any + id: string; + specversion: string; type?: string; - specversion?: string; source?: string; - id?: string; time?: string; schemaurl?: string; contenttype?: string; diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts index f2622e591497..e9586a9bd820 100644 --- a/packages/google-cloud-serverless/src/index.ts +++ b/packages/google-cloud-serverless/src/index.ts @@ -121,6 +121,8 @@ export { consoleLoggingIntegration, wrapMcpServerWithSentry, NODE_VERSION, + featureFlagsIntegration, + type FeatureFlagsIntegration, } from '@sentry/node'; export { diff --git a/packages/google-cloud-serverless/test/gcpfunction/cloud_event.test.ts b/packages/google-cloud-serverless/test/gcpfunction/cloud_event.test.ts index 758b37c0253b..4d34e630814c 100644 --- a/packages/google-cloud-serverless/test/gcpfunction/cloud_event.test.ts +++ b/packages/google-cloud-serverless/test/gcpfunction/cloud_event.test.ts @@ -45,6 +45,8 @@ describe('wrapCloudEventFunction', () => { function handleCloudEvent(fn: CloudEventFunctionWithCallback): Promise { return new Promise((resolve, reject) => { const context = { + id: 'test-event-id', + specversion: '1.0', type: 'event.type', }; @@ -232,6 +234,10 @@ describe('wrapCloudEventFunction', () => { const handler: CloudEventFunction = _context => 42; const wrappedHandler = wrapCloudEventFunction(handler); await handleCloudEvent(wrappedHandler); - expect(mockScope.setContext).toBeCalledWith('gcp.function.context', { type: 'event.type' }); + expect(mockScope.setContext).toBeCalledWith('gcp.function.context', { + id: 'test-event-id', + specversion: '1.0', + type: 'event.type', + }); }); }); diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 2c881cce3993..ff770802eed9 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -85,7 +85,7 @@ "@sentry/opentelemetry": "9.30.0", "@sentry/react": "9.30.0", "@sentry/vercel-edge": "9.30.0", - "@sentry/webpack-plugin": "3.5.0", + "@sentry/webpack-plugin": "^3.5.0", "chalk": "3.0.0", "resolve": "1.22.8", "rollup": "4.35.0", diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index ffce8a8b0641..fe05624ba15e 100644 --- a/packages/nextjs/src/config/types.ts +++ b/packages/nextjs/src/config/types.ts @@ -426,9 +426,12 @@ export type SentryBuildOptions = { * Tunnel Sentry requests through this route on the Next.js server, to circumvent ad-blockers blocking Sentry events * from being sent. This option should be a path (for example: '/error-monitoring'). * + * - Pass `true` to auto-generate a random, ad-blocker-resistant route for each build + * - Pass a string path (e.g., '/monitoring') to use a custom route + * * NOTE: This feature only works with Next.js 11+ */ - tunnelRoute?: string; + tunnelRoute?: string | boolean; /** * Tree shakes Sentry SDK logger statements from the bundle. diff --git a/packages/nextjs/src/config/webpack.ts b/packages/nextjs/src/config/webpack.ts index 8898b3495ba9..aeef52d4e309 100644 --- a/packages/nextjs/src/config/webpack.ts +++ b/packages/nextjs/src/config/webpack.ts @@ -692,7 +692,9 @@ function addValueInjectionLoader( const isomorphicValues = { // `rewritesTunnel` set by the user in Next.js config _sentryRewritesTunnelPath: - userSentryOptions.tunnelRoute !== undefined && userNextConfig.output !== 'export' + userSentryOptions.tunnelRoute !== undefined && + userNextConfig.output !== 'export' && + typeof userSentryOptions.tunnelRoute === 'string' ? `${userNextConfig.basePath ?? ''}${userSentryOptions.tunnelRoute}` : undefined, diff --git a/packages/nextjs/src/config/withSentryConfig.ts b/packages/nextjs/src/config/withSentryConfig.ts index b94ce6187f97..ac1e76231eb8 100644 --- a/packages/nextjs/src/config/withSentryConfig.ts +++ b/packages/nextjs/src/config/withSentryConfig.ts @@ -75,6 +75,15 @@ export function withSentryConfig(nextConfig?: C, sentryBuildOptions: SentryBu } } +/** + * Generates a random tunnel route path that's less likely to be blocked by ad-blockers + */ +function generateRandomTunnelRoute(): string { + // Generate a random 8-character alphanumeric string + const randomString = Math.random().toString(36).substring(2, 10); + return `/${randomString}`; +} + // Modify the materialized object form of the user's next config by deleting the `sentry` property and wrapping the // `webpack` property function getFinalConfigObject( @@ -93,7 +102,14 @@ function getFinalConfigObject( ); } } else { - setUpTunnelRewriteRules(incomingUserNextConfigObject, userSentryOptions.tunnelRoute); + const resolvedTunnelRoute = + typeof userSentryOptions.tunnelRoute === 'boolean' + ? generateRandomTunnelRoute() + : userSentryOptions.tunnelRoute; + + // Update the global options object to use the resolved value everywhere + userSentryOptions.tunnelRoute = resolvedTunnelRoute; + setUpTunnelRewriteRules(incomingUserNextConfigObject, resolvedTunnelRoute); } } @@ -363,8 +379,11 @@ function setUpBuildTimeVariables( ): void { const assetPrefix = userNextConfig.assetPrefix || userNextConfig.basePath || ''; const basePath = userNextConfig.basePath ?? ''; + const rewritesTunnelPath = - userSentryOptions.tunnelRoute !== undefined && userNextConfig.output !== 'export' + userSentryOptions.tunnelRoute !== undefined && + userNextConfig.output !== 'export' && + typeof userSentryOptions.tunnelRoute === 'string' ? `${basePath}${userSentryOptions.tunnelRoute}` : undefined; @@ -432,7 +451,7 @@ function getInstrumentationClientFileContents(): string | void { ['src', 'instrumentation-client.ts'], ['src', 'instrumentation-client.js'], ['instrumentation-client.ts'], - ['instrumentation-client.ts'], + ['instrumentation-client.js'], ]; for (const pathSegments of potentialInstrumentationClientFileLocations) { diff --git a/packages/nextjs/test/utils/tunnelRoute.test.ts b/packages/nextjs/test/utils/tunnelRoute.test.ts index fb228375f1e0..8382e66ca7d4 100644 --- a/packages/nextjs/test/utils/tunnelRoute.test.ts +++ b/packages/nextjs/test/utils/tunnelRoute.test.ts @@ -81,3 +81,40 @@ describe('applyTunnelRouteOption()', () => { expect(options.tunnel).toBe('/my-error-monitoring-route?o=2222222&p=3333333&r=us'); }); }); + +describe('Random tunnel route generation', () => { + it('Works when tunnelRoute is true and generates random-looking paths', () => { + globalWithInjectedValues._sentryRewritesTunnelPath = '/abc123def'; // Simulated random path + const options: any = { + dsn: 'https://11111111111111111111111111111111@o2222222.ingest.sentry.io/3333333', + } as BrowserOptions; + + applyTunnelRouteOption(options); + + expect(options.tunnel).toBe('/abc123def?o=2222222&p=3333333'); + expect(options.tunnel).toMatch(/^\/[a-z0-9]+\?o=2222222&p=3333333$/); + }); + + it('Works with region DSNs when tunnelRoute is true', () => { + globalWithInjectedValues._sentryRewritesTunnelPath = '/x7h9k2m'; // Simulated random path + const options: any = { + dsn: 'https://11111111111111111111111111111111@o2222222.ingest.eu.sentry.io/3333333', + } as BrowserOptions; + + applyTunnelRouteOption(options); + + expect(options.tunnel).toBe('/x7h9k2m?o=2222222&p=3333333&r=eu'); + expect(options.tunnel).toMatch(/^\/[a-z0-9]+\?o=2222222&p=3333333&r=eu$/); + }); + + it('Does not apply tunnel when tunnelRoute is false', () => { + globalWithInjectedValues._sentryRewritesTunnelPath = undefined; + const options: any = { + dsn: 'https://11111111111111111111111111111111@o2222222.ingest.sentry.io/3333333', + } as BrowserOptions; + + applyTunnelRouteOption(options); + + expect(options.tunnel).toBeUndefined(); + }); +}); diff --git a/packages/node/package.json b/packages/node/package.json index f06faf863bd6..3f124ed8db48 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -94,9 +94,9 @@ "@opentelemetry/resources": "^1.30.1", "@opentelemetry/sdk-trace-base": "^1.30.1", "@opentelemetry/semantic-conventions": "^1.34.0", - "@prisma/instrumentation": "6.8.2", "@sentry/core": "9.30.0", "@sentry/opentelemetry": "9.30.0", + "@prisma/instrumentation": "6.9.0", "import-in-the-middle": "^1.13.1", "minimatch": "^9.0.0" }, diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 589937b21fd4..cf951c3db8b6 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -139,6 +139,7 @@ export { consoleLoggingIntegration, consoleIntegration, wrapMcpServerWithSentry, + featureFlagsIntegration, } from '@sentry/core'; export type { @@ -158,6 +159,7 @@ export type { Thread, User, Span, + FeatureFlagsIntegration, } from '@sentry/core'; export { logger }; diff --git a/packages/node/src/integrations/tracing/graphql.ts b/packages/node/src/integrations/tracing/graphql.ts index 841baf46754d..5301ad5180b0 100644 --- a/packages/node/src/integrations/tracing/graphql.ts +++ b/packages/node/src/integrations/tracing/graphql.ts @@ -1,4 +1,5 @@ import type { AttributeValue } from '@opentelemetry/api'; +import { SpanStatusCode } from '@opentelemetry/api'; import { GraphQLInstrumentation } from '@opentelemetry/instrumentation-graphql'; import type { IntegrationFn } from '@sentry/core'; import { defineIntegration, getRootSpan, spanToJSON } from '@sentry/core'; @@ -45,9 +46,16 @@ export const instrumentGraphql = generateInstrumentOnce( return { ...options, - responseHook(span) { + responseHook(span, result) { addOriginToSpan(span, 'auto.graphql.otel.graphql'); + // We want to ensure spans are marked as errored if there are errors in the result + // We only do that if the span is not already marked with a status + const resultWithMaybeError = result as { errors?: { message: string }[] }; + if (resultWithMaybeError.errors?.length && !spanToJSON(span).status) { + span.setStatus({ code: SpanStatusCode.ERROR }); + } + const attributes = spanToJSON(span).data; // If operation.name is not set, we fall back to use operation.type only diff --git a/packages/node/src/integrations/tracing/vercelai/index.ts b/packages/node/src/integrations/tracing/vercelai/index.ts index 5c7a0ed5d959..9557d62b5e04 100644 --- a/packages/node/src/integrations/tracing/vercelai/index.ts +++ b/packages/node/src/integrations/tracing/vercelai/index.ts @@ -182,14 +182,12 @@ const _vercelAIIntegration = ((options: VercelAiOptions = {}) => { continue; } - if (attributes[AI_USAGE_COMPLETION_TOKENS_ATTRIBUTE] != undefined) { - attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE] = attributes[AI_USAGE_COMPLETION_TOKENS_ATTRIBUTE]; - delete attributes[AI_USAGE_COMPLETION_TOKENS_ATTRIBUTE]; - } - if (attributes[AI_USAGE_PROMPT_TOKENS_ATTRIBUTE] != undefined) { - attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE] = attributes[AI_USAGE_PROMPT_TOKENS_ATTRIBUTE]; - delete attributes[AI_USAGE_PROMPT_TOKENS_ATTRIBUTE]; - } + renameAttributeKey( + attributes, + AI_USAGE_COMPLETION_TOKENS_ATTRIBUTE, + GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE, + ); + renameAttributeKey(attributes, AI_USAGE_PROMPT_TOKENS_ATTRIBUTE, GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE); if ( typeof attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE] === 'number' && typeof attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE] === 'number' @@ -199,22 +197,10 @@ const _vercelAIIntegration = ((options: VercelAiOptions = {}) => { } // Rename AI SDK attributes to standardized gen_ai attributes - if (attributes[AI_PROMPT_MESSAGES_ATTRIBUTE] != undefined) { - attributes['gen_ai.request.messages'] = attributes[AI_PROMPT_MESSAGES_ATTRIBUTE]; - delete attributes[AI_PROMPT_MESSAGES_ATTRIBUTE]; - } - if (attributes[AI_RESPONSE_TEXT_ATTRIBUTE] != undefined) { - attributes['gen_ai.response.text'] = attributes[AI_RESPONSE_TEXT_ATTRIBUTE]; - delete attributes[AI_RESPONSE_TEXT_ATTRIBUTE]; - } - if (attributes[AI_RESPONSE_TOOL_CALLS_ATTRIBUTE] != undefined) { - attributes['gen_ai.response.tool_calls'] = attributes[AI_RESPONSE_TOOL_CALLS_ATTRIBUTE]; - delete attributes[AI_RESPONSE_TOOL_CALLS_ATTRIBUTE]; - } - if (attributes[AI_PROMPT_TOOLS_ATTRIBUTE] != undefined) { - attributes['gen_ai.request.available_tools'] = attributes[AI_PROMPT_TOOLS_ATTRIBUTE]; - delete attributes[AI_PROMPT_TOOLS_ATTRIBUTE]; - } + renameAttributeKey(attributes, AI_PROMPT_MESSAGES_ATTRIBUTE, 'gen_ai.request.messages'); + renameAttributeKey(attributes, AI_RESPONSE_TEXT_ATTRIBUTE, 'gen_ai.response.text'); + renameAttributeKey(attributes, AI_RESPONSE_TOOL_CALLS_ATTRIBUTE, 'gen_ai.response.tool_calls'); + renameAttributeKey(attributes, AI_PROMPT_TOOLS_ATTRIBUTE, 'gen_ai.request.available_tools'); } } @@ -274,3 +260,14 @@ const _vercelAIIntegration = ((options: VercelAiOptions = {}) => { * }); */ export const vercelAIIntegration = defineIntegration(_vercelAIIntegration); + +/** + * Renames an attribute key in the provided attributes object if the old key exists. + * This function safely handles null and undefined values. + */ +function renameAttributeKey(attributes: Record, oldKey: string, newKey: string): void { + if (attributes[oldKey] != null) { + attributes[newKey] = attributes[oldKey]; + delete attributes[oldKey]; + } +} diff --git a/packages/node/test/integration/scope.test.ts b/packages/node/test/integration/scope.test.ts index 43af4214980d..6f2acaf267ee 100644 --- a/packages/node/test/integration/scope.test.ts +++ b/packages/node/test/integration/scope.test.ts @@ -1,7 +1,6 @@ import { getCapturedScopesOnSpan, getCurrentScope } from '@sentry/core'; import { getClient } from '@sentry/opentelemetry'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { clearGlobalScope } from '../../../core/test/lib/clear-global-scope'; import * as Sentry from '../../src/'; import type { NodeClient } from '../../src/sdk/client'; import { cleanupOtel, mockSdkInit, resetGlobals } from '../helpers/mockSdkInit'; @@ -240,7 +239,7 @@ describe('Integration | Scope', () => { describe('global scope', () => { beforeEach(() => { - clearGlobalScope(); + resetGlobals(); }); it('works before calling init', () => { diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index c97a63d39844..d9dbd9a531b6 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -46,9 +46,8 @@ "@sentry/browser": "9.30.0", "@sentry/core": "9.30.0", "@sentry/node": "9.30.0", - "@sentry/opentelemetry": "9.30.0", - "@sentry/rollup-plugin": "3.5.0", - "@sentry/vite-plugin": "3.2.4", + "@sentry/rollup-plugin": "^3.5.0", + "@sentry/vite-plugin": "^3.5.0", "@sentry/vue": "9.30.0" }, "devDependencies": { diff --git a/packages/opentelemetry/src/propagator.ts b/packages/opentelemetry/src/propagator.ts index b5e493b31fd8..30429a490b14 100644 --- a/packages/opentelemetry/src/propagator.ts +++ b/packages/opentelemetry/src/propagator.ts @@ -2,7 +2,7 @@ import type { Baggage, Context, Span, SpanContext, TextMapGetter, TextMapSetter import { context, INVALID_TRACEID, propagation, trace, TraceFlags } from '@opentelemetry/api'; import { isTracingSuppressed, W3CBaggagePropagator } from '@opentelemetry/core'; import { ATTR_URL_FULL, SEMATTRS_HTTP_URL } from '@opentelemetry/semantic-conventions'; -import type { continueTrace, DynamicSamplingContext, Options } from '@sentry/core'; +import type { Client, continueTrace, DynamicSamplingContext, Options, Scope } from '@sentry/core'; import { generateSentryTraceHeader, getClient, @@ -152,8 +152,12 @@ export function shouldPropagateTraceForUrl( /** * Get propagation injection data for the given context. + * The additional options can be passed to override the scope and client that is otherwise derived from the context. */ -export function getInjectionData(context: Context): { +export function getInjectionData( + context: Context, + options: { scope?: Scope; client?: Client } = {}, +): { dynamicSamplingContext: Partial | undefined; traceId: string | undefined; spanId: string | undefined; @@ -190,8 +194,8 @@ export function getInjectionData(context: Context): { // Else we try to use the propagation context from the scope // The only scenario where this should happen is when we neither have a span, nor an incoming trace - const scope = getScopesFromContext(context)?.scope || getCurrentScope(); - const client = getClient(); + const scope = options.scope || getScopesFromContext(context)?.scope || getCurrentScope(); + const client = options.client || getClient(); const propagationContext = scope.getPropagationContext(); const dynamicSamplingContext = client ? getDynamicSamplingContextFromScope(client, scope) : undefined; diff --git a/packages/opentelemetry/src/spanExporter.ts b/packages/opentelemetry/src/spanExporter.ts index 3c6b41de60f5..6430f0f23da5 100644 --- a/packages/opentelemetry/src/spanExporter.ts +++ b/packages/opentelemetry/src/spanExporter.ts @@ -14,6 +14,7 @@ import type { import { captureEvent, convertSpanLinksForEnvelope, + debounce, getCapturedScopesOnSpan, getDynamicSamplingContextFromSpan, getStatusMessage, @@ -49,8 +50,6 @@ interface FinishedSpanBucket { * A Sentry-specific exporter that converts OpenTelemetry Spans to Sentry Spans & Transactions. */ export class SentrySpanExporter { - private _flushTimeout: ReturnType | undefined; - /* * A quick explanation on the buckets: We do bucketing of finished spans for efficiency. This span exporter is * accumulating spans until a root span is encountered and then it flushes all the spans that are descendants of that @@ -74,6 +73,8 @@ export class SentrySpanExporter { // Essentially a a set of span ids that are already sent. The values are expiration // times in this cache so we don't hold onto them indefinitely. private _sentSpans: Map; + /* Internally, we use a debounced flush to give some wiggle room to the span processor to accumulate more spans. */ + private _debouncedFlush: ReturnType; public constructor(options?: { /** Lower bound of time in seconds until spans that are buffered but have not been sent as part of a transaction get cleared from memory. */ @@ -84,50 +85,13 @@ export class SentrySpanExporter { this._lastCleanupTimestampInS = Math.floor(Date.now() / 1000); this._spansToBucketEntry = new WeakMap(); this._sentSpans = new Map(); + this._debouncedFlush = debounce(this.flush.bind(this), 1, { maxWait: 100 }); } /** - * Check if a span with the given ID has already been sent using the `_sentSpans` as a cache. - * Purges "expired" spans from the cache upon checking. - * @param spanId The span id to check. - * @returns Whether the span is already sent in the past X seconds. + * Export a single span. + * This is called by the span processor whenever a span is ended. */ - public isSpanAlreadySent(spanId: string): boolean { - const expirationTime = this._sentSpans.get(spanId); - if (expirationTime) { - if (Date.now() >= expirationTime) { - this._sentSpans.delete(spanId); // Remove expired span - } else { - return true; - } - } - return false; - } - - /** Remove "expired" span id entries from the _sentSpans cache. */ - public flushSentSpanCache(): void { - const currentTimestamp = Date.now(); - // Note, it is safe to delete items from the map as we go: https://stackoverflow.com/a/35943995/90297 - for (const [spanId, expirationTime] of this._sentSpans.entries()) { - if (expirationTime <= currentTimestamp) { - this._sentSpans.delete(spanId); - } - } - } - - /** Check if a node is a completed root node or a node whose parent has already been sent */ - public nodeIsCompletedRootNode(node: SpanNode): node is SpanNodeCompleted { - return !!node.span && (!node.parentNode || this.isSpanAlreadySent(node.parentNode.id)); - } - - /** Get all completed root nodes from a list of nodes */ - public getCompletedRootNodes(nodes: SpanNode[]): SpanNodeCompleted[] { - // TODO: We should be able to remove the explicit `node is SpanNodeCompleted` type guard - // once we stop supporting TS < 5.5 - return nodes.filter((node): node is SpanNodeCompleted => this.nodeIsCompletedRootNode(node)); - } - - /** Export a single span. */ public export(span: ReadableSpan): void { const currentTimestampInS = Math.floor(Date.now() / 1000); @@ -159,26 +123,20 @@ export class SentrySpanExporter { // If the span doesn't have a local parent ID (it's a root span), we're gonna flush all the ended spans const localParentId = getLocalParentId(span); - if (!localParentId || this.isSpanAlreadySent(localParentId)) { - this._clearTimeout(); - - // If we got a parent span, we try to send the span tree - // Wait a tick for this, to ensure we avoid race conditions - this._flushTimeout = setTimeout(() => { - this.flush(); - }, 1); + if (!localParentId || this._sentSpans.has(localParentId)) { + this._debouncedFlush(); } } - /** Try to flush any pending spans immediately. */ + /** + * Try to flush any pending spans immediately. + * This is called internally by the exporter (via _debouncedFlush), + * but can also be triggered externally if we force-flush. + */ public flush(): void { - this._clearTimeout(); + const finishedSpans = this._finishedSpanBuckets.flatMap(bucket => (bucket ? Array.from(bucket.spans) : [])); - const finishedSpans: ReadableSpan[] = this._finishedSpanBuckets.flatMap(bucket => - bucket ? Array.from(bucket.spans) : [], - ); - - this.flushSentSpanCache(); + this._flushSentSpanCache(); const sentSpans = this._maybeSend(finishedSpans); const sentSpanCount = sentSpans.size; @@ -197,20 +155,20 @@ export class SentrySpanExporter { bucketEntry.spans.delete(span); } } + // Cancel a pending debounced flush, if there is one + // This can be relevant if we directly flush, circumventing the debounce + // in that case, we want to cancel any pending debounced flush + this._debouncedFlush.cancel(); } - /** Clear the exporter. */ + /** + * Clear the exporter. + * This is called when the span processor is shut down. + */ public clear(): void { this._finishedSpanBuckets = this._finishedSpanBuckets.fill(undefined); - this._clearTimeout(); - } - - /** Clear the flush timeout. */ - private _clearTimeout(): void { - if (this._flushTimeout) { - clearTimeout(this._flushTimeout); - this._flushTimeout = undefined; - } + this._sentSpans.clear(); + this._debouncedFlush.cancel(); } /** @@ -226,7 +184,7 @@ export class SentrySpanExporter { const grouped = groupSpansWithParents(spans); const sentSpans = new Set(); - const rootNodes = this.getCompletedRootNodes(grouped); + const rootNodes = this._getCompletedRootNodes(grouped); for (const root of rootNodes) { const span = root.span; @@ -257,6 +215,29 @@ export class SentrySpanExporter { return sentSpans; } + + /** Remove "expired" span id entries from the _sentSpans cache. */ + private _flushSentSpanCache(): void { + const currentTimestamp = Date.now(); + // Note, it is safe to delete items from the map as we go: https://stackoverflow.com/a/35943995/90297 + for (const [spanId, expirationTime] of this._sentSpans.entries()) { + if (expirationTime <= currentTimestamp) { + this._sentSpans.delete(spanId); + } + } + } + + /** Check if a node is a completed root node or a node whose parent has already been sent */ + private _nodeIsCompletedRootNodeOrHasSentParent(node: SpanNode): node is SpanNodeCompleted { + return !!node.span && (!node.parentNode || this._sentSpans.has(node.parentNode.id)); + } + + /** Get all completed root nodes from a list of nodes */ + private _getCompletedRootNodes(nodes: SpanNode[]): SpanNodeCompleted[] { + // TODO: We should be able to remove the explicit `node is SpanNodeCompleted` type guard + // once we stop supporting TS < 5.5 + return nodes.filter((node): node is SpanNodeCompleted => this._nodeIsCompletedRootNodeOrHasSentParent(node)); + } } function parseSpan(span: ReadableSpan): { op?: string; origin?: SpanOrigin; source?: TransactionSource } { diff --git a/packages/opentelemetry/src/utils/getTraceData.ts b/packages/opentelemetry/src/utils/getTraceData.ts index 88ab5a349cdf..3c41c4f949de 100644 --- a/packages/opentelemetry/src/utils/getTraceData.ts +++ b/packages/opentelemetry/src/utils/getTraceData.ts @@ -1,5 +1,5 @@ import * as api from '@opentelemetry/api'; -import type { SerializedTraceData, Span } from '@sentry/core'; +import type { Client, Scope, SerializedTraceData, Span } from '@sentry/core'; import { dynamicSamplingContextToSentryBaggageHeader, generateSentryTraceHeader, @@ -12,8 +12,12 @@ import { getContextFromScope } from './contextData'; * Otel-specific implementation of `getTraceData`. * @see `@sentry/core` version of `getTraceData` for more information */ -export function getTraceData({ span }: { span?: Span } = {}): SerializedTraceData { - let ctx = api.context.active(); +export function getTraceData({ + span, + scope, + client, +}: { span?: Span; scope?: Scope; client?: Client } = {}): SerializedTraceData { + let ctx = (scope && getContextFromScope(scope)) ?? api.context.active(); if (span) { const { scope } = getCapturedScopesOnSpan(span); @@ -21,7 +25,7 @@ export function getTraceData({ span }: { span?: Span } = {}): SerializedTraceDat ctx = (scope && getContextFromScope(scope)) || api.trace.setSpan(api.context.active(), span); } - const { traceId, spanId, sampled, dynamicSamplingContext } = getInjectionData(ctx); + const { traceId, spanId, sampled, dynamicSamplingContext } = getInjectionData(ctx, { scope, client }); return { 'sentry-trace': generateSentryTraceHeader(traceId, spanId, sampled), diff --git a/packages/opentelemetry/test/utils/getTraceData.test.ts b/packages/opentelemetry/test/utils/getTraceData.test.ts index a4fc5919e1b6..d94e86930619 100644 --- a/packages/opentelemetry/test/utils/getTraceData.test.ts +++ b/packages/opentelemetry/test/utils/getTraceData.test.ts @@ -1,9 +1,10 @@ import { context, trace } from '@opentelemetry/api'; -import { getCurrentScope, setAsyncContextStrategy } from '@sentry/core'; +import { getCurrentScope, Scope, setAsyncContextStrategy } from '@sentry/core'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { getTraceData } from '../../src/utils/getTraceData'; import { makeTraceState } from '../../src/utils/makeTraceState'; import { cleanupOtel, mockSdkInit } from '../helpers/mockSdkInit'; +import { getDefaultTestClientOptions, TestClient } from '../helpers/TestClient'; describe('getTraceData', () => { beforeEach(() => { @@ -52,6 +53,32 @@ describe('getTraceData', () => { }); }); + it('allows to pass a scope & client directly', () => { + getCurrentScope().setPropagationContext({ + traceId: '12345678901234567890123456789099', + sampleRand: 0.44, + }); + + const customClient = new TestClient( + getDefaultTestClientOptions({ tracesSampleRate: 1, dsn: 'https://123@sentry.io/42' }), + ); + + // note: Right now, this only works properly if the scope is linked to a context + const scope = new Scope(); + scope.setPropagationContext({ + traceId: '12345678901234567890123456789012', + sampleRand: 0.42, + }); + scope.setClient(customClient); + + const traceData = getTraceData({ client: customClient, scope }); + + expect(traceData['sentry-trace']).toMatch(/^12345678901234567890123456789012-[a-f0-9]{16}$/); + expect(traceData.baggage).toEqual( + 'sentry-environment=production,sentry-public_key=123,sentry-trace_id=12345678901234567890123456789012', + ); + }); + it('returns propagationContext DSC data if no span is available', () => { getCurrentScope().setPropagationContext({ traceId: '12345678901234567890123456789012', diff --git a/packages/pino-transport/.eslintrc.js b/packages/pino-transport/.eslintrc.js new file mode 100644 index 000000000000..01c6be4c7080 --- /dev/null +++ b/packages/pino-transport/.eslintrc.js @@ -0,0 +1,12 @@ +module.exports = { + env: { + node: true, + }, + extends: ['../../.eslintrc.js'], + overrides: [ + { + files: ['src/**/*.ts'], + rules: {}, + }, + ], +}; diff --git a/packages/pino-transport/LICENSE b/packages/pino-transport/LICENSE new file mode 100644 index 000000000000..5251db3eaaca --- /dev/null +++ b/packages/pino-transport/LICENSE @@ -0,0 +1,16 @@ +MIT License + +Copyright (c) 2025 Functional Software, Inc. dba Sentry + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/pino-transport/README.md b/packages/pino-transport/README.md new file mode 100644 index 000000000000..45480f802cb1 --- /dev/null +++ b/packages/pino-transport/README.md @@ -0,0 +1,31 @@ +# @sentry/pino-transport + +[![npm version](https://img.shields.io/npm/v/@sentry/pino-transport.svg)](https://www.npmjs.com/package/@sentry/pino-transport) +[![npm dm](https://img.shields.io/npm/dm/@sentry/pino-transport.svg)](https://www.npmjs.com/package/@sentry/pino-transport) +[![npm dt](https://img.shields.io/npm/dt/@sentry/pino-transport.svg)](https://www.npmjs.com/package/@sentry/pino-transport) + +**This package is currently in alpha. Breaking changes may still occur.** + +A Pino transport for integrating [Pino](https://github.com/pinojs/pino) logging with [Sentry](https://sentry.io). This transport automatically captures log messages as Sentry events and breadcrumbs, making it easy to monitor your application's logs in Sentry. + +## Installation + +```bash +npm install @sentry/node @sentry/pino-transport +# or +yarn add @sentry/node @sentry/pino-transport +``` + +## Usage + +TODO: Add usage instructions + +## Requirements + +- Node.js 18 or higher +- Pino 8.0.0 or higher +- @sentry/node must be configured in your application + +## License + +MIT diff --git a/packages/pino-transport/package.json b/packages/pino-transport/package.json new file mode 100644 index 000000000000..0fdf499c306c --- /dev/null +++ b/packages/pino-transport/package.json @@ -0,0 +1,77 @@ +{ + "name": "@sentry/pino-transport", + "version": "9.30.0", + "description": "Pino transport for Sentry SDK", + "repository": "git://github.com/getsentry/sentry-javascript.git", + "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/pino-transport", + "author": "Sentry", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "files": [ + "/build" + ], + "main": "build/cjs/index.js", + "module": "build/esm/index.js", + "types": "build/types/index.d.ts", + "exports": { + "./package.json": "./package.json", + ".": { + "import": { + "types": "./build/types/index.d.ts", + "default": "./build/esm/index.js" + }, + "require": { + "types": "./build/types/index.d.ts", + "default": "./build/cjs/index.js" + } + } + }, + "typesVersions": { + "<5.0": { + "build/types/index.d.ts": [ + "build/types-ts3.8/index.d.ts" + ] + } + }, + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@sentry/core": "9.30.0" + }, + "peerDependencies": { + "pino": "^8.0.0 || ^9.0.0" + }, + "devDependencies": { + "@types/node": "^18.19.1", + "pino": "^9.0.0" + }, + "scripts": { + "build": "run-p build:transpile build:types", + "build:dev": "yarn build", + "build:transpile": "rollup -c rollup.npm.config.mjs", + "build:types": "run-s build:types:core build:types:downlevel", + "build:types:core": "tsc -p tsconfig.types.json", + "build:types:downlevel": "yarn downlevel-dts build/types build/types-ts3.8 --to ts3.8", + "build:watch": "run-p build:transpile:watch build:types:watch", + "build:dev:watch": "yarn build:watch", + "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", + "build:types:watch": "tsc -p tsconfig.types.json --watch", + "build:tarball": "npm pack", + "circularDepCheck": "madge --circular src/index.ts", + "clean": "rimraf build coverage sentry-pino-transport-*.tgz", + "fix": "eslint . --format stylish --fix", + "lint": "eslint . --format stylish", + "lint:es-compatibility": "es-check es2022 ./build/cjs/*.js && es-check es2022 ./build/esm/*.js --module", + "test": "yarn test:unit", + "test:unit": "vitest run", + "test:watch": "vitest --watch", + "yalc:publish": "yalc publish --push --sig" + }, + "volta": { + "extends": "../../package.json" + }, + "sideEffects": false +} diff --git a/packages/pino-transport/rollup.npm.config.mjs b/packages/pino-transport/rollup.npm.config.mjs new file mode 100644 index 000000000000..84a06f2fb64a --- /dev/null +++ b/packages/pino-transport/rollup.npm.config.mjs @@ -0,0 +1,3 @@ +import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; + +export default makeNPMConfigVariants(makeBaseNPMConfig()); diff --git a/packages/pino-transport/src/index.ts b/packages/pino-transport/src/index.ts new file mode 100644 index 000000000000..849aa99b4f7c --- /dev/null +++ b/packages/pino-transport/src/index.ts @@ -0,0 +1,2 @@ +// TODO: Implement this +export {}; diff --git a/packages/pino-transport/test/index.test.ts b/packages/pino-transport/test/index.test.ts new file mode 100644 index 000000000000..9329d9cbaede --- /dev/null +++ b/packages/pino-transport/test/index.test.ts @@ -0,0 +1,8 @@ +import { describe, expect, it } from 'vitest'; +import * as index from '../src'; + +describe('pino-transport', () => { + it('should be defined', () => { + expect(index).toBeDefined(); + }); +}); diff --git a/packages/pino-transport/tsconfig.json b/packages/pino-transport/tsconfig.json new file mode 100644 index 000000000000..b9683a850600 --- /dev/null +++ b/packages/pino-transport/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + + "include": ["src/**/*"], + + "compilerOptions": { + "lib": ["es2018", "es2020.string"], + "module": "Node16" + } +} diff --git a/packages/pino-transport/tsconfig.test.json b/packages/pino-transport/tsconfig.test.json new file mode 100644 index 000000000000..4c24dbbea96e --- /dev/null +++ b/packages/pino-transport/tsconfig.test.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "include": ["test/**/*", "src/**/*", "vite.config.ts"], + "compilerOptions": { + "types": ["vitest/globals", "node"] + } +} diff --git a/packages/pino-transport/tsconfig.types.json b/packages/pino-transport/tsconfig.types.json new file mode 100644 index 000000000000..f35cdd6b5d81 --- /dev/null +++ b/packages/pino-transport/tsconfig.types.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "build/types", + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "stripInternal": true + } +} diff --git a/packages/pino-transport/vite.config.ts b/packages/pino-transport/vite.config.ts new file mode 100644 index 000000000000..4ac6027d5789 --- /dev/null +++ b/packages/pino-transport/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'node', + }, +}); diff --git a/packages/react-router/package.json b/packages/react-router/package.json index 9a3e4954c07c..98f4906546ec 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -43,7 +43,7 @@ "@sentry/core": "9.30.0", "@sentry/node": "9.30.0", "@sentry/react": "9.30.0", - "@sentry/vite-plugin": "^3.2.4", + "@sentry/vite-plugin": "^3.5.0", "glob": "11.0.1" }, "devDependencies": { diff --git a/packages/remix/package.json b/packages/remix/package.json index c63cdd69e82f..102eb9f226d6 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -71,7 +71,6 @@ "@sentry/cli": "^2.46.0", "@sentry/core": "9.30.0", "@sentry/node": "9.30.0", - "@sentry/opentelemetry": "9.30.0", "@sentry/react": "9.30.0", "glob": "^10.3.4", "yargs": "^17.6.0" diff --git a/packages/remix/src/cloudflare/index.ts b/packages/remix/src/cloudflare/index.ts index 3d3d17e1da27..46c443cac39f 100644 --- a/packages/remix/src/cloudflare/index.ts +++ b/packages/remix/src/cloudflare/index.ts @@ -32,6 +32,7 @@ export type { EventHint, ErrorEvent, Exception, + FeatureFlagsIntegration, Session, SeverityLevel, Span, @@ -109,4 +110,5 @@ export { spanToTraceHeader, spanToBaggageHeader, updateSpanName, + featureFlagsIntegration, } from '@sentry/core'; diff --git a/packages/remix/test/integration/package.json b/packages/remix/test/integration/package.json index 7f4172b0b42f..4e15e7de7398 100644 --- a/packages/remix/test/integration/package.json +++ b/packages/remix/test/integration/package.json @@ -27,7 +27,6 @@ "@sentry/browser": "file:../../../browser", "@sentry/core": "file:../../../core", "@sentry/node": "file:../../../node", - "@sentry/opentelemetry": "file:../../../opentelemetry", "@sentry/react": "file:../../../react", "@sentry-internal/browser-utils": "file:../../../browser-utils", "@sentry-internal/replay": "file:../../../replay-internal", diff --git a/packages/replay-internal/src/util/debounce.ts b/packages/replay-internal/src/util/debounce.ts index 8948b937febd..ea357ff1885c 100644 --- a/packages/replay-internal/src/util/debounce.ts +++ b/packages/replay-internal/src/util/debounce.ts @@ -1,3 +1,4 @@ +import { debounce as debounceCore } from '@sentry/core'; import { setTimeout } from '@sentry-internal/browser-utils'; type DebouncedCallback = { @@ -27,46 +28,9 @@ type DebounceOptions = { maxWait?: number }; * - `cancel`: Cancels the debouncing process and resets the debouncing timer */ export function debounce(func: CallbackFunction, wait: number, options?: DebounceOptions): DebouncedCallback { - let callbackReturnValue: unknown; - - let timerId: ReturnType | undefined; - let maxTimerId: ReturnType | undefined; - - const maxWait = options?.maxWait ? Math.max(options.maxWait, wait) : 0; - - function invokeFunc(): unknown { - cancelTimers(); - callbackReturnValue = func(); - return callbackReturnValue; - } - - function cancelTimers(): void { - timerId !== undefined && clearTimeout(timerId); - maxTimerId !== undefined && clearTimeout(maxTimerId); - timerId = maxTimerId = undefined; - } - - function flush(): unknown { - if (timerId !== undefined || maxTimerId !== undefined) { - return invokeFunc(); - } - return callbackReturnValue; - } - - function debounced(): unknown { - if (timerId) { - clearTimeout(timerId); - } - timerId = setTimeout(invokeFunc, wait); - - if (maxWait && maxTimerId === undefined) { - maxTimerId = setTimeout(invokeFunc, maxWait); - } - - return callbackReturnValue; - } - - debounced.cancel = cancelTimers; - debounced.flush = flush; - return debounced; + return debounceCore(func, wait, { + ...options, + // @ts-expect-error - Not quite sure why these types do not match, but this is fine + setTimeoutImpl: setTimeout, + }); } diff --git a/packages/solidstart/package.json b/packages/solidstart/package.json index c1d8f7d88022..1d6033ee6f9a 100644 --- a/packages/solidstart/package.json +++ b/packages/solidstart/package.json @@ -68,7 +68,6 @@ "dependencies": { "@sentry/core": "9.30.0", "@sentry/node": "9.30.0", - "@sentry/opentelemetry": "9.30.0", "@sentry/solid": "9.30.0", "@sentry/vite-plugin": "2.22.6" }, diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json index ba7a4afd4a0c..c80789eb32f4 100644 --- a/packages/sveltekit/package.json +++ b/packages/sveltekit/package.json @@ -51,9 +51,8 @@ "@sentry/cloudflare": "9.30.0", "@sentry/core": "9.30.0", "@sentry/node": "9.30.0", - "@sentry/opentelemetry": "9.30.0", "@sentry/svelte": "9.30.0", - "@sentry/vite-plugin": "3.2.4", + "@sentry/vite-plugin": "^3.5.0", "magic-string": "0.30.7", "recast": "0.23.11", "sorcery": "1.0.0" diff --git a/packages/sveltekit/src/worker/index.ts b/packages/sveltekit/src/worker/index.ts index 3614922072ec..2dde8c61a1dc 100644 --- a/packages/sveltekit/src/worker/index.ts +++ b/packages/sveltekit/src/worker/index.ts @@ -82,6 +82,8 @@ export { supabaseIntegration, instrumentSupabaseClient, zodErrorsIntegration, + featureFlagsIntegration, + type FeatureFlagsIntegration, } from '@sentry/cloudflare'; /** diff --git a/packages/tanstackstart-react/package.json b/packages/tanstackstart-react/package.json index ccfbefa37bbb..2abd5e0fbedc 100644 --- a/packages/tanstackstart-react/package.json +++ b/packages/tanstackstart-react/package.json @@ -55,7 +55,6 @@ "@sentry-internal/browser-utils": "9.30.0", "@sentry/core": "9.30.0", "@sentry/node": "9.30.0", - "@sentry/opentelemetry": "9.30.0", "@sentry/react": "9.30.0" }, "scripts": { diff --git a/packages/vercel-edge/src/index.ts b/packages/vercel-edge/src/index.ts index ff1231c8a1f8..303d40144ec3 100644 --- a/packages/vercel-edge/src/index.ts +++ b/packages/vercel-edge/src/index.ts @@ -8,6 +8,7 @@ export type { EventHint, ErrorEvent, Exception, + FeatureFlagsIntegration, Session, SeverityLevel, Span, @@ -90,6 +91,7 @@ export { spanToBaggageHeader, wrapMcpServerWithSentry, consoleLoggingIntegration, + featureFlagsIntegration, } from '@sentry/core'; export { VercelEdgeClient } from './client'; diff --git a/yarn.lock b/yarn.lock index 0deb47ad0828..5c958751e59d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5878,10 +5878,10 @@ resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.28.tgz#d45e01c4a56f143ee69c54dd6b12eade9e270a73" integrity sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw== -"@prisma/instrumentation@6.8.2": - version "6.8.2" - resolved "https://registry.yarnpkg.com/@prisma/instrumentation/-/instrumentation-6.8.2.tgz#77a87a37f67ab35eaaf8ff629f889e9e11a465ac" - integrity sha512-5NCTbZjw7a+WIZ/ey6G8SY+YKcyM2zBF0hOT1muvqC9TbVtTCr5Qv3RL/2iNDOzLUHEvo4I1uEfioyfuNOGK8Q== +"@prisma/instrumentation@6.9.0": + version "6.9.0" + resolved "https://registry.yarnpkg.com/@prisma/instrumentation/-/instrumentation-6.9.0.tgz#2de2def755e39847fc1a38687cfa92a3f115b2b1" + integrity sha512-HFfr89v7WEbygdTzh1t171SUMYlkFRTXf48QthDc1cKduEsGIsOdt1QhOlpF7VK+yMg9EXHaXQo5Z8lQ7WtEYA== dependencies: "@opentelemetry/instrumentation" "^0.52.0 || ^0.53.0 || ^0.54.0 || ^0.55.0 || ^0.56.0 || ^0.57.0" @@ -6566,11 +6566,6 @@ resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-2.22.6.tgz#829d6caf2c95c1c46108336de4e1049e6521435e" integrity sha512-V2g1Y1I5eSe7dtUVMBvAJr8BaLRr4CLrgNgtPaZyMT4Rnps82SrZ5zqmEkLXPumlXhLUWR6qzoMNN2u+RXVXfQ== -"@sentry/babel-plugin-component-annotate@3.2.4": - version "3.2.4" - resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-3.2.4.tgz#c0877df6e5ce227bf51754bf27da2fa5227af847" - integrity sha512-yBzRn3GEUSv1RPtE4xB4LnuH74ZxtdoRJ5cmQ9i6mzlmGDxlrnKuvem5++AolZTE9oJqAD3Tx2rd1PqmpWnLoA== - "@sentry/babel-plugin-component-annotate@3.5.0": version "3.5.0" resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-3.5.0.tgz#1b0d01f903b725da876117d551610085c3dd21c7" @@ -6590,20 +6585,6 @@ magic-string "0.30.8" unplugin "1.0.1" -"@sentry/bundler-plugin-core@3.2.4": - version "3.2.4" - resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-3.2.4.tgz#4d892490be3cbb127c7c4ed00fcd1b525129fb1e" - integrity sha512-YMj9XW5W2JA89EeweE7CPKLDz245LBsI1JhCmqpt/bjSvmsSIAAPsLYnvIJBS3LQFm0OhtG8NB54PTi96dAcMA== - dependencies: - "@babel/core" "^7.18.5" - "@sentry/babel-plugin-component-annotate" "3.2.4" - "@sentry/cli" "2.42.2" - dotenv "^16.3.1" - find-up "^5.0.0" - glob "^9.3.2" - magic-string "0.30.8" - unplugin "1.0.1" - "@sentry/bundler-plugin-core@3.5.0": version "3.5.0" resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-3.5.0.tgz#b62af5be1b1a862e7062181655829c556c7d7c0b" @@ -6732,7 +6713,7 @@ "@sentry/cli-win32-i686" "2.46.0" "@sentry/cli-win32-x64" "2.46.0" -"@sentry/rollup-plugin@3.5.0": +"@sentry/rollup-plugin@^3.5.0": version "3.5.0" resolved "https://registry.yarnpkg.com/@sentry/rollup-plugin/-/rollup-plugin-3.5.0.tgz#9015c48e00257f8440597167498499804371329b" integrity sha512-aMPCvdNMkv//LZYjYCJsEcNiNiaQFinBO75+9NJVEe1OrKNdGqDi3hky2ll7zuY+xozEtZCZcUKJJz/aAYAS8A== @@ -6748,15 +6729,15 @@ "@sentry/bundler-plugin-core" "2.22.6" unplugin "1.0.1" -"@sentry/vite-plugin@3.2.4", "@sentry/vite-plugin@^3.2.4": - version "3.2.4" - resolved "https://registry.yarnpkg.com/@sentry/vite-plugin/-/vite-plugin-3.2.4.tgz#da87534d645c116ef579161d44636a34eee41a25" - integrity sha512-ZRn5TLlq5xtwKOqaWP+XqS1PYVfbBCgsbMk7wW2Ly6EgF9wYePvtLqKgYnE3hwPg2LpBnRPR2ti1ohlUkR+wXA== +"@sentry/vite-plugin@^3.5.0": + version "3.5.0" + resolved "https://registry.yarnpkg.com/@sentry/vite-plugin/-/vite-plugin-3.5.0.tgz#138fc535c97e69eb8032d57c02aba9c161c7654a" + integrity sha512-jUnpTdpicG8wefamw7eNo2uO+Q3KCbOAiF76xH4gfNHSW6TN2hBfOtmLu7J+ive4c0Al3+NEHz19bIPR0lkwWg== dependencies: - "@sentry/bundler-plugin-core" "3.2.4" + "@sentry/bundler-plugin-core" "3.5.0" unplugin "1.0.1" -"@sentry/webpack-plugin@3.5.0": +"@sentry/webpack-plugin@^3.5.0": version "3.5.0" resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-3.5.0.tgz#cde95534f1e945a4002d47465aeda01d382cd279" integrity sha512-xvclj0QY2HyU7uJLzOlHSrZQBDwfnGKJxp8mmlU4L7CwmK+8xMCqlO7tYZoqE4K/wU3c2xpXql70x8qmvNMxzQ== @@ -10264,6 +10245,11 @@ atob@^2.1.2: resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== +atomic-sleep@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" + integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ== + autoprefixer@^10.4.13, autoprefixer@^10.4.19, autoprefixer@^10.4.20, autoprefixer@^10.4.8: version "10.4.20" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.20.tgz#5caec14d43976ef42e32dcb4bd62878e96be5b3b" @@ -15681,6 +15667,11 @@ fast-printf@^1.6.9: dependencies: boolean "^3.1.4" +fast-redact@^3.1.1: + version "3.5.0" + resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.5.0.tgz#e9ea02f7e57d0cd8438180083e93077e496285e4" + integrity sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A== + fast-safe-stringify@2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" @@ -22405,6 +22396,11 @@ ohash@^1.1.3, ohash@^1.1.4: resolved "https://registry.yarnpkg.com/ohash/-/ohash-1.1.4.tgz#ae8d83014ab81157d2c285abf7792e2995fadd72" integrity sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g== +on-exit-leak-free@^2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz#fed195c9ebddb7d9e4c3842f93f281ac8dadd3b8" + integrity sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA== + on-finished@2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" @@ -23319,6 +23315,35 @@ pinkie@^2.0.0: resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= +pino-abstract-transport@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz#de241578406ac7b8a33ce0d77ae6e8a0b3b68a60" + integrity sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw== + dependencies: + split2 "^4.0.0" + +pino-std-serializers@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz#7c625038b13718dbbd84ab446bd673dc52259e3b" + integrity sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA== + +pino@^9.0.0: + version "9.7.0" + resolved "https://registry.yarnpkg.com/pino/-/pino-9.7.0.tgz#ff7cd86eb3103ee620204dbd5ca6ffda8b53f645" + integrity sha512-vnMCM6xZTb1WDmLvtG2lE/2p+t9hDEIvTWJsu6FejkE62vB7gDhvzrpFR4Cw2to+9JNQxVnkAKVPA1KPB98vWg== + dependencies: + atomic-sleep "^1.0.0" + fast-redact "^3.1.1" + on-exit-leak-free "^2.1.0" + pino-abstract-transport "^2.0.0" + pino-std-serializers "^7.0.0" + process-warning "^5.0.0" + quick-format-unescaped "^4.0.3" + real-require "^0.2.0" + safe-stable-stringify "^2.3.1" + sonic-boom "^4.0.1" + thread-stream "^3.0.0" + pirates@^4.0.1: version "4.0.6" resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9" @@ -24238,6 +24263,11 @@ process-relative-require@^1.0.0: dependencies: node-modules-path "^1.0.0" +process-warning@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-5.0.0.tgz#566e0bf79d1dff30a72d8bbbe9e8ecefe8d378d7" + integrity sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA== + process@^0.11.10: version "0.11.10" resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" @@ -24420,6 +24450,11 @@ queue-tick@^1.0.1: resolved "https://registry.yarnpkg.com/queue-tick/-/queue-tick-1.0.1.tgz#f6f07ac82c1fd60f82e098b417a80e52f1f4c142" integrity sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag== +quick-format-unescaped@^4.0.3: + version "4.0.4" + resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz#93ef6dd8d3453cbc7970dd614fad4c5954d6b5a7" + integrity sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg== + quick-lru@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" @@ -24810,6 +24845,11 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +real-require@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/real-require/-/real-require-0.2.0.tgz#209632dea1810be2ae063a6ac084fee7e33fba78" + integrity sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg== + realistic-structured-clone@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/realistic-structured-clone/-/realistic-structured-clone-3.0.0.tgz#7b518049ce2dad41ac32b421cd297075b00e3e35" @@ -26427,6 +26467,13 @@ solid-use@^0.8.0: resolved "https://registry.yarnpkg.com/solid-use/-/solid-use-0.8.0.tgz#d46258c45edb0f4c621285e0ad1aa6b6a674d79b" integrity sha512-YX+XmcKLvSx3bwMimMhFy40ZkDnShnUcEw6cW6fSscwKEgl1TG3GlgAvkBmQ3AeWjvQSd8+HGTr82ImsrjkkqA== +sonic-boom@^4.0.1: + version "4.2.0" + resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-4.2.0.tgz#e59a525f831210fa4ef1896428338641ac1c124d" + integrity sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww== + dependencies: + atomic-sleep "^1.0.0" + sorcery@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/sorcery/-/sorcery-1.0.0.tgz#b5bb81fb9706c0c240f5f2d3214b4d2be649e07f" @@ -26675,7 +26722,7 @@ split2@^3.2.2: dependencies: readable-stream "^3.0.0" -split2@^4.1.0: +split2@^4.0.0, split2@^4.1.0: version "4.2.0" resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4" integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg== @@ -27148,7 +27195,6 @@ stylus@0.59.0, stylus@^0.59.0: sucrase@^3.27.0, sucrase@^3.35.0, sucrase@getsentry/sucrase#es2020-polyfills: version "3.36.0" - uid fd682f6129e507c00bb4e6319cc5d6b767e36061 resolved "https://codeload.github.com/getsentry/sucrase/tar.gz/fd682f6129e507c00bb4e6319cc5d6b767e36061" dependencies: "@jridgewell/gen-mapping" "^0.3.2" @@ -27566,6 +27612,13 @@ thenify-all@^1.0.0: dependencies: any-promise "^1.0.0" +thread-stream@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-3.1.0.tgz#4b2ef252a7c215064507d4ef70c05a5e2d34c4f1" + integrity sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A== + dependencies: + real-require "^0.2.0" + throttleit@2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-2.1.0.tgz#a7e4aa0bf4845a5bd10daa39ea0c783f631a07b4"