diff --git a/.craft.yml b/.craft.yml index 5efdae48aa01..e8bbfaf3b3b5 100644 --- a/.craft.yml +++ b/.craft.yml @@ -88,6 +88,9 @@ targets: - name: npm id: '@sentry/gatsby' includeNames: /^sentry-gatsby-\d.*\.tgz$/ + - name: npm + id: '@sentry/astro' + includeNames: /^sentry-astro-\d.*\.tgz$/ ## 7. Other Packages ## 7.1 @@ -180,3 +183,5 @@ targets: onlyIfPresent: /^sentry-bun-\d.*\.tgz$/ 'npm:@sentry/vercel-edge': onlyIfPresent: /^sentry-vercel-edge-\d.*\.tgz$/ + 'npm:@sentry/ember': + onlyIfPresent: /^sentry-ember-\d.*\.tgz$/ diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 84c26af7e9b1..3191f92adefb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -356,7 +356,7 @@ jobs: - name: Pack run: yarn build:tarball - name: Archive artifacts - uses: actions/upload-artifact@v3.1.2 + uses: actions/upload-artifact@v3.1.3 with: name: ${{ github.sha }} path: | @@ -379,7 +379,7 @@ jobs: - name: Set up Node uses: actions/setup-node@v3 with: - node-version: ${{ env.DEFAULT_NODE_VERSION }} + node-version-file: 'package.json' - name: Restore caches uses: ./.github/actions/restore-cache env: @@ -406,7 +406,7 @@ jobs: - name: Set up Node uses: actions/setup-node@v3 with: - node-version: ${{ matrix.node }} + node-version-file: 'package.json' - name: Set up Bun uses: oven-sh/setup-bun@v1 - name: Restore caches @@ -419,6 +419,38 @@ jobs: - name: Compute test coverage uses: codecov/codecov-action@v3 + job_deno_unit_tests: + name: Deno Unit Tests + needs: [job_get_metadata, job_build] + timeout-minutes: 10 + runs-on: ubuntu-20.04 + strategy: + fail-fast: false + steps: + - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) + uses: actions/checkout@v4 + with: + ref: ${{ env.HEAD_COMMIT }} + - name: Set up Node + uses: actions/setup-node@v3 + with: + node-version-file: 'package.json' + - name: Set up Deno + uses: denoland/setup-deno@v1.1.3 + with: + deno-version: v1.37.1 + - name: Restore caches + uses: ./.github/actions/restore-cache + env: + DEPENDENCY_CACHE_KEY: ${{ needs.job_build.outputs.dependency_cache_key }} + - name: Run tests + run: | + cd packages/deno + yarn build + yarn test + - name: Compute test coverage + uses: codecov/codecov-action@v3 + job_node_unit_tests: name: Node (${{ matrix.node }}) Unit Tests needs: [job_get_metadata, job_build] @@ -823,6 +855,7 @@ jobs: 'standard-frontend-react-tracing-import', 'sveltekit', 'generic-ts3.8', + 'node-experimental-fastify-app', ] build-command: - false @@ -894,6 +927,7 @@ jobs: job_browser_build_tests, job_browser_unit_tests, job_bun_unit_tests, + job_deno_unit_tests, job_node_unit_tests, job_nextjs_integration_test, job_node_integration_tests, @@ -951,7 +985,7 @@ jobs: GITHUB_TOKEN: ${{ github.token }} - name: Upload results - uses: actions/upload-artifact@v3.1.2 + uses: actions/upload-artifact@v3.1.3 if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository with: name: ${{ steps.process.outputs.artifactName }} diff --git a/.gitignore b/.gitignore index 777b23658572..d6eee47e4eed 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,8 @@ jest/transformers/*.js # node tarballs packages/*/sentry-*.tgz .nxcache +# The Deno types are downloaded before building +packages/deno/lib.deno.d.ts # logs yarn-error.log diff --git a/.vscode/extensions.json b/.vscode/extensions.json index da74f03528af..3ad96b1733d5 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -4,6 +4,7 @@ "recommendations": [ "esbenp.prettier-vscode", "dbaeumer.vscode-eslint", - "augustocdias.tasks-shell-input" - ], + "augustocdias.tasks-shell-input", + "denoland.vscode-deno" + ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index d3c8a08448c6..96bd2dfb42b9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -28,5 +28,6 @@ { "mode": "auto" } - ] + ], + "deno.enablePaths": ["packages/deno/test"] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 70c9c3702fd2..58bd0ab2a355 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,67 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 7.74.0 + +### Important Changes + +- **feat(astro): Add `sentryAstro` integration (#9218)** + +This Release introduces the first alpha version of our new SDK for Astro. +At this time, the SDK is considered experimental and things might break and change in future versions. + +The core of the SDK is an Astro integration which you easily add to your Astro config: + +```js +// astro.config.js +import { defineConfig } from "astro/config"; +import sentry from "@sentry/astro"; + +export default defineConfig({ + integrations: [ + sentry({ + dsn: "__DSN__", + sourceMapsUploadOptions: { + project: "astro", + authToken: process.env.SENTRY_AUTH_TOKEN, + }, + }), + ], +}); +``` + +Check out the [README](./packages/astro/README.md) for usage instructions and what to expect from this alpha release. + +### Other Changes + +- feat(core): Add `addIntegration` utility (#9186) +- feat(core): Add `continueTrace` method (#9164) +- feat(node-experimental): Add NodeFetch integration (#9226) +- feat(node-experimental): Use native OTEL Spans (#9161, #9214) +- feat(node-experimental): Sample in OTEL Sampler (#9203) +- feat(serverlesss): Allow disabling transaction traces (#9154) +- feat(tracing): Allow direct pg module to enable esbuild support (#9227) +- feat(utils): Move common node ANR code to utils (#9191) +- feat(vue): Expose `VueIntegration` to initialize vue app later (#9180) +- fix: Don't set `referrerPolicy` on serverside fetch transports (#9200) +- fix: Ensure we never mutate options passed to `init` (#9162) +- fix(ember): Avoid pulling in utils at build time (#9221) +- fix(ember): Drop undefined config values (#9175) +- fix(node): Ensure mysql integration works without callback (#9222) +- fix(node): Only require `inspector` when needed (#9149) +- fix(node): Remove ANR `debug` option and instead add logger.isEnabled() (#9230) +- fix(node): Strip `.mjs` and `.cjs` extensions from module name (#9231) +- fix(replay): bump rrweb to 2.0.1 (#9240) +- fix(replay): Fix potential broken CSS in styled-components (#9234) +- fix(sveltekit): Flush in server wrappers before exiting (#9153) +- fix(types): Update signature of `processEvent` integration hook (#9151) +- fix(utils): Dereference DOM events after they have servered their purpose (#9224) +- ref(integrations): Refactor pluggable integrations to use `processEvent` (#9021) +- ref(serverless): Properly deprecate `rethrowAfterCapture` option (#9159) +- ref(utils): Deprecate `walk` method (#9157) + +Work in this release contributed by @aldenquimby. Thank you for your contributions! + ## 7.73.0 ### Important Changes diff --git a/package.json b/package.json index 6ae7e4f1d2cf..b5cbf9ddc38e 100644 --- a/package.json +++ b/package.json @@ -27,23 +27,25 @@ "postpublish": "lerna run --stream --concurrency 1 postpublish", "test": "lerna run --ignore \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests,overhead-metrics}\" test", "test:unit": "lerna run --ignore \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests,overhead-metrics}\" test:unit", - "test-ci-browser": "lerna run test --ignore \"@sentry/{bun,node,node-experimental,opentelemetry-node,serverless,nextjs,remix,gatsby,sveltekit,vercel-edge}\" --ignore \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests,overhead-metrics}\"", + "test-ci-browser": "lerna run test --ignore \"@sentry/{bun,deno,node,node-experimental,opentelemetry-node,serverless,nextjs,remix,gatsby,sveltekit,vercel-edge}\" --ignore \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests,overhead-metrics}\"", "test-ci-node": "ts-node ./scripts/node-unit-tests.ts", "test-ci-bun": "lerna run test --scope @sentry/bun", "test:update-snapshots": "lerna run test:update-snapshots", "yalc:publish": "lerna run yalc:publish" }, "volta": { - "node": "16.19.0", + "node": "18.17.0", "yarn": "1.22.19" }, "workspaces": [ "packages/angular", "packages/angular-ivy", + "packages/astro", "packages/browser", "packages/browser-integration-tests", "packages/bun", "packages/core", + "packages/deno", "packages/e2e-tests", "packages/ember", "packages/eslint-config-sdk", @@ -126,7 +128,8 @@ "yalc": "^1.0.0-pre.53" }, "resolutions": { - "**/agent-base": "5" + "**/agent-base": "5", + "**/terser/source-map": "0.7.4" }, "version": "0.0.0", "name": "sentry-javascript" diff --git a/packages/angular-ivy/src/sdk.ts b/packages/angular-ivy/src/sdk.ts index fcbbbce399d0..b2dc1f5f34d8 100644 --- a/packages/angular-ivy/src/sdk.ts +++ b/packages/angular-ivy/src/sdk.ts @@ -1,6 +1,7 @@ import { VERSION } from '@angular/core'; import type { BrowserOptions } from '@sentry/browser'; import { defaultIntegrations, init as browserInit, SDK_VERSION, setContext } from '@sentry/browser'; +import type { SdkMetadata } from '@sentry/types'; import { logger } from '@sentry/utils'; import { IS_DEBUG_BUILD } from './flags'; @@ -9,8 +10,21 @@ import { IS_DEBUG_BUILD } from './flags'; * Inits the Angular SDK */ export function init(options: BrowserOptions): void { - options._metadata = options._metadata || {}; - options._metadata.sdk = { + const opts = { + _metadata: {} as SdkMetadata, + // Filter out TryCatch integration as it interferes with our Angular `ErrorHandler`: + // TryCatch would catch certain errors before they reach the `ErrorHandler` and thus provide a + // lower fidelity error than what `SentryErrorHandler` (see errorhandler.ts) would provide. + // see: + // - https://github.com/getsentry/sentry-javascript/issues/5417#issuecomment-1453407097 + // - https://github.com/getsentry/sentry-javascript/issues/2744 + defaultIntegrations: defaultIntegrations.filter(integration => { + return integration.name !== 'TryCatch'; + }), + ...options, + }; + + opts._metadata.sdk = opts._metadata.sdk || { name: 'sentry.javascript.angular-ivy', packages: [ { @@ -21,20 +35,8 @@ export function init(options: BrowserOptions): void { version: SDK_VERSION, }; - // Filter out TryCatch integration as it interferes with our Angular `ErrorHandler`: - // TryCatch would catch certain errors before they reach the `ErrorHandler` and thus provide a - // lower fidelity error than what `SentryErrorHandler` (see errorhandler.ts) would provide. - // see: - // - https://github.com/getsentry/sentry-javascript/issues/5417#issuecomment-1453407097 - // - https://github.com/getsentry/sentry-javascript/issues/2744 - if (options.defaultIntegrations === undefined) { - options.defaultIntegrations = defaultIntegrations.filter(integration => { - return integration.name !== 'TryCatch'; - }); - } - checkAndSetAngularVersion(); - browserInit(options); + browserInit(opts); } function checkAndSetAngularVersion(): void { diff --git a/packages/angular/src/sdk.ts b/packages/angular/src/sdk.ts index e50cece043d0..975ec24e38d3 100755 --- a/packages/angular/src/sdk.ts +++ b/packages/angular/src/sdk.ts @@ -1,6 +1,7 @@ import { VERSION } from '@angular/core'; import type { BrowserOptions } from '@sentry/browser'; import { defaultIntegrations, init as browserInit, SDK_VERSION, setContext } from '@sentry/browser'; +import type { SdkMetadata } from '@sentry/types'; import { logger } from '@sentry/utils'; import { IS_DEBUG_BUILD } from './flags'; @@ -9,8 +10,21 @@ import { IS_DEBUG_BUILD } from './flags'; * Inits the Angular SDK */ export function init(options: BrowserOptions): void { - options._metadata = options._metadata || {}; - options._metadata.sdk = { + const opts = { + _metadata: {} as SdkMetadata, + // Filter out TryCatch integration as it interferes with our Angular `ErrorHandler`: + // TryCatch would catch certain errors before they reach the `ErrorHandler` and thus provide a + // lower fidelity error than what `SentryErrorHandler` (see errorhandler.ts) would provide. + // see: + // - https://github.com/getsentry/sentry-javascript/issues/5417#issuecomment-1453407097 + // - https://github.com/getsentry/sentry-javascript/issues/2744 + defaultIntegrations: defaultIntegrations.filter(integration => { + return integration.name !== 'TryCatch'; + }), + ...options, + }; + + opts._metadata.sdk = opts._metadata.sdk || { name: 'sentry.javascript.angular', packages: [ { @@ -21,20 +35,8 @@ export function init(options: BrowserOptions): void { version: SDK_VERSION, }; - // Filter out TryCatch integration as it interferes with our Angular `ErrorHandler`: - // TryCatch would catch certain errors before they reach the `ErrorHandler` and thus provide a - // lower fidelity error than what `SentryErrorHandler` (see errorhandler.ts) would provide. - // see: - // - https://github.com/getsentry/sentry-javascript/issues/5417#issuecomment-1453407097 - // - https://github.com/getsentry/sentry-javascript/issues/2744 - if (options.defaultIntegrations === undefined) { - options.defaultIntegrations = defaultIntegrations.filter(integration => { - return integration.name !== 'TryCatch'; - }); - } - checkAndSetAngularVersion(); - browserInit(options); + browserInit(opts); } function checkAndSetAngularVersion(): void { diff --git a/packages/astro/.eslintrc.cjs b/packages/astro/.eslintrc.cjs new file mode 100644 index 000000000000..c706032aaf35 --- /dev/null +++ b/packages/astro/.eslintrc.cjs @@ -0,0 +1,22 @@ +module.exports = { + env: { + browser: true, + node: true, + }, + extends: ['../../.eslintrc.js'], + overrides: [ + { + files: ['vite.config.ts'], + parserOptions: { + project: ['tsconfig.test.json'], + }, + }, + { + files: ['src/integration/**', 'src/server/**'], + rules: { + '@sentry-internal/sdk/no-optional-chaining': 'off', + '@sentry-internal/sdk/no-nullish-coalescing': 'off', + }, + }, + ], +}; diff --git a/packages/astro/.npmignore b/packages/astro/.npmignore new file mode 100644 index 000000000000..ded80d725803 --- /dev/null +++ b/packages/astro/.npmignore @@ -0,0 +1,10 @@ +# The paths in this file are specified so that they align with the file structure in `./build` after this file is copied +# into it by the prepack script `scripts/prepack.ts`. + +* + +!/cjs/**/* +!/esm/**/* +!/types/**/* +!/types-ts3.8/**/* +!/integration/**/* diff --git a/packages/astro/LICENSE b/packages/astro/LICENSE new file mode 100644 index 000000000000..d11896ba1181 --- /dev/null +++ b/packages/astro/LICENSE @@ -0,0 +1,14 @@ +Copyright (c) 2023 Sentry (https://sentry.io) and individual contributors. All rights reserved. + +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/astro/README.md b/packages/astro/README.md new file mode 100644 index 000000000000..a2738298a67c --- /dev/null +++ b/packages/astro/README.md @@ -0,0 +1,125 @@ +

+ + Sentry + +

+ +# Official Sentry SDK for Astro + +[![npm version](https://img.shields.io/npm/v/@sentry/astro.svg)](https://www.npmjs.com/package/@sentry/astro) +[![npm dm](https://img.shields.io/npm/dm/@sentry/astro.svg)](https://www.npmjs.com/package/@sentry/astro) +[![npm dt](https://img.shields.io/npm/dt/@sentry/astro.svg)](https://www.npmjs.com/package/@sentry/astro) + + + +## Experimental Note + +This SDK is experimental and in Alpha state. Breaking changes can occurr at any time. +If you have feedback or encounter any bugs, feel free to [open an issue](https://github.com/getsentry/sentry-javascript/issues/new/choose). + +## General + +This package is a wrapper around `@sentry/node` for the server and `@sentry/browser` for the client side. + +## Installation and Setup + +### 1. Registering the Sentry Astro integration: + +Add the `sentryAstro` integration to your `astro.config.mjs` file: + +```javascript +import { sentryAstro } from "@sentry/astro/integration"; + +export default defineConfig({ + // Rest of your Astro project config + integrations: [ + sentryAstro({ + dsn: '__DSN__', + }), + ], +}) +``` + +This is the easiest way to configure Sentry in an Astro project. +You can pass a few additional options to `sentryAstro` but the SDK comes preconfigured in an opinionated way. +If you want to fully customize your SDK setup, you can do so, too: + +### 2. [Optional] Uploading Source Maps + +To upload source maps to Sentry, simply add the `project` and `authToken` options to `sentryAstro`: + +```js +// astro.config.mjs +import { sentryAstro } from "@sentry/astro/integration"; + +export default defineConfig({ + // Rest of your Astro project config + integrations: [ + sentryAstro({ + dsn: '__DSN__', + project: 'your-project-slug', + authToken: import.meta.env('SENTRY_AUTH_TOKEN'), + }), + ], +}) +``` + +You can also define these values as environment variables in e.g. a `.env` file +or in you CI configuration: + +```sh +SENTRY_PROJECT="your-project" +SENTRY_AUTH_TOKEN="your-token" +``` + +Follow [this guide](https://docs.sentry.io/product/accounts/auth-tokens/#organization-auth-tokens) to create an auth token. + +### 3. [Optional] Advanced Configuration + +To fully customize and configure Sentry in an Astro project, follow step 1 and in addition, +add a `sentry.client.config.(js|ts)` and `sentry.server.config(js|ts)` file to the root directory of your project. +Inside these files, you can call `Sentry.init()` and use the full range of Sentry options. + +Configuring the client SDK: + +```js +// sentry.client.config.ts or sentry.server.config.ts +import * as Sentry from "@sentry/astro"; + +Sentry.init({ + dsn: "__DSN__", + beforeSend(event) { + console.log("Sending event on the client"); + return event; + }, + tracesSampler: () => {/* ... */} +}); +``` + +**Important**: Once you created a sentry config file, the SDK options passed to `sentryAstro` will be ignored for the respective runtime. You can also only define create of the two files. + +#### 3.1 Custom file location + +If you want to move the `sentry.*.config` files to another location, +you can specify the file path, relative to the project root, in `sentryAstro`: + +```js +// astro.config.mjs +import { sentryAstro } from "@sentry/astro/integration"; + +export default defineConfig({ + // Rest of your Astro project config + integrations: [ + sentryAstro({ + dsn: '__DSN__', + clientInitPath: '.config/sentry.client.init.js', + serverInitPath: '.config/sentry.server.init.js', + }), + ], +}) +``` diff --git a/packages/astro/package.json b/packages/astro/package.json new file mode 100644 index 000000000000..c3c193a3a6d1 --- /dev/null +++ b/packages/astro/package.json @@ -0,0 +1,77 @@ +{ + "name": "@sentry/astro", + "version": "7.73.0", + "description": "Official Sentry SDK for Astro", + "repository": "git://github.com/getsentry/sentry-javascript.git", + "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/astro", + "keywords": [ + "withastro", + "astro-component", + "sentry", + "apm" + ], + "author": "Sentry", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "type": "module", + "main": "build/cjs/index.client.js", + "module": "build/esm/index.server.js", + "browser": "build/esm/index.client.js", + "types": "build/types/index.types.d.ts", + "exports": { + ".": { + "node": "./build/esm/index.server.js", + "browser": "./build/esm/index.client.js", + "import": "./build/esm/index.client.js", + "require": "./build/cjs/index.server.js", + "types": "./build/types/index.types.d.ts" + } + }, + "publishConfig": { + "access": "public" + }, + "peerDependencies": { + "astro": "3.x" + }, + "dependencies": { + "@sentry/browser": "7.73.0", + "@sentry/node": "7.73.0", + "@sentry/core": "7.73.0", + "@sentry/utils": "7.73.0", + "@sentry/types": "7.73.0", + "@sentry/vite-plugin": "^2.8.0" + }, + "devDependencies": { + "astro": "^3.2.3", + "rollup": "^3.20.2", + "vite": "4.0.5" + }, + "scripts": { + "build": "run-p build:transpile build:types", + "build:dev": "yarn build", + "build:transpile": "rollup -c rollup.npm.config.js --bundleConfigAsCjs", + "build:types": "tsc -p tsconfig.types.json", + "build:watch": "run-p build:transpile:watch build:types:watch", + "build:dev:watch": "yarn build:watch", + "build:transpile:watch": "rollup -c rollup.npm.config.js --bundleConfigAsCjs --watch", + "build:types:watch": "tsc -p tsconfig.types.json --watch", + "build:tarball": "ts-node ../../scripts/prepack.ts && npm pack ./build", + "circularDepCheck": "madge --circular src/index.client.ts && madge --circular src/index.server.ts && madge --circular src/index.types.ts", + "clean": "rimraf build coverage sentry-astro-*.tgz", + "fix": "run-s fix:eslint fix:prettier", + "fix:eslint": "eslint . --format stylish --fix", + "fix:prettier": "prettier --write \"{src,test,scripts}/**/**.ts\"", + "lint": "run-s lint:prettier lint:eslint", + "lint:eslint": "eslint . --format stylish", + "lint:prettier": "prettier --check \"{src,test,scripts}/**/**.ts\"", + "test": "yarn test:unit", + "test:unit": "vitest run", + "test:watch": "vitest --watch", + "yalc:publish": "ts-node ../../scripts/prepack.ts && yalc publish build --push" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/packages/astro/rollup.npm.config.js b/packages/astro/rollup.npm.config.js new file mode 100644 index 000000000000..06dd0b3e4ec1 --- /dev/null +++ b/packages/astro/rollup.npm.config.js @@ -0,0 +1,17 @@ +import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js'; + +const variants = makeNPMConfigVariants( + makeBaseNPMConfig({ + entrypoints: ['src/index.server.ts', 'src/index.client.ts'], + packageSpecificConfig: { + output: { + dynamicImportInCjs: true, + exports: 'named', + }, + }, + // Astro is Node 18+ no need to add polyfills + addPolyfills: false, + }), +); + +export default variants; diff --git a/packages/astro/scripts/syncIntegration.ts b/packages/astro/scripts/syncIntegration.ts new file mode 100644 index 000000000000..006d9b3237ac --- /dev/null +++ b/packages/astro/scripts/syncIntegration.ts @@ -0,0 +1,20 @@ +/* eslint-disable no-console */ + +import * as fse from 'fs-extra'; +import * as path from 'path'; + +const buildDir = path.resolve('build'); +const srcIntegrationDir = path.resolve(path.join('src', 'integration')); +const destIntegrationDir = path.resolve(path.join(buildDir, 'integration')); + +try { + fse.copySync(srcIntegrationDir, destIntegrationDir, { + filter: (src, _) => { + return !src.endsWith('.md'); + }, + }); + console.log('\nCopied Astro integration to ./build/integration\n'); +} catch (e) { + console.error('\nError while copying integration to build dir:'); + console.error(e); +} diff --git a/packages/astro/src/client/sdk.ts b/packages/astro/src/client/sdk.ts new file mode 100644 index 000000000000..aa32e9dcc095 --- /dev/null +++ b/packages/astro/src/client/sdk.ts @@ -0,0 +1,42 @@ +import type { BrowserOptions } from '@sentry/browser'; +import { BrowserTracing, init as initBrowserSdk } from '@sentry/browser'; +import { configureScope, hasTracingEnabled } from '@sentry/core'; +import { addOrUpdateIntegration } from '@sentry/utils'; + +import { applySdkMetadata } from '../common/metadata'; + +// Treeshakable guard to remove all code related to tracing +declare const __SENTRY_TRACING__: boolean; + +/** + * Initialize the client side of the Sentry Astro SDK. + * + * @param options Configuration options for the SDK. + */ +export function init(options: BrowserOptions): void { + applySdkMetadata(options, ['astro', 'browser']); + + addClientIntegrations(options); + + initBrowserSdk(options); + + configureScope(scope => { + scope.setTag('runtime', 'browser'); + }); +} + +function addClientIntegrations(options: BrowserOptions): void { + let integrations = options.integrations || []; + + // This evaluates to true unless __SENTRY_TRACING__ is text-replaced with "false", + // in which case everything inside will get treeshaken away + if (typeof __SENTRY_TRACING__ === 'undefined' || __SENTRY_TRACING__) { + if (hasTracingEnabled(options)) { + const defaultBrowserTracingIntegration = new BrowserTracing({}); + + integrations = addOrUpdateIntegration(defaultBrowserTracingIntegration, integrations); + } + } + + options.integrations = integrations; +} diff --git a/packages/astro/src/common/metadata.ts b/packages/astro/src/common/metadata.ts new file mode 100644 index 000000000000..ddd53f27362a --- /dev/null +++ b/packages/astro/src/common/metadata.ts @@ -0,0 +1,31 @@ +import { SDK_VERSION } from '@sentry/core'; +import type { Options, SdkInfo } from '@sentry/types'; + +const PACKAGE_NAME_PREFIX = 'npm:@sentry/'; + +/** + * A builder for the SDK metadata in the options for the SDK initialization. + * + * Note: This function is identical to `buildMetadata` in Remix and NextJS and SvelteKit. + * We don't extract it for bundle size reasons. + * @see https://github.com/getsentry/sentry-javascript/pull/7404 + * @see https://github.com/getsentry/sentry-javascript/pull/4196 + * + * If you make changes to this function consider updating the others as well. + * + * @param options SDK options object that gets mutated + * @param names list of package names + */ +export function applySdkMetadata(options: Options, names: string[]): void { + options._metadata = options._metadata || {}; + options._metadata.sdk = + options._metadata.sdk || + ({ + name: 'sentry.javascript.astro', + packages: names.map(name => ({ + name: `${PACKAGE_NAME_PREFIX}${name}`, + version: SDK_VERSION, + })), + version: SDK_VERSION, + } as SdkInfo); +} diff --git a/packages/astro/src/index.client.ts b/packages/astro/src/index.client.ts new file mode 100644 index 000000000000..2b85c05c3af1 --- /dev/null +++ b/packages/astro/src/index.client.ts @@ -0,0 +1,3 @@ +export * from '@sentry/browser'; + +export { init } from './client/sdk'; diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts new file mode 100644 index 000000000000..7a28bf907d48 --- /dev/null +++ b/packages/astro/src/index.server.ts @@ -0,0 +1,65 @@ +// Node SDK exports +// Unfortunately, we cannot `export * from '@sentry/node'` because in prod builds, +// Vite puts these exports into a `default` property (Sentry.default) rather than +// on the top - level namespace. + +import { sentryAstro } from './integration'; + +// Hence, we export everything from the Node SDK explicitly: +export { + addGlobalEventProcessor, + addBreadcrumb, + captureException, + captureEvent, + captureMessage, + captureCheckIn, + configureScope, + createTransport, + extractTraceparentData, + getActiveTransaction, + getHubFromCarrier, + getCurrentHub, + Hub, + makeMain, + Scope, + startTransaction, + SDK_VERSION, + setContext, + setExtra, + setExtras, + setTag, + setTags, + setUser, + spanStatusfromHttpCode, + trace, + withScope, + autoDiscoverNodePerformanceMonitoringIntegrations, + makeNodeTransport, + defaultIntegrations, + defaultStackParser, + lastEventId, + flush, + close, + getSentryRelease, + addRequestDataToEvent, + DEFAULT_USER_INCLUDES, + extractRequestData, + deepReadDirSync, + Integrations, + Handlers, + setMeasurement, + getActiveSpan, + startSpan, + // eslint-disable-next-line deprecation/deprecation + startActiveSpan, + startInactiveSpan, + startSpanManual, + continueTrace, +} from '@sentry/node'; + +// We can still leave this for the carrier init and type exports +export * from '@sentry/node'; + +export { init } from './server/sdk'; + +export default sentryAstro; diff --git a/packages/astro/src/index.types.ts b/packages/astro/src/index.types.ts new file mode 100644 index 000000000000..e8ff7457f597 --- /dev/null +++ b/packages/astro/src/index.types.ts @@ -0,0 +1,25 @@ +/* eslint-disable import/export */ + +// We export everything from both the client part of the SDK and from the server part. +// Some of the exports collide, which is not allowed, unless we redifine the colliding +// exports in this file - which we do below. +export * from './index.client'; +export * from './index.server'; + +import type { Integration, Options, StackParser } from '@sentry/types'; + +import type * as clientSdk from './index.client'; +import type * as serverSdk from './index.server'; + +/** Initializes Sentry Astro SDK */ +export declare function init(options: Options | clientSdk.BrowserOptions | serverSdk.NodeOptions): void; + +// We export a merged Integrations object so that users can (at least typing-wise) use all integrations everywhere. +export declare const Integrations: typeof clientSdk.Integrations & typeof serverSdk.Integrations; + +export declare const defaultIntegrations: Integration[]; +export declare const defaultStackParser: StackParser; + +export declare function close(timeout?: number | undefined): PromiseLike; +export declare function flush(timeout?: number | undefined): PromiseLike; +export declare function lastEventId(): string | undefined; diff --git a/packages/astro/src/integration/index.ts b/packages/astro/src/integration/index.ts new file mode 100644 index 000000000000..0432fb8108d3 --- /dev/null +++ b/packages/astro/src/integration/index.ts @@ -0,0 +1,81 @@ +/* eslint-disable no-console */ +import { sentryVitePlugin } from '@sentry/vite-plugin'; +import type { AstroIntegration } from 'astro'; +import * as fs from 'fs'; +import * as path from 'path'; + +import { buildClientSnippet, buildSdkInitFileImportSnippet, buildServerSnippet } from './snippets'; +import type { SentryOptions } from './types'; + +const PKG_NAME = '@sentry/astro'; + +export const sentryAstro = (options: SentryOptions = {}): AstroIntegration => { + return { + name: PKG_NAME, + hooks: { + 'astro:config:setup': async ({ updateConfig, injectScript }) => { + // The third param here enables loading of all env vars, regardless of prefix + // see: https://main.vitejs.dev/config/#using-environment-variables-in-config + + // TODO: Ideally, we want to load the environment with vite like this: + // const env = loadEnv('production', process.cwd(), ''); + // However, this currently throws a build error. + // Will revisit this later. + const env = process.env; + + const uploadOptions = options.sourceMapsUploadOptions || {}; + + const shouldUploadSourcemaps = uploadOptions?.enabled ?? true; + const authToken = uploadOptions.authToken || env.SENTRY_AUTH_TOKEN; + + if (shouldUploadSourcemaps && authToken) { + updateConfig({ + vite: { + build: { + sourcemap: true, + }, + plugins: [ + sentryVitePlugin({ + org: uploadOptions.org ?? env.SENTRY_ORG, + project: uploadOptions.project ?? env.SENTRY_PROJECT, + authToken: uploadOptions.authToken ?? env.SENTRY_AUTH_TOKEN, + telemetry: uploadOptions.telemetry ?? true, + }), + ], + }, + }); + } + + const pathToClientInit = options.clientInitPath + ? path.resolve(options.clientInitPath) + : findDefaultSdkInitFile('client'); + const pathToServerInit = options.serverInitPath + ? path.resolve(options.serverInitPath) + : findDefaultSdkInitFile('server'); + + if (pathToClientInit) { + options.debug && console.log(`[sentry-astro] Using ${pathToClientInit} for client init.`); + injectScript('page', buildSdkInitFileImportSnippet(pathToClientInit)); + } else { + options.debug && console.log('[sentry-astro] Using default client init.'); + injectScript('page', buildClientSnippet(options || {})); + } + + if (pathToServerInit) { + options.debug && console.log(`[sentry-astro] Using ${pathToServerInit} for server init.`); + injectScript('page-ssr', buildSdkInitFileImportSnippet(pathToServerInit)); + } else { + options.debug && console.log('[sentry-astro] Using default server init.'); + injectScript('page-ssr', buildServerSnippet(options || {})); + } + }, + }, + }; +}; + +function findDefaultSdkInitFile(type: 'server' | 'client'): string | undefined { + const fileExtensions = ['ts', 'js', 'tsx', 'jsx', 'mjs', 'cjs', 'mts']; + return fileExtensions + .map(ext => path.resolve(path.join(process.cwd(), `sentry.${type}.config.${ext}`))) + .find(filename => fs.existsSync(filename)); +} diff --git a/packages/astro/src/integration/snippets.ts b/packages/astro/src/integration/snippets.ts new file mode 100644 index 000000000000..28d03ea443eb --- /dev/null +++ b/packages/astro/src/integration/snippets.ts @@ -0,0 +1,45 @@ +import type { SentryOptions } from './types'; + +/** + * Creates a snippet that imports a Sentry.init file. + */ +export function buildSdkInitFileImportSnippet(filePath: string): string { + return `import "${filePath}";`; +} + +/** + * Creates a snippet that initializes Sentry on the client by choosing + * default options. + */ +export function buildClientSnippet(options: SentryOptions): string { + return `import * as Sentry from "@sentry/astro"; + +Sentry.init({ + ${buildCommonInitOptions(options)} + integrations: [new Sentry.BrowserTracing(), new Sentry.Replay()], + replaysSessionSampleRate: ${options.replaysSessionSampleRate ?? 0.1}, + replaysOnErrorSampleRate: ${options.replaysOnErrorSampleRate ?? 1.0}, +});`; +} + +/** + * Creates a snippet that initializes Sentry on the server by choosing + * default options. + */ +export function buildServerSnippet(options: SentryOptions): string { + return `import * as Sentry from "@sentry/astro"; + +Sentry.init({ + ${buildCommonInitOptions(options)} +});`; +} + +const buildCommonInitOptions = (options: SentryOptions): string => `dsn: ${ + options.dsn ? JSON.stringify(options.dsn) : 'import.meta.env.PUBLIC_SENTRY_DSN' +}, + debug: ${options.debug ? true : false}, + environment: ${options.environment ? JSON.stringify(options.environment) : 'import.meta.env.PUBLIC_VERCEL_ENV'}, + release: ${options.release ? JSON.stringify(options.release) : 'import.meta.env.PUBLIC_VERCEL_GIT_COMMIT_SHA'}, + tracesSampleRate: ${options.tracesSampleRate ?? 1.0},${ + options.sampleRate ? `\n sampleRate: ${options.sampleRate},` : '' +}`; diff --git a/packages/astro/src/integration/types.ts b/packages/astro/src/integration/types.ts new file mode 100644 index 000000000000..d54c948401ca --- /dev/null +++ b/packages/astro/src/integration/types.ts @@ -0,0 +1,86 @@ +import type { BrowserOptions } from '@sentry/browser'; +import type { Options } from '@sentry/types'; + +type SdkInitPaths = { + /** + * Path to a `sentry.client.config.(js|ts)` file that contains a `Sentry.init` call. + * + * If this option is not specified, the default location (`/sentry.client.config.(js|ts)`) + * will be used to look up the config file. + * If there is no file at the default location either, the SDK will initalize with the options + * specified in the `sentryAstro` integration or with default options. + */ + clientInitPath?: string; + + /** + * Path to a `sentry.server.config.(js|ts)` file that contains a `Sentry.init` call. + * + * If this option is not specified, the default location (`/sentry.server.config.(js|ts)`) + * will be used to look up the config file. + * If there is no file at the default location either, the SDK will initalize with the options + * specified in the `sentryAstro` integration or with default options. + */ + serverInitPath?: string; +}; + +type SourceMapsOptions = { + /** + * Options for the Sentry Vite plugin to customize the source maps upload process. + * + * These options are always read from the `sentryAstro` integration. + * Do not define them in the `sentry.client.config.(js|ts)` or `sentry.server.config.(js|ts)` files. + */ + sourceMapsUploadOptions?: { + /** + * If this flag is `true`, and an auth token is detected, the Sentry integration will + * automatically generate and upload source maps to Sentry during a production build. + * + * @default true + */ + enabled?: boolean; + + /** + * The auth token to use when uploading source maps to Sentry. + * + * Instead of specifying this option, you can also set the `SENTRY_AUTH_TOKEN` environment variable. + * + * To create an auth token, follow this guide: + * @see https://docs.sentry.io/product/accounts/auth-tokens/#organization-auth-tokens + */ + authToken?: string; + + /** + * The organization slug of your Sentry organization. + * Instead of specifying this option, you can also set the `SENTRY_ORG` environment variable. + */ + org?: string; + + /** + * The project slug of your Sentry project. + * Instead of specifying this option, you can also set the `SENTRY_PROJECT` environment variable. + */ + project?: string; + + /** + * If this flag is `true`, the Sentry plugin will collect some telemetry data and send it to Sentry. + * It will not collect any sensitive or user-specific data. + * + * @default true + */ + telemetry?: boolean; + }; +}; + +/** + * A subset of Sentry SDK options that can be set via the `sentryAstro` integration. + * Some options (e.g. integrations) are set by default and cannot be changed here. + * + * If you want a more fine-grained control over the SDK, with all options, + * you can call Sentry.init in `sentry.client.config.(js|ts)` or `sentry.server.config.(js|ts)` files. + * + * If you specify a dedicated init file, the SDK options passed to `sentryAstro` will be ignored. + */ +export type SentryOptions = SdkInitPaths & + Pick & + Pick & + SourceMapsOptions; diff --git a/packages/astro/src/server/sdk.ts b/packages/astro/src/server/sdk.ts new file mode 100644 index 000000000000..8c867ca46fc2 --- /dev/null +++ b/packages/astro/src/server/sdk.ts @@ -0,0 +1,19 @@ +import { configureScope } from '@sentry/core'; +import type { NodeOptions } from '@sentry/node'; +import { init as initNodeSdk } from '@sentry/node'; + +import { applySdkMetadata } from '../common/metadata'; + +/** + * + * @param options + */ +export function init(options: NodeOptions): void { + applySdkMetadata(options, ['astro', 'node']); + + initNodeSdk(options); + + configureScope(scope => { + scope.setTag('runtime', 'node'); + }); +} diff --git a/packages/astro/test/client/sdk.test.ts b/packages/astro/test/client/sdk.test.ts new file mode 100644 index 000000000000..74a4dc4562ef --- /dev/null +++ b/packages/astro/test/client/sdk.test.ts @@ -0,0 +1,124 @@ +import type { BrowserClient } from '@sentry/browser'; +import * as SentryBrowser from '@sentry/browser'; +import { BrowserTracing, getCurrentHub, SDK_VERSION, WINDOW } from '@sentry/browser'; +import { vi } from 'vitest'; + +import { init } from '../../../astro/src/client/sdk'; + +const browserInit = vi.spyOn(SentryBrowser, 'init'); + +describe('Sentry client SDK', () => { + describe('init', () => { + afterEach(() => { + vi.clearAllMocks(); + WINDOW.__SENTRY__.hub = undefined; + }); + + it('adds Astro metadata to the SDK options', () => { + expect(browserInit).not.toHaveBeenCalled(); + + init({}); + + expect(browserInit).toHaveBeenCalledTimes(1); + expect(browserInit).toHaveBeenCalledWith( + expect.objectContaining({ + _metadata: { + sdk: { + name: 'sentry.javascript.astro', + version: SDK_VERSION, + packages: [ + { name: 'npm:@sentry/astro', version: SDK_VERSION }, + { name: 'npm:@sentry/browser', version: SDK_VERSION }, + ], + }, + }, + }), + ); + }); + + it('sets the runtime tag on the scope', () => { + const currentScope = getCurrentHub().getScope(); + + // @ts-expect-error need access to protected _tags attribute + expect(currentScope._tags).toEqual({}); + + init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' }); + + // @ts-expect-error need access to protected _tags attribute + expect(currentScope._tags).toEqual({ runtime: 'browser' }); + }); + + describe('automatically adds integrations', () => { + it.each([ + ['tracesSampleRate', { tracesSampleRate: 0 }], + ['tracesSampler', { tracesSampler: () => 1.0 }], + ['enableTracing', { enableTracing: true }], + ])('adds the BrowserTracing integration if tracing is enabled via %s', (_, tracingOptions) => { + init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + ...tracingOptions, + }); + + const integrationsToInit = browserInit.mock.calls[0][0]?.integrations; + const browserTracing = (getCurrentHub().getClient() as BrowserClient)?.getIntegrationById('BrowserTracing'); + + expect(integrationsToInit).toContainEqual(expect.objectContaining({ name: 'BrowserTracing' })); + expect(browserTracing).toBeDefined(); + }); + + it.each([ + ['enableTracing', { enableTracing: false }], + ['no tracing option set', {}], + ])("doesn't add the BrowserTracing integration if tracing is disabled via %s", (_, tracingOptions) => { + init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + ...tracingOptions, + }); + + const integrationsToInit = browserInit.mock.calls[0][0]?.integrations; + const browserTracing = (getCurrentHub().getClient() as BrowserClient)?.getIntegrationById('BrowserTracing'); + + expect(integrationsToInit).not.toContainEqual(expect.objectContaining({ name: 'BrowserTracing' })); + expect(browserTracing).toBeUndefined(); + }); + + it("doesn't add the BrowserTracing integration if `__SENTRY_TRACING__` is set to false", () => { + globalThis.__SENTRY_TRACING__ = false; + + init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + enableTracing: true, + }); + + const integrationsToInit = browserInit.mock.calls[0][0]?.integrations; + const browserTracing = (getCurrentHub().getClient() as BrowserClient)?.getIntegrationById('BrowserTracing'); + + expect(integrationsToInit).not.toContainEqual(expect.objectContaining({ name: 'BrowserTracing' })); + expect(browserTracing).toBeUndefined(); + + delete globalThis.__SENTRY_TRACING__; + }); + + it('Overrides the automatically default BrowserTracing instance with a a user-provided instance', () => { + init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [new BrowserTracing({ finalTimeout: 10, startTransactionOnLocationChange: false })], + enableTracing: true, + }); + + const integrationsToInit = browserInit.mock.calls[0][0]?.integrations; + + const browserTracing = (getCurrentHub().getClient() as BrowserClient)?.getIntegrationById( + 'BrowserTracing', + ) as BrowserTracing; + const options = browserTracing.options; + + expect(integrationsToInit).toContainEqual(expect.objectContaining({ name: 'BrowserTracing' })); + expect(browserTracing).toBeDefined(); + + // This shows that the user-configured options are still here + expect(options.finalTimeout).toEqual(10); + }); + }); + }); +}); diff --git a/packages/astro/test/integration/index.test.ts b/packages/astro/test/integration/index.test.ts new file mode 100644 index 000000000000..fe876023c9c4 --- /dev/null +++ b/packages/astro/test/integration/index.test.ts @@ -0,0 +1,103 @@ +import { vi } from 'vitest'; + +import { sentryAstro } from '../../src/integration'; + +const sentryVitePluginSpy = vi.fn(() => 'sentryVitePlugin'); + +vi.mock('@sentry/vite-plugin', () => ({ + // @ts-expect-error - just mocking around + sentryVitePlugin: vi.fn(args => sentryVitePluginSpy(args)), +})); + +process.env = { + ...process.env, + SENTRY_AUTH_TOKEN: 'my-token', +}; + +describe('sentryAstro integration', () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it('has a name', () => { + const integration = sentryAstro({}); + expect(integration.name).toBe('@sentry/astro'); + }); + + it('enables source maps and adds the sentry vite plugin if an auth token is detected', async () => { + const integration = sentryAstro({ + sourceMapsUploadOptions: { enabled: true, org: 'my-org', project: 'my-project', telemetry: false }, + }); + const updateConfig = vi.fn(); + const injectScript = vi.fn(); + + expect(integration.hooks['astro:config:setup']).toBeDefined(); + // @ts-expect-error - the hook exists and we only need to pass what we actually use + await integration.hooks['astro:config:setup']({ updateConfig, injectScript }); + + expect(updateConfig).toHaveBeenCalledTimes(1); + expect(updateConfig).toHaveBeenCalledWith({ + vite: { + build: { + sourcemap: true, + }, + plugins: ['sentryVitePlugin'], + }, + }); + + expect(sentryVitePluginSpy).toHaveBeenCalledTimes(1); + expect(sentryVitePluginSpy).toHaveBeenCalledWith({ + authToken: 'my-token', + org: 'my-org', + project: 'my-project', + telemetry: false, + }); + }); + + it("doesn't enable source maps if `sourceMapsUploadOptions.enabled` is `false`", async () => { + const integration = sentryAstro({ + sourceMapsUploadOptions: { enabled: false }, + }); + const updateConfig = vi.fn(); + const injectScript = vi.fn(); + + expect(integration.hooks['astro:config:setup']).toBeDefined(); + // @ts-expect-error - the hook exists and we only need to pass what we actually use + await integration.hooks['astro:config:setup']({ updateConfig, injectScript }); + + expect(updateConfig).toHaveBeenCalledTimes(0); + expect(sentryVitePluginSpy).toHaveBeenCalledTimes(0); + }); + + it('injects client and server init scripts', async () => { + const integration = sentryAstro({}); + const updateConfig = vi.fn(); + const injectScript = vi.fn(); + + expect(integration.hooks['astro:config:setup']).toBeDefined(); + // @ts-expect-error - the hook exists and we only need to pass what we actually use + await integration.hooks['astro:config:setup']({ updateConfig, injectScript }); + + expect(injectScript).toHaveBeenCalledTimes(2); + expect(injectScript).toHaveBeenCalledWith('page', expect.stringContaining('Sentry.init')); + expect(injectScript).toHaveBeenCalledWith('page-ssr', expect.stringContaining('Sentry.init')); + }); + + it('injects client and server init scripts from custom paths', async () => { + const integration = sentryAstro({ + clientInitPath: 'my-client-init-path.js', + serverInitPath: 'my-server-init-path.js', + }); + + const updateConfig = vi.fn(); + const injectScript = vi.fn(); + + expect(integration.hooks['astro:config:setup']).toBeDefined(); + // @ts-expect-error - the hook exists and we only need to pass what we actually use + await integration.hooks['astro:config:setup']({ updateConfig, injectScript }); + + expect(injectScript).toHaveBeenCalledTimes(2); + expect(injectScript).toHaveBeenCalledWith('page', expect.stringContaining('my-client-init-path.js')); + expect(injectScript).toHaveBeenCalledWith('page-ssr', expect.stringContaining('my-server-init-path.js')); + }); +}); diff --git a/packages/astro/test/integration/snippets.test.ts b/packages/astro/test/integration/snippets.test.ts new file mode 100644 index 000000000000..60406b652bf8 --- /dev/null +++ b/packages/astro/test/integration/snippets.test.ts @@ -0,0 +1,93 @@ +import { buildClientSnippet, buildSdkInitFileImportSnippet, buildServerSnippet } from '../../src/integration/snippets'; + +const allSdkOptions = { + dsn: 'my-dsn', + release: '1.0.0', + environment: 'staging', + sampleRate: 0.2, + tracesSampleRate: 0.3, + replaysOnErrorSampleRate: 0.4, + replaysSessionSampleRate: 0.5, + debug: true, +}; + +describe('buildClientSnippet', () => { + it('returns a basic Sentry init call with default options', () => { + const snippet = buildClientSnippet({}); + expect(snippet).toMatchInlineSnapshot(` + "import * as Sentry from \\"@sentry/astro\\"; + + Sentry.init({ + dsn: import.meta.env.PUBLIC_SENTRY_DSN, + debug: false, + environment: import.meta.env.PUBLIC_VERCEL_ENV, + release: import.meta.env.PUBLIC_VERCEL_GIT_COMMIT_SHA, + tracesSampleRate: 1, + integrations: [new Sentry.BrowserTracing(), new Sentry.Replay()], + replaysSessionSampleRate: 0.1, + replaysOnErrorSampleRate: 1, + });" + `); + }); + + it('returns a basic Sentry init call with custom options', () => { + const snippet = buildClientSnippet(allSdkOptions); + + expect(snippet).toMatchInlineSnapshot(` + "import * as Sentry from \\"@sentry/astro\\"; + + Sentry.init({ + dsn: \\"my-dsn\\", + debug: true, + environment: \\"staging\\", + release: \\"1.0.0\\", + tracesSampleRate: 0.3, + sampleRate: 0.2, + integrations: [new Sentry.BrowserTracing(), new Sentry.Replay()], + replaysSessionSampleRate: 0.5, + replaysOnErrorSampleRate: 0.4, + });" + `); + }); +}); + +describe('buildServerSnippet', () => { + it('returns a basic Sentry init call with default options', () => { + const snippet = buildServerSnippet({}); + expect(snippet).toMatchInlineSnapshot(` + "import * as Sentry from \\"@sentry/astro\\"; + + Sentry.init({ + dsn: import.meta.env.PUBLIC_SENTRY_DSN, + debug: false, + environment: import.meta.env.PUBLIC_VERCEL_ENV, + release: import.meta.env.PUBLIC_VERCEL_GIT_COMMIT_SHA, + tracesSampleRate: 1, + });" + `); + }); + + it('returns a basic Sentry init call with custom options', () => { + const snippet = buildServerSnippet(allSdkOptions); + + expect(snippet).toMatchInlineSnapshot(` + "import * as Sentry from \\"@sentry/astro\\"; + + Sentry.init({ + dsn: \\"my-dsn\\", + debug: true, + environment: \\"staging\\", + release: \\"1.0.0\\", + tracesSampleRate: 0.3, + sampleRate: 0.2, + });" + `); + }); +}); + +describe('buildSdkInitFileImportSnippet', () => { + it('returns a snippet that imports a file', () => { + const snippet = buildSdkInitFileImportSnippet('./my-file.ts'); + expect(snippet).toBe('import "./my-file.ts";'); + }); +}); diff --git a/packages/astro/test/server/index.server.test.ts b/packages/astro/test/server/index.server.test.ts new file mode 100644 index 000000000000..f319ef90eaad --- /dev/null +++ b/packages/astro/test/server/index.server.test.ts @@ -0,0 +1,7 @@ +import sentryAstro from '../../src/index.server'; +describe('server SDK', () => { + it('exports the astro integration as a default export', () => { + const integration = sentryAstro(); + expect(integration.name).toBe('@sentry/astro'); + }); +}); diff --git a/packages/astro/test/server/sdk.test.ts b/packages/astro/test/server/sdk.test.ts new file mode 100644 index 000000000000..0e178f7ae45a --- /dev/null +++ b/packages/astro/test/server/sdk.test.ts @@ -0,0 +1,52 @@ +import { getCurrentHub } from '@sentry/core'; +import * as SentryNode from '@sentry/node'; +import { SDK_VERSION } from '@sentry/node'; +import { GLOBAL_OBJ } from '@sentry/utils'; +import { vi } from 'vitest'; + +import { init } from '../../src/server/sdk'; + +const nodeInit = vi.spyOn(SentryNode, 'init'); + +describe('Sentry server SDK', () => { + describe('init', () => { + afterEach(() => { + vi.clearAllMocks(); + GLOBAL_OBJ.__SENTRY__.hub = undefined; + }); + + it('adds Astro metadata to the SDK options', () => { + expect(nodeInit).not.toHaveBeenCalled(); + + init({}); + + expect(nodeInit).toHaveBeenCalledTimes(1); + expect(nodeInit).toHaveBeenCalledWith( + expect.objectContaining({ + _metadata: { + sdk: { + name: 'sentry.javascript.astro', + version: SDK_VERSION, + packages: [ + { name: 'npm:@sentry/astro', version: SDK_VERSION }, + { name: 'npm:@sentry/node', version: SDK_VERSION }, + ], + }, + }, + }), + ); + }); + + it('sets the runtime tag on the scope', () => { + const currentScope = getCurrentHub().getScope(); + + // @ts-expect-error need access to protected _tags attribute + expect(currentScope._tags).toEqual({}); + + init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' }); + + // @ts-expect-error need access to protected _tags attribute + expect(currentScope._tags).toEqual({ runtime: 'node' }); + }); + }); +}); diff --git a/packages/astro/tsconfig.dev.json b/packages/astro/tsconfig.dev.json new file mode 100644 index 000000000000..cc931d956044 --- /dev/null +++ b/packages/astro/tsconfig.dev.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.dev.json", + + "include": ["scripts/**/*"], + + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "Node", + }, + + "ts-node": { + "esm": true + } +} diff --git a/packages/astro/tsconfig.json b/packages/astro/tsconfig.json new file mode 100644 index 000000000000..bf45a09f2d71 --- /dev/null +++ b/packages/astro/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + + "include": ["src/**/*"], + + "compilerOptions": { + // package-specific options + } +} diff --git a/packages/astro/tsconfig.test.json b/packages/astro/tsconfig.test.json new file mode 100644 index 000000000000..3fbe012384ee --- /dev/null +++ b/packages/astro/tsconfig.test.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + + "include": ["test/**/*", "vite.config.ts"], + + "compilerOptions": { + // should include all types from `./tsconfig.json` plus types for all test frameworks used + "types": ["node", "vitest/globals"] + } +} diff --git a/packages/astro/tsconfig.types.json b/packages/astro/tsconfig.types.json new file mode 100644 index 000000000000..65455f66bd75 --- /dev/null +++ b/packages/astro/tsconfig.types.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "outDir": "build/types" + } +} diff --git a/packages/astro/vite.config.ts b/packages/astro/vite.config.ts new file mode 100644 index 000000000000..6a035a7635e7 --- /dev/null +++ b/packages/astro/vite.config.ts @@ -0,0 +1,13 @@ +import type { UserConfig } from 'vitest'; + +import baseConfig from '../../vite/vite.config'; + +export default { + ...baseConfig, + test: { + // test exists, no idea why TS doesn't recognize it + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ...(baseConfig as UserConfig & { test: any }).test, + environment: 'jsdom', + }, +}; diff --git a/packages/browser-integration-tests/suites/replay/exceptions/template.html b/packages/browser-integration-tests/suites/replay/exceptions/template.html new file mode 100644 index 000000000000..0915a77b0cd9 --- /dev/null +++ b/packages/browser-integration-tests/suites/replay/exceptions/template.html @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/packages/browser-integration-tests/suites/replay/exceptions/test.ts b/packages/browser-integration-tests/suites/replay/exceptions/test.ts new file mode 100644 index 000000000000..203550d55759 --- /dev/null +++ b/packages/browser-integration-tests/suites/replay/exceptions/test.ts @@ -0,0 +1,35 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../utils/fixtures'; +import { shouldSkipReplayTest } from '../../../utils/replayHelpers'; + +sentryTest('exceptions within rrweb and re-thrown and annotated', async ({ getLocalTestPath, page, browserName }) => { + if (shouldSkipReplayTest() || browserName !== 'chromium') { + sentryTest.skip(); + } + + const url = await getLocalTestPath({ testDir: __dirname }); + + await page.goto(url); + + expect( + await page.evaluate(() => { + try { + const s = new CSSStyleSheet(); + s.insertRule('body::-ms-expand{display: none}'); + s.insertRule('body {background-color: #fff;}'); + return s.cssRules.length; + } catch { + return false; + } + }), + ).toBe(false); + + expect( + await page.evaluate(() => { + const s = new CSSStyleSheet(); + s.insertRule('body {background-color: #fff;}'); + return s.cssRules.length; + }), + ).toBe(1); +}); diff --git a/packages/browser/package.json b/packages/browser/package.json index 55f9e15565e8..ce789c53caaf 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -51,7 +51,7 @@ "node-fetch": "^2.6.0", "playwright": "^1.31.1", "sinon": "^7.3.2", - "webpack": "^4.30.0" + "webpack": "^4.47.0" }, "scripts": { "build": "run-p build:transpile build:bundle build:types", diff --git a/packages/browser/src/exports.ts b/packages/browser/src/exports.ts index f46b55f45214..3d3aec477731 100644 --- a/packages/browser/src/exports.ts +++ b/packages/browser/src/exports.ts @@ -23,6 +23,7 @@ export type { ReportDialogOptions } from './helpers'; export { addGlobalEventProcessor, addBreadcrumb, + addIntegration, captureException, captureEvent, captureMessage, @@ -41,6 +42,7 @@ export { startSpan, startInactiveSpan, startSpanManual, + continueTrace, SDK_VERSION, setContext, setExtra, diff --git a/packages/bun/package.json b/packages/bun/package.json index 38db742a8c84..6cb29b2e403c 100644 --- a/packages/bun/package.json +++ b/packages/bun/package.json @@ -44,7 +44,7 @@ "build:types:watch": "tsc -p tsconfig.types.json --watch", "build:tarball": "ts-node ../../scripts/prepack.ts && npm pack ./build", "circularDepCheck": "madge --circular src/index.ts", - "clean": "rimraf build coverage sentry-node-*.tgz", + "clean": "rimraf build coverage sentry-bun-*.tgz", "fix": "run-s fix:eslint fix:prettier", "fix:eslint": "eslint . --format stylish --fix", "fix:prettier": "prettier --write \"{src,test,scripts}/**/**.ts\"", diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index d1c4a69f0ae5..c8428ab8e106 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -26,6 +26,7 @@ export type { BunOptions } from './types'; export { addGlobalEventProcessor, addBreadcrumb, + addIntegration, captureException, captureEvent, captureMessage, @@ -59,6 +60,7 @@ export { startSpan, startInactiveSpan, startSpanManual, + continueTrace, } from '@sentry/core'; export type { SpanStatusType } from '@sentry/core'; export { autoDiscoverNodePerformanceMonitoringIntegrations } from '@sentry/node'; diff --git a/packages/bun/src/transports/index.ts b/packages/bun/src/transports/index.ts index 96e3e99957fb..ad9832795fc4 100644 --- a/packages/bun/src/transports/index.ts +++ b/packages/bun/src/transports/index.ts @@ -15,7 +15,6 @@ export function makeFetchTransport(options: BunTransportOptions): Transport { const requestOptions: RequestInit = { body: request.body, method: 'POST', - referrerPolicy: 'origin', headers: options.headers, }; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 81fcd7c3af8d..f14f5d4aaf2f 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -46,7 +46,7 @@ export { createTransport } from './transports/base'; export { makeOfflineTransport } from './transports/offline'; export { makeMultiplexedTransport } from './transports/multiplexed'; export { SDK_VERSION } from './version'; -export { getIntegrationsToSetup } from './integration'; +export { getIntegrationsToSetup, addIntegration } from './integration'; export { FunctionToString, InboundFilters } from './integrations'; export { prepareEvent } from './utils/prepareEvent'; export { createCheckInEnvelope } from './checkin'; diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts index aa8968edc8dc..b4d32ea38e87 100644 --- a/packages/core/src/integration.ts +++ b/packages/core/src/integration.ts @@ -124,6 +124,18 @@ export function setupIntegration(client: Client, integration: Integration, integ __DEBUG_BUILD__ && logger.log(`Integration installed: ${integration.name}`); } +/** Add an integration to the current hub's client. */ +export function addIntegration(integration: Integration): void { + const client = getCurrentHub().getClient(); + + if (!client || !client.addIntegration) { + __DEBUG_BUILD__ && logger.warn(`Cannot add integration "${integration.name}" because no SDK Client is available.`); + return; + } + + client.addIntegration(integration); +} + // Polyfill for Array.findIndex(), which is not supported in ES5 function findIndex(arr: T[], callback: (item: T) => boolean): number { for (let i = 0; i < arr.length; i++) { diff --git a/packages/core/src/tracing/hubextensions.ts b/packages/core/src/tracing/hubextensions.ts index ad6858d41e4a..007612d8fb34 100644 --- a/packages/core/src/tracing/hubextensions.ts +++ b/packages/core/src/tracing/hubextensions.ts @@ -1,11 +1,11 @@ -import type { ClientOptions, CustomSamplingContext, Options, SamplingContext, TransactionContext } from '@sentry/types'; -import { isNaN, logger } from '@sentry/utils'; +import type { ClientOptions, CustomSamplingContext, TransactionContext } from '@sentry/types'; +import { logger } from '@sentry/utils'; import type { Hub } from '../hub'; import { getMainCarrier } from '../hub'; -import { hasTracingEnabled } from '../utils/hasTracingEnabled'; import { registerErrorInstrumentation } from './errors'; import { IdleTransaction } from './idletransaction'; +import { sampleTransaction } from './sampling'; import { Transaction } from './transaction'; /** Returns all trace headers that are currently on the top scope. */ @@ -20,126 +20,6 @@ function traceHeaders(this: Hub): { [key: string]: string } { : {}; } -/** - * Makes a sampling decision for the given transaction and stores it on the transaction. - * - * Called every time a transaction is created. Only transactions which emerge with a `sampled` value of `true` will be - * sent to Sentry. - * - * @param transaction: The transaction needing a sampling decision - * @param options: The current client's options, so we can access `tracesSampleRate` and/or `tracesSampler` - * @param samplingContext: Default and user-provided data which may be used to help make the decision - * - * @returns The given transaction with its `sampled` value set - */ -function sample( - transaction: T, - options: Pick, - samplingContext: SamplingContext, -): T { - // nothing to do if tracing is not enabled - if (!hasTracingEnabled(options)) { - transaction.sampled = false; - return transaction; - } - - // if the user has forced a sampling decision by passing a `sampled` value in their transaction context, go with that - if (transaction.sampled !== undefined) { - transaction.setMetadata({ - sampleRate: Number(transaction.sampled), - }); - return transaction; - } - - // we would have bailed already if neither `tracesSampler` nor `tracesSampleRate` nor `enableTracing` were defined, so one of these should - // work; prefer the hook if so - let sampleRate; - if (typeof options.tracesSampler === 'function') { - sampleRate = options.tracesSampler(samplingContext); - transaction.setMetadata({ - sampleRate: Number(sampleRate), - }); - } else if (samplingContext.parentSampled !== undefined) { - sampleRate = samplingContext.parentSampled; - } else if (typeof options.tracesSampleRate !== 'undefined') { - sampleRate = options.tracesSampleRate; - transaction.setMetadata({ - sampleRate: Number(sampleRate), - }); - } else { - // When `enableTracing === true`, we use a sample rate of 100% - sampleRate = 1; - transaction.setMetadata({ - sampleRate, - }); - } - - // Since this is coming from the user (or from a function provided by the user), who knows what we might get. (The - // only valid values are booleans or numbers between 0 and 1.) - if (!isValidSampleRate(sampleRate)) { - __DEBUG_BUILD__ && logger.warn('[Tracing] Discarding transaction because of invalid sample rate.'); - transaction.sampled = false; - return transaction; - } - - // if the function returned 0 (or false), or if `tracesSampleRate` is 0, it's a sign the transaction should be dropped - if (!sampleRate) { - __DEBUG_BUILD__ && - logger.log( - `[Tracing] Discarding transaction because ${ - typeof options.tracesSampler === 'function' - ? 'tracesSampler returned 0 or false' - : 'a negative sampling decision was inherited or tracesSampleRate is set to 0' - }`, - ); - transaction.sampled = false; - return transaction; - } - - // Now we roll the dice. Math.random is inclusive of 0, but not of 1, so strict < is safe here. In case sampleRate is - // a boolean, the < comparison will cause it to be automatically cast to 1 if it's true and 0 if it's false. - transaction.sampled = Math.random() < (sampleRate as number | boolean); - - // if we're not going to keep it, we're done - if (!transaction.sampled) { - __DEBUG_BUILD__ && - logger.log( - `[Tracing] Discarding transaction because it's not included in the random sample (sampling rate = ${Number( - sampleRate, - )})`, - ); - return transaction; - } - - __DEBUG_BUILD__ && logger.log(`[Tracing] starting ${transaction.op} transaction - ${transaction.name}`); - return transaction; -} - -/** - * Checks the given sample rate to make sure it is valid type and value (a boolean, or a number between 0 and 1). - */ -function isValidSampleRate(rate: unknown): boolean { - // we need to check NaN explicitly because it's of type 'number' and therefore wouldn't get caught by this typecheck - // eslint-disable-next-line @typescript-eslint/no-explicit-any - if (isNaN(rate) || !(typeof rate === 'number' || typeof rate === 'boolean')) { - __DEBUG_BUILD__ && - logger.warn( - `[Tracing] Given sample rate is invalid. Sample rate must be a boolean or a number between 0 and 1. Got ${JSON.stringify( - rate, - )} of type ${JSON.stringify(typeof rate)}.`, - ); - return false; - } - - // in case sampleRate is a boolean, it will get automatically cast to 1 if it's true and 0 if it's false - if (rate < 0 || rate > 1) { - __DEBUG_BUILD__ && - logger.warn(`[Tracing] Given sample rate is invalid. Sample rate must be between 0 and 1. Got ${rate}.`); - return false; - } - return true; -} - /** * Creates a new transaction and adds a sampling decision if it doesn't yet have one. * @@ -177,7 +57,7 @@ The transaction will not be sampled. Please use the ${configInstrumenter} instru } let transaction = new Transaction(transactionContext, this); - transaction = sample(transaction, options, { + transaction = sampleTransaction(transaction, options, { parentSampled: transactionContext.parentSampled, transactionContext, ...customSamplingContext, @@ -207,7 +87,7 @@ export function startIdleTransaction( const options: Partial = (client && client.getOptions()) || {}; let transaction = new IdleTransaction(transactionContext, hub, idleTimeout, finalTimeout, heartbeatInterval, onScope); - transaction = sample(transaction, options, { + transaction = sampleTransaction(transaction, options, { parentSampled: transactionContext.parentSampled, transactionContext, ...customSamplingContext, diff --git a/packages/core/src/tracing/index.ts b/packages/core/src/tracing/index.ts index c5be88f8c350..f0356a528c2d 100644 --- a/packages/core/src/tracing/index.ts +++ b/packages/core/src/tracing/index.ts @@ -7,7 +7,15 @@ export { extractTraceparentData, getActiveTransaction } from './utils'; // eslint-disable-next-line deprecation/deprecation export { SpanStatus } from './spanstatus'; export type { SpanStatusType } from './span'; -// eslint-disable-next-line deprecation/deprecation -export { trace, getActiveSpan, startSpan, startInactiveSpan, startActiveSpan, startSpanManual } from './trace'; +export { + trace, + getActiveSpan, + startSpan, + startInactiveSpan, + // eslint-disable-next-line deprecation/deprecation + startActiveSpan, + startSpanManual, + continueTrace, +} from './trace'; export { getDynamicSamplingContextFromClient } from './dynamicSamplingContext'; export { setMeasurement } from './measurement'; diff --git a/packages/core/src/tracing/sampling.ts b/packages/core/src/tracing/sampling.ts new file mode 100644 index 000000000000..4b357b7bf1be --- /dev/null +++ b/packages/core/src/tracing/sampling.ts @@ -0,0 +1,122 @@ +import type { Options, SamplingContext } from '@sentry/types'; +import { isNaN, logger } from '@sentry/utils'; + +import { hasTracingEnabled } from '../utils/hasTracingEnabled'; +import type { Transaction } from './transaction'; + +/** + * Makes a sampling decision for the given transaction and stores it on the transaction. + * + * Called every time a transaction is created. Only transactions which emerge with a `sampled` value of `true` will be + * sent to Sentry. + * + * This method muttes the given `transaction` and will set the `sampled` value on it. + * It returns the same transaction, for convenience. + */ +export function sampleTransaction( + transaction: T, + options: Pick, + samplingContext: SamplingContext, +): T { + // nothing to do if tracing is not enabled + if (!hasTracingEnabled(options)) { + transaction.sampled = false; + return transaction; + } + + // if the user has forced a sampling decision by passing a `sampled` value in their transaction context, go with that + if (transaction.sampled !== undefined) { + transaction.setMetadata({ + sampleRate: Number(transaction.sampled), + }); + return transaction; + } + + // we would have bailed already if neither `tracesSampler` nor `tracesSampleRate` nor `enableTracing` were defined, so one of these should + // work; prefer the hook if so + let sampleRate; + if (typeof options.tracesSampler === 'function') { + sampleRate = options.tracesSampler(samplingContext); + transaction.setMetadata({ + sampleRate: Number(sampleRate), + }); + } else if (samplingContext.parentSampled !== undefined) { + sampleRate = samplingContext.parentSampled; + } else if (typeof options.tracesSampleRate !== 'undefined') { + sampleRate = options.tracesSampleRate; + transaction.setMetadata({ + sampleRate: Number(sampleRate), + }); + } else { + // When `enableTracing === true`, we use a sample rate of 100% + sampleRate = 1; + transaction.setMetadata({ + sampleRate, + }); + } + + // Since this is coming from the user (or from a function provided by the user), who knows what we might get. (The + // only valid values are booleans or numbers between 0 and 1.) + if (!isValidSampleRate(sampleRate)) { + __DEBUG_BUILD__ && logger.warn('[Tracing] Discarding transaction because of invalid sample rate.'); + transaction.sampled = false; + return transaction; + } + + // if the function returned 0 (or false), or if `tracesSampleRate` is 0, it's a sign the transaction should be dropped + if (!sampleRate) { + __DEBUG_BUILD__ && + logger.log( + `[Tracing] Discarding transaction because ${ + typeof options.tracesSampler === 'function' + ? 'tracesSampler returned 0 or false' + : 'a negative sampling decision was inherited or tracesSampleRate is set to 0' + }`, + ); + transaction.sampled = false; + return transaction; + } + + // Now we roll the dice. Math.random is inclusive of 0, but not of 1, so strict < is safe here. In case sampleRate is + // a boolean, the < comparison will cause it to be automatically cast to 1 if it's true and 0 if it's false. + transaction.sampled = Math.random() < (sampleRate as number | boolean); + + // if we're not going to keep it, we're done + if (!transaction.sampled) { + __DEBUG_BUILD__ && + logger.log( + `[Tracing] Discarding transaction because it's not included in the random sample (sampling rate = ${Number( + sampleRate, + )})`, + ); + return transaction; + } + + __DEBUG_BUILD__ && logger.log(`[Tracing] starting ${transaction.op} transaction - ${transaction.name}`); + return transaction; +} + +/** + * Checks the given sample rate to make sure it is valid type and value (a boolean, or a number between 0 and 1). + */ +function isValidSampleRate(rate: unknown): boolean { + // we need to check NaN explicitly because it's of type 'number' and therefore wouldn't get caught by this typecheck + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if (isNaN(rate) || !(typeof rate === 'number' || typeof rate === 'boolean')) { + __DEBUG_BUILD__ && + logger.warn( + `[Tracing] Given sample rate is invalid. Sample rate must be a boolean or a number between 0 and 1. Got ${JSON.stringify( + rate, + )} of type ${JSON.stringify(typeof rate)}.`, + ); + return false; + } + + // in case sampleRate is a boolean, it will get automatically cast to 1 if it's true and 0 if it's false + if (rate < 0 || rate > 1) { + __DEBUG_BUILD__ && + logger.warn(`[Tracing] Given sample rate is invalid. Sample rate must be between 0 and 1. Got ${rate}.`); + return false; + } + return true; +} diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index 8f9b226b4afb..4572eed79ee9 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -1,5 +1,5 @@ import type { TransactionContext } from '@sentry/types'; -import { isThenable } from '@sentry/utils'; +import { dropUndefinedKeys, isThenable, logger, tracingContextFromHeaders } from '@sentry/utils'; import type { Hub } from '../hub'; import { getCurrentHub } from '../hub'; @@ -203,6 +203,48 @@ export function getActiveSpan(): Span | undefined { return getCurrentHub().getScope().getSpan(); } +/** + * Continue a trace from `sentry-trace` and `baggage` values. + * These values can be obtained from incoming request headers, + * or in the browser from `` and `` HTML tags. + * + * It also takes an optional `request` option, which if provided will also be added to the scope & transaction metadata. + * The callback receives a transactionContext that may be used for `startTransaction` or `startSpan`. + */ +export function continueTrace( + { + sentryTrace, + baggage, + }: { + sentryTrace: Parameters[0]; + baggage: Parameters[1]; + }, + callback: (transactionContext: Partial) => V, +): V { + const hub = getCurrentHub(); + const currentScope = hub.getScope(); + + const { traceparentData, dynamicSamplingContext, propagationContext } = tracingContextFromHeaders( + sentryTrace, + baggage, + ); + + currentScope.setPropagationContext(propagationContext); + + if (__DEBUG_BUILD__ && traceparentData) { + logger.log(`[Tracing] Continuing trace ${traceparentData.traceId}.`); + } + + const transactionContext: Partial = { + ...traceparentData, + metadata: dropUndefinedKeys({ + dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext, + }), + }; + + return callback(transactionContext); +} + function createChildSpanOrTransaction( hub: Hub, parentSpan: Span | undefined, diff --git a/packages/core/test/lib/base.test.ts b/packages/core/test/lib/base.test.ts index 27e34a1402d4..2d5f8ff05db8 100644 --- a/packages/core/test/lib/base.test.ts +++ b/packages/core/test/lib/base.test.ts @@ -78,6 +78,9 @@ describe('BaseClient', () => { }); test('handles being passed an invalid Dsn', () => { + // Hide warning logs in the test + jest.spyOn(console, 'error').mockImplementation(() => {}); + const options = getDefaultTestClientOptions({ dsn: 'abc' }); const client = new TestClient(options); diff --git a/packages/core/test/lib/integration.test.ts b/packages/core/test/lib/integration.test.ts index f431d30b2140..7ffcdb572994 100644 --- a/packages/core/test/lib/integration.test.ts +++ b/packages/core/test/lib/integration.test.ts @@ -1,6 +1,8 @@ import type { Integration, Options } from '@sentry/types'; +import { logger } from '@sentry/utils'; -import { getIntegrationsToSetup, installedIntegrations, setupIntegration } from '../../src/integration'; +import { Hub, makeMain } from '../../src/hub'; +import { addIntegration, getIntegrationsToSetup, installedIntegrations, setupIntegration } from '../../src/integration'; import { getDefaultTestClientOptions, TestClient } from '../mocks/client'; function getTestClient(): TestClient { @@ -559,3 +561,51 @@ describe('setupIntegration', () => { expect(sendEvent).not.toHaveBeenCalled(); }); }); + +describe('addIntegration', () => { + beforeEach(function () { + // Reset the (global!) list of installed integrations + installedIntegrations.splice(0, installedIntegrations.length); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('works with a client setup', () => { + const warnings = jest.spyOn(logger, 'warn'); + + class CustomIntegration implements Integration { + name = 'test'; + setupOnce = jest.fn(); + } + + const client = getTestClient(); + const hub = new Hub(client); + makeMain(hub); + + const integration = new CustomIntegration(); + addIntegration(integration); + + expect(integration.setupOnce).toHaveBeenCalledTimes(1); + expect(warnings).not.toHaveBeenCalled(); + }); + + it('works without a client setup', () => { + const warnings = jest.spyOn(logger, 'warn'); + class CustomIntegration implements Integration { + name = 'test'; + setupOnce = jest.fn(); + } + + const hub = new Hub(); + makeMain(hub); + + const integration = new CustomIntegration(); + addIntegration(integration); + + expect(integration.setupOnce).not.toHaveBeenCalled(); + expect(warnings).toHaveBeenCalledTimes(1); + expect(warnings).toHaveBeenCalledWith('Cannot add integration "test" because no SDK Client is available.'); + }); +}); diff --git a/packages/core/test/lib/tracing/trace.test.ts b/packages/core/test/lib/tracing/trace.test.ts index 2480d449a9d9..144ec35f1f0e 100644 --- a/packages/core/test/lib/tracing/trace.test.ts +++ b/packages/core/test/lib/tracing/trace.test.ts @@ -1,5 +1,5 @@ import { addTracingExtensions, Hub, makeMain } from '../../../src'; -import { startSpan } from '../../../src/tracing'; +import { continueTrace, startSpan } from '../../../src/tracing'; import { getDefaultTestClientOptions, TestClient } from '../../mocks/client'; beforeAll(() => { @@ -170,3 +170,154 @@ describe('startSpan', () => { }); }); }); + +describe('continueTrace', () => { + beforeEach(() => { + const options = getDefaultTestClientOptions({ tracesSampleRate: 0.0 }); + client = new TestClient(options); + hub = new Hub(client); + makeMain(hub); + }); + + it('works without trace & baggage data', () => { + const expectedContext = { + metadata: {}, + }; + + const result = continueTrace({ sentryTrace: undefined, baggage: undefined }, ctx => { + expect(ctx).toEqual(expectedContext); + return ctx; + }); + + expect(result).toEqual(expectedContext); + + const scope = hub.getScope(); + + expect(scope.getPropagationContext()).toEqual({ + sampled: undefined, + spanId: expect.any(String), + traceId: expect.any(String), + }); + + expect(scope['_sdkProcessingMetadata']).toEqual({}); + }); + + it('works with trace data', () => { + const expectedContext = { + metadata: { + dynamicSamplingContext: {}, + }, + parentSampled: false, + parentSpanId: '1121201211212012', + traceId: '12312012123120121231201212312012', + }; + + const result = continueTrace( + { + sentryTrace: '12312012123120121231201212312012-1121201211212012-0', + baggage: undefined, + }, + ctx => { + expect(ctx).toEqual(expectedContext); + return ctx; + }, + ); + + expect(result).toEqual(expectedContext); + + const scope = hub.getScope(); + + expect(scope.getPropagationContext()).toEqual({ + sampled: false, + parentSpanId: '1121201211212012', + spanId: expect.any(String), + traceId: '12312012123120121231201212312012', + }); + + expect(scope['_sdkProcessingMetadata']).toEqual({}); + }); + + it('works with trace & baggage data', () => { + const expectedContext = { + metadata: { + dynamicSamplingContext: { + environment: 'production', + version: '1.0', + }, + }, + parentSampled: true, + parentSpanId: '1121201211212012', + traceId: '12312012123120121231201212312012', + }; + + const result = continueTrace( + { + sentryTrace: '12312012123120121231201212312012-1121201211212012-1', + baggage: 'sentry-version=1.0,sentry-environment=production', + }, + ctx => { + expect(ctx).toEqual(expectedContext); + return ctx; + }, + ); + + expect(result).toEqual(expectedContext); + + const scope = hub.getScope(); + + expect(scope.getPropagationContext()).toEqual({ + dsc: { + environment: 'production', + version: '1.0', + }, + sampled: true, + parentSpanId: '1121201211212012', + spanId: expect.any(String), + traceId: '12312012123120121231201212312012', + }); + + expect(scope['_sdkProcessingMetadata']).toEqual({}); + }); + + it('works with trace & 3rd party baggage data', () => { + const expectedContext = { + metadata: { + dynamicSamplingContext: { + environment: 'production', + version: '1.0', + }, + }, + parentSampled: true, + parentSpanId: '1121201211212012', + traceId: '12312012123120121231201212312012', + }; + + const result = continueTrace( + { + sentryTrace: '12312012123120121231201212312012-1121201211212012-1', + baggage: 'sentry-version=1.0,sentry-environment=production,dogs=great,cats=boring', + }, + ctx => { + expect(ctx).toEqual(expectedContext); + return ctx; + }, + ); + + expect(result).toEqual(expectedContext); + + const scope = hub.getScope(); + + expect(scope.getPropagationContext()).toEqual({ + dsc: { + environment: 'production', + version: '1.0', + }, + sampled: true, + parentSpanId: '1121201211212012', + spanId: expect.any(String), + traceId: '12312012123120121231201212312012', + }); + + expect(scope['_sdkProcessingMetadata']).toEqual({}); + }); +}); diff --git a/packages/core/test/lib/transports/multiplexed.test.ts b/packages/core/test/lib/transports/multiplexed.test.ts index 446d185f9301..bcfb65736c5e 100644 --- a/packages/core/test/lib/transports/multiplexed.test.ts +++ b/packages/core/test/lib/transports/multiplexed.test.ts @@ -88,6 +88,9 @@ describe('makeMultiplexedTransport', () => { }); it('Falls back to options DSN when a matched DSN is invalid', async () => { + // Hide warning logs in the test + jest.spyOn(console, 'error').mockImplementation(() => {}); + expect.assertions(1); const makeTransport = makeMultiplexedTransport( @@ -99,6 +102,8 @@ describe('makeMultiplexedTransport', () => { const transport = makeTransport({ url: DSN1_URL, ...transportOptions }); await transport.send(ERROR_ENVELOPE); + + jest.clearAllMocks(); }); it('DSN can be overridden via match callback', async () => { diff --git a/packages/deno/.eslintrc.js b/packages/deno/.eslintrc.js new file mode 100644 index 000000000000..b92652708339 --- /dev/null +++ b/packages/deno/.eslintrc.js @@ -0,0 +1,18 @@ +module.exports = { + extends: ['../../.eslintrc.js'], + ignorePatterns: ['lib.deno.d.ts', 'scripts/*.mjs'], + rules: { + '@sentry-internal/sdk/no-optional-chaining': 'off', + '@sentry-internal/sdk/no-nullish-coalescing': 'off', + '@sentry-internal/sdk/no-unsupported-es6-methods': 'off', + '@sentry-internal/sdk/no-class-field-initializers': 'off', + }, + overrides: [ + { + files: ['./test/*.ts'], + rules: { + 'import/no-unresolved': 'off', + }, + }, + ], +}; diff --git a/packages/deno/LICENSE b/packages/deno/LICENSE new file mode 100644 index 000000000000..d11896ba1181 --- /dev/null +++ b/packages/deno/LICENSE @@ -0,0 +1,14 @@ +Copyright (c) 2023 Sentry (https://sentry.io) and individual contributors. All rights reserved. + +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/deno/README.md b/packages/deno/README.md new file mode 100644 index 000000000000..5be987a249af --- /dev/null +++ b/packages/deno/README.md @@ -0,0 +1,64 @@ +

+ + Sentry + +

+ +# Official Sentry SDK for Deno (Beta) + +[![npm version](https://img.shields.io/npm/v/@sentry/deno.svg)](https://www.npmjs.com/package/@sentry/deno) +[![npm dm](https://img.shields.io/npm/dm/@sentry/deno.svg)](https://www.npmjs.com/package/@sentry/deno) +[![npm dt](https://img.shields.io/npm/dt/@sentry/deno.svg)](https://www.npmjs.com/package/@sentry/deno) + +## Links + +- [Official SDK Docs](https://docs.sentry.io/quickstart/) +- [TypeDoc](http://getsentry.github.io/sentry-javascript/) + +The Sentry Deno SDK is in beta. Please help us improve the SDK by [reporting any issues or giving us feedback](https://github.com/getsentry/sentry-javascript/issues). + +## Usage + +To use this SDK, call `Sentry.init(options)` as early as possible in the main entry module. This will initialize the SDK and +hook into the environment. Note that you can turn off almost all side effects using the respective options. + +```javascript +import * as Sentry from 'npm:@sentry/deno'; + +Sentry.init({ + dsn: '__DSN__', + // ... +}); +``` + +To set context information or send manual events, use the exported functions of `@sentry/deno`. Note that these +functions will not perform any action before you have called `init()`: + +```javascript +// Set user information, as well as tags and further extras +Sentry.configureScope(scope => { + scope.setExtra('battery', 0.7); + scope.setTag('user_mode', 'admin'); + scope.setUser({ id: '4711' }); + // scope.clear(); +}); + +// Add a breadcrumb for future events +Sentry.addBreadcrumb({ + message: 'My Breadcrumb', + // ... +}); + +// Capture exceptions, messages or manual events +Sentry.captureMessage('Hello, world!'); +Sentry.captureException(new Error('Good bye')); +Sentry.captureEvent({ + message: 'Manual', + stacktrace: [ + // ... + ], +}); +``` + + + diff --git a/packages/deno/jest.config.js b/packages/deno/jest.config.js new file mode 100644 index 000000000000..24f49ab59a4c --- /dev/null +++ b/packages/deno/jest.config.js @@ -0,0 +1 @@ +module.exports = require('../../jest/jest.config.js'); diff --git a/packages/deno/package.json b/packages/deno/package.json new file mode 100644 index 000000000000..be3a7aa7ca9a --- /dev/null +++ b/packages/deno/package.json @@ -0,0 +1,64 @@ +{ + "name": "@sentry/deno", + "version": "7.73.0", + "description": "Official Sentry SDK for Deno", + "repository": "git://github.com/getsentry/sentry-javascript.git", + "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/deno", + "author": "Sentry", + "license": "MIT", + "main": "build/index.js", + "module": "build/index.js", + "types": "build/index.d.ts", + "private": true, + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@sentry/core": "7.73.0", + "@sentry/browser": "7.73.0", + "@sentry/types": "7.73.0", + "@sentry/utils": "7.73.0", + "lru_map": "^0.3.3" + }, + "devDependencies": { + "@types/node": "20.8.2", + "@rollup/plugin-commonjs": "^25.0.5", + "@rollup/plugin-typescript": "^11.1.5", + "rollup-plugin-dts": "^6.1.0" + }, + "scripts": { + "deno-types": "node ./scripts/download-deno-types.mjs", + "build": "run-s build:transpile build:types", + "build:dev": "yarn build", + "build:transpile": "yarn deno-types && rollup -c rollup.config.js", + "build:types": "run-s deno-types build:types:tsc build:types:bundle", + "build:types:tsc": "tsc -p tsconfig.types.json", + "build:types:bundle": "rollup -c rollup.types.config.js", + "circularDepCheck": "madge --circular src/index.ts", + "clean": "rimraf build coverage", + "prefix": "yarn deno-types", + "fix": "run-s fix:eslint fix:prettier", + "fix:eslint": "eslint . --format stylish --fix", + "fix:prettier": "prettier --write \"{src,test,scripts}/**/**.ts\"", + "prelint": "yarn deno-types", + "lint": "run-s lint:prettier lint:eslint", + "lint:eslint": "eslint . --format stylish", + "lint:prettier": "prettier --check \"{src,test,scripts}/**/**.ts\"", + "install:deno": "node ./scripts/install-deno.mjs", + "test": "run-s deno-types install:deno test:types test:unit", + "test:types": "deno check ./build/index.js", + "test:unit": "deno test --allow-read --allow-run", + "test:unit:update": "deno test --allow-read --allow-write --allow-run -- --update" + }, + "volta": { + "extends": "../../package.json" + }, + "sideEffects": false, + "madge": { + "detectiveOptions": { + "ts": { + "skipTypeImports": true + } + } + } +} diff --git a/packages/deno/rollup.config.js b/packages/deno/rollup.config.js new file mode 100644 index 000000000000..48123037a596 --- /dev/null +++ b/packages/deno/rollup.config.js @@ -0,0 +1,24 @@ +import nodeResolve from '@rollup/plugin-node-resolve'; +import commonjs from '@rollup/plugin-commonjs'; +import sucrase from '@rollup/plugin-sucrase'; + +export default { + input: ['src/index.ts'], + output: { + dir: 'build', + sourcemap: true, + preserveModules: false, + strict: false, + freeze: false, + interop: 'auto', + format: 'esm', + banner: '/// ', + }, + plugins: [ + nodeResolve({ + extensions: ['.mjs', '.js', '.json', '.node', '.ts', '.tsx'], + }), + commonjs(), + sucrase({ transforms: ['typescript'] }), + ], +}; diff --git a/packages/deno/rollup.types.config.js b/packages/deno/rollup.types.config.js new file mode 100644 index 000000000000..d8123b6c5cd3 --- /dev/null +++ b/packages/deno/rollup.types.config.js @@ -0,0 +1,17 @@ +import dts from 'rollup-plugin-dts'; + +export default { + input: './build/index.d.ts', + output: [{ file: 'build/index.d.ts', format: 'es' }], + plugins: [ + dts({ respectExternal: true }), + // The bundled types contain a declaration for the __DEBUG_BUILD__ global + // This can result in errors about duplicate global declarations so we strip it out! + { + name: 'strip-global', + renderChunk(code) { + return { code: code.replace(/declare global \{\s*const __DEBUG_BUILD__: boolean;\s*\}/g, '') }; + }, + }, + ], +}; diff --git a/packages/deno/scripts/download-deno-types.mjs b/packages/deno/scripts/download-deno-types.mjs new file mode 100644 index 000000000000..33bdfcf5ebb7 --- /dev/null +++ b/packages/deno/scripts/download-deno-types.mjs @@ -0,0 +1,7 @@ +import { writeFileSync, existsSync } from 'fs'; +import { download } from './download.mjs'; + +if (!existsSync('lib.deno.d.ts')) { + const code = await download('https://github.com/denoland/deno/releases/download/v1.37.1/lib.deno.d.ts'); + writeFileSync('lib.deno.d.ts', code); +} diff --git a/packages/deno/scripts/download.mjs b/packages/deno/scripts/download.mjs new file mode 100644 index 000000000000..25bcc39f583a --- /dev/null +++ b/packages/deno/scripts/download.mjs @@ -0,0 +1,10 @@ +/** Download a url to a string */ +export async function download(url) { + try { + return await fetch(url).then(res => res.text()); + } catch (e) { + // eslint-disable-next-line no-console + console.error('Failed to download', url, e); + process.exit(1); + } +} diff --git a/packages/deno/scripts/install-deno.mjs b/packages/deno/scripts/install-deno.mjs new file mode 100644 index 000000000000..aa7235a278ff --- /dev/null +++ b/packages/deno/scripts/install-deno.mjs @@ -0,0 +1,26 @@ +import { execSync } from 'child_process'; + +import { download } from './download.mjs'; + +try { + execSync('deno --version', { stdio: 'inherit' }); +} catch (_) { + // eslint-disable-next-line no-console + console.error('Deno is not installed. Installing...'); + if (process.platform === 'win32') { + // TODO + // eslint-disable-next-line no-console + console.error('Please install Deno manually: https://docs.deno.com/runtime/manual/getting_started/installation'); + process.exit(1); + } else { + const script = await download('https://deno.land/x/install/install.sh'); + + try { + execSync(script, { stdio: 'inherit' }); + } catch (e) { + // eslint-disable-next-line no-console + console.error('Failed to install Deno', e); + process.exit(1); + } + } +} diff --git a/packages/deno/src/client.ts b/packages/deno/src/client.ts new file mode 100644 index 000000000000..3eb7db655428 --- /dev/null +++ b/packages/deno/src/client.ts @@ -0,0 +1,44 @@ +import type { ServerRuntimeClientOptions } from '@sentry/core'; +import { SDK_VERSION, ServerRuntimeClient } from '@sentry/core'; + +import type { DenoClientOptions } from './types'; + +function getHostName(): string | undefined { + const result = Deno.permissions.querySync({ name: 'sys', kind: 'hostname' }); + return result.state === 'granted' ? Deno.hostname() : undefined; +} + +/** + * The Sentry Deno SDK Client. + * + * @see DenoClientOptions for documentation on configuration options. + * @see SentryClient for usage documentation. + */ +export class DenoClient extends ServerRuntimeClient { + /** + * Creates a new Deno SDK instance. + * @param options Configuration options for this SDK. + */ + public constructor(options: DenoClientOptions) { + options._metadata = options._metadata || {}; + options._metadata.sdk = options._metadata.sdk || { + name: 'sentry.javascript.deno', + packages: [ + { + name: 'denoland:sentry', + version: SDK_VERSION, + }, + ], + version: SDK_VERSION, + }; + + const clientOptions: ServerRuntimeClientOptions = { + ...options, + platform: 'deno', + runtime: { name: 'deno', version: Deno.version.deno }, + serverName: options.serverName || getHostName(), + }; + + super(clientOptions); + } +} diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts new file mode 100644 index 000000000000..6a92adea2513 --- /dev/null +++ b/packages/deno/src/index.ts @@ -0,0 +1,77 @@ +export type { + Breadcrumb, + BreadcrumbHint, + PolymorphicRequest, + Request, + SdkInfo, + Event, + EventHint, + Exception, + Session, + // eslint-disable-next-line deprecation/deprecation + Severity, + SeverityLevel, + Span, + StackFrame, + Stacktrace, + Thread, + Transaction, + User, +} from '@sentry/types'; +export type { AddRequestDataToEventOptions } from '@sentry/utils'; + +export type { DenoOptions } from './types'; + +export { + addGlobalEventProcessor, + addBreadcrumb, + captureException, + captureEvent, + captureMessage, + close, + configureScope, + createTransport, + extractTraceparentData, + flush, + getActiveTransaction, + getHubFromCarrier, + getCurrentHub, + Hub, + lastEventId, + makeMain, + runWithAsyncContext, + Scope, + startTransaction, + SDK_VERSION, + setContext, + setExtra, + setExtras, + setTag, + setTags, + setUser, + spanStatusfromHttpCode, + trace, + withScope, + captureCheckIn, + setMeasurement, + getActiveSpan, + startSpan, + startInactiveSpan, + startSpanManual, +} from '@sentry/core'; +export type { SpanStatusType } from '@sentry/core'; + +export { DenoClient } from './client'; + +export { defaultIntegrations, init } from './sdk'; + +import { Integrations as CoreIntegrations } from '@sentry/core'; + +import * as DenoIntegrations from './integrations'; + +const INTEGRATIONS = { + ...CoreIntegrations, + ...DenoIntegrations, +}; + +export { INTEGRATIONS as Integrations }; diff --git a/packages/deno/src/integrations/context.ts b/packages/deno/src/integrations/context.ts new file mode 100644 index 000000000000..49269c81be4e --- /dev/null +++ b/packages/deno/src/integrations/context.ts @@ -0,0 +1,64 @@ +import type { Event, EventProcessor, Integration } from '@sentry/types'; + +function getOSName(): string { + switch (Deno.build.os) { + case 'darwin': + return 'macOS'; + case 'linux': + return 'Linux'; + case 'windows': + return 'Windows'; + default: + return Deno.build.os; + } +} + +function getOSRelease(): string | undefined { + return Deno.permissions.querySync({ name: 'sys', kind: 'osRelease' }).state === 'granted' + ? Deno.osRelease() + : undefined; +} + +async function denoRuntime(event: Event): Promise { + event.contexts = { + ...{ + app: { + app_start_time: new Date(Date.now() - performance.now()).toISOString(), + }, + device: { + arch: Deno.build.arch, + // eslint-disable-next-line no-restricted-globals + processor_count: navigator.hardwareConcurrency, + }, + os: { + name: getOSName(), + version: getOSRelease(), + }, + v8: { + name: 'v8', + version: Deno.version.v8, + }, + typescript: { + name: 'TypeScript', + version: Deno.version.typescript, + }, + }, + ...event.contexts, + }; + + return event; +} + +/** Adds Electron context to events. */ +export class DenoContext implements Integration { + /** @inheritDoc */ + public static id = 'DenoContext'; + + /** @inheritDoc */ + public name: string = DenoContext.id; + + /** @inheritDoc */ + public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void): void { + addGlobalEventProcessor(async (event: Event) => denoRuntime(event)); + } +} diff --git a/packages/deno/src/integrations/contextlines.ts b/packages/deno/src/integrations/contextlines.ts new file mode 100644 index 000000000000..47cd3a09218d --- /dev/null +++ b/packages/deno/src/integrations/contextlines.ts @@ -0,0 +1,116 @@ +import type { Event, EventProcessor, Integration, StackFrame } from '@sentry/types'; +import { addContextToFrame } from '@sentry/utils'; +import { LRUMap } from 'lru_map'; + +const FILE_CONTENT_CACHE = new LRUMap(100); +const DEFAULT_LINES_OF_CONTEXT = 7; + +/** + * Resets the file cache. Exists for testing purposes. + * @hidden + */ +export function resetFileContentCache(): void { + FILE_CONTENT_CACHE.clear(); +} + +/** + * Reads file contents and caches them in a global LRU cache. + * + * @param filename filepath to read content from. + */ +async function readSourceFile(filename: string): Promise { + const cachedFile = FILE_CONTENT_CACHE.get(filename); + // We have a cache hit + if (cachedFile !== undefined) { + return cachedFile; + } + + let content: string | null = null; + try { + content = await Deno.readTextFile(filename); + } catch (_) { + // + } + + FILE_CONTENT_CACHE.set(filename, content); + return content; +} + +interface ContextLinesOptions { + /** + * Sets the number of context lines for each frame when loading a file. + * Defaults to 7. + * + * Set to 0 to disable loading and inclusion of source files. + */ + frameContextLines?: number; +} + +/** Add node modules / packages to the event */ +export class ContextLines implements Integration { + /** + * @inheritDoc + */ + public static id = 'ContextLines'; + + /** + * @inheritDoc + */ + public name: string = ContextLines.id; + + public constructor(private readonly _options: ContextLinesOptions = {}) {} + + /** Get's the number of context lines to add */ + private get _contextLines(): number { + return this._options.frameContextLines !== undefined ? this._options.frameContextLines : DEFAULT_LINES_OF_CONTEXT; + } + + /** + * @inheritDoc + */ + public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void): void { + addGlobalEventProcessor(event => this.addSourceContext(event)); + } + + /** Processes an event and adds context lines */ + public async addSourceContext(event: Event): Promise { + if (this._contextLines > 0 && event.exception && event.exception.values) { + for (const exception of event.exception.values) { + if (exception.stacktrace && exception.stacktrace.frames) { + await this.addSourceContextToFrames(exception.stacktrace.frames); + } + } + } + + return event; + } + + /** Adds context lines to frames */ + public async addSourceContextToFrames(frames: StackFrame[]): Promise { + const contextLines = this._contextLines; + + for (const frame of frames) { + // Only add context if we have a filename and it hasn't already been added + if (frame.filename && frame.in_app && frame.context_line === undefined) { + const permission = await Deno.permissions.query({ + name: 'read', + path: frame.filename, + }); + + if (permission.state == 'granted') { + const sourceFile = await readSourceFile(frame.filename); + + if (sourceFile) { + try { + const lines = sourceFile.split('\n'); + addContextToFrame(lines, frame, contextLines); + } catch (_) { + // anomaly, being defensive in case + // unlikely to ever happen in practice but can definitely happen in theory + } + } + } + } + } + } +} diff --git a/packages/deno/src/integrations/globalhandlers.ts b/packages/deno/src/integrations/globalhandlers.ts new file mode 100644 index 000000000000..7e4d2e003673 --- /dev/null +++ b/packages/deno/src/integrations/globalhandlers.ts @@ -0,0 +1,165 @@ +import type { ServerRuntimeClient } from '@sentry/core'; +import { flush, getCurrentHub } from '@sentry/core'; +import type { Event, EventHint, Hub, Integration, Primitive, StackParser } from '@sentry/types'; +import { addExceptionMechanism, eventFromUnknownInput, isPrimitive } from '@sentry/utils'; + +type GlobalHandlersIntegrationsOptionKeys = 'error' | 'unhandledrejection'; + +/** JSDoc */ +type GlobalHandlersIntegrations = Record; + +let isExiting = false; + +/** Global handlers */ +export class GlobalHandlers implements Integration { + /** + * @inheritDoc + */ + public static id = 'GlobalHandlers'; + + /** + * @inheritDoc + */ + public name: string = GlobalHandlers.id; + + /** JSDoc */ + private readonly _options: GlobalHandlersIntegrations; + + /** + * Stores references functions to installing handlers. Will set to undefined + * after they have been run so that they are not used twice. + */ + private _installFunc: Record void) | undefined> = { + error: installGlobalErrorHandler, + unhandledrejection: installGlobalUnhandledRejectionHandler, + }; + + /** JSDoc */ + public constructor(options?: GlobalHandlersIntegrations) { + this._options = { + error: true, + unhandledrejection: true, + ...options, + }; + } + /** + * @inheritDoc + */ + public setupOnce(): void { + const options = this._options; + + // We can disable guard-for-in as we construct the options object above + do checks against + // `this._installFunc` for the property. + // eslint-disable-next-line guard-for-in + for (const key in options) { + const installFunc = this._installFunc[key as GlobalHandlersIntegrationsOptionKeys]; + if (installFunc && options[key as GlobalHandlersIntegrationsOptionKeys]) { + installFunc(); + this._installFunc[key as GlobalHandlersIntegrationsOptionKeys] = undefined; + } + } + } +} + +function installGlobalErrorHandler(): void { + globalThis.addEventListener('error', data => { + if (isExiting) { + return; + } + + const [hub, stackParser] = getHubAndOptions(); + const { message, error } = data; + + const event = eventFromUnknownInput(getCurrentHub, stackParser, error || message); + + event.level = 'fatal'; + + addMechanismAndCapture(hub, error, event, 'error'); + + // Stop the app from exiting for now + data.preventDefault(); + isExiting = true; + + void flush().then(() => { + // rethrow to replicate Deno default behavior + throw error; + }); + }); +} + +function installGlobalUnhandledRejectionHandler(): void { + globalThis.addEventListener('unhandledrejection', (e: PromiseRejectionEvent) => { + if (isExiting) { + return; + } + + const [hub, stackParser] = getHubAndOptions(); + let error = e; + + // dig the object of the rejection out of known event types + try { + if ('reason' in e) { + error = e.reason; + } + } catch (_oO) { + // no-empty + } + + const event = isPrimitive(error) + ? eventFromRejectionWithPrimitive(error) + : eventFromUnknownInput(getCurrentHub, stackParser, error, undefined); + + event.level = 'fatal'; + + addMechanismAndCapture(hub, error as unknown as Error, event, 'unhandledrejection'); + + // Stop the app from exiting for now + e.preventDefault(); + isExiting = true; + + void flush().then(() => { + // rethrow to replicate Deno default behavior + throw error; + }); + }); +} + +/** + * Create an event from a promise rejection where the `reason` is a primitive. + * + * @param reason: The `reason` property of the promise rejection + * @returns An Event object with an appropriate `exception` value + */ +function eventFromRejectionWithPrimitive(reason: Primitive): Event { + return { + exception: { + values: [ + { + type: 'UnhandledRejection', + // String() is needed because the Primitive type includes symbols (which can't be automatically stringified) + value: `Non-Error promise rejection captured with value: ${String(reason)}`, + }, + ], + }, + }; +} + +function addMechanismAndCapture(hub: Hub, error: EventHint['originalException'], event: Event, type: string): void { + addExceptionMechanism(event, { + handled: false, + type, + }); + hub.captureEvent(event, { + originalException: error, + }); +} + +function getHubAndOptions(): [Hub, StackParser] { + const hub = getCurrentHub(); + const client = hub.getClient(); + const options = (client && client.getOptions()) || { + stackParser: () => [], + attachStacktrace: false, + }; + return [hub, options.stackParser]; +} diff --git a/packages/deno/src/integrations/index.ts b/packages/deno/src/integrations/index.ts new file mode 100644 index 000000000000..97e439649bfc --- /dev/null +++ b/packages/deno/src/integrations/index.ts @@ -0,0 +1,4 @@ +export { DenoContext } from './context'; +export { GlobalHandlers } from './globalhandlers'; +export { NormalizePaths } from './normalizepaths'; +export { ContextLines } from './contextlines'; diff --git a/packages/deno/src/integrations/normalizepaths.ts b/packages/deno/src/integrations/normalizepaths.ts new file mode 100644 index 000000000000..bf8a3986c93d --- /dev/null +++ b/packages/deno/src/integrations/normalizepaths.ts @@ -0,0 +1,100 @@ +import type { Event, EventProcessor, Integration } from '@sentry/types'; +import { createStackParser, dirname, nodeStackLineParser } from '@sentry/utils'; + +function appRootFromErrorStack(error: Error): string | undefined { + // We know at the other end of the stack from here is the entry point that called 'init' + // We assume that this stacktrace will traverse the root of the app + const frames = createStackParser(nodeStackLineParser())(error.stack || ''); + + const paths = frames + // We're only interested in frames that are in_app with filenames + .filter(f => f.in_app && f.filename) + .map( + f => + (f.filename as string) + .replace(/^[A-Z]:/, '') // remove Windows-style prefix + .replace(/\\/g, '/') // replace all `\` instances with `/` + .split('/') + .filter(seg => seg !== ''), // remove empty segments + ) as string[][]; + + if (paths.length == 0) { + return undefined; + } + + if (paths.length == 1) { + // Assume the single file is in the root + return dirname(paths[0].join('/')); + } + + // Iterate over the paths and bail out when they no longer have a common root + let i = 0; + while (paths[0][i] && paths.every(w => w[i] === paths[0][i])) { + i++; + } + + return paths[0].slice(0, i).join('/'); +} + +function getCwd(): string | undefined { + // We don't want to prompt for permissions so we only get the cwd if + // permissions are already granted + const permission = Deno.permissions.querySync({ name: 'read', path: './' }); + + try { + if (permission.state == 'granted') { + return Deno.cwd(); + } + } catch (_) { + // + } + + return undefined; +} + +// Cached here +let appRoot: string | undefined; + +function getAppRoot(error: Error): string | undefined { + if (appRoot === undefined) { + appRoot = getCwd() || appRootFromErrorStack(error); + } + + return appRoot; +} + +/** Normalises paths to the app root directory. */ +export class NormalizePaths implements Integration { + /** @inheritDoc */ + public static id = 'NormalizePaths'; + + /** @inheritDoc */ + public name: string = NormalizePaths.id; + + /** @inheritDoc */ + public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void): void { + // This error.stack hopefully contains paths that traverse the app cwd + const error = new Error(); + + addGlobalEventProcessor((event: Event): Event | null => { + const appRoot = getAppRoot(error); + + if (appRoot) { + for (const exception of event.exception?.values || []) { + for (const frame of exception.stacktrace?.frames || []) { + if (frame.filename && frame.in_app) { + const startIndex = frame.filename.indexOf(appRoot); + + if (startIndex > -1) { + const endIndex = startIndex + appRoot.length; + frame.filename = `app://${frame.filename.substring(endIndex)}`; + } + } + } + } + } + + return event; + }); + } +} diff --git a/packages/deno/src/sdk.ts b/packages/deno/src/sdk.ts new file mode 100644 index 000000000000..cff16148453e --- /dev/null +++ b/packages/deno/src/sdk.ts @@ -0,0 +1,102 @@ +import { Breadcrumbs, Dedupe, LinkedErrors } from '@sentry/browser'; +import type { ServerRuntimeClientOptions } from '@sentry/core'; +import { getIntegrationsToSetup, initAndBind, Integrations as CoreIntegrations } from '@sentry/core'; +import type { StackParser } from '@sentry/types'; +import { createStackParser, nodeStackLineParser, stackParserFromStackParserOptions } from '@sentry/utils'; + +import { DenoClient } from './client'; +import { ContextLines, DenoContext, GlobalHandlers, NormalizePaths } from './integrations'; +import { makeFetchTransport } from './transports'; +import type { DenoOptions } from './types'; + +export const defaultIntegrations = [ + // Common + new CoreIntegrations.InboundFilters(), + new CoreIntegrations.FunctionToString(), + // From Browser + new Dedupe(), + new LinkedErrors(), + new Breadcrumbs({ + dom: false, + history: false, + xhr: false, + }), + // Deno Specific + new DenoContext(), + new ContextLines(), + new NormalizePaths(), + new GlobalHandlers(), +]; + +const defaultStackParser: StackParser = createStackParser(nodeStackLineParser()); + +/** + * The Sentry Deno SDK Client. + * + * To use this SDK, call the {@link init} function as early as possible in the + * main entry module. To set context information or send manual events, use the + * provided methods. + * + * @example + * ``` + * + * import { init } from 'npm:@sentry/deno'; + * + * init({ + * dsn: '__DSN__', + * // ... + * }); + * ``` + * + * @example + * ``` + * + * import { configureScope } from 'npm:@sentry/deno'; + * configureScope((scope: Scope) => { + * scope.setExtra({ battery: 0.7 }); + * scope.setTag({ user_mode: 'admin' }); + * scope.setUser({ id: '4711' }); + * }); + * ``` + * + * @example + * ``` + * + * import { addBreadcrumb } from 'npm:@sentry/deno'; + * addBreadcrumb({ + * message: 'My Breadcrumb', + * // ... + * }); + * ``` + * + * @example + * ``` + * + * import * as Sentry from 'npm:@sentry/deno'; + * Sentry.captureMessage('Hello, world!'); + * Sentry.captureException(new Error('Good bye')); + * Sentry.captureEvent({ + * message: 'Manual', + * stacktrace: [ + * // ... + * ], + * }); + * ``` + * + * @see {@link DenoOptions} for documentation on configuration options. + */ +export function init(options: DenoOptions = {}): void { + options.defaultIntegrations = + options.defaultIntegrations === false + ? [] + : [...(Array.isArray(options.defaultIntegrations) ? options.defaultIntegrations : defaultIntegrations)]; + + const clientOptions: ServerRuntimeClientOptions = { + ...options, + stackParser: stackParserFromStackParserOptions(options.stackParser || defaultStackParser), + integrations: getIntegrationsToSetup(options), + transport: options.transport || makeFetchTransport, + }; + + initAndBind(DenoClient, clientOptions); +} diff --git a/packages/deno/src/transports/index.ts b/packages/deno/src/transports/index.ts new file mode 100644 index 000000000000..62da327c5d83 --- /dev/null +++ b/packages/deno/src/transports/index.ts @@ -0,0 +1,46 @@ +import { createTransport } from '@sentry/core'; +import type { BaseTransportOptions, Transport, TransportMakeRequestResponse, TransportRequest } from '@sentry/types'; +import { rejectedSyncPromise } from '@sentry/utils'; + +export interface DenoTransportOptions extends BaseTransportOptions { + /** Custom headers for the transport. Used by the XHRTransport and FetchTransport */ + headers?: { [key: string]: string }; +} + +/** + * Creates a Transport that uses the Fetch API to send events to Sentry. + */ +export function makeFetchTransport(options: DenoTransportOptions): Transport { + const url = new URL(options.url); + + if (Deno.permissions.querySync({ name: 'net', host: url.host }).state !== 'granted') { + // eslint-disable-next-line no-console + console.warn(`Sentry SDK requires 'net' permission to send events. +Run with '--allow-net=${url.host}' to grant the requires permissions.`); + } + + function makeRequest(request: TransportRequest): PromiseLike { + const requestOptions: RequestInit = { + body: request.body, + method: 'POST', + referrerPolicy: 'origin', + headers: options.headers, + }; + + try { + return fetch(options.url, requestOptions).then(response => { + return { + statusCode: response.status, + headers: { + 'x-sentry-rate-limits': response.headers.get('X-Sentry-Rate-Limits'), + 'retry-after': response.headers.get('Retry-After'), + }, + }; + }); + } catch (e) { + return rejectedSyncPromise(e); + } + } + + return createTransport(options, makeRequest); +} diff --git a/packages/deno/src/types.ts b/packages/deno/src/types.ts new file mode 100644 index 000000000000..50310589666a --- /dev/null +++ b/packages/deno/src/types.ts @@ -0,0 +1,59 @@ +import type { ClientOptions, Options, TracePropagationTargets } from '@sentry/types'; + +import type { DenoTransportOptions } from './transports'; + +export interface BaseDenoOptions { + /** + * List of strings/regex controlling to which outgoing requests + * the SDK will attach tracing headers. + * + * By default the SDK will attach those headers to all outgoing + * requests. If this option is provided, the SDK will match the + * request URL of outgoing requests against the items in this + * array, and only attach tracing headers if a match was found. + * + * @example + * ```js + * Sentry.init({ + * tracePropagationTargets: ['api.site.com'], + * }); + * ``` + */ + tracePropagationTargets?: TracePropagationTargets; + + /** Sets an optional server name (device name) */ + serverName?: string; + + // TODO (v8): Remove this in v8 + /** + * @deprecated Moved to constructor options of the `Http` and `Undici` integration. + * @example + * ```js + * Sentry.init({ + * integrations: [ + * new Sentry.Integrations.Http({ + * tracing: { + * shouldCreateSpanForRequest: (url: string) => false, + * } + * }); + * ], + * }); + * ``` + */ + shouldCreateSpanForRequest?(this: void, url: string): boolean; + + /** Callback that is executed when a fatal global error occurs. */ + onFatalError?(this: void, error: Error): void; +} + +/** + * Configuration options for the Sentry Deno SDK + * @see @sentry/types Options for more information. + */ +export interface DenoOptions extends Options, BaseDenoOptions {} + +/** + * Configuration options for the Sentry Deno SDK Client class + * @see DenoClient for more information. + */ +export interface DenoClientOptions extends ClientOptions, BaseDenoOptions {} diff --git a/packages/deno/test/__snapshots__/mod.test.ts.snap b/packages/deno/test/__snapshots__/mod.test.ts.snap new file mode 100644 index 000000000000..749d3ce9d238 --- /dev/null +++ b/packages/deno/test/__snapshots__/mod.test.ts.snap @@ -0,0 +1,222 @@ +export const snapshot = {}; + +snapshot[`captureException 1`] = ` +{ + contexts: { + app: { + app_start_time: "{{time}}", + }, + device: { + arch: "{{arch}}", + processor_count: 0, + }, + os: { + name: "{{platform}}", + }, + runtime: { + name: "deno", + version: "1.37.1", + }, + trace: { + span_id: "{{id}}", + trace_id: "{{id}}", + }, + typescript: { + name: "TypeScript", + version: "{{version}}", + }, + v8: { + name: "v8", + version: "{{version}}", + }, + }, + environment: "production", + event_id: "{{id}}", + exception: { + values: [ + { + mechanism: { + handled: true, + type: "generic", + }, + stacktrace: { + frames: [ + { + colno: 20, + filename: "ext:cli/40_testing.js", + function: "outerWrapped", + in_app: false, + lineno: 488, + }, + { + colno: 33, + filename: "ext:cli/40_testing.js", + function: "exitSanitizer", + in_app: false, + lineno: 474, + }, + { + colno: 31, + filename: "ext:cli/40_testing.js", + function: "resourceSanitizer", + in_app: false, + lineno: 425, + }, + { + colno: 33, + filename: "ext:cli/40_testing.js", + function: "asyncOpSanitizer", + in_app: false, + lineno: 192, + }, + { + colno: 11, + filename: "ext:cli/40_testing.js", + function: "innerWrapped", + in_app: false, + lineno: 543, + }, + { + colno: 24, + context_line: " hub.captureException(something());", + filename: "app:///test/mod.test.ts", + function: "", + in_app: true, + lineno: 43, + post_context: [ + "", + " await delay(200);", + " await assertSnapshot(t, ev);", + "});", + "", + "Deno.test('captureMessage', async t => {", + " let ev: Event | undefined;", + ], + pre_context: [ + " ev = event;", + " });", + "", + " function something() {", + " return new Error('Some unhandled error');", + " }", + "", + ], + }, + { + colno: 12, + context_line: " return new Error('Some unhandled error');", + filename: "app:///test/mod.test.ts", + function: "something", + in_app: true, + lineno: 40, + post_context: [ + " }", + "", + " hub.captureException(something());", + "", + " await delay(200);", + " await assertSnapshot(t, ev);", + "});", + ], + pre_context: [ + "Deno.test('captureException', async t => {", + " let ev: Event | undefined;", + " const [hub] = getTestClient(event => {", + " ev = event;", + " });", + "", + " function something() {", + ], + }, + ], + }, + type: "Error", + value: "Some unhandled error", + }, + ], + }, + platform: "deno", + sdk: { + integrations: [ + "InboundFilters", + "FunctionToString", + "Dedupe", + "LinkedErrors", + "Breadcrumbs", + "DenoContext", + "ContextLines", + "NormalizePaths", + "GlobalHandlers", + ], + name: "sentry.javascript.deno", + packages: [ + { + name: "denoland:sentry", + version: "{{version}}", + }, + ], + version: "{{version}}", + }, + timestamp: 0, +} +`; + +snapshot[`captureMessage 1`] = ` +{ + contexts: { + app: { + app_start_time: "{{time}}", + }, + device: { + arch: "{{arch}}", + processor_count: 0, + }, + os: { + name: "{{platform}}", + }, + runtime: { + name: "deno", + version: "1.37.1", + }, + trace: { + span_id: "{{id}}", + trace_id: "{{id}}", + }, + typescript: { + name: "TypeScript", + version: "{{version}}", + }, + v8: { + name: "v8", + version: "{{version}}", + }, + }, + environment: "production", + event_id: "{{id}}", + level: "info", + message: "Some error message", + platform: "deno", + sdk: { + integrations: [ + "InboundFilters", + "FunctionToString", + "Dedupe", + "LinkedErrors", + "Breadcrumbs", + "DenoContext", + "ContextLines", + "NormalizePaths", + "GlobalHandlers", + ], + name: "sentry.javascript.deno", + packages: [ + { + name: "denoland:sentry", + version: "{{version}}", + }, + ], + version: "{{version}}", + }, + timestamp: 0, +} +`; diff --git a/packages/deno/test/example.ts b/packages/deno/test/example.ts new file mode 100644 index 000000000000..6f93bd288afd --- /dev/null +++ b/packages/deno/test/example.ts @@ -0,0 +1,14 @@ +import * as Sentry from '../build/index.js'; + +Sentry.init({ + dsn: 'https://1234@some-domain.com/4505526893805568', +}); + +Sentry.addBreadcrumb({ message: 'My Breadcrumb' }); + +// eslint-disable-next-line no-console +console.log('App has started'); + +setTimeout(() => { + Deno.exit(); +}, 1_000); diff --git a/packages/deno/test/mod.test.ts b/packages/deno/test/mod.test.ts new file mode 100644 index 000000000000..1724125415cd --- /dev/null +++ b/packages/deno/test/mod.test.ts @@ -0,0 +1,76 @@ +import { assertEquals } from 'https://deno.land/std@0.202.0/assert/assert_equals.ts'; +import { assertSnapshot } from 'https://deno.land/std@0.202.0/testing/snapshot.ts'; +import type { Event, Integration } from 'npm:@sentry/types'; +import { createStackParser, nodeStackLineParser } from 'npm:@sentry/utils'; + +import { defaultIntegrations, DenoClient, Hub, Scope } from '../build/index.js'; +import { getNormalizedEvent } from './normalize.ts'; +import { makeTestTransport } from './transport.ts'; + +function getTestClient(callback: (event?: Event) => void, integrations: Integration[] = []): [Hub, DenoClient] { + const client = new DenoClient({ + dsn: 'https://233a45e5efe34c47a3536797ce15dafa@nothing.here/5650507', + debug: true, + integrations: [...defaultIntegrations, ...integrations], + stackParser: createStackParser(nodeStackLineParser()), + transport: makeTestTransport(envelope => { + callback(getNormalizedEvent(envelope)); + }), + }); + + const scope = new Scope(); + const hub = new Hub(client, scope); + + return [hub, client]; +} + +function delay(time: number): Promise { + return new Promise(resolve => { + setTimeout(resolve, time); + }); +} + +Deno.test('captureException', async t => { + let ev: Event | undefined; + const [hub] = getTestClient(event => { + ev = event; + }); + + function something() { + return new Error('Some unhandled error'); + } + + hub.captureException(something()); + + await delay(200); + await assertSnapshot(t, ev); +}); + +Deno.test('captureMessage', async t => { + let ev: Event | undefined; + const [hub] = getTestClient(event => { + ev = event; + }); + + hub.captureMessage('Some error message'); + + await delay(200); + await assertSnapshot(t, ev); +}); + +Deno.test('App runs without errors', async _ => { + const cmd = new Deno.Command('deno', { + args: ['run', '--allow-net=some-domain.com', './test/example.ts'], + stdout: 'piped', + stderr: 'piped', + }); + + const output = await cmd.output(); + assertEquals(output.success, true); + + const td = new TextDecoder(); + const outString = td.decode(output.stdout); + const errString = td.decode(output.stderr); + assertEquals(outString, 'App has started\n'); + assertEquals(errString, ''); +}); diff --git a/packages/deno/test/normalize.ts b/packages/deno/test/normalize.ts new file mode 100644 index 000000000000..b36cf4ac52a9 --- /dev/null +++ b/packages/deno/test/normalize.ts @@ -0,0 +1,207 @@ +/* eslint-disable complexity */ +import type { Envelope, Event, Session, Transaction } from 'npm:@sentry/types'; +import { forEachEnvelopeItem } from 'npm:@sentry/utils'; + +type EventOrSession = Event | Transaction | Session; + +export function getNormalizedEvent(envelope: Envelope): Event | undefined { + let event: Event | undefined; + + forEachEnvelopeItem(envelope, item => { + const [headers, body] = item; + + if (headers.type === 'event') { + event = body as Event; + } + }); + + return normalize(event) as Event | undefined; +} + +export function normalize(event: EventOrSession | undefined): EventOrSession | undefined { + if (event === undefined) { + return undefined; + } + + if (eventIsSession(event)) { + return normalizeSession(event as Session); + } else { + return normalizeEvent(event as Event); + } +} + +export function eventIsSession(data: EventOrSession): boolean { + return !!(data as Session)?.sid; +} + +/** + * Normalizes a session so that in can be compared to an expected event + * + * All properties that are timestamps, versions, ids or variables that may vary + * by platform are replaced with placeholder strings + */ +function normalizeSession(session: Session): Session { + if (session.sid) { + session.sid = '{{id}}'; + } + + if (session.started) { + session.started = 0; + } + + if (session.timestamp) { + session.timestamp = 0; + } + + if (session.duration) { + session.duration = 0; + } + + return session; +} + +/** + * Normalizes an event so that in can be compared to an expected event + * + * All properties that are timestamps, versions, ids or variables that may vary + * by platform are replaced with placeholder strings + */ +function normalizeEvent(event: Event): Event { + if (event.sdk?.version) { + event.sdk.version = '{{version}}'; + } + + if (event?.sdk?.packages) { + for (const pkg of event?.sdk?.packages) { + if (pkg.version) { + pkg.version = '{{version}}'; + } + } + } + + if (event.contexts?.app?.app_start_time) { + event.contexts.app.app_start_time = '{{time}}'; + } + + if (event.contexts?.typescript?.version) { + event.contexts.typescript.version = '{{version}}'; + } + + if (event.contexts?.v8?.version) { + event.contexts.v8.version = '{{version}}'; + } + + if (event.contexts?.deno) { + if (event.contexts.deno?.version) { + event.contexts.deno.version = '{{version}}'; + } + if (event.contexts.deno?.target) { + event.contexts.deno.target = '{{target}}'; + } + } + + if (event.contexts?.device?.arch) { + event.contexts.device.arch = '{{arch}}'; + } + + if (event.contexts?.device?.memory_size) { + event.contexts.device.memory_size = 0; + } + + if (event.contexts?.device?.free_memory) { + event.contexts.device.free_memory = 0; + } + + if (event.contexts?.device?.processor_count) { + event.contexts.device.processor_count = 0; + } + + if (event.contexts?.device?.processor_frequency) { + event.contexts.device.processor_frequency = 0; + } + + if (event.contexts?.device?.cpu_description) { + event.contexts.device.cpu_description = '{{cpu}}'; + } + + if (event.contexts?.device?.screen_resolution) { + event.contexts.device.screen_resolution = '{{screen}}'; + } + + if (event.contexts?.device?.screen_density) { + event.contexts.device.screen_density = 1; + } + + if (event.contexts?.device?.language) { + event.contexts.device.language = '{{language}}'; + } + + if (event.contexts?.os?.name) { + event.contexts.os.name = '{{platform}}'; + } + + if (event.contexts?.os?.version) { + event.contexts.os.version = '{{version}}'; + } + + if (event.contexts?.trace) { + event.contexts.trace.span_id = '{{id}}'; + event.contexts.trace.trace_id = '{{id}}'; + delete event.contexts.trace.tags; + } + + if (event.start_timestamp) { + event.start_timestamp = 0; + } + + if (event.exception?.values?.[0].stacktrace?.frames) { + // Exlcude Deno frames since these may change between versions + event.exception.values[0].stacktrace.frames = event.exception.values[0].stacktrace.frames.filter( + frame => !frame.filename?.includes('deno:'), + ); + } + + event.timestamp = 0; + // deno-lint-ignore no-explicit-any + if ((event as any).start_timestamp) { + // deno-lint-ignore no-explicit-any + (event as any).start_timestamp = 0; + } + + event.event_id = '{{id}}'; + + if (event.spans) { + for (const span of event.spans) { + // deno-lint-ignore no-explicit-any + const spanAny = span as any; + + if (spanAny.span_id) { + spanAny.span_id = '{{id}}'; + } + + if (spanAny.parent_span_id) { + spanAny.parent_span_id = '{{id}}'; + } + + if (spanAny.start_timestamp) { + spanAny.start_timestamp = 0; + } + + if (spanAny.timestamp) { + spanAny.timestamp = 0; + } + + if (spanAny.trace_id) { + spanAny.trace_id = '{{id}}'; + } + } + } + + if (event.breadcrumbs) { + for (const breadcrumb of event.breadcrumbs) { + breadcrumb.timestamp = 0; + } + } + + return event; +} diff --git a/packages/deno/test/transport.ts b/packages/deno/test/transport.ts new file mode 100644 index 000000000000..2eaeed6eeef6 --- /dev/null +++ b/packages/deno/test/transport.ts @@ -0,0 +1,30 @@ +import { createTransport } from 'npm:@sentry/core'; +import type { + BaseTransportOptions, + Envelope, + Transport, + TransportMakeRequestResponse, + TransportRequest, +} from 'npm:@sentry/types'; +import { parseEnvelope } from 'npm:@sentry/utils'; + +export interface TestTransportOptions extends BaseTransportOptions { + callback: (envelope: Envelope) => void; +} + +/** + * Creates a Transport that uses the Fetch API to send events to Sentry. + */ +export function makeTestTransport(callback: (envelope: Envelope) => void) { + return (options: BaseTransportOptions): Transport => { + async function doCallback(request: TransportRequest): Promise { + await callback(parseEnvelope(request.body, new TextEncoder(), new TextDecoder())); + + return Promise.resolve({ + statusCode: 200, + }); + } + + return createTransport(options, doCallback); + }; +} diff --git a/packages/deno/tsconfig.build.json b/packages/deno/tsconfig.build.json new file mode 100644 index 000000000000..87025d5676c5 --- /dev/null +++ b/packages/deno/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "include": ["./lib.deno.d.ts", "src/**/*"], + "compilerOptions": { + "outDir": "build", + "lib": ["esnext"], + "module": "esnext", + "target": "esnext", + "declaration": true, + "declarationMap": false + } +} diff --git a/packages/deno/tsconfig.json b/packages/deno/tsconfig.json new file mode 100644 index 000000000000..fdd107c1ed78 --- /dev/null +++ b/packages/deno/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "include": ["./lib.deno.d.ts", "src/**/*", "example.ts"], + "compilerOptions": { + "lib": ["esnext"], + "module": "esnext", + "target": "esnext" + } +} diff --git a/packages/deno/tsconfig.test.json b/packages/deno/tsconfig.test.json new file mode 100644 index 000000000000..548e94149758 --- /dev/null +++ b/packages/deno/tsconfig.test.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "include": ["./lib.deno.d.ts", "test/**/*"], + "compilerOptions": { + "lib": ["esnext"], + "module": "esnext", + "target": "esnext" + } +} diff --git a/packages/deno/tsconfig.types.json b/packages/deno/tsconfig.types.json new file mode 100644 index 000000000000..d6d1e9a548c9 --- /dev/null +++ b/packages/deno/tsconfig.types.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "outDir": "build" + } +} diff --git a/packages/e2e-tests/package.json b/packages/e2e-tests/package.json index 73790f4ddc4a..0e713a9afed2 100644 --- a/packages/e2e-tests/package.json +++ b/packages/e2e-tests/package.json @@ -17,7 +17,7 @@ "test:validate-test-app-setups": "ts-node validate-test-app-setups.ts", "test:prepare": "ts-node prepare.ts", "test:validate": "run-s test:validate-configuration test:validate-test-app-setups", - "clean": "rimraf tmp node_modules && yarn clean:test-applications", + "clean": "rimraf tmp node_modules pnpm-lock.yaml && yarn clean:test-applications", "clean:test-applications": "rimraf test-applications/**/{node_modules,dist,build,.next,.sveltekit,pnpm-lock.yaml}" }, "devDependencies": { diff --git a/packages/e2e-tests/test-applications/node-experimental-fastify-app/.gitignore b/packages/e2e-tests/test-applications/node-experimental-fastify-app/.gitignore new file mode 100644 index 000000000000..1521c8b7652b --- /dev/null +++ b/packages/e2e-tests/test-applications/node-experimental-fastify-app/.gitignore @@ -0,0 +1 @@ +dist diff --git a/packages/e2e-tests/test-applications/node-experimental-fastify-app/.npmrc b/packages/e2e-tests/test-applications/node-experimental-fastify-app/.npmrc new file mode 100644 index 000000000000..c6b3ef9b3eaa --- /dev/null +++ b/packages/e2e-tests/test-applications/node-experimental-fastify-app/.npmrc @@ -0,0 +1,2 @@ +@sentry:registry=http://localhost:4873 +@sentry-internal:registry=http://localhost:4873 diff --git a/packages/e2e-tests/test-applications/node-experimental-fastify-app/event-proxy-server.ts b/packages/e2e-tests/test-applications/node-experimental-fastify-app/event-proxy-server.ts new file mode 100644 index 000000000000..67cf80b4dabf --- /dev/null +++ b/packages/e2e-tests/test-applications/node-experimental-fastify-app/event-proxy-server.ts @@ -0,0 +1,253 @@ +import type { Envelope, EnvelopeItem, Event } from '@sentry/types'; +import { parseEnvelope } from '@sentry/utils'; +import * as fs from 'fs'; +import * as http from 'http'; +import * as https from 'https'; +import type { AddressInfo } from 'net'; +import * as os from 'os'; +import * as path from 'path'; +import * as util from 'util'; +import * as zlib from 'zlib'; + +const readFile = util.promisify(fs.readFile); +const writeFile = util.promisify(fs.writeFile); + +interface EventProxyServerOptions { + /** Port to start the event proxy server at. */ + port: number; + /** The name for the proxy server used for referencing it with listener functions */ + proxyServerName: string; +} + +interface SentryRequestCallbackData { + envelope: Envelope; + rawProxyRequestBody: string; + rawSentryResponseBody: string; + sentryResponseStatusCode?: number; +} + +/** + * Starts an event proxy server that will proxy events to sentry when the `tunnel` option is used. Point the `tunnel` + * option to this server (like this `tunnel: http://localhost:${port option}/`). + */ +export async function startEventProxyServer(options: EventProxyServerOptions): Promise { + const eventCallbackListeners: Set<(data: string) => void> = new Set(); + + const proxyServer = http.createServer((proxyRequest, proxyResponse) => { + const proxyRequestChunks: Uint8Array[] = []; + + proxyRequest.addListener('data', (chunk: Buffer) => { + proxyRequestChunks.push(chunk); + }); + + proxyRequest.addListener('error', err => { + throw err; + }); + + proxyRequest.addListener('end', () => { + const proxyRequestBody = + proxyRequest.headers['content-encoding'] === 'gzip' + ? zlib.gunzipSync(Buffer.concat(proxyRequestChunks)).toString() + : Buffer.concat(proxyRequestChunks).toString(); + + let envelopeHeader = JSON.parse(proxyRequestBody.split('\n')[0]); + + if (!envelopeHeader.dsn) { + throw new Error('[event-proxy-server] No dsn on envelope header. Please set tunnel option.'); + } + + const { origin, pathname, host } = new URL(envelopeHeader.dsn); + + const projectId = pathname.substring(1); + const sentryIngestUrl = `${origin}/api/${projectId}/envelope/`; + + proxyRequest.headers.host = host; + + const sentryResponseChunks: Uint8Array[] = []; + + const sentryRequest = https.request( + sentryIngestUrl, + { headers: proxyRequest.headers, method: proxyRequest.method }, + sentryResponse => { + sentryResponse.addListener('data', (chunk: Buffer) => { + proxyResponse.write(chunk, 'binary'); + sentryResponseChunks.push(chunk); + }); + + sentryResponse.addListener('end', () => { + eventCallbackListeners.forEach(listener => { + const rawSentryResponseBody = Buffer.concat(sentryResponseChunks).toString(); + + const data: SentryRequestCallbackData = { + envelope: parseEnvelope(proxyRequestBody, new TextEncoder(), new TextDecoder()), + rawProxyRequestBody: proxyRequestBody, + rawSentryResponseBody, + sentryResponseStatusCode: sentryResponse.statusCode, + }; + + listener(Buffer.from(JSON.stringify(data)).toString('base64')); + }); + proxyResponse.end(); + }); + + sentryResponse.addListener('error', err => { + throw err; + }); + + proxyResponse.writeHead(sentryResponse.statusCode || 500, sentryResponse.headers); + }, + ); + + sentryRequest.write(Buffer.concat(proxyRequestChunks), 'binary'); + sentryRequest.end(); + }); + }); + + const proxyServerStartupPromise = new Promise(resolve => { + proxyServer.listen(options.port, () => { + resolve(); + }); + }); + + const eventCallbackServer = http.createServer((eventCallbackRequest, eventCallbackResponse) => { + eventCallbackResponse.statusCode = 200; + eventCallbackResponse.setHeader('connection', 'keep-alive'); + + const callbackListener = (data: string): void => { + eventCallbackResponse.write(data.concat('\n'), 'utf8'); + }; + + eventCallbackListeners.add(callbackListener); + + eventCallbackRequest.on('close', () => { + eventCallbackListeners.delete(callbackListener); + }); + + eventCallbackRequest.on('error', () => { + eventCallbackListeners.delete(callbackListener); + }); + }); + + const eventCallbackServerStartupPromise = new Promise(resolve => { + eventCallbackServer.listen(0, () => { + const port = String((eventCallbackServer.address() as AddressInfo).port); + void registerCallbackServerPort(options.proxyServerName, port).then(resolve); + }); + }); + + await eventCallbackServerStartupPromise; + await proxyServerStartupPromise; + return; +} + +export async function waitForRequest( + proxyServerName: string, + callback: (eventData: SentryRequestCallbackData) => Promise | boolean, +): Promise { + const eventCallbackServerPort = await retrieveCallbackServerPort(proxyServerName); + + return new Promise((resolve, reject) => { + const request = http.request(`http://localhost:${eventCallbackServerPort}/`, {}, response => { + let eventContents = ''; + + response.on('error', err => { + reject(err); + }); + + response.on('data', (chunk: Buffer) => { + const chunkString = chunk.toString('utf8'); + chunkString.split('').forEach(char => { + if (char === '\n') { + const eventCallbackData: SentryRequestCallbackData = JSON.parse( + Buffer.from(eventContents, 'base64').toString('utf8'), + ); + const callbackResult = callback(eventCallbackData); + if (typeof callbackResult !== 'boolean') { + callbackResult.then( + match => { + if (match) { + response.destroy(); + resolve(eventCallbackData); + } + }, + err => { + throw err; + }, + ); + } else if (callbackResult) { + response.destroy(); + resolve(eventCallbackData); + } + eventContents = ''; + } else { + eventContents = eventContents.concat(char); + } + }); + }); + }); + + request.end(); + }); +} + +export function waitForEnvelopeItem( + proxyServerName: string, + callback: (envelopeItem: EnvelopeItem) => Promise | boolean, +): Promise { + return new Promise((resolve, reject) => { + waitForRequest(proxyServerName, async eventData => { + const envelopeItems = eventData.envelope[1]; + for (const envelopeItem of envelopeItems) { + if (await callback(envelopeItem)) { + resolve(envelopeItem); + return true; + } + } + return false; + }).catch(reject); + }); +} + +export function waitForError( + proxyServerName: string, + callback: (transactionEvent: Event) => Promise | boolean, +): Promise { + return new Promise((resolve, reject) => { + waitForEnvelopeItem(proxyServerName, async envelopeItem => { + const [envelopeItemHeader, envelopeItemBody] = envelopeItem; + if (envelopeItemHeader.type === 'event' && (await callback(envelopeItemBody as Event))) { + resolve(envelopeItemBody as Event); + return true; + } + return false; + }).catch(reject); + }); +} + +export function waitForTransaction( + proxyServerName: string, + callback: (transactionEvent: Event) => Promise | boolean, +): Promise { + return new Promise((resolve, reject) => { + waitForEnvelopeItem(proxyServerName, async envelopeItem => { + const [envelopeItemHeader, envelopeItemBody] = envelopeItem; + if (envelopeItemHeader.type === 'transaction' && (await callback(envelopeItemBody as Event))) { + resolve(envelopeItemBody as Event); + return true; + } + return false; + }).catch(reject); + }); +} + +const TEMP_FILE_PREFIX = 'event-proxy-server-'; + +async function registerCallbackServerPort(serverName: string, port: string): Promise { + const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); + await writeFile(tmpFilePath, port, { encoding: 'utf8' }); +} + +function retrieveCallbackServerPort(serverName: string): Promise { + const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); + return readFile(tmpFilePath, 'utf8'); +} diff --git a/packages/e2e-tests/test-applications/node-experimental-fastify-app/package.json b/packages/e2e-tests/test-applications/node-experimental-fastify-app/package.json new file mode 100644 index 000000000000..8ada1cb5d82e --- /dev/null +++ b/packages/e2e-tests/test-applications/node-experimental-fastify-app/package.json @@ -0,0 +1,32 @@ +{ + "name": "node-experimental-fastify-app", + "version": "1.0.0", + "private": true, + "scripts": { + "start": "node src/app.js", + "test": "playwright test", + "clean": "npx rimraf node_modules pnpm-lock.yaml", + "test:build": "pnpm install", + "test:assert": "pnpm test" + }, + "dependencies": { + "@sentry/node-experimental": "latest || *", + "@sentry/types": "latest || *", + "@sentry/core": "latest || *", + "@sentry/utils": "latest || *", + "@sentry/node": "latest || *", + "@sentry/opentelemetry-node": "latest || *", + "@sentry-internal/tracing": "latest || *", + "@types/node": "18.15.1", + "fastify": "4.23.2", + "fastify-plugin": "4.5.1", + "typescript": "4.9.5", + "ts-node": "10.9.1" + }, + "devDependencies": { + "@playwright/test": "^1.38.1" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/packages/e2e-tests/test-applications/node-experimental-fastify-app/playwright.config.ts b/packages/e2e-tests/test-applications/node-experimental-fastify-app/playwright.config.ts new file mode 100644 index 000000000000..f39997dc76e8 --- /dev/null +++ b/packages/e2e-tests/test-applications/node-experimental-fastify-app/playwright.config.ts @@ -0,0 +1,62 @@ +import type { PlaywrightTestConfig } from '@playwright/test'; +import { devices } from '@playwright/test'; + +const fastifyPort = 3030; +const eventProxyPort = 3031; + +/** + * See https://playwright.dev/docs/test-configuration. + */ +const config: PlaywrightTestConfig = { + testDir: './tests', + /* Maximum time one test can run for. */ + timeout: 60 * 1000, + expect: { + /** + * Maximum time expect() should wait for the condition to be met. + * For example in `await expect(locator).toHaveText();` + */ + timeout: 10000, + }, + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + retries: 0, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'list', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ + actionTimeout: 0, + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: `http://localhost:${fastifyPort}`, + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + }, + }, + ], + + /* Run your local dev server before starting the tests */ + webServer: [ + { + command: 'pnpm ts-node-script start-event-proxy.ts', + port: eventProxyPort, + }, + { + command: 'pnpm start', + port: fastifyPort, + }, + ], +}; + +export default config; diff --git a/packages/e2e-tests/test-applications/node-experimental-fastify-app/src/app.js b/packages/e2e-tests/test-applications/node-experimental-fastify-app/src/app.js new file mode 100644 index 000000000000..50fe45767504 --- /dev/null +++ b/packages/e2e-tests/test-applications/node-experimental-fastify-app/src/app.js @@ -0,0 +1,82 @@ +require('./tracing'); + +const Sentry = require('@sentry/node-experimental'); +const { fastify } = require('fastify'); +const fastifyPlugin = require('fastify-plugin'); +const http = require('http'); + +const FastifySentry = fastifyPlugin(async (fastify, options) => { + fastify.decorateRequest('_sentryContext', null); + + fastify.addHook('onError', async (_request, _reply, error) => { + Sentry.captureException(error); + }); +}); + +const app = fastify(); +const port = 3030; + +app.register(FastifySentry); + +app.get('/test-success', function (req, res) { + res.send({ version: 'v1' }); +}); + +app.get('/test-param/:param', function (req, res) { + res.send({ paramWas: req.params.param }); +}); + +app.get('/test-inbound-headers', function (req, res) { + const headers = req.headers; + + res.send({ headers }); +}); + +app.get('/test-outgoing-http', async function (req, res) { + const data = await makeHttpRequest('http://localhost:3030/test-inbound-headers'); + + res.send(data); +}); + +app.get('/test-outgoing-fetch', async function (req, res) { + const response = await fetch('http://localhost:3030/test-inbound-headers'); + const data = await response.json(); + + res.send(data); +}); + +app.get('/test-transaction', async function (req, res) { + Sentry.startSpan({ name: 'test-span' }, () => { + Sentry.startSpan({ name: 'child-span' }, () => {}); + }); + + res.send({}); +}); + +app.get('/test-error', async function (req, res) { + const exceptionId = Sentry.captureException(new Error('This is an error')); + + await Sentry.flush(2000); + + res.send({ exceptionId }); +}); + +app.listen({ port: port }); + +function makeHttpRequest(url) { + return new Promise(resolve => { + const data = []; + + http + .request(url, httpRes => { + httpRes.on('data', chunk => { + data.push(chunk); + }); + httpRes.on('end', () => { + const json = JSON.parse(Buffer.concat(data).toString()); + resolve(json); + }); + }) + .end(); + }); +} diff --git a/packages/e2e-tests/test-applications/node-experimental-fastify-app/src/tracing.js b/packages/e2e-tests/test-applications/node-experimental-fastify-app/src/tracing.js new file mode 100644 index 000000000000..e571a4374a9e --- /dev/null +++ b/packages/e2e-tests/test-applications/node-experimental-fastify-app/src/tracing.js @@ -0,0 +1,10 @@ +const Sentry = require('@sentry/node-experimental'); + +Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: process.env.E2E_TEST_DSN, + integrations: [], + debug: true, + tracesSampleRate: 1, + tunnel: 'http://localhost:3031/', // proxy server +}); diff --git a/packages/e2e-tests/test-applications/node-experimental-fastify-app/start-event-proxy.ts b/packages/e2e-tests/test-applications/node-experimental-fastify-app/start-event-proxy.ts new file mode 100644 index 000000000000..7ae352993f3c --- /dev/null +++ b/packages/e2e-tests/test-applications/node-experimental-fastify-app/start-event-proxy.ts @@ -0,0 +1,6 @@ +import { startEventProxyServer } from './event-proxy-server'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'node-experimental-fastify-app', +}); diff --git a/packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/errors.test.ts b/packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/errors.test.ts new file mode 100644 index 000000000000..4656ba23e7de --- /dev/null +++ b/packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/errors.test.ts @@ -0,0 +1,39 @@ +import { test, expect } from '@playwright/test'; +import axios, { AxiosError } from 'axios'; + +const authToken = process.env.E2E_TEST_AUTH_TOKEN; +const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG; +const sentryTestProject = process.env.E2E_TEST_SENTRY_TEST_PROJECT; +const EVENT_POLLING_TIMEOUT = 30_000; + +test('Sends exception to Sentry', async ({ baseURL }) => { + const { data } = await axios.get(`${baseURL}/test-error`); + const { exceptionId } = data; + + const url = `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${exceptionId}/`; + + console.log(`Polling for error eventId: ${exceptionId}`); + + await expect + .poll( + async () => { + try { + const response = await axios.get(url, { headers: { Authorization: `Bearer ${authToken}` } }); + + return response.status; + } catch (e) { + if (e instanceof AxiosError && e.response) { + if (e.response.status !== 404) { + throw e; + } else { + return e.response.status; + } + } else { + throw e; + } + } + }, + { timeout: EVENT_POLLING_TIMEOUT }, + ) + .toBe(200); +}); diff --git a/packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/propagation.test.ts b/packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/propagation.test.ts new file mode 100644 index 000000000000..8dbcb590b331 --- /dev/null +++ b/packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/propagation.test.ts @@ -0,0 +1,191 @@ +import { test, expect } from '@playwright/test'; +import { Span } from '@sentry/types'; +import axios from 'axios'; +import { waitForTransaction } from '../event-proxy-server'; + +const authToken = process.env.E2E_TEST_AUTH_TOKEN; +const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG; +const sentryTestProject = process.env.E2E_TEST_SENTRY_TEST_PROJECT; +const EVENT_POLLING_TIMEOUT = 30_000; + +test('Propagates trace for outgoing http requests', async ({ baseURL }) => { + const inboundTransactionPromise = waitForTransaction('node-experimental-fastify-app', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /test-inbound-headers' + ); + }); + + const outboundTransactionPromise = waitForTransaction('node-experimental-fastify-app', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /test-outgoing-http' + ); + }); + + const { data } = await axios.get(`${baseURL}/test-outgoing-http`); + + const inboundTransaction = await inboundTransactionPromise; + const outboundTransaction = await outboundTransactionPromise; + + const traceId = outboundTransaction?.contexts?.trace?.trace_id; + const outgoingHttpSpan = outboundTransaction?.spans?.find(span => span.op === 'http.client') as + | ReturnType + | undefined; + + expect(outgoingHttpSpan).toBeDefined(); + + const outgoingHttpSpanId = outgoingHttpSpan?.span_id; + + expect(traceId).toEqual(expect.any(String)); + + // data is passed through from the inbound request, to verify we have the correct headers set + const inboundHeaderSentryTrace = data.headers?.['sentry-trace']; + const inboundHeaderBaggage = data.headers?.['baggage']; + + expect(inboundHeaderSentryTrace).toEqual(`${traceId}-${outgoingHttpSpanId}-1`); + expect(inboundHeaderBaggage).toBeDefined(); + + const baggage = (inboundHeaderBaggage || '').split(','); + expect(baggage).toEqual( + expect.arrayContaining([ + 'sentry-environment=qa', + `sentry-trace_id=${traceId}`, + expect.stringMatching(/sentry-public_key=/), + ]), + ); + + expect(outboundTransaction).toEqual( + expect.objectContaining({ + contexts: expect.objectContaining({ + trace: { + data: { + url: 'http://localhost:3030/test-outgoing-http', + 'otel.kind': 'SERVER', + 'http.response.status_code': 200, + }, + op: 'http.server', + span_id: expect.any(String), + status: 'ok', + tags: { + 'http.status_code': 200, + }, + trace_id: traceId, + }, + }), + }), + ); + + expect(inboundTransaction).toEqual( + expect.objectContaining({ + contexts: expect.objectContaining({ + trace: { + data: { + url: 'http://localhost:3030/test-inbound-headers', + 'otel.kind': 'SERVER', + 'http.response.status_code': 200, + }, + op: 'http.server', + parent_span_id: outgoingHttpSpanId, + span_id: expect.any(String), + status: 'ok', + tags: { + 'http.status_code': 200, + }, + trace_id: traceId, + }, + }), + }), + ); +}); + +test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => { + const inboundTransactionPromise = waitForTransaction('node-experimental-fastify-app', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /test-inbound-headers' + ); + }); + + const outboundTransactionPromise = waitForTransaction('node-experimental-fastify-app', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /test-outgoing-fetch' + ); + }); + + const { data } = await axios.get(`${baseURL}/test-outgoing-fetch`); + + const inboundTransaction = await inboundTransactionPromise; + const outboundTransaction = await outboundTransactionPromise; + + const traceId = outboundTransaction?.contexts?.trace?.trace_id; + const outgoingHttpSpan = outboundTransaction?.spans?.find(span => span.op === 'http.client') as + | ReturnType + | undefined; + + expect(outgoingHttpSpan).toBeDefined(); + + const outgoingHttpSpanId = outgoingHttpSpan?.span_id; + + expect(traceId).toEqual(expect.any(String)); + + // data is passed through from the inbound request, to verify we have the correct headers set + const inboundHeaderSentryTrace = data.headers?.['sentry-trace']; + const inboundHeaderBaggage = data.headers?.['baggage']; + + expect(inboundHeaderSentryTrace).toEqual(`${traceId}-${outgoingHttpSpanId}-1`); + expect(inboundHeaderBaggage).toBeDefined(); + + const baggage = (inboundHeaderBaggage || '').split(','); + expect(baggage).toEqual( + expect.arrayContaining([ + 'sentry-environment=qa', + `sentry-trace_id=${traceId}`, + expect.stringMatching(/sentry-public_key=/), + ]), + ); + + expect(outboundTransaction).toEqual( + expect.objectContaining({ + contexts: expect.objectContaining({ + trace: { + data: { + url: 'http://localhost:3030/test-outgoing-fetch', + 'otel.kind': 'SERVER', + 'http.response.status_code': 200, + }, + op: 'http.server', + span_id: expect.any(String), + status: 'ok', + tags: { + 'http.status_code': 200, + }, + trace_id: traceId, + }, + }), + }), + ); + + expect(inboundTransaction).toEqual( + expect.objectContaining({ + contexts: expect.objectContaining({ + trace: { + data: { + url: 'http://localhost:3030/test-inbound-headers', + 'otel.kind': 'SERVER', + 'http.response.status_code': 200, + }, + op: 'http.server', + parent_span_id: outgoingHttpSpanId, + span_id: expect.any(String), + status: 'ok', + tags: { + 'http.status_code': 200, + }, + trace_id: traceId, + }, + }), + }), + ); +}); diff --git a/packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/transactions.test.ts b/packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/transactions.test.ts new file mode 100644 index 000000000000..f34beaa63926 --- /dev/null +++ b/packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/transactions.test.ts @@ -0,0 +1,124 @@ +import { test, expect } from '@playwright/test'; +import { waitForTransaction } from '../event-proxy-server'; +import axios, { AxiosError } from 'axios'; + +const authToken = process.env.E2E_TEST_AUTH_TOKEN; +const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG; +const sentryTestProject = process.env.E2E_TEST_SENTRY_TEST_PROJECT; +const EVENT_POLLING_TIMEOUT = 30_000; + +test('Sends an API route transaction', async ({ baseURL }) => { + const pageloadTransactionEventPromise = waitForTransaction('node-experimental-fastify-app', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /test-transaction' + ); + }); + + await axios.get(`${baseURL}/test-transaction`); + + const transactionEvent = await pageloadTransactionEventPromise; + const transactionEventId = transactionEvent.event_id; + + expect(transactionEvent).toEqual( + expect.objectContaining({ + contexts: expect.objectContaining({ + trace: { + data: { + url: 'http://localhost:3030/test-transaction', + 'otel.kind': 'SERVER', + 'http.response.status_code': 200, + }, + op: 'http.server', + span_id: expect.any(String), + status: 'ok', + tags: { + 'http.status_code': 200, + }, + trace_id: expect.any(String), + }, + }), + + spans: [ + { + data: { + 'plugin.name': 'fastify -> app-auto-0', + 'fastify.type': 'request_handler', + 'http.route': '/test-transaction', + 'otel.kind': 'INTERNAL', + }, + description: 'request handler - fastify -> app-auto-0', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.any(String), + origin: 'auto.http.otel.fastify', + }, + { + data: { + 'otel.kind': 'INTERNAL', + }, + description: 'test-span', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.any(String), + origin: 'manual', + }, + { + data: { + 'otel.kind': 'INTERNAL', + }, + description: 'child-span', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.any(String), + origin: 'manual', + }, + ], + tags: { + 'http.status_code': 200, + }, + transaction: 'GET /test-transaction', + type: 'transaction', + transaction_info: { + source: 'route', + }, + }), + ); + + await expect + .poll( + async () => { + try { + const response = await axios.get( + `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${transactionEventId}/`, + { headers: { Authorization: `Bearer ${authToken}` } }, + ); + + return response.status; + } catch (e) { + if (e instanceof AxiosError && e.response) { + if (e.response.status !== 404) { + throw e; + } else { + return e.response.status; + } + } else { + throw e; + } + } + }, + { + timeout: EVENT_POLLING_TIMEOUT, + }, + ) + .toBe(200); +}); diff --git a/packages/e2e-tests/test-applications/node-experimental-fastify-app/tsconfig.json b/packages/e2e-tests/test-applications/node-experimental-fastify-app/tsconfig.json new file mode 100644 index 000000000000..17bd2c1f4c00 --- /dev/null +++ b/packages/e2e-tests/test-applications/node-experimental-fastify-app/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "types": ["node"], + "esModuleInterop": true, + "lib": ["dom", "dom.iterable", "esnext"], + "strict": true, + "outDir": "dist" + }, + "include": ["*.ts"] +} diff --git a/packages/e2e-tests/test-registry.npmrc b/packages/e2e-tests/test-registry.npmrc index c35d987cca9f..fd8ba6605a28 100644 --- a/packages/e2e-tests/test-registry.npmrc +++ b/packages/e2e-tests/test-registry.npmrc @@ -1,3 +1,6 @@ @sentry:registry=http://localhost:4873 @sentry-internal:registry=http://localhost:4873 //localhost:4873/:_authToken=some-token + +# Do not notify about npm updates +update-notifier=false diff --git a/packages/e2e-tests/verdaccio-config/config.yaml b/packages/e2e-tests/verdaccio-config/config.yaml index 80a5afc70008..938b877a50e5 100644 --- a/packages/e2e-tests/verdaccio-config/config.yaml +++ b/packages/e2e-tests/verdaccio-config/config.yaml @@ -44,6 +44,12 @@ packages: unpublish: $all # proxy: npmjs # Don't proxy for E2E tests! + '@sentry/astro': + access: $all + publish: $all + unpublish: $all + # proxy: npmjs # Don't proxy for E2E tests! + '@sentry/browser': access: $all publish: $all @@ -62,6 +68,12 @@ packages: unpublish: $all # proxy: npmjs # Don't proxy for E2E tests! + '@sentry/deno': + access: $all + publish: $all + unpublish: $all + # proxy: npmjs # Don't proxy for E2E tests! + '@sentry/ember': access: $all publish: $all diff --git a/packages/ember/index.js b/packages/ember/index.js index de05e5d6089f..96e79bccf704 100644 --- a/packages/ember/index.js +++ b/packages/ember/index.js @@ -30,7 +30,7 @@ module.exports = { included() { const app = this._findHost(); const config = app.project.config(app.env); - const addonConfig = config['@sentry/ember'] || {}; + const addonConfig = dropUndefinedKeys(config['@sentry/ember'] || {}); if (!isSerializable(addonConfig)) { // eslint-disable-next-line no-console @@ -101,3 +101,15 @@ function isScalar(val) { function isPlainObject(obj) { return typeof obj === 'object' && obj.constructor === Object && obj.toString() === '[object Object]'; } + +function dropUndefinedKeys(obj) { + const newObj = {}; + + for (const key in obj) { + if (obj[key] !== undefined) { + newObj[key] = obj[key]; + } + } + + return newObj; +} diff --git a/packages/eslint-config-sdk/package.json b/packages/eslint-config-sdk/package.json index ea8c546667c0..2ee491464a9e 100644 --- a/packages/eslint-config-sdk/package.json +++ b/packages/eslint-config-sdk/package.json @@ -24,7 +24,7 @@ "@typescript-eslint/eslint-plugin": "^5.48.0", "@typescript-eslint/parser": "^5.48.0", "eslint-config-prettier": "^6.11.0", - "eslint-plugin-deprecation": "^1.1.0", + "eslint-plugin-deprecation": "^1.5.0", "eslint-plugin-import": "^2.22.0", "eslint-plugin-jest": "^27.2.2", "eslint-plugin-jsdoc": "^30.0.3", diff --git a/packages/eslint-config-sdk/src/index.js b/packages/eslint-config-sdk/src/index.js index e9d72743f99a..efbeb3047a33 100644 --- a/packages/eslint-config-sdk/src/index.js +++ b/packages/eslint-config-sdk/src/index.js @@ -16,8 +16,13 @@ module.exports = { { // Configuration for typescript files files: ['*.ts', '*.tsx', '*.d.ts'], - extends: ['plugin:@typescript-eslint/recommended', 'prettier/@typescript-eslint', 'plugin:import/typescript'], - plugins: ['@typescript-eslint', 'jsdoc', 'deprecation'], + extends: [ + 'plugin:@typescript-eslint/recommended', + 'prettier/@typescript-eslint', + 'plugin:import/typescript', + 'plugin:deprecation/recommended', + ], + plugins: ['@typescript-eslint', 'jsdoc'], parser: '@typescript-eslint/parser', rules: { // We want to guard against using the equality operator with empty arrays @@ -87,9 +92,6 @@ module.exports = { // Make sure Promises are handled appropriately '@typescript-eslint/no-floating-promises': 'error', - // Do not use deprecated methods - 'deprecation/deprecation': 'error', - // sort imports 'simple-import-sort/sort': 'error', 'sort-imports': 'off', diff --git a/packages/integrations/src/contextlines.ts b/packages/integrations/src/contextlines.ts index 3bc483958b42..d528477718c1 100644 --- a/packages/integrations/src/contextlines.ts +++ b/packages/integrations/src/contextlines.ts @@ -1,4 +1,4 @@ -import type { Event, EventProcessor, Hub, Integration, StackFrame } from '@sentry/types'; +import type { Event, Integration, StackFrame } from '@sentry/types'; import { addContextToFrame, GLOBAL_OBJ, stripUrlQueryAndFragment } from '@sentry/utils'; const WINDOW = GLOBAL_OBJ as typeof GLOBAL_OBJ & Window; @@ -44,17 +44,20 @@ export class ContextLines implements Integration { /** * @inheritDoc */ - public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { - addGlobalEventProcessor(event => { - const self = getCurrentHub().getIntegration(ContextLines); - if (!self) { - return event; - } - return this.addSourceContext(event); - }); + public setupOnce(_addGlobaleventProcessor: unknown, _getCurrentHub: unknown): void { + // noop + } + + /** @inheritDoc */ + public processEvent(event: Event): Event { + return this.addSourceContext(event); } - /** Processes an event and adds context lines */ + /** + * Processes an event and adds context lines. + * + * TODO (v8): Make this internal/private + */ public addSourceContext(event: Event): Event { const doc = WINDOW.document; const htmlFilename = WINDOW.location && stripUrlQueryAndFragment(WINDOW.location.href); diff --git a/packages/integrations/src/dedupe.ts b/packages/integrations/src/dedupe.ts index 8f156e76784d..49865de3cd79 100644 --- a/packages/integrations/src/dedupe.ts +++ b/packages/integrations/src/dedupe.ts @@ -1,4 +1,4 @@ -import type { Event, EventProcessor, Exception, Hub, Integration, StackFrame } from '@sentry/types'; +import type { Event, Exception, Integration, StackFrame } from '@sentry/types'; import { logger } from '@sentry/utils'; /** Deduplication filter */ @@ -22,36 +22,32 @@ export class Dedupe implements Integration { this.name = Dedupe.id; } + /** @inheritDoc */ + public setupOnce(_addGlobaleventProcessor: unknown, _getCurrentHub: unknown): void { + // noop + } + /** * @inheritDoc */ - public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { - const eventProcessor: EventProcessor = currentEvent => { - // We want to ignore any non-error type events, e.g. transactions or replays - // These should never be deduped, and also not be compared against as _previousEvent. - if (currentEvent.type) { - return currentEvent; - } + public processEvent(currentEvent: Event): Event | null { + // We want to ignore any non-error type events, e.g. transactions or replays + // These should never be deduped, and also not be compared against as _previousEvent. + if (currentEvent.type) { + return currentEvent; + } - const self = getCurrentHub().getIntegration(Dedupe); - if (self) { - // Juuust in case something goes wrong - try { - if (_shouldDropEvent(currentEvent, self._previousEvent)) { - __DEBUG_BUILD__ && logger.warn('Event dropped due to being a duplicate of previously captured event.'); - return null; - } - } catch (_oO) { - return (self._previousEvent = currentEvent); - } - - return (self._previousEvent = currentEvent); + // Juuust in case something goes wrong + try { + if (_shouldDropEvent(currentEvent, this._previousEvent)) { + __DEBUG_BUILD__ && logger.warn('Event dropped due to being a duplicate of previously captured event.'); + return null; } - return currentEvent; - }; + } catch (_oO) { + return (this._previousEvent = currentEvent); + } - eventProcessor.id = this.name; - addGlobalEventProcessor(eventProcessor); + return (this._previousEvent = currentEvent); } } diff --git a/packages/integrations/src/extraerrordata.ts b/packages/integrations/src/extraerrordata.ts index 86d9343ef5e3..0ac2729e3baf 100644 --- a/packages/integrations/src/extraerrordata.ts +++ b/packages/integrations/src/extraerrordata.ts @@ -1,9 +1,9 @@ -import type { Contexts, Event, EventHint, EventProcessor, ExtendedError, Hub, Integration } from '@sentry/types'; +import type { Contexts, Event, EventHint, ExtendedError, Integration } from '@sentry/types'; import { addNonEnumerableProperty, isError, isPlainObject, logger, normalize } from '@sentry/utils'; /** JSDoc */ interface ExtraErrorDataOptions { - depth?: number; + depth: number; } /** Patch toString calls to return proper name for wrapped functions */ @@ -24,7 +24,7 @@ export class ExtraErrorData implements Integration { /** * @inheritDoc */ - public constructor(options?: ExtraErrorDataOptions) { + public constructor(options?: Partial) { this.name = ExtraErrorData.id; this._options = { @@ -36,94 +36,99 @@ export class ExtraErrorData implements Integration { /** * @inheritDoc */ - public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { - addGlobalEventProcessor((event: Event, hint: EventHint) => { - const self = getCurrentHub().getIntegration(ExtraErrorData); - if (!self) { - return event; - } - return self.enhanceEventWithErrorData(event, hint); - }); + public setupOnce(_addGlobaleventProcessor: unknown, _getCurrentHub: unknown): void { + // noop + } + + /** @inheritDoc */ + public processEvent(event: Event, hint: EventHint): Event { + return this.enhanceEventWithErrorData(event, hint); } /** - * Attaches extracted information from the Error object to extra field in the Event + * Attaches extracted information from the Error object to extra field in the Event. + * + * TODO (v8): Drop this public function. */ public enhanceEventWithErrorData(event: Event, hint: EventHint = {}): Event { - if (!hint.originalException || !isError(hint.originalException)) { - return event; - } - const exceptionName = (hint.originalException as ExtendedError).name || hint.originalException.constructor.name; + return _enhanceEventWithErrorData(event, hint, this._options.depth); + } +} - const errorData = this._extractErrorData(hint.originalException as ExtendedError); +function _enhanceEventWithErrorData(event: Event, hint: EventHint = {}, depth: number): Event { + if (!hint.originalException || !isError(hint.originalException)) { + return event; + } + const exceptionName = (hint.originalException as ExtendedError).name || hint.originalException.constructor.name; - if (errorData) { - const contexts: Contexts = { - ...event.contexts, - }; + const errorData = _extractErrorData(hint.originalException as ExtendedError); - const normalizedErrorData = normalize(errorData, this._options.depth); + if (errorData) { + const contexts: Contexts = { + ...event.contexts, + }; - if (isPlainObject(normalizedErrorData)) { - // We mark the error data as "already normalized" here, because we don't want other normalization procedures to - // potentially truncate the data we just already normalized, with a certain depth setting. - addNonEnumerableProperty(normalizedErrorData, '__sentry_skip_normalization__', true); - contexts[exceptionName] = normalizedErrorData; - } + const normalizedErrorData = normalize(errorData, depth); - return { - ...event, - contexts, - }; + if (isPlainObject(normalizedErrorData)) { + // We mark the error data as "already normalized" here, because we don't want other normalization procedures to + // potentially truncate the data we just already normalized, with a certain depth setting. + addNonEnumerableProperty(normalizedErrorData, '__sentry_skip_normalization__', true); + contexts[exceptionName] = normalizedErrorData; } - return event; + return { + ...event, + contexts, + }; } - /** - * Extract extra information from the Error object - */ - private _extractErrorData(error: ExtendedError): Record | null { - // We are trying to enhance already existing event, so no harm done if it won't succeed - try { - const nativeKeys = [ - 'name', - 'message', - 'stack', - 'line', - 'column', - 'fileName', - 'lineNumber', - 'columnNumber', - 'toJSON', - ]; - - const extraErrorInfo: Record = {}; - - // We want only enumerable properties, thus `getOwnPropertyNames` is redundant here, as we filter keys anyway. - for (const key of Object.keys(error)) { - if (nativeKeys.indexOf(key) !== -1) { - continue; - } - const value = error[key]; - extraErrorInfo[key] = isError(value) ? value.toString() : value; + return event; +} + +/** + * Extract extra information from the Error object + */ +function _extractErrorData(error: ExtendedError): Record | null { + // We are trying to enhance already existing event, so no harm done if it won't succeed + try { + const nativeKeys = [ + 'name', + 'message', + 'stack', + 'line', + 'column', + 'fileName', + 'lineNumber', + 'columnNumber', + 'toJSON', + ]; + + const extraErrorInfo: Record = {}; + + // We want only enumerable properties, thus `getOwnPropertyNames` is redundant here, as we filter keys anyway. + for (const key of Object.keys(error)) { + if (nativeKeys.indexOf(key) !== -1) { + continue; } + const value = error[key]; + extraErrorInfo[key] = isError(value) ? value.toString() : value; + } - // Check if someone attached `toJSON` method to grab even more properties (eg. axios is doing that) - if (typeof error.toJSON === 'function') { - const serializedError = error.toJSON() as Record; + // Check if someone attached `toJSON` method to grab even more properties (eg. axios is doing that) + if (typeof error.toJSON === 'function') { + const serializedError = error.toJSON() as Record; - for (const key of Object.keys(serializedError)) { - const value = serializedError[key]; - extraErrorInfo[key] = isError(value) ? value.toString() : value; - } + for (const key of Object.keys(serializedError)) { + const value = serializedError[key]; + extraErrorInfo[key] = isError(value) ? value.toString() : value; } - - return extraErrorInfo; - } catch (oO) { - __DEBUG_BUILD__ && logger.error('Unable to extract extra data from the Error object:', oO); } - return null; + return extraErrorInfo; + } catch (oO) { + __DEBUG_BUILD__ && logger.error('Unable to extract extra data from the Error object:', oO); } + + return null; } diff --git a/packages/integrations/src/rewriteframes.ts b/packages/integrations/src/rewriteframes.ts index 1564d54a4970..67f146e650bd 100644 --- a/packages/integrations/src/rewriteframes.ts +++ b/packages/integrations/src/rewriteframes.ts @@ -1,4 +1,4 @@ -import type { Event, EventProcessor, Hub, Integration, StackFrame, Stacktrace } from '@sentry/types'; +import type { Event, Integration, StackFrame, Stacktrace } from '@sentry/types'; import { basename, relative } from '@sentry/utils'; type StackFrameIteratee = (frame: StackFrame) => StackFrame; @@ -43,17 +43,18 @@ export class RewriteFrames implements Integration { /** * @inheritDoc */ - public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { - addGlobalEventProcessor(event => { - const self = getCurrentHub().getIntegration(RewriteFrames); - if (self) { - return self.process(event); - } - return event; - }); + public setupOnce(_addGlobaleventProcessor: unknown, _getCurrentHub: unknown): void { + // noop } - /** JSDoc */ + /** @inheritDoc */ + public processEvent(event: Event): Event { + return this.process(event); + } + + /** + * TODO (v8): Make this private/internal + */ public process(originalEvent: Event): Event { let processedEvent = originalEvent; diff --git a/packages/integrations/src/sessiontiming.ts b/packages/integrations/src/sessiontiming.ts index 584163ce008e..016d71f336e3 100644 --- a/packages/integrations/src/sessiontiming.ts +++ b/packages/integrations/src/sessiontiming.ts @@ -1,4 +1,4 @@ -import type { Event, EventProcessor, Hub, Integration } from '@sentry/types'; +import type { Event, Integration } from '@sentry/types'; /** This function adds duration since Sentry was initialized till the time event was sent */ export class SessionTiming implements Integration { @@ -23,18 +23,17 @@ export class SessionTiming implements Integration { /** * @inheritDoc */ - public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { - addGlobalEventProcessor(event => { - const self = getCurrentHub().getIntegration(SessionTiming); - if (self) { - return self.process(event); - } - return event; - }); + public setupOnce(_addGlobaleventProcessor: unknown, _getCurrentHub: unknown): void { + // noop + } + + /** @inheritDoc */ + public processEvent(event: Event): Event { + return this.process(event); } /** - * @inheritDoc + * TODO (v8): make this private/internal */ public process(event: Event): Event { const now = Date.now(); diff --git a/packages/integrations/src/transaction.ts b/packages/integrations/src/transaction.ts index 1bb3ebfa816d..ae9f826cba55 100644 --- a/packages/integrations/src/transaction.ts +++ b/packages/integrations/src/transaction.ts @@ -1,4 +1,4 @@ -import type { Event, EventProcessor, Hub, Integration, StackFrame } from '@sentry/types'; +import type { Event, Integration, StackFrame } from '@sentry/types'; /** Add node transaction to the event */ export class Transaction implements Integration { @@ -19,43 +19,40 @@ export class Transaction implements Integration { /** * @inheritDoc */ - public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { - addGlobalEventProcessor(event => { - const self = getCurrentHub().getIntegration(Transaction); - if (self) { - return self.process(event); - } - return event; - }); + public setupOnce(_addGlobaleventProcessor: unknown, _getCurrentHub: unknown): void { + // noop + } + + /** @inheritDoc */ + public processEvent(event: Event): Event { + return this.processEvent(event); } /** - * @inheritDoc + * TODO (v8): Make this private/internal */ public process(event: Event): Event { - const frames = this._getFramesFromEvent(event); + const frames = _getFramesFromEvent(event); // use for loop so we don't have to reverse whole frames array for (let i = frames.length - 1; i >= 0; i--) { const frame = frames[i]; if (frame.in_app === true) { - event.transaction = this._getTransaction(frame); + event.transaction = _getTransaction(frame); break; } } return event; } +} - /** JSDoc */ - private _getFramesFromEvent(event: Event): StackFrame[] { - const exception = event.exception && event.exception.values && event.exception.values[0]; - return (exception && exception.stacktrace && exception.stacktrace.frames) || []; - } +function _getFramesFromEvent(event: Event): StackFrame[] { + const exception = event.exception && event.exception.values && event.exception.values[0]; + return (exception && exception.stacktrace && exception.stacktrace.frames) || []; +} - /** JSDoc */ - private _getTransaction(frame: StackFrame): string { - return frame.module || frame.function ? `${frame.module || '?'}/${frame.function || '?'}` : ''; - } +function _getTransaction(frame: StackFrame): string { + return frame.module || frame.function ? `${frame.module || '?'}/${frame.function || '?'}` : ''; } diff --git a/packages/integrations/test/dedupe.test.ts b/packages/integrations/test/dedupe.test.ts index 7ffc30d1bdcf..f4a703662e0c 100644 --- a/packages/integrations/test/dedupe.test.ts +++ b/packages/integrations/test/dedupe.test.ts @@ -1,4 +1,4 @@ -import type { Event as SentryEvent, EventProcessor, Exception, Hub, StackFrame, Stacktrace } from '@sentry/types'; +import type { Event as SentryEvent, Exception, StackFrame, Stacktrace } from '@sentry/types'; import { _shouldDropEvent, Dedupe } from '../src/dedupe'; @@ -176,47 +176,29 @@ describe('Dedupe', () => { }); }); - describe('setupOnce', () => { - let dedupeFunc: EventProcessor; - - beforeEach(function () { + describe('processEvent', () => { + it('ignores consecutive errors', () => { const integration = new Dedupe(); - const addGlobalEventProcessor = (callback: EventProcessor) => { - dedupeFunc = callback; - }; - - const getCurrentHub = () => { - return { - getIntegration() { - return integration; - }, - } as unknown as Hub; - }; - - integration.setupOnce(addGlobalEventProcessor, getCurrentHub); - }); - it('ignores consecutive errors', () => { - expect(dedupeFunc(clone(exceptionEvent), {})).not.toBeNull(); - expect(dedupeFunc(clone(exceptionEvent), {})).toBeNull(); - expect(dedupeFunc(clone(exceptionEvent), {})).toBeNull(); + expect(integration.processEvent(clone(exceptionEvent))).not.toBeNull(); + expect(integration.processEvent(clone(exceptionEvent))).toBeNull(); + expect(integration.processEvent(clone(exceptionEvent))).toBeNull(); }); it('ignores transactions between errors', () => { - expect(dedupeFunc(clone(exceptionEvent), {})).not.toBeNull(); + const integration = new Dedupe(); + + expect(integration.processEvent(clone(exceptionEvent))).not.toBeNull(); expect( - dedupeFunc( - { - event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', - message: 'someMessage', - transaction: 'wat', - type: 'transaction', - }, - {}, - ), + integration.processEvent({ + event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', + message: 'someMessage', + transaction: 'wat', + type: 'transaction', + }), ).not.toBeNull(); - expect(dedupeFunc(clone(exceptionEvent), {})).toBeNull(); - expect(dedupeFunc(clone(exceptionEvent), {})).toBeNull(); + expect(integration.processEvent(clone(exceptionEvent))).toBeNull(); + expect(integration.processEvent(clone(exceptionEvent))).toBeNull(); }); }); }); diff --git a/packages/nextjs/src/client/index.ts b/packages/nextjs/src/client/index.ts index 0adda732916b..9de007bd2e95 100644 --- a/packages/nextjs/src/client/index.ts +++ b/packages/nextjs/src/client/index.ts @@ -44,14 +44,17 @@ const globalWithInjectedValues = global as typeof global & { /** Inits the Sentry NextJS SDK on the browser with the React SDK. */ export function init(options: BrowserOptions): void { - applyTunnelRouteOption(options); - buildMetadata(options, ['nextjs', 'react']); + const opts = { + environment: getVercelEnv(true) || process.env.NODE_ENV, + ...options, + }; - options.environment = options.environment || getVercelEnv(true) || process.env.NODE_ENV; + applyTunnelRouteOption(opts); + buildMetadata(opts, ['nextjs', 'react']); - addClientIntegrations(options); + addClientIntegrations(opts); - reactInit(options); + reactInit(opts); configureScope(scope => { scope.setTag('runtime', 'browser'); diff --git a/packages/nextjs/src/edge/index.ts b/packages/nextjs/src/edge/index.ts index 0f13ff9cfccd..336a84b6b85c 100644 --- a/packages/nextjs/src/edge/index.ts +++ b/packages/nextjs/src/edge/index.ts @@ -1,4 +1,5 @@ import { SDK_VERSION } from '@sentry/core'; +import type { SdkMetadata } from '@sentry/types'; import type { VercelEdgeOptions } from '@sentry/vercel-edge'; import { init as vercelEdgeInit } from '@sentry/vercel-edge'; @@ -6,8 +7,12 @@ export type EdgeOptions = VercelEdgeOptions; /** Inits the Sentry NextJS SDK on the Edge Runtime. */ export function init(options: VercelEdgeOptions = {}): void { - options._metadata = options._metadata || {}; - options._metadata.sdk = options._metadata.sdk || { + const opts = { + _metadata: {} as SdkMetadata, + ...options, + }; + + opts._metadata.sdk = opts._metadata.sdk || { name: 'sentry.javascript.nextjs', packages: [ { @@ -18,7 +23,7 @@ export function init(options: VercelEdgeOptions = {}): void { version: SDK_VERSION, }; - vercelEdgeInit(options); + vercelEdgeInit(opts); } /** diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index 23f3bc61e4a3..7a782ec63a23 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -64,7 +64,14 @@ const IS_VERCEL = !!process.env.VERCEL; /** Inits the Sentry NextJS SDK on node. */ export function init(options: NodeOptions): void { - if (__DEBUG_BUILD__ && options.debug) { + const opts = { + environment: process.env.SENTRY_ENVIRONMENT || getVercelEnv(false) || process.env.NODE_ENV, + ...options, + // Right now we only capture frontend sessions for Next.js + autoSessionTracking: false, + }; + + if (__DEBUG_BUILD__ && opts.debug) { logger.enable(); } @@ -75,16 +82,11 @@ export function init(options: NodeOptions): void { return; } - buildMetadata(options, ['nextjs', 'node']); - - options.environment = - options.environment || process.env.SENTRY_ENVIRONMENT || getVercelEnv(false) || process.env.NODE_ENV; + buildMetadata(opts, ['nextjs', 'node']); - addServerIntegrations(options); - // Right now we only capture frontend sessions for Next.js - options.autoSessionTracking = false; + addServerIntegrations(opts); - nodeInit(options); + nodeInit(opts); const filterTransactions: EventProcessor = event => { return event.type === 'transaction' && event.transaction === '/404' ? null : event; diff --git a/packages/node-experimental/package.json b/packages/node-experimental/package.json index 75b177ade68d..b13ce269e821 100644 --- a/packages/node-experimental/package.json +++ b/packages/node-experimental/package.json @@ -24,10 +24,11 @@ }, "dependencies": { "@opentelemetry/api": "~1.6.0", + "@opentelemetry/core": "~1.17.0", "@opentelemetry/context-async-hooks": "~1.17.0", "@opentelemetry/instrumentation": "~0.43.0", "@opentelemetry/instrumentation-express": "~0.33.1", - "@opentelemetry/instrumentation-fastify": "~0.32.2", + "@opentelemetry/instrumentation-fastify": "~0.32.3", "@opentelemetry/instrumentation-graphql": "~0.35.1", "@opentelemetry/instrumentation-http": "~0.43.0", "@opentelemetry/instrumentation-mongodb": "~0.37.0", @@ -44,7 +45,8 @@ "@sentry/node": "7.73.0", "@sentry/opentelemetry-node": "7.73.0", "@sentry/types": "7.73.0", - "@sentry/utils": "7.73.0" + "@sentry/utils": "7.73.0", + "opentelemetry-instrumentation-fetch-node": "1.1.0" }, "scripts": { "build": "run-p build:transpile build:types", diff --git a/packages/node-experimental/src/constants.ts b/packages/node-experimental/src/constants.ts index dc714590556a..8d06aa411c1c 100644 --- a/packages/node-experimental/src/constants.ts +++ b/packages/node-experimental/src/constants.ts @@ -1,3 +1,21 @@ import { createContextKey } from '@opentelemetry/api'; export const OTEL_CONTEXT_HUB_KEY = createContextKey('sentry_hub'); + +export const OTEL_ATTR_ORIGIN = 'sentry.origin'; +export const OTEL_ATTR_OP = 'sentry.op'; +export const OTEL_ATTR_SOURCE = 'sentry.source'; + +export const OTEL_ATTR_PARENT_SAMPLED = 'sentry.parentSampled'; + +export const OTEL_ATTR_BREADCRUMB_TYPE = 'sentry.breadcrumb.type'; +export const OTEL_ATTR_BREADCRUMB_LEVEL = 'sentry.breadcrumb.level'; +export const OTEL_ATTR_BREADCRUMB_EVENT_ID = 'sentry.breadcrumb.event_id'; +export const OTEL_ATTR_BREADCRUMB_CATEGORY = 'sentry.breadcrumb.category'; +export const OTEL_ATTR_BREADCRUMB_DATA = 'sentry.breadcrumb.data'; +export const OTEL_ATTR_SENTRY_SAMPLE_RATE = 'sentry.sample_rate'; + +export const SENTRY_TRACE_HEADER = 'sentry-trace'; +export const SENTRY_BAGGAGE_HEADER = 'baggage'; + +export const SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY = createContextKey('SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY'); diff --git a/packages/node-experimental/src/index.ts b/packages/node-experimental/src/index.ts index 3c7fa347cf94..db1abe96495a 100644 --- a/packages/node-experimental/src/index.ts +++ b/packages/node-experimental/src/index.ts @@ -12,7 +12,9 @@ export { INTEGRATIONS as Integrations }; export { getAutoPerformanceIntegrations } from './integrations/getAutoPerformanceIntegrations'; export * as Handlers from './sdk/handlers'; export * from './sdk/trace'; +export { getActiveSpan } from './utils/getActiveSpan'; export { getCurrentHub, getHubFromCarrier } from './sdk/hub'; +export type { Span } from './types'; export { makeNodeTransport, @@ -39,7 +41,6 @@ export { makeMain, runWithAsyncContext, Scope, - startTransaction, SDK_VERSION, setContext, setExtra, @@ -67,10 +68,8 @@ export type { Exception, Session, SeverityLevel, - Span, StackFrame, Stacktrace, Thread, - Transaction, User, } from '@sentry/node'; diff --git a/packages/node-experimental/src/integrations/express.ts b/packages/node-experimental/src/integrations/express.ts index 95b9527c8498..0bbe3a19a11d 100644 --- a/packages/node-experimental/src/integrations/express.ts +++ b/packages/node-experimental/src/integrations/express.ts @@ -2,7 +2,7 @@ import type { Instrumentation } from '@opentelemetry/instrumentation'; import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express'; import type { Integration } from '@sentry/types'; -import { addOriginToOtelSpan } from '../utils/addOriginToSpan'; +import { addOriginToSpan } from '../utils/addOriginToSpan'; import { NodePerformanceIntegration } from './NodePerformanceIntegration'; /** @@ -31,7 +31,7 @@ export class Express extends NodePerformanceIntegration implements Integra return [ new ExpressInstrumentation({ requestHook(span) { - addOriginToOtelSpan(span, 'auto.http.otel.express'); + addOriginToSpan(span, 'auto.http.otel.express'); }, }), ]; diff --git a/packages/node-experimental/src/integrations/fastify.ts b/packages/node-experimental/src/integrations/fastify.ts index b84301967616..4d32037887b1 100644 --- a/packages/node-experimental/src/integrations/fastify.ts +++ b/packages/node-experimental/src/integrations/fastify.ts @@ -2,7 +2,7 @@ import type { Instrumentation } from '@opentelemetry/instrumentation'; import { FastifyInstrumentation } from '@opentelemetry/instrumentation-fastify'; import type { Integration } from '@sentry/types'; -import { addOriginToOtelSpan } from '../utils/addOriginToSpan'; +import { addOriginToSpan } from '../utils/addOriginToSpan'; import { NodePerformanceIntegration } from './NodePerformanceIntegration'; /** @@ -31,7 +31,7 @@ export class Fastify extends NodePerformanceIntegration implements Integra return [ new FastifyInstrumentation({ requestHook(span) { - addOriginToOtelSpan(span, 'auto.http.otel.fastify'); + addOriginToSpan(span, 'auto.http.otel.fastify'); }, }), ]; diff --git a/packages/node-experimental/src/integrations/graphql.ts b/packages/node-experimental/src/integrations/graphql.ts index 87749a0f54a2..b4a529df713e 100644 --- a/packages/node-experimental/src/integrations/graphql.ts +++ b/packages/node-experimental/src/integrations/graphql.ts @@ -2,7 +2,7 @@ import type { Instrumentation } from '@opentelemetry/instrumentation'; import { GraphQLInstrumentation } from '@opentelemetry/instrumentation-graphql'; import type { Integration } from '@sentry/types'; -import { addOriginToOtelSpan } from '../utils/addOriginToSpan'; +import { addOriginToSpan } from '../utils/addOriginToSpan'; import { NodePerformanceIntegration } from './NodePerformanceIntegration'; /** @@ -32,7 +32,7 @@ export class GraphQL extends NodePerformanceIntegration implements Integra new GraphQLInstrumentation({ ignoreTrivialResolveSpans: true, responseHook(span) { - addOriginToOtelSpan(span, 'auto.graphql.otel.graphql'); + addOriginToSpan(span, 'auto.graphql.otel.graphql'); }, }), ]; diff --git a/packages/node-experimental/src/integrations/http.ts b/packages/node-experimental/src/integrations/http.ts index 6a4b8766a242..5b939e2ead20 100644 --- a/packages/node-experimental/src/integrations/http.ts +++ b/packages/node-experimental/src/integrations/http.ts @@ -1,25 +1,19 @@ -import type { Attributes } from '@opentelemetry/api'; +import type { Span } from '@opentelemetry/api'; import { SpanKind } from '@opentelemetry/api'; import { registerInstrumentations } from '@opentelemetry/instrumentation'; import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; -import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; -import { hasTracingEnabled, isSentryRequestUrl, Transaction } from '@sentry/core'; -import { getCurrentHub } from '@sentry/node'; -import { _INTERNAL_getSentrySpan } from '@sentry/opentelemetry-node'; +import { hasTracingEnabled, isSentryRequestUrl } from '@sentry/core'; import type { EventProcessor, Hub, Integration } from '@sentry/types'; +import { stringMatchesSomePattern } from '@sentry/utils'; import type { ClientRequest, IncomingMessage, ServerResponse } from 'http'; -import type { NodeExperimentalClient, OtelSpan } from '../types'; +import { OTEL_ATTR_ORIGIN } from '../constants'; +import { setSpanMetadata } from '../opentelemetry/spanData'; +import type { NodeExperimentalClient } from '../sdk/client'; +import { getCurrentHub } from '../sdk/hub'; import { getRequestSpanData } from '../utils/getRequestSpanData'; import { getRequestUrl } from '../utils/getRequestUrl'; - -interface TracingOptions { - /** - * Function determining whether or not to create spans to track outgoing requests to the given URL. - * By default, spans will be created for all outgoing requests. - */ - shouldCreateSpanForRequest?: (url: string) => boolean; -} +import { getSpanKind } from '../utils/getSpanKind'; interface HttpOptions { /** @@ -32,7 +26,12 @@ interface HttpOptions { * Whether tracing spans should be created for requests * Defaults to false */ - tracing?: TracingOptions | boolean; + spans?: boolean; + + /** + * Do not capture spans or breadcrumbs for outgoing HTTP requests to URLs matching the given patterns. + */ + ignoreOutgoingRequests?: (string | RegExp)[]; } /** @@ -54,12 +53,16 @@ export class Http implements Integration { */ public name: string; + /** + * If spans for HTTP requests should be captured. + */ + public shouldCreateSpansForRequests: boolean; + private _unload?: () => void; private readonly _breadcrumbs: boolean; - // undefined: default behavior based on tracing settings - private readonly _tracing: boolean | undefined; - private _shouldCreateSpans: boolean; - private _shouldCreateSpanForRequest?: (url: string) => boolean; + // If this is undefined, use default behavior based on client settings + private readonly _spans: boolean | undefined; + private _ignoreOutgoingRequests: (string | RegExp)[]; /** * @inheritDoc @@ -67,12 +70,12 @@ export class Http implements Integration { public constructor(options: HttpOptions = {}) { this.name = Http.id; this._breadcrumbs = typeof options.breadcrumbs === 'undefined' ? true : options.breadcrumbs; - this._tracing = typeof options.tracing === 'undefined' ? undefined : !!options.tracing; - this._shouldCreateSpans = false; + this._spans = typeof options.spans === 'undefined' ? undefined : options.spans; - if (options.tracing && typeof options.tracing === 'object') { - this._shouldCreateSpanForRequest = options.tracing.shouldCreateSpanForRequest; - } + this._ignoreOutgoingRequests = options.ignoreOutgoingRequests || []; + + // Properly set in setupOnce based on client settings + this.shouldCreateSpansForRequests = false; } /** @@ -80,14 +83,16 @@ export class Http implements Integration { */ public setupOnce(_addGlobalEventProcessor: (callback: EventProcessor) => void, _getCurrentHub: () => Hub): void { // No need to instrument if we don't want to track anything - if (!this._breadcrumbs && this._tracing === false) { + if (!this._breadcrumbs && this._spans === false) { return; } const client = getCurrentHub().getClient(); const clientOptions = client?.getOptions(); - this._shouldCreateSpans = typeof this._tracing === 'undefined' ? hasTracingEnabled(clientOptions) : this._tracing; + // This is used in the sampler function + this.shouldCreateSpansForRequests = + typeof this._spans === 'boolean' ? this._spans : hasTracingEnabled(clientOptions); // Register instrumentations we care about this._unload = registerInstrumentations({ @@ -95,7 +100,20 @@ export class Http implements Integration { new HttpInstrumentation({ ignoreOutgoingRequestHook: request => { const url = getRequestUrl(request); - return url ? isSentryRequestUrl(url, getCurrentHub()) : false; + + if (!url) { + return false; + } + + if (isSentryRequestUrl(url, getCurrentHub())) { + return true; + } + + if (this._ignoreOutgoingRequests.length && stringMatchesSomePattern(url, this._ignoreOutgoingRequests)) { + return true; + } + + return false; }, ignoreIncomingRequestHook: request => { @@ -111,20 +129,14 @@ export class Http implements Integration { requireParentforOutgoingSpans: true, requireParentforIncomingSpans: false, requestHook: (span, req) => { - this._updateSentrySpan(span as unknown as OtelSpan, req); + this._updateSpan(span, req); }, responseHook: (span, res) => { - this._addRequestBreadcrumb(span as unknown as OtelSpan, res); + this._addRequestBreadcrumb(span, res); }, }), ], }); - - this._shouldCreateSpanForRequest = - // eslint-disable-next-line deprecation/deprecation - this._shouldCreateSpanForRequest || clientOptions?.shouldCreateSpanForRequest; - - client?.on?.('otelSpanEnd', this._onSpanEnd); } /** @@ -134,69 +146,18 @@ export class Http implements Integration { this._unload?.(); } - private _onSpanEnd: (otelSpan: unknown, mutableOptions: { drop: boolean }) => void = ( - otelSpan: unknown, - mutableOptions: { drop: boolean }, - ) => { - if (!this._shouldCreateSpans) { - mutableOptions.drop = true; - return; - } - - if (this._shouldCreateSpanForRequest) { - const url = getHttpUrl((otelSpan as OtelSpan).attributes); - if (url && !this._shouldCreateSpanForRequest(url)) { - mutableOptions.drop = true; - return; - } - } - - return; - }; + /** Update the span with data we need. */ + private _updateSpan(span: Span, request: ClientRequest | IncomingMessage): void { + span.setAttribute(OTEL_ATTR_ORIGIN, 'auto.http.otel.http'); - /** Update the Sentry span data based on the OTEL span. */ - private _updateSentrySpan(span: OtelSpan, request: ClientRequest | IncomingMessage): void { - const data = getRequestSpanData(span); - const { attributes } = span; - - const sentrySpan = _INTERNAL_getSentrySpan(span.spanContext().spanId); - if (!sentrySpan) { - return; + if (getSpanKind(span) === SpanKind.SERVER) { + setSpanMetadata(span, { request }); } - - sentrySpan.origin = 'auto.http.otel.http'; - - const additionalData: Record = { - url: data.url, - }; - - if (sentrySpan instanceof Transaction && span.kind === SpanKind.SERVER) { - sentrySpan.setMetadata({ request }); - } - - if (attributes[SemanticAttributes.HTTP_STATUS_CODE]) { - const statusCode = attributes[SemanticAttributes.HTTP_STATUS_CODE] as string; - additionalData['http.response.status_code'] = statusCode; - - sentrySpan.setTag('http.status_code', statusCode); - } - - if (data['http.query']) { - additionalData['http.query'] = data['http.query'].slice(1); - } - if (data['http.fragment']) { - additionalData['http.fragment'] = data['http.fragment'].slice(1); - } - - Object.keys(additionalData).forEach(prop => { - const value = additionalData[prop]; - sentrySpan.setData(prop, value); - }); } /** Add a breadcrumb for outgoing requests. */ - private _addRequestBreadcrumb(span: OtelSpan, response: IncomingMessage | ServerResponse): void { - if (!this._breadcrumbs || span.kind !== SpanKind.CLIENT) { + private _addRequestBreadcrumb(span: Span, response: IncomingMessage | ServerResponse): void { + if (!this._breadcrumbs || getSpanKind(span) !== SpanKind.CLIENT) { return; } @@ -220,8 +181,3 @@ export class Http implements Integration { ); } } - -function getHttpUrl(attributes: Attributes): string | undefined { - const url = attributes[SemanticAttributes.HTTP_URL]; - return typeof url === 'string' ? url : undefined; -} diff --git a/packages/node-experimental/src/integrations/index.ts b/packages/node-experimental/src/integrations/index.ts index efe485d6c1b6..35858c52c7a1 100644 --- a/packages/node-experimental/src/integrations/index.ts +++ b/packages/node-experimental/src/integrations/index.ts @@ -26,6 +26,7 @@ export { export { Express } from './express'; export { Http } from './http'; +export { NodeFetch } from './node-fetch'; export { Fastify } from './fastify'; export { GraphQL } from './graphql'; export { Mongo } from './mongo'; diff --git a/packages/node-experimental/src/integrations/mongo.ts b/packages/node-experimental/src/integrations/mongo.ts index aea5d0a7d3fb..f8be482be946 100644 --- a/packages/node-experimental/src/integrations/mongo.ts +++ b/packages/node-experimental/src/integrations/mongo.ts @@ -2,7 +2,7 @@ import type { Instrumentation } from '@opentelemetry/instrumentation'; import { MongoDBInstrumentation } from '@opentelemetry/instrumentation-mongodb'; import type { Integration } from '@sentry/types'; -import { addOriginToOtelSpan } from '../utils/addOriginToSpan'; +import { addOriginToSpan } from '../utils/addOriginToSpan'; import { NodePerformanceIntegration } from './NodePerformanceIntegration'; /** @@ -31,7 +31,7 @@ export class Mongo extends NodePerformanceIntegration implements Integrati return [ new MongoDBInstrumentation({ responseHook(span) { - addOriginToOtelSpan(span, 'auto.db.otel.mongo'); + addOriginToSpan(span, 'auto.db.otel.mongo'); }, }), ]; diff --git a/packages/node-experimental/src/integrations/mongoose.ts b/packages/node-experimental/src/integrations/mongoose.ts index 8f6eb65adb8b..a5361a620bc2 100644 --- a/packages/node-experimental/src/integrations/mongoose.ts +++ b/packages/node-experimental/src/integrations/mongoose.ts @@ -2,7 +2,7 @@ import type { Instrumentation } from '@opentelemetry/instrumentation'; import { MongooseInstrumentation } from '@opentelemetry/instrumentation-mongoose'; import type { Integration } from '@sentry/types'; -import { addOriginToOtelSpan } from '../utils/addOriginToSpan'; +import { addOriginToSpan } from '../utils/addOriginToSpan'; import { NodePerformanceIntegration } from './NodePerformanceIntegration'; /** @@ -31,7 +31,7 @@ export class Mongoose extends NodePerformanceIntegration implements Integr return [ new MongooseInstrumentation({ responseHook(span) { - addOriginToOtelSpan(span, 'auto.db.otel.mongoose'); + addOriginToSpan(span, 'auto.db.otel.mongoose'); }, }), ]; diff --git a/packages/node-experimental/src/integrations/mysql2.ts b/packages/node-experimental/src/integrations/mysql2.ts index b78b56bdd0ab..9a87de98fd66 100644 --- a/packages/node-experimental/src/integrations/mysql2.ts +++ b/packages/node-experimental/src/integrations/mysql2.ts @@ -2,7 +2,7 @@ import type { Instrumentation } from '@opentelemetry/instrumentation'; import { MySQL2Instrumentation } from '@opentelemetry/instrumentation-mysql2'; import type { Integration } from '@sentry/types'; -import { addOriginToOtelSpan } from '../utils/addOriginToSpan'; +import { addOriginToSpan } from '../utils/addOriginToSpan'; import { NodePerformanceIntegration } from './NodePerformanceIntegration'; /** @@ -31,7 +31,7 @@ export class Mysql2 extends NodePerformanceIntegration implements Integrat return [ new MySQL2Instrumentation({ responseHook(span) { - addOriginToOtelSpan(span, 'auto.db.otel.mysql2'); + addOriginToSpan(span, 'auto.db.otel.mysql2'); }, }), ]; diff --git a/packages/node-experimental/src/integrations/node-fetch.ts b/packages/node-experimental/src/integrations/node-fetch.ts new file mode 100644 index 000000000000..73c1eaab27ae --- /dev/null +++ b/packages/node-experimental/src/integrations/node-fetch.ts @@ -0,0 +1,123 @@ +import type { Span } from '@opentelemetry/api'; +import { SpanKind } from '@opentelemetry/api'; +import { registerInstrumentations } from '@opentelemetry/instrumentation'; +import { hasTracingEnabled } from '@sentry/core'; +import type { EventProcessor, Hub, Integration } from '@sentry/types'; +import { FetchInstrumentation } from 'opentelemetry-instrumentation-fetch-node'; + +import { OTEL_ATTR_ORIGIN } from '../constants'; +import type { NodeExperimentalClient } from '../sdk/client'; +import { getCurrentHub } from '../sdk/hub'; +import { getRequestSpanData } from '../utils/getRequestSpanData'; +import { getSpanKind } from '../utils/getSpanKind'; + +interface NodeFetchOptions { + /** + * Whether breadcrumbs should be recorded for requests + * Defaults to true + */ + breadcrumbs?: boolean; + + /** + * Whether tracing spans should be created for requests + * Defaults to false + */ + spans?: boolean; +} + +/** + * Fetch instrumentation based on opentelemetry-instrumentation-fetch. + * This instrumentation does two things: + * * Create breadcrumbs for outgoing requests + * * Create spans for outgoing requests + */ +export class NodeFetch implements Integration { + /** + * @inheritDoc + */ + public static id: string = 'NodeFetch'; + + /** + * @inheritDoc + */ + public name: string; + + /** + * If spans for HTTP requests should be captured. + */ + public shouldCreateSpansForRequests: boolean; + + private _unload?: () => void; + private readonly _breadcrumbs: boolean; + // If this is undefined, use default behavior based on client settings + private readonly _spans: boolean | undefined; + + /** + * @inheritDoc + */ + public constructor(options: NodeFetchOptions = {}) { + this.name = NodeFetch.id; + this._breadcrumbs = typeof options.breadcrumbs === 'undefined' ? true : options.breadcrumbs; + this._spans = typeof options.spans === 'undefined' ? undefined : options.spans; + + // Properly set in setupOnce based on client settings + this.shouldCreateSpansForRequests = false; + } + + /** + * @inheritDoc + */ + public setupOnce(_addGlobalEventProcessor: (callback: EventProcessor) => void, _getCurrentHub: () => Hub): void { + // No need to instrument if we don't want to track anything + if (!this._breadcrumbs && this._spans === false) { + return; + } + + const client = getCurrentHub().getClient(); + const clientOptions = client?.getOptions(); + + // This is used in the sampler function + this.shouldCreateSpansForRequests = + typeof this._spans === 'boolean' ? this._spans : hasTracingEnabled(clientOptions); + + // Register instrumentations we care about + this._unload = registerInstrumentations({ + instrumentations: [ + new FetchInstrumentation({ + onRequest: ({ span }: { span: Span }) => { + this._updateSpan(span); + this._addRequestBreadcrumb(span); + }, + }), + ], + }); + } + + /** + * Unregister this integration. + */ + public unregister(): void { + this._unload?.(); + } + + /** Update the span with data we need. */ + private _updateSpan(span: Span): void { + span.setAttribute(OTEL_ATTR_ORIGIN, 'auto.http.otel.node_fetch'); + } + + /** Add a breadcrumb for outgoing requests. */ + private _addRequestBreadcrumb(span: Span): void { + if (!this._breadcrumbs || getSpanKind(span) !== SpanKind.CLIENT) { + return; + } + + const data = getRequestSpanData(span); + getCurrentHub().addBreadcrumb({ + category: 'http', + data: { + ...data, + }, + type: 'http', + }); + } +} diff --git a/packages/node-experimental/src/integrations/postgres.ts b/packages/node-experimental/src/integrations/postgres.ts index 4ecab8d685f2..85584f8a6507 100644 --- a/packages/node-experimental/src/integrations/postgres.ts +++ b/packages/node-experimental/src/integrations/postgres.ts @@ -2,7 +2,7 @@ import type { Instrumentation } from '@opentelemetry/instrumentation'; import { PgInstrumentation } from '@opentelemetry/instrumentation-pg'; import type { Integration } from '@sentry/types'; -import { addOriginToOtelSpan } from '../utils/addOriginToSpan'; +import { addOriginToSpan } from '../utils/addOriginToSpan'; import { NodePerformanceIntegration } from './NodePerformanceIntegration'; /** @@ -32,7 +32,7 @@ export class Postgres extends NodePerformanceIntegration implements Integr new PgInstrumentation({ requireParentSpan: true, requestHook(span) { - addOriginToOtelSpan(span, 'auto.db.otel.postgres'); + addOriginToSpan(span, 'auto.db.otel.postgres'); }, }), ]; diff --git a/packages/node-experimental/src/opentelemetry/propagator.ts b/packages/node-experimental/src/opentelemetry/propagator.ts new file mode 100644 index 000000000000..7aa43271b72c --- /dev/null +++ b/packages/node-experimental/src/opentelemetry/propagator.ts @@ -0,0 +1,131 @@ +import type { Baggage, Context, SpanContext, TextMapGetter, TextMapSetter } from '@opentelemetry/api'; +import { propagation, trace, TraceFlags } from '@opentelemetry/api'; +import { isTracingSuppressed, W3CBaggagePropagator } from '@opentelemetry/core'; +import { getDynamicSamplingContextFromClient } from '@sentry/core'; +import type { DynamicSamplingContext, PropagationContext } from '@sentry/types'; +import { generateSentryTraceHeader, SENTRY_BAGGAGE_KEY_PREFIX, tracingContextFromHeaders } from '@sentry/utils'; + +import { getCurrentHub } from '../sdk/hub'; +import { SENTRY_BAGGAGE_HEADER, SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY, SENTRY_TRACE_HEADER } from './../constants'; +import { getSpanScope } from './spanData'; + +/** + * Injects and extracts `sentry-trace` and `baggage` headers from carriers. + */ +export class SentryPropagator extends W3CBaggagePropagator { + /** + * @inheritDoc + */ + public inject(context: Context, carrier: unknown, setter: TextMapSetter): void { + if (isTracingSuppressed(context)) { + return; + } + + let baggage = propagation.getBaggage(context) || propagation.createBaggage({}); + + const propagationContext = context.getValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY) as + | PropagationContext + | undefined; + + const { spanId, traceId, sampled } = getSentryTraceData(context, propagationContext); + + const dynamicSamplingContext = propagationContext ? getDsc(context, propagationContext, traceId) : undefined; + + if (dynamicSamplingContext) { + baggage = Object.entries(dynamicSamplingContext).reduce((b, [dscKey, dscValue]) => { + if (dscValue) { + return b.setEntry(`${SENTRY_BAGGAGE_KEY_PREFIX}${dscKey}`, { value: dscValue }); + } + return b; + }, baggage); + } + + setter.set(carrier, SENTRY_TRACE_HEADER, generateSentryTraceHeader(traceId, spanId, sampled)); + + super.inject(propagation.setBaggage(context, baggage), carrier, setter); + } + + /** + * @inheritDoc + */ + public extract(context: Context, carrier: unknown, getter: TextMapGetter): Context { + const maybeSentryTraceHeader: string | string[] | undefined = getter.get(carrier, SENTRY_TRACE_HEADER); + const maybeBaggageHeader = getter.get(carrier, SENTRY_BAGGAGE_HEADER); + + const sentryTraceHeader = maybeSentryTraceHeader + ? Array.isArray(maybeSentryTraceHeader) + ? maybeSentryTraceHeader[0] + : maybeSentryTraceHeader + : undefined; + + const { propagationContext } = tracingContextFromHeaders(sentryTraceHeader, maybeBaggageHeader); + + // Add propagation context to context + const contextWithPropagationContext = context.setValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY, propagationContext); + + const spanContext: SpanContext = { + traceId: propagationContext.traceId, + spanId: propagationContext.parentSpanId || '', + isRemote: true, + traceFlags: propagationContext.sampled === true ? TraceFlags.SAMPLED : TraceFlags.NONE, + }; + + // Add remote parent span context + return trace.setSpanContext(contextWithPropagationContext, spanContext); + } + + /** + * @inheritDoc + */ + public fields(): string[] { + return [SENTRY_TRACE_HEADER, SENTRY_BAGGAGE_HEADER]; + } +} + +function getDsc( + context: Context, + propagationContext: PropagationContext, + traceId: string | undefined, +): DynamicSamplingContext | undefined { + // If we have a DSC on the propagation context, we just use it + if (propagationContext.dsc) { + return propagationContext.dsc; + } + + // Else, we try to generate a new one + const client = getCurrentHub().getClient(); + const activeSpan = trace.getSpan(context); + const scope = activeSpan ? getSpanScope(activeSpan) : undefined; + + if (client) { + return getDynamicSamplingContextFromClient(traceId || propagationContext.traceId, client, scope); + } + + return undefined; +} + +function getSentryTraceData( + context: Context, + propagationContext: PropagationContext | undefined, +): { + spanId: string | undefined; + traceId: string | undefined; + sampled: boolean | undefined; +} { + const span = trace.getSpan(context); + const spanContext = span && span.spanContext(); + + const traceId = spanContext ? spanContext.traceId : propagationContext?.traceId; + + // We have a few scenarios here: + // If we have an active span, and it is _not_ remote, we just use the span's ID + // If we have an active span that is remote, we do not want to use the spanId, as we don't want to attach it to the parent span + // If `isRemote === true`, the span is bascially virtual + // If we don't have a local active span, we use the generated spanId from the propagationContext + const spanId = spanContext && !spanContext.isRemote ? spanContext.spanId : propagationContext?.spanId; + + // eslint-disable-next-line no-bitwise + const sampled = spanContext ? Boolean(spanContext.traceFlags & TraceFlags.SAMPLED) : propagationContext?.sampled; + + return { traceId, spanId, sampled }; +} diff --git a/packages/node-experimental/src/opentelemetry/sampler.ts b/packages/node-experimental/src/opentelemetry/sampler.ts new file mode 100644 index 000000000000..373c3b314b70 --- /dev/null +++ b/packages/node-experimental/src/opentelemetry/sampler.ts @@ -0,0 +1,193 @@ +/* eslint-disable no-bitwise */ +import type { Attributes, Context, SpanContext } from '@opentelemetry/api'; +import { isSpanContextValid, trace, TraceFlags } from '@opentelemetry/api'; +import type { Sampler, SamplingResult } from '@opentelemetry/sdk-trace-base'; +import { SamplingDecision } from '@opentelemetry/sdk-trace-base'; +import { hasTracingEnabled } from '@sentry/core'; +import type { Client, ClientOptions, PropagationContext, SamplingContext } from '@sentry/types'; +import { isNaN, logger } from '@sentry/utils'; + +import { + OTEL_ATTR_PARENT_SAMPLED, + OTEL_ATTR_SENTRY_SAMPLE_RATE, + SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY, +} from '../constants'; + +/** + * A custom OTEL sampler that uses Sentry sampling rates to make it's decision + */ +export class SentrySampler implements Sampler { + private _client: Client; + + public constructor(client: Client) { + this._client = client; + } + + /** @inheritDoc */ + public shouldSample( + context: Context, + traceId: string, + spanName: string, + _spanKind: unknown, + _attributes: unknown, + _links: unknown, + ): SamplingResult { + const options = this._client.getOptions(); + + if (!hasTracingEnabled(options)) { + return { decision: SamplingDecision.NOT_RECORD }; + } + + const parentContext = trace.getSpanContext(context); + + let parentSampled: boolean | undefined = undefined; + + // Only inherit sample rate if `traceId` is the same + // Note for testing: `isSpanContextValid()` checks the format of the traceId/spanId, so we need to pass valid ones + if (parentContext && isSpanContextValid(parentContext) && parentContext.traceId === traceId) { + if (parentContext.isRemote) { + parentSampled = getParentRemoteSampled(parentContext, context); + __DEBUG_BUILD__ && + logger.log(`[Tracing] Inheriting remote parent's sampled decision for ${spanName}: ${parentSampled}`); + } else { + parentSampled = Boolean(parentContext.traceFlags & TraceFlags.SAMPLED); + __DEBUG_BUILD__ && + logger.log(`[Tracing] Inheriting parent's sampled decision for ${spanName}: ${parentSampled}`); + } + } + + const sampleRate = getSampleRate(options, { + transactionContext: { + name: spanName, + parentSampled, + }, + parentSampled, + }); + + const attributes: Attributes = { + [OTEL_ATTR_SENTRY_SAMPLE_RATE]: Number(sampleRate), + }; + + if (typeof parentSampled === 'boolean') { + attributes[OTEL_ATTR_PARENT_SAMPLED] = parentSampled; + } + + // Since this is coming from the user (or from a function provided by the user), who knows what we might get. (The + // only valid values are booleans or numbers between 0 and 1.) + if (!isValidSampleRate(sampleRate)) { + __DEBUG_BUILD__ && logger.warn('[Tracing] Discarding span because of invalid sample rate.'); + + return { + decision: SamplingDecision.NOT_RECORD, + attributes, + }; + } + + // if the function returned 0 (or false), or if `tracesSampleRate` is 0, it's a sign the transaction should be dropped + if (!sampleRate) { + __DEBUG_BUILD__ && + logger.log( + `[Tracing] Discarding span because ${ + typeof options.tracesSampler === 'function' + ? 'tracesSampler returned 0 or false' + : 'a negative sampling decision was inherited or tracesSampleRate is set to 0' + }`, + ); + + return { + decision: SamplingDecision.NOT_RECORD, + attributes, + }; + } + + // Now we roll the dice. Math.random is inclusive of 0, but not of 1, so strict < is safe here. In case sampleRate is + // a boolean, the < comparison will cause it to be automatically cast to 1 if it's true and 0 if it's false. + const isSampled = Math.random() < (sampleRate as number | boolean); + + // if we're not going to keep it, we're done + if (!isSampled) { + __DEBUG_BUILD__ && + logger.log( + `[Tracing] Discarding span because it's not included in the random sample (sampling rate = ${Number( + sampleRate, + )})`, + ); + + return { + decision: SamplingDecision.NOT_RECORD, + attributes, + }; + } + + return { + decision: SamplingDecision.RECORD_AND_SAMPLED, + attributes, + }; + } + + /** Returns the sampler name or short description with the configuration. */ + public toString(): string { + return 'SentrySampler'; + } +} + +function getSampleRate( + options: Pick, + samplingContext: SamplingContext, +): number | boolean { + if (typeof options.tracesSampler === 'function') { + return options.tracesSampler(samplingContext); + } + + if (samplingContext.parentSampled !== undefined) { + return samplingContext.parentSampled; + } + + if (typeof options.tracesSampleRate !== 'undefined') { + return options.tracesSampleRate; + } + + // When `enableTracing === true`, we use a sample rate of 100% + if (options.enableTracing) { + return 1; + } + + return 0; +} + +/** + * Checks the given sample rate to make sure it is valid type and value (a boolean, or a number between 0 and 1). + */ +function isValidSampleRate(rate: unknown): boolean { + // we need to check NaN explicitly because it's of type 'number' and therefore wouldn't get caught by this typecheck + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if (isNaN(rate) || !(typeof rate === 'number' || typeof rate === 'boolean')) { + __DEBUG_BUILD__ && + logger.warn( + `[Tracing] Given sample rate is invalid. Sample rate must be a boolean or a number between 0 and 1. Got ${JSON.stringify( + rate, + )} of type ${JSON.stringify(typeof rate)}.`, + ); + return false; + } + + // in case sampleRate is a boolean, it will get automatically cast to 1 if it's true and 0 if it's false + if (rate < 0 || rate > 1) { + __DEBUG_BUILD__ && + logger.warn(`[Tracing] Given sample rate is invalid. Sample rate must be between 0 and 1. Got ${rate}.`); + return false; + } + return true; +} + +function getPropagationContext(parentContext: Context): PropagationContext | undefined { + return parentContext.getValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY) as PropagationContext | undefined; +} + +function getParentRemoteSampled(spanContext: SpanContext, context: Context): boolean | undefined { + const traceId = spanContext.traceId; + const traceparentData = getPropagationContext(context); + + // Only inherit sample rate if `traceId` is the same + return traceparentData && traceId === traceparentData.traceId ? traceparentData.sampled : undefined; +} diff --git a/packages/node-experimental/src/opentelemetry/spanData.ts b/packages/node-experimental/src/opentelemetry/spanData.ts new file mode 100644 index 000000000000..e8fe58506866 --- /dev/null +++ b/packages/node-experimental/src/opentelemetry/spanData.ts @@ -0,0 +1,52 @@ +import type { Span } from '@opentelemetry/api'; +import type { Hub, Scope, TransactionMetadata } from '@sentry/types'; + +import type { AbstractSpan } from '../types'; + +// We store the parent span, scope & metadata in separate weakmaps, so we can access them for a given span +// This way we can enhance the data that an OTEL Span natively gives us +// and since we are using weakmaps, we do not need to clean up after ourselves +const SpanScope = new WeakMap(); +const SpanHub = new WeakMap(); +const SpanParent = new WeakMap(); +const SpanMetadata = new WeakMap>(); + +/** Set the Sentry scope on an OTEL span. */ +export function setSpanScope(span: AbstractSpan, scope: Scope): void { + SpanScope.set(span, scope); +} + +/** Get the Sentry scope of an OTEL span. */ +export function getSpanScope(span: AbstractSpan): Scope | undefined { + return SpanScope.get(span); +} + +/** Set the Sentry hub on an OTEL span. */ +export function setSpanHub(span: AbstractSpan, hub: Hub): void { + SpanHub.set(span, hub); +} + +/** Get the Sentry hub of an OTEL span. */ +export function getSpanHub(span: AbstractSpan): Hub | undefined { + return SpanHub.get(span); +} + +/** Set the parent OTEL span on an OTEL span. */ +export function setSpanParent(span: AbstractSpan, parentSpan: Span): void { + SpanParent.set(span, parentSpan); +} + +/** Get the parent OTEL span of an OTEL span. */ +export function getSpanParent(span: AbstractSpan): Span | undefined { + return SpanParent.get(span); +} + +/** Set metadata for an OTEL span. */ +export function setSpanMetadata(span: AbstractSpan, metadata: Partial): void { + SpanMetadata.set(span, metadata); +} + +/** Get metadata for an OTEL span. */ +export function getSpanMetadata(span: AbstractSpan): Partial | undefined { + return SpanMetadata.get(span); +} diff --git a/packages/node-experimental/src/opentelemetry/spanExporter.ts b/packages/node-experimental/src/opentelemetry/spanExporter.ts new file mode 100644 index 000000000000..e242f74d6104 --- /dev/null +++ b/packages/node-experimental/src/opentelemetry/spanExporter.ts @@ -0,0 +1,325 @@ +import type { Span } from '@opentelemetry/api'; +import { SpanKind } from '@opentelemetry/api'; +import type { ExportResult } from '@opentelemetry/core'; +import { ExportResultCode } from '@opentelemetry/core'; +import type { ReadableSpan, Span as SdkTraceBaseSpan, SpanExporter } from '@opentelemetry/sdk-trace-base'; +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; +import { flush } from '@sentry/core'; +import { mapOtelStatus, parseOtelSpanDescription } from '@sentry/opentelemetry-node'; +import type { DynamicSamplingContext, Span as SentrySpan, SpanOrigin, TransactionSource } from '@sentry/types'; +import { logger } from '@sentry/utils'; + +import { + OTEL_ATTR_OP, + OTEL_ATTR_ORIGIN, + OTEL_ATTR_PARENT_SAMPLED, + OTEL_ATTR_SENTRY_SAMPLE_RATE, + OTEL_ATTR_SOURCE, +} from '../constants'; +import { getCurrentHub } from '../sdk/hub'; +import { NodeExperimentalScope } from '../sdk/scope'; +import type { NodeExperimentalTransaction } from '../sdk/transaction'; +import { startTransaction } from '../sdk/transaction'; +import { convertOtelTimeToSeconds } from '../utils/convertOtelTimeToSeconds'; +import { getRequestSpanData } from '../utils/getRequestSpanData'; +import type { SpanNode } from '../utils/groupSpansWithParents'; +import { groupSpansWithParents } from '../utils/groupSpansWithParents'; +import { getSpanHub, getSpanMetadata, getSpanScope } from './spanData'; + +type SpanNodeCompleted = SpanNode & { span: ReadableSpan }; + +/** + * A Sentry-specific exporter that converts OpenTelemetry Spans to Sentry Spans & Transactions. + */ +export class SentrySpanExporter implements SpanExporter { + private _finishedSpans: ReadableSpan[]; + private _stopped: boolean; + + public constructor() { + this._stopped = false; + this._finishedSpans = []; + } + + /** @inheritDoc */ + public export(spans: ReadableSpan[], resultCallback: (result: ExportResult) => void): void { + if (this._stopped) { + return resultCallback({ + code: ExportResultCode.FAILED, + error: new Error('Exporter has been stopped'), + }); + } + + const openSpanCount = this._finishedSpans.length; + const newSpanCount = spans.length; + + this._finishedSpans.push(...spans); + + const remainingSpans = maybeSend(this._finishedSpans); + + const remainingOpenSpanCount = remainingSpans.length; + const sentSpanCount = openSpanCount + newSpanCount - remainingOpenSpanCount; + + __DEBUG_BUILD__ && + logger.log(`SpanExporter exported ${sentSpanCount} spans, ${remainingOpenSpanCount} unsent spans remaining`); + + this._finishedSpans = remainingSpans.filter(span => { + const shouldDrop = shouldCleanupSpan(span, 5 * 60); + __DEBUG_BUILD__ && + shouldDrop && + logger.log( + `SpanExporter dropping span ${span.name} (${ + span.spanContext().spanId + }) because it is pending for more than 5 minutes.`, + ); + return !shouldDrop; + }); + + resultCallback({ code: ExportResultCode.SUCCESS }); + } + + /** @inheritDoc */ + public shutdown(): Promise { + this._stopped = true; + this._finishedSpans = []; + return this.forceFlush(); + } + + /** @inheritDoc */ + public async forceFlush(): Promise { + await flush(); + } +} + +/** + * Send the given spans, but only if they are part of a finished transaction. + * + * Returns the unsent spans. + * Spans remain unsent when their parent span is not yet finished. + * This will happen regularly, as child spans are generally finished before their parents. + * But it _could_ also happen because, for whatever reason, a parent span was lost. + * In this case, we'll eventually need to clean this up. + */ +function maybeSend(spans: ReadableSpan[]): ReadableSpan[] { + const grouped = groupSpansWithParents(spans); + const remaining = new Set(grouped); + + const rootNodes = getCompletedRootNodes(grouped); + + rootNodes.forEach(root => { + remaining.delete(root); + const span = root.span; + const transaction = createTransactionForOtelSpan(span); + + root.children.forEach(child => { + createAndFinishSpanForOtelSpan(child, transaction, remaining); + }); + + // Now finish the transaction, which will send it together with all the spans + // We make sure to use the current span as the activeSpan for this transaction + const scope = getSpanScope(span); + const forkedScope = NodeExperimentalScope.clone( + scope as NodeExperimentalScope | undefined, + ) as NodeExperimentalScope; + forkedScope.activeSpan = span as unknown as Span; + + transaction.finishWithScope(convertOtelTimeToSeconds(span.endTime), forkedScope); + }); + + return Array.from(remaining) + .map(node => node.span) + .filter((span): span is ReadableSpan => !!span); +} + +function getCompletedRootNodes(nodes: SpanNode[]): SpanNodeCompleted[] { + return nodes.filter((node): node is SpanNodeCompleted => !!node.span && !node.parentNode); +} + +function shouldCleanupSpan(span: ReadableSpan, maxStartTimeOffsetSeconds: number): boolean { + const cutoff = Date.now() / 1000 - maxStartTimeOffsetSeconds; + return convertOtelTimeToSeconds(span.startTime) < cutoff; +} + +function parseSpan(span: ReadableSpan): { op?: string; origin?: SpanOrigin; source?: TransactionSource } { + const attributes = span.attributes; + + const origin = attributes[OTEL_ATTR_ORIGIN] as SpanOrigin | undefined; + const op = attributes[OTEL_ATTR_OP] as string | undefined; + const source = attributes[OTEL_ATTR_SOURCE] as TransactionSource | undefined; + + return { origin, op, source }; +} + +function createTransactionForOtelSpan(span: ReadableSpan): NodeExperimentalTransaction { + const scope = getSpanScope(span); + const hub = getSpanHub(span) || getCurrentHub(); + const spanContext = span.spanContext(); + const spanId = spanContext.spanId; + const traceId = spanContext.traceId; + const parentSpanId = span.parentSpanId; + + const parentSampled = span.attributes[OTEL_ATTR_PARENT_SAMPLED] as boolean | undefined; + const dynamicSamplingContext: DynamicSamplingContext | undefined = scope + ? scope.getPropagationContext().dsc + : undefined; + + const { op, description, tags, data, origin, source } = getSpanData(span as SdkTraceBaseSpan); + const metadata = getSpanMetadata(span); + + const transaction = startTransaction(hub, { + spanId, + traceId, + parentSpanId, + parentSampled, + name: description, + op, + instrumenter: 'otel', + status: mapOtelStatus(span as SdkTraceBaseSpan), + startTimestamp: convertOtelTimeToSeconds(span.startTime), + metadata: { + dynamicSamplingContext, + source, + sampleRate: span.attributes[OTEL_ATTR_SENTRY_SAMPLE_RATE] as number | undefined, + ...metadata, + }, + data: removeSentryAttributes(data), + origin, + tags, + sampled: true, + }) as NodeExperimentalTransaction; + + transaction.setContext('otel', { + attributes: removeSentryAttributes(span.attributes), + resource: span.resource.attributes, + }); + + return transaction; +} + +function createAndFinishSpanForOtelSpan(node: SpanNode, sentryParentSpan: SentrySpan, remaining: Set): void { + remaining.delete(node); + const span = node.span; + + const shouldDrop = !span; + + // If this span should be dropped, we still want to create spans for the children of this + if (shouldDrop) { + node.children.forEach(child => { + createAndFinishSpanForOtelSpan(child, sentryParentSpan, remaining); + }); + return; + } + + const spanId = span.spanContext().spanId; + const { attributes } = span; + + const { op, description, tags, data, origin } = getSpanData(span as SdkTraceBaseSpan); + const allData = { ...removeSentryAttributes(attributes), ...data }; + + const sentrySpan = sentryParentSpan.startChild({ + description, + op, + data: allData, + status: mapOtelStatus(span as SdkTraceBaseSpan), + instrumenter: 'otel', + startTimestamp: convertOtelTimeToSeconds(span.startTime), + spanId, + origin, + tags, + }); + + node.children.forEach(child => { + createAndFinishSpanForOtelSpan(child, sentrySpan, remaining); + }); + + sentrySpan.finish(convertOtelTimeToSeconds(span.endTime)); +} + +function getSpanData(span: ReadableSpan): { + tags: Record; + data: Record; + op?: string; + description: string; + source?: TransactionSource; + origin?: SpanOrigin; +} { + const { op: definedOp, source: definedSource, origin } = parseSpan(span); + const { + op: inferredOp, + description, + source: inferredSource, + data: inferredData, + } = parseOtelSpanDescription(span as SdkTraceBaseSpan); + + const op = definedOp || inferredOp; + const source = definedSource || inferredSource; + + const tags = getTags(span); + const data = { ...inferredData, ...getData(span) }; + + return { + op, + description, + source, + origin, + tags, + data, + }; +} + +/** + * Remove custom `sentry.` attribtues we do not need to send. + * These are more carrier attributes we use inside of the SDK, we do not need to send them to the API. + */ +function removeSentryAttributes(data: Record): Record { + const cleanedData = { ...data }; + + /* eslint-disable @typescript-eslint/no-dynamic-delete */ + delete cleanedData[OTEL_ATTR_PARENT_SAMPLED]; + delete cleanedData[OTEL_ATTR_ORIGIN]; + delete cleanedData[OTEL_ATTR_OP]; + delete cleanedData[OTEL_ATTR_SOURCE]; + delete cleanedData[OTEL_ATTR_SENTRY_SAMPLE_RATE]; + /* eslint-enable @typescript-eslint/no-dynamic-delete */ + + return cleanedData; +} + +function getTags(span: ReadableSpan): Record { + const attributes = span.attributes; + const tags: Record = {}; + + if (attributes[SemanticAttributes.HTTP_STATUS_CODE]) { + const statusCode = attributes[SemanticAttributes.HTTP_STATUS_CODE] as string; + + tags['http.status_code'] = statusCode; + } + + return tags; +} + +function getData(span: ReadableSpan): Record { + const attributes = span.attributes; + const data: Record = { + 'otel.kind': SpanKind[span.kind], + }; + + if (attributes[SemanticAttributes.HTTP_STATUS_CODE]) { + const statusCode = attributes[SemanticAttributes.HTTP_STATUS_CODE] as string; + data['http.response.status_code'] = statusCode; + } + + const requestData = getRequestSpanData(span); + + if (requestData.url) { + data.url = requestData.url; + } + + if (requestData['http.query']) { + data['http.query'] = requestData['http.query'].slice(1); + } + if (requestData['http.fragment']) { + data['http.fragment'] = requestData['http.fragment'].slice(1); + } + + return data; +} diff --git a/packages/node-experimental/src/opentelemetry/spanProcessor.ts b/packages/node-experimental/src/opentelemetry/spanProcessor.ts new file mode 100644 index 000000000000..c7e07d11aa8e --- /dev/null +++ b/packages/node-experimental/src/opentelemetry/spanProcessor.ts @@ -0,0 +1,99 @@ +import type { Context } from '@opentelemetry/api'; +import { ROOT_CONTEXT, SpanKind, trace } from '@opentelemetry/api'; +import type { Span, SpanProcessor as SpanProcessorInterface } from '@opentelemetry/sdk-trace-base'; +import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base'; +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; +import { maybeCaptureExceptionForTimedEvent } from '@sentry/opentelemetry-node'; +import type { Hub } from '@sentry/types'; +import { logger } from '@sentry/utils'; + +import { OTEL_CONTEXT_HUB_KEY } from '../constants'; +import { Http } from '../integrations'; +import { NodeFetch } from '../integrations/node-fetch'; +import type { NodeExperimentalClient } from '../sdk/client'; +import { getCurrentHub } from '../sdk/hub'; +import { getSpanHub, setSpanHub, setSpanParent, setSpanScope } from './spanData'; +import { SentrySpanExporter } from './spanExporter'; + +/** + * Converts OpenTelemetry Spans to Sentry Spans and sends them to Sentry via + * the Sentry SDK. + */ +export class SentrySpanProcessor extends BatchSpanProcessor implements SpanProcessorInterface { + public constructor() { + super(new SentrySpanExporter()); + } + + /** + * @inheritDoc + */ + public onStart(span: Span, parentContext: Context): void { + // This is a reliable way to get the parent span - because this is exactly how the parent is identified in the OTEL SDK + const parentSpan = trace.getSpan(parentContext); + const hub = parentContext.getValue(OTEL_CONTEXT_HUB_KEY) as Hub | undefined; + + // We need access to the parent span in order to be able to move up the span tree for breadcrumbs + if (parentSpan) { + setSpanParent(span, parentSpan); + } + + // The root context does not have a hub stored, so we check for this specifically + // We do this instead of just falling back to `getCurrentHub` to avoid attaching the wrong hub + let actualHub = hub; + if (parentContext === ROOT_CONTEXT) { + actualHub = getCurrentHub(); + } + + // We need the scope at time of span creation in order to apply it to the event when the span is finished + if (actualHub) { + setSpanScope(span, actualHub.getScope()); + setSpanHub(span, actualHub); + } + + __DEBUG_BUILD__ && logger.log(`[Tracing] Starting span "${span.name}" (${span.spanContext().spanId})`); + + return super.onStart(span, parentContext); + } + + /** @inheritDoc */ + public onEnd(span: Span): void { + __DEBUG_BUILD__ && logger.log(`[Tracing] Finishing span "${span.name}" (${span.spanContext().spanId})`); + + if (!shouldCaptureSentrySpan(span)) { + // Prevent this being called to super.onEnd(), which would pass this to the span exporter + return; + } + + // Capture exceptions as events + const hub = getSpanHub(span) || getCurrentHub(); + span.events.forEach(event => { + maybeCaptureExceptionForTimedEvent(hub, event, span); + }); + + return super.onEnd(span); + } +} + +function shouldCaptureSentrySpan(span: Span): boolean { + const client = getCurrentHub().getClient(); + const httpIntegration = client ? client.getIntegration(Http) : undefined; + const fetchIntegration = client ? client.getIntegration(NodeFetch) : undefined; + + // If we encounter a client or server span with url & method, we assume this comes from the http instrumentation + // In this case, if `shouldCreateSpansForRequests` is false, we want to _record_ the span but not _sample_ it, + // So we can generate a breadcrumb for it but no span will be sent + if ( + (span.kind === SpanKind.CLIENT || span.kind === SpanKind.SERVER) && + span.attributes[SemanticAttributes.HTTP_URL] && + span.attributes[SemanticAttributes.HTTP_METHOD] + ) { + const shouldCreateSpansForRequests = + span.attributes['http.client'] === 'fetch' + ? fetchIntegration?.shouldCreateSpansForRequests + : httpIntegration?.shouldCreateSpansForRequests; + + return shouldCreateSpansForRequests !== false; + } + + return true; +} diff --git a/packages/node-experimental/src/sdk/client.ts b/packages/node-experimental/src/sdk/client.ts index 29f68980f008..a3145475e307 100644 --- a/packages/node-experimental/src/sdk/client.ts +++ b/packages/node-experimental/src/sdk/client.ts @@ -1,5 +1,6 @@ import type { Tracer } from '@opentelemetry/api'; import { trace } from '@opentelemetry/api'; +import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; import type { EventHint, Scope } from '@sentry/node'; import { NodeClient, SDK_VERSION } from '@sentry/node'; import type { Event } from '@sentry/types'; @@ -8,12 +9,13 @@ import type { NodeExperimentalClient as NodeExperimentalClientInterface, NodeExperimentalClientOptions, } from '../types'; -import { OtelScope } from './scope'; +import { NodeExperimentalScope } from './scope'; /** * A client built on top of the NodeClient, which provides some otel-specific things on top. */ export class NodeExperimentalClient extends NodeClient implements NodeExperimentalClientInterface { + public traceProvider: BasicTracerProvider | undefined; private _tracer: Tracer | undefined; public constructor(options: ConstructorParameters[0]) { @@ -54,16 +56,30 @@ export class NodeExperimentalClient extends NodeClient implements NodeExperiment return super.getOptions(); } + /** + * @inheritDoc + */ + public async flush(timeout?: number): Promise { + const provider = this.traceProvider; + const spanProcessor = provider?.activeSpanProcessor; + + if (spanProcessor) { + await spanProcessor.forceFlush(); + } + + return super.flush(timeout); + } + /** * Extends the base `_prepareEvent` so that we can properly handle `captureContext`. - * This uses `Scope.clone()`, which we need to replace with `OtelScope.clone()` for this client. + * This uses `Scope.clone()`, which we need to replace with `NodeExperimentalScope.clone()` for this client. */ protected _prepareEvent(event: Event, hint: EventHint, scope?: Scope): PromiseLike { let actualScope = scope; // Remove `captureContext` hint and instead clone already here if (hint && hint.captureContext) { - actualScope = OtelScope.clone(scope); + actualScope = NodeExperimentalScope.clone(scope); delete hint.captureContext; } diff --git a/packages/node-experimental/src/sdk/hub.ts b/packages/node-experimental/src/sdk/hub.ts index 8220265e600c..50958d13c84d 100644 --- a/packages/node-experimental/src/sdk/hub.ts +++ b/packages/node-experimental/src/sdk/hub.ts @@ -3,12 +3,14 @@ import { Hub } from '@sentry/core'; import type { Client } from '@sentry/types'; import { getGlobalSingleton, GLOBAL_OBJ } from '@sentry/utils'; -import { OtelScope } from './scope'; +import { NodeExperimentalScope } from './scope'; -/** A custom hub that ensures we always creat an OTEL scope. */ - -class OtelHub extends Hub { - public constructor(client?: Client, scope: Scope = new OtelScope()) { +/** + * A custom hub that ensures we always creat an OTEL scope. + * Exported only for testing + */ +export class NodeExperimentalHub extends Hub { + public constructor(client?: Client, scope: Scope = new NodeExperimentalScope()) { super(client, scope); } @@ -17,7 +19,7 @@ class OtelHub extends Hub { */ public pushScope(): Scope { // We want to clone the content of prev scope - const scope = OtelScope.clone(this.getScope()); + const scope = NodeExperimentalScope.clone(this.getScope()); this.getStack().push({ client: this.getClient(), scope, @@ -29,11 +31,11 @@ class OtelHub extends Hub { /** * ******************************************************************************* * Everything below here is a copy of the stuff from core's hub.ts, - * only that we make sure to create our custom OtelScope instead of the default Scope. + * only that we make sure to create our custom NodeExperimentalScope instead of the default Scope. * This is necessary to get the correct breadcrumbs behavior. * - * Basically, this overwrites all places that do `new Scope()` with `new OtelScope()`. - * Which in turn means overwriting all places that do `new Hub()` and make sure to pass in a OtelScope instead. + * Basically, this overwrites all places that do `new Scope()` with `new NodeExperimentalScope()`. + * Which in turn means overwriting all places that do `new Hub()` and make sure to pass in a NodeExperimentalScope instead. * ******************************************************************************* */ @@ -77,7 +79,7 @@ export function getCurrentHub(): Hub { * @hidden */ export function getHubFromCarrier(carrier: Carrier): Hub { - return getGlobalSingleton('hub', () => new OtelHub(), carrier); + return getGlobalSingleton('hub', () => new NodeExperimentalHub(), carrier); } /** @@ -89,14 +91,17 @@ export function ensureHubOnCarrier(carrier: Carrier, parent: Hub = getGlobalHub( // If there's no hub on current domain, or it's an old API, assign a new one if (!hasHubOnCarrier(carrier) || getHubFromCarrier(carrier).isOlderThan(API_VERSION)) { const globalHubTopStack = parent.getStackTop(); - setHubOnCarrier(carrier, new OtelHub(globalHubTopStack.client, OtelScope.clone(globalHubTopStack.scope))); + setHubOnCarrier( + carrier, + new NodeExperimentalHub(globalHubTopStack.client, NodeExperimentalScope.clone(globalHubTopStack.scope)), + ); } } function getGlobalHub(registry: Carrier = getMainCarrier()): Hub { // If there's no hub, or its an old API, assign a new one if (!hasHubOnCarrier(registry) || getHubFromCarrier(registry).isOlderThan(API_VERSION)) { - setHubOnCarrier(registry, new OtelHub()); + setHubOnCarrier(registry, new NodeExperimentalHub()); } // Return hub that lives on a global object diff --git a/packages/node-experimental/src/sdk/hubextensions.ts b/packages/node-experimental/src/sdk/hubextensions.ts index 4971226fee01..07ee08c1f7f9 100644 --- a/packages/node-experimental/src/sdk/hubextensions.ts +++ b/packages/node-experimental/src/sdk/hubextensions.ts @@ -1,11 +1,5 @@ -import type { startTransaction } from '@sentry/core'; import { addTracingExtensions as _addTracingExtensions, getMainCarrier } from '@sentry/core'; -import type { Breadcrumb, Hub, Transaction } from '@sentry/types'; -import { dateTimestampInSeconds } from '@sentry/utils'; - -import type { TransactionWithBreadcrumbs } from '../types'; - -const DEFAULT_MAX_BREADCRUMBS = 100; +import type { CustomSamplingContext, TransactionContext } from '@sentry/types'; /** * Add tracing extensions, ensuring a patched `startTransaction` to work with OTEL. @@ -19,62 +13,18 @@ export function addTracingExtensions(): void { } carrier.__SENTRY__.extensions = carrier.__SENTRY__.extensions || {}; - if (carrier.__SENTRY__.extensions.startTransaction) { - carrier.__SENTRY__.extensions.startTransaction = getPatchedStartTransaction( - carrier.__SENTRY__.extensions.startTransaction as typeof startTransaction, - ); - } -} - -/** - * We patch the `startTransaction` function to ensure we create a `TransactionWithBreadcrumbs` instead of a regular `Transaction`. - */ -function getPatchedStartTransaction(_startTransaction: typeof startTransaction): typeof startTransaction { - return function (this: Hub, ...args) { - const transaction = _startTransaction.apply(this, args); - - return patchTransaction(transaction); - }; -} - -function patchTransaction(transaction: Transaction): TransactionWithBreadcrumbs { - return new Proxy(transaction as TransactionWithBreadcrumbs, { - get(target, prop, receiver) { - if (prop === 'addBreadcrumb') { - return addBreadcrumb; - } - if (prop === 'getBreadcrumbs') { - return getBreadcrumbs; - } - if (prop === '_breadcrumbs') { - const breadcrumbs = Reflect.get(target, prop, receiver); - return breadcrumbs || []; - } - return Reflect.get(target, prop, receiver); - }, - }); -} - -/** Add a breadcrumb to a transaction. */ -function addBreadcrumb(this: TransactionWithBreadcrumbs, breadcrumb: Breadcrumb, maxBreadcrumbs?: number): void { - const maxCrumbs = typeof maxBreadcrumbs === 'number' ? maxBreadcrumbs : DEFAULT_MAX_BREADCRUMBS; - - // No data has been changed, so don't notify scope listeners - if (maxCrumbs <= 0) { - return; + if (carrier.__SENTRY__.extensions.startTransaction !== startTransactionNoop) { + carrier.__SENTRY__.extensions.startTransaction = startTransactionNoop; } - - const mergedBreadcrumb = { - timestamp: dateTimestampInSeconds(), - ...breadcrumb, - }; - - const breadcrumbs = this._breadcrumbs; - breadcrumbs.push(mergedBreadcrumb); - this._breadcrumbs = breadcrumbs.length > maxCrumbs ? breadcrumbs.slice(-maxCrumbs) : breadcrumbs; } -/** Get all breadcrumbs from a transaction. */ -function getBreadcrumbs(this: TransactionWithBreadcrumbs): Breadcrumb[] { - return this._breadcrumbs; +function startTransactionNoop( + _transactionContext: TransactionContext, + _customSamplingContext?: CustomSamplingContext, +): unknown { + // eslint-disable-next-line no-console + console.warn('startTransaction is a noop in @sentry/node-experimental. Use `startSpan` instead.'); + // We return an object here as hub.ts checks for the result of this + // and renders a different warning if this is empty + return {}; } diff --git a/packages/node-experimental/src/sdk/init.ts b/packages/node-experimental/src/sdk/init.ts index 070728367925..c33a90f037d7 100644 --- a/packages/node-experimental/src/sdk/init.ts +++ b/packages/node-experimental/src/sdk/init.ts @@ -1,24 +1,38 @@ import { hasTracingEnabled } from '@sentry/core'; import { defaultIntegrations as defaultNodeIntegrations, init as initNode } from '@sentry/node'; +import type { Integration } from '@sentry/types'; +import { parseSemver } from '@sentry/utils'; import { getAutoPerformanceIntegrations } from '../integrations/getAutoPerformanceIntegrations'; import { Http } from '../integrations/http'; +import { NodeFetch } from '../integrations/node-fetch'; import type { NodeExperimentalOptions } from '../types'; import { NodeExperimentalClient } from './client'; +import { getCurrentHub } from './hub'; import { initOtel } from './initOtel'; import { setOtelContextAsyncContextStrategy } from './otelAsyncContextStrategy'; +const NODE_VERSION: ReturnType = parseSemver(process.versions.node); const ignoredDefaultIntegrations = ['Http', 'Undici']; -export const defaultIntegrations = [ +export const defaultIntegrations: Integration[] = [ ...defaultNodeIntegrations.filter(i => !ignoredDefaultIntegrations.includes(i.name)), new Http(), ]; +// Only add NodeFetch if Node >= 16, as previous versions do not support it +if (NODE_VERSION.major && NODE_VERSION.major >= 16) { + defaultIntegrations.push(new NodeFetch()); +} + /** * Initialize Sentry for Node. */ export function init(options: NodeExperimentalOptions | undefined = {}): void { + // Ensure we register our own global hub before something else does + // This will register the NodeExperimentalHub as the global hub + getCurrentHub(); + const isTracingEnabled = hasTracingEnabled(options); options.defaultIntegrations = diff --git a/packages/node-experimental/src/sdk/initOtel.ts b/packages/node-experimental/src/sdk/initOtel.ts index 3ed0e2ab2b2b..b60dac87aeda 100644 --- a/packages/node-experimental/src/sdk/initOtel.ts +++ b/packages/node-experimental/src/sdk/initOtel.ts @@ -1,22 +1,33 @@ import { diag, DiagLogLevel } from '@opentelemetry/api'; import { Resource } from '@opentelemetry/resources'; -import { AlwaysOnSampler, BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; +import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; -import { getCurrentHub, SDK_VERSION } from '@sentry/core'; -import { SentryPropagator, SentrySpanProcessor } from '@sentry/opentelemetry-node'; +import { SDK_VERSION } from '@sentry/core'; import { logger } from '@sentry/utils'; +import { SentryPropagator } from '../opentelemetry/propagator'; +import { SentrySampler } from '../opentelemetry/sampler'; +import { SentrySpanProcessor } from '../opentelemetry/spanProcessor'; import type { NodeExperimentalClient } from '../types'; +import { setupEventContextTrace } from '../utils/setupEventContextTrace'; import { SentryContextManager } from './../opentelemetry/contextManager'; +import { getCurrentHub } from './hub'; /** * Initialize OpenTelemetry for Node. - * We use the @sentry/opentelemetry-node package to communicate with OpenTelemetry. */ -export function initOtel(): () => void { +export function initOtel(): void { const client = getCurrentHub().getClient(); - if (client?.getOptions().debug) { + if (!client) { + __DEBUG_BUILD__ && + logger.warn( + 'No client available, skipping OpenTelemetry setup. This probably means that `Sentry.init()` was not called before `initOtel()`.', + ); + return; + } + + if (client.getOptions().debug) { const otelLogger = new Proxy(logger as typeof logger & { verbose: (typeof logger)['debug'] }, { get(target, prop, receiver) { const actualProp = prop === 'verbose' ? 'debug' : prop; @@ -27,14 +38,23 @@ export function initOtel(): () => void { diag.setLogger(otelLogger, DiagLogLevel.DEBUG); } + setupEventContextTrace(client); + + const provider = setupOtel(client); + client.traceProvider = provider; +} + +/** Just exported for tests. */ +export function setupOtel(client: NodeExperimentalClient): BasicTracerProvider { // Create and configure NodeTracerProvider const provider = new BasicTracerProvider({ - sampler: new AlwaysOnSampler(), + sampler: new SentrySampler(client), resource: new Resource({ [SemanticResourceAttributes.SERVICE_NAME]: 'node-experimental', [SemanticResourceAttributes.SERVICE_NAMESPACE]: 'sentry', [SemanticResourceAttributes.SERVICE_VERSION]: SDK_VERSION, }), + forceFlushTimeoutMillis: 500, }); provider.addSpanProcessor(new SentrySpanProcessor()); @@ -47,9 +67,5 @@ export function initOtel(): () => void { contextManager, }); - // Cleanup function - return () => { - void provider.forceFlush(); - void provider.shutdown(); - }; + return provider; } diff --git a/packages/node-experimental/src/sdk/scope.ts b/packages/node-experimental/src/sdk/scope.ts index 12fcc6862904..39f931936ccf 100644 --- a/packages/node-experimental/src/sdk/scope.ts +++ b/packages/node-experimental/src/sdk/scope.ts @@ -1,16 +1,34 @@ +import type { Span } from '@opentelemetry/api'; +import type { TimedEvent } from '@opentelemetry/sdk-trace-base'; import { Scope } from '@sentry/core'; -import type { Breadcrumb } from '@sentry/types'; +import type { Breadcrumb, SeverityLevel, Span as SentrySpan } from '@sentry/types'; +import { dateTimestampInSeconds, dropUndefinedKeys, logger, normalize } from '@sentry/utils'; -import type { TransactionWithBreadcrumbs } from '../types'; -import { getActiveSpan } from './trace'; +import { + OTEL_ATTR_BREADCRUMB_CATEGORY, + OTEL_ATTR_BREADCRUMB_DATA, + OTEL_ATTR_BREADCRUMB_EVENT_ID, + OTEL_ATTR_BREADCRUMB_LEVEL, + OTEL_ATTR_BREADCRUMB_TYPE, +} from '../constants'; +import { getSpanParent } from '../opentelemetry/spanData'; +import { convertOtelTimeToSeconds } from '../utils/convertOtelTimeToSeconds'; +import { getActiveSpan, getRootSpan } from '../utils/getActiveSpan'; +import { spanHasEvents } from '../utils/spanTypes'; /** A fork of the classic scope with some otel specific stuff. */ -export class OtelScope extends Scope { +export class NodeExperimentalScope extends Scope { + /** + * This can be set to ensure the scope uses _this_ span as the active one, + * instead of using getActiveSpan(). + */ + public activeSpan: Span | undefined; + /** * @inheritDoc */ public static clone(scope?: Scope): Scope { - const newScope = new OtelScope(); + const newScope = new NodeExperimentalScope(); if (scope) { newScope._breadcrumbs = [...scope['_breadcrumbs']]; newScope._tags = { ...scope['_tags'] }; @@ -31,14 +49,42 @@ export class OtelScope extends Scope { return newScope; } + /** + * In node-experimental, scope.getSpan() always returns undefined. + * Instead, use the global `getActiveSpan()`. + */ + public getSpan(): undefined { + __DEBUG_BUILD__ && + logger.warn('Calling getSpan() is a noop in @sentry/node-experimental. Use `getActiveSpan()` instead.'); + + return undefined; + } + + /** + * In node-experimental, scope.setSpan() is a noop. + * Instead, use the global `startSpan()` to define the active span. + */ + public setSpan(_span: SentrySpan): this { + __DEBUG_BUILD__ && + logger.warn('Calling setSpan() is a noop in @sentry/node-experimental. Use `startSpan()` instead.'); + + return this; + } + /** * @inheritDoc */ public addBreadcrumb(breadcrumb: Breadcrumb, maxBreadcrumbs?: number): this { - const transaction = getActiveTransaction(); + const activeSpan = this.activeSpan || getActiveSpan(); + const rootSpan = activeSpan ? getRootSpan(activeSpan) : undefined; - if (transaction && transaction.addBreadcrumb) { - transaction.addBreadcrumb(breadcrumb, maxBreadcrumbs); + if (rootSpan) { + const mergedBreadcrumb = { + timestamp: dateTimestampInSeconds(), + ...breadcrumb, + }; + + rootSpan.addEvent(...breadcrumbToOtelEvent(mergedBreadcrumb)); return this; } @@ -49,18 +95,94 @@ export class OtelScope extends Scope { * @inheritDoc */ protected _getBreadcrumbs(): Breadcrumb[] { - const transaction = getActiveTransaction(); - const transactionBreadcrumbs = transaction && transaction.getBreadcrumbs ? transaction.getBreadcrumbs() : []; + const span = this.activeSpan || getActiveSpan(); + + const spanBreadcrumbs = span ? getBreadcrumbsForSpan(span) : []; - return this._breadcrumbs.concat(transactionBreadcrumbs); + return spanBreadcrumbs.length > 0 ? this._breadcrumbs.concat(spanBreadcrumbs) : this._breadcrumbs; } } /** - * This gets the currently active transaction, - * and ensures to wrap it so that we can store breadcrumbs on it. + * Get all breadcrumbs for the given span as well as it's parents. */ -function getActiveTransaction(): TransactionWithBreadcrumbs | undefined { - const activeSpan = getActiveSpan(); - return activeSpan && (activeSpan.transaction as TransactionWithBreadcrumbs | undefined); +function getBreadcrumbsForSpan(span: Span): Breadcrumb[] { + const events = span ? getOtelEvents(span) : []; + + return events.map(otelEventToBreadcrumb); +} + +function breadcrumbToOtelEvent(breadcrumb: Breadcrumb): Parameters { + const name = breadcrumb.message || ''; + + const dataAttrs = serializeBreadcrumbData(breadcrumb.data); + + return [ + name, + dropUndefinedKeys({ + [OTEL_ATTR_BREADCRUMB_TYPE]: breadcrumb.type, + [OTEL_ATTR_BREADCRUMB_LEVEL]: breadcrumb.level, + [OTEL_ATTR_BREADCRUMB_EVENT_ID]: breadcrumb.event_id, + [OTEL_ATTR_BREADCRUMB_CATEGORY]: breadcrumb.category, + ...dataAttrs, + }), + breadcrumb.timestamp ? new Date(breadcrumb.timestamp * 1000) : undefined, + ]; +} + +function serializeBreadcrumbData(data: Breadcrumb['data']): undefined | Record { + if (!data || Object.keys(data).length === 0) { + return undefined; + } + + try { + const normalizedData = normalize(data); + return { + [OTEL_ATTR_BREADCRUMB_DATA]: JSON.stringify(normalizedData), + }; + } catch (e) { + return undefined; + } +} + +function otelEventToBreadcrumb(event: TimedEvent): Breadcrumb { + const attributes = event.attributes || {}; + + const type = attributes[OTEL_ATTR_BREADCRUMB_TYPE] as string | undefined; + const level = attributes[OTEL_ATTR_BREADCRUMB_LEVEL] as SeverityLevel | undefined; + const eventId = attributes[OTEL_ATTR_BREADCRUMB_EVENT_ID] as string | undefined; + const category = attributes[OTEL_ATTR_BREADCRUMB_CATEGORY] as string | undefined; + const dataStr = attributes[OTEL_ATTR_BREADCRUMB_DATA] as string | undefined; + + const breadcrumb: Breadcrumb = dropUndefinedKeys({ + timestamp: convertOtelTimeToSeconds(event.time), + message: event.name, + type, + level, + event_id: eventId, + category, + }); + + if (typeof dataStr === 'string') { + try { + const data = JSON.parse(dataStr); + breadcrumb.data = data; + } catch (e) {} // eslint-disable-line no-empty + } + + return breadcrumb; +} + +function getOtelEvents(span: Span, events: TimedEvent[] = []): TimedEvent[] { + if (spanHasEvents(span)) { + events.push(...span.events); + } + + // Go up parent chain and collect events + const parent = getSpanParent(span); + if (parent) { + return getOtelEvents(parent, events); + } + + return events; } diff --git a/packages/node-experimental/src/sdk/trace.ts b/packages/node-experimental/src/sdk/trace.ts index 1faf780ec5c7..72047f4478a3 100644 --- a/packages/node-experimental/src/sdk/trace.ts +++ b/packages/node-experimental/src/sdk/trace.ts @@ -1,11 +1,14 @@ -import type { Span as OtelSpan, Tracer } from '@opentelemetry/api'; -import { trace } from '@opentelemetry/api'; -import { getCurrentHub, hasTracingEnabled, Transaction } from '@sentry/core'; -import { _INTERNAL_getSentrySpan } from '@sentry/opentelemetry-node'; -import type { Span, TransactionContext } from '@sentry/types'; +import type { Tracer } from '@opentelemetry/api'; +import { SpanStatusCode } from '@opentelemetry/api'; +import type { Span } from '@opentelemetry/sdk-trace-base'; +import { hasTracingEnabled } from '@sentry/core'; import { isThenable } from '@sentry/utils'; -import type { NodeExperimentalClient } from '../types'; +import { OTEL_ATTR_OP, OTEL_ATTR_ORIGIN, OTEL_ATTR_SOURCE } from '../constants'; +import { setSpanMetadata } from '../opentelemetry/spanData'; +import type { NodeExperimentalClient, NodeExperimentalSpanContext } from '../types'; +import { spanIsSdkTraceBaseSpan } from '../utils/spanTypes'; +import { getCurrentHub } from './hub'; /** * Wraps a function with a transaction/span and finishes the span after the function is done. @@ -18,32 +21,33 @@ import type { NodeExperimentalClient } from '../types'; * or you didn't set `tracesSampleRate`, this function will not generate spans * and the `span` returned from the callback will be undefined. */ -export function startSpan(context: TransactionContext, callback: (span: Span | undefined) => T): T { +export function startSpan(spanContext: NodeExperimentalSpanContext, callback: (span: Span | undefined) => T): T { const tracer = getTracer(); if (!tracer) { return callback(undefined); } - const name = context.name || context.description || context.op || ''; + const { name } = spanContext; - return tracer.startActiveSpan(name, (span: OtelSpan): T => { - const otelSpanId = span.spanContext().spanId; - - const sentrySpan = _INTERNAL_getSentrySpan(otelSpanId); - - if (sentrySpan && isTransaction(sentrySpan) && context.metadata) { - sentrySpan.setMetadata(context.metadata); + return tracer.startActiveSpan(name, (span): T => { + function finishSpan(): void { + span.end(); } - function finishSpan(): void { + // This is just a sanity check - in reality, this should not happen as we control the tracer, + // but to ensure type saftey we rather bail out here than to pass an invalid type out + if (!spanIsSdkTraceBaseSpan(span)) { span.end(); + return callback(undefined); } + _applySentryAttributesToSpan(span, spanContext); + let maybePromiseResult: T; try { - maybePromiseResult = callback(sentrySpan); + maybePromiseResult = callback(span); } catch (e) { - sentrySpan && sentrySpan.setStatus('internal_error'); + span.setStatus({ code: SpanStatusCode.ERROR }); finishSpan(); throw e; } @@ -54,7 +58,7 @@ export function startSpan(context: TransactionContext, callback: (span: Span finishSpan(); }, () => { - sentrySpan && sentrySpan.setStatus('internal_error'); + span.setStatus({ code: SpanStatusCode.ERROR }); finishSpan(); }, ); @@ -81,50 +85,26 @@ export const startActiveSpan = startSpan; * or you didn't set `tracesSampleRate` or `tracesSampler`, this function will not generate spans * and the `span` returned from the callback will be undefined. */ -export function startInactiveSpan(context: TransactionContext): Span | undefined { +export function startInactiveSpan(spanContext: NodeExperimentalSpanContext): Span | undefined { const tracer = getTracer(); if (!tracer) { return undefined; } - const name = context.name || context.description || context.op || ''; - const otelSpan = tracer.startSpan(name); + const { name } = spanContext; - const otelSpanId = otelSpan.spanContext().spanId; + const span = tracer.startSpan(name); - const sentrySpan = _INTERNAL_getSentrySpan(otelSpanId); - - if (!sentrySpan) { + // This is just a sanity check - in reality, this should not happen as we control the tracer, + // but to ensure type saftey we rather bail out here than to pass an invalid type out + if (!spanIsSdkTraceBaseSpan(span)) { + span.end(); return undefined; } - if (isTransaction(sentrySpan) && context.metadata) { - sentrySpan.setMetadata(context.metadata); - } - - // Monkey-patch `finish()` to finish the OTEL span instead - // This will also in turn finish the Sentry Span, so no need to call this ourselves - const wrappedSentrySpan = new Proxy(sentrySpan, { - get(target, prop, receiver) { - if (prop === 'finish') { - return () => { - otelSpan.end(); - }; - } - return Reflect.get(target, prop, receiver); - }, - }); - - return wrappedSentrySpan; -} + _applySentryAttributesToSpan(span, spanContext); -/** - * Returns the currently active span. - */ -export function getActiveSpan(): Span | undefined { - const otelSpan = trace.getActiveSpan(); - const spanId = otelSpan && otelSpan.spanContext().spanId; - return spanId ? _INTERNAL_getSentrySpan(spanId) : undefined; + return span; } function getTracer(): Tracer | undefined { @@ -136,6 +116,22 @@ function getTracer(): Tracer | undefined { return client && client.tracer; } -function isTransaction(span: Span): span is Transaction { - return span instanceof Transaction; +function _applySentryAttributesToSpan(span: Span, spanContext: NodeExperimentalSpanContext): void { + const { origin, op, source, metadata } = spanContext; + + if (origin) { + span.setAttribute(OTEL_ATTR_ORIGIN, origin); + } + + if (op) { + span.setAttribute(OTEL_ATTR_OP, op); + } + + if (source) { + span.setAttribute(OTEL_ATTR_SOURCE, source); + } + + if (metadata) { + setSpanMetadata(span, metadata); + } } diff --git a/packages/node-experimental/src/sdk/transaction.ts b/packages/node-experimental/src/sdk/transaction.ts new file mode 100644 index 000000000000..382d9a926f5d --- /dev/null +++ b/packages/node-experimental/src/sdk/transaction.ts @@ -0,0 +1,48 @@ +import type { Hub } from '@sentry/core'; +import { Transaction } from '@sentry/core'; +import type { ClientOptions, Hub as HubInterface, Scope, TransactionContext } from '@sentry/types'; +import { uuid4 } from '@sentry/utils'; + +/** + * This is a fork of core's tracing/hubextensions.ts _startTransaction, + * with some OTEL specifics. + */ +export function startTransaction(hub: HubInterface, transactionContext: TransactionContext): Transaction { + const client = hub.getClient(); + const options: Partial = (client && client.getOptions()) || {}; + + const transaction = new NodeExperimentalTransaction(transactionContext, hub as Hub); + // Since we do not do sampling here, we assume that this is _always_ sampled + // Any sampling decision happens in OpenTelemetry's sampler + transaction.initSpanRecorder(options._experiments && (options._experiments.maxSpans as number)); + + if (client && client.emit) { + client.emit('startTransaction', transaction); + } + return transaction; +} + +/** + * This is a fork of the base Transaction with OTEL specific stuff added. + */ +export class NodeExperimentalTransaction extends Transaction { + /** + * Finish the transaction, but apply the given scope instead of the current one. + */ + public finishWithScope(endTimestamp?: number, scope?: Scope): string | undefined { + const event = this._finishTransaction(endTimestamp); + + if (!event) { + return undefined; + } + + const client = this._hub.getClient(); + + if (!client) { + return undefined; + } + + const eventId = uuid4(); + return client.captureEvent(event, { event_id: eventId }, scope); + } +} diff --git a/packages/node-experimental/src/types.ts b/packages/node-experimental/src/types.ts index 0fd9a6922a78..8878a5fd2a8c 100644 --- a/packages/node-experimental/src/types.ts +++ b/packages/node-experimental/src/types.ts @@ -1,29 +1,34 @@ -import type { Tracer } from '@opentelemetry/api'; -import type { Span as OtelSpan } from '@opentelemetry/sdk-trace-base'; +import type { Span as WriteableSpan, Tracer } from '@opentelemetry/api'; +import type { BasicTracerProvider, ReadableSpan, Span } from '@opentelemetry/sdk-trace-base'; import type { NodeClient, NodeOptions } from '@sentry/node'; -import type { Breadcrumb, Transaction } from '@sentry/types'; +import type { SpanOrigin, TransactionMetadata, TransactionSource } from '@sentry/types'; export type NodeExperimentalOptions = NodeOptions; export type NodeExperimentalClientOptions = ConstructorParameters[0]; export interface NodeExperimentalClient extends NodeClient { tracer: Tracer; + traceProvider: BasicTracerProvider | undefined; getOptions(): NodeExperimentalClientOptions; } +export interface NodeExperimentalSpanContext { + name: string; + op?: string; + metadata?: Partial; + origin?: SpanOrigin; + source?: TransactionSource; +} + /** - * This is a fork of the base Transaction with OTEL specific stuff added. - * Note that we do not solve this via an actual subclass, but by wrapping this in a proxy when we need it - - * as we can't easily control all the places a transaction may be created. + * The base `Span` type is basically a `WriteableSpan`. + * There are places where we basically want to allow passing _any_ span, + * so in these cases we type this as `AbstractSpan` which could be either a regular `Span` or a `ReadableSpan`. + * You'll have to make sur to check revelant fields before accessing them. + * + * Note that technically, the `Span` exported from `@opentelemwetry/sdk-trace-base` matches this, + * but we cannot be 100% sure that we are actually getting such a span, so this type is more defensive. */ -export interface TransactionWithBreadcrumbs extends Transaction { - _breadcrumbs: Breadcrumb[]; - - /** Get all breadcrumbs added to this transaction. */ - getBreadcrumbs(): Breadcrumb[]; - - /** Add a breadcrumb to this transaction. */ - addBreadcrumb(breadcrumb: Breadcrumb, maxBreadcrumbs?: number): void; -} +export type AbstractSpan = WriteableSpan | ReadableSpan; -export type { OtelSpan }; +export type { Span }; diff --git a/packages/node-experimental/src/utils/addOriginToSpan.ts b/packages/node-experimental/src/utils/addOriginToSpan.ts index 4320d31d7fce..007f55bb1e05 100644 --- a/packages/node-experimental/src/utils/addOriginToSpan.ts +++ b/packages/node-experimental/src/utils/addOriginToSpan.ts @@ -1,14 +1,9 @@ -// We are using the broader OtelSpan type from api here, as this is also what integrations etc. use -import type { Span as OtelSpan } from '@opentelemetry/api'; -import { _INTERNAL_getSentrySpan } from '@sentry/opentelemetry-node'; +import type { Span } from '@opentelemetry/api'; import type { SpanOrigin } from '@sentry/types'; -/** Adds an origin to an OTEL Span. */ -export function addOriginToOtelSpan(otelSpan: OtelSpan, origin: SpanOrigin): void { - const sentrySpan = _INTERNAL_getSentrySpan(otelSpan.spanContext().spanId); - if (!sentrySpan) { - return; - } +import { OTEL_ATTR_ORIGIN } from '../constants'; - sentrySpan.origin = origin; +/** Adds an origin to an OTEL Span. */ +export function addOriginToSpan(span: Span, origin: SpanOrigin): void { + span.setAttribute(OTEL_ATTR_ORIGIN, origin); } diff --git a/packages/node-experimental/src/utils/convertOtelTimeToSeconds.ts b/packages/node-experimental/src/utils/convertOtelTimeToSeconds.ts new file mode 100644 index 000000000000..64087aeffc4d --- /dev/null +++ b/packages/node-experimental/src/utils/convertOtelTimeToSeconds.ts @@ -0,0 +1,4 @@ +/** Convert an OTEL time to seconds */ +export function convertOtelTimeToSeconds([seconds, nano]: [number, number]): number { + return seconds + nano / 1_000_000_000; +} diff --git a/packages/node-experimental/src/utils/getActiveSpan.ts b/packages/node-experimental/src/utils/getActiveSpan.ts new file mode 100644 index 000000000000..240842770a68 --- /dev/null +++ b/packages/node-experimental/src/utils/getActiveSpan.ts @@ -0,0 +1,25 @@ +import type { Span } from '@opentelemetry/api'; +import { trace } from '@opentelemetry/api'; + +import { getSpanParent } from '../opentelemetry/spanData'; + +/** + * Returns the currently active span. + */ +export function getActiveSpan(): Span | undefined { + return trace.getActiveSpan(); +} + +/** + * Get the root span for the given span. + * The given span may be the root span itself. + */ +export function getRootSpan(span: Span): Span { + let parent: Span = span; + + while (getSpanParent(parent)) { + parent = getSpanParent(parent) as Span; + } + + return parent; +} diff --git a/packages/node-experimental/src/utils/getRequestSpanData.ts b/packages/node-experimental/src/utils/getRequestSpanData.ts index ca89f5a2b976..0154f8e4cd3e 100644 --- a/packages/node-experimental/src/utils/getRequestSpanData.ts +++ b/packages/node-experimental/src/utils/getRequestSpanData.ts @@ -1,18 +1,30 @@ +import type { Span } from '@opentelemetry/api'; +import type { ReadableSpan } from '@opentelemetry/sdk-trace-base'; import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; import type { SanitizedRequestData } from '@sentry/types'; import { getSanitizedUrlString, parseUrl } from '@sentry/utils'; -import type { OtelSpan } from '../types'; +import { spanHasAttributes } from './spanTypes'; /** * Get sanitizied request data from an OTEL span. */ -export function getRequestSpanData(span: OtelSpan): SanitizedRequestData { - const data: SanitizedRequestData = { - url: span.attributes[SemanticAttributes.HTTP_URL] as string, - 'http.method': (span.attributes[SemanticAttributes.HTTP_METHOD] as string) || 'GET', +export function getRequestSpanData(span: Span | ReadableSpan): Partial { + // The base `Span` type has no `attributes`, so we need to guard here against that + if (!spanHasAttributes(span)) { + return {}; + } + + const data: Partial = { + url: span.attributes[SemanticAttributes.HTTP_URL] as string | undefined, + 'http.method': span.attributes[SemanticAttributes.HTTP_METHOD] as string | undefined, }; + // Default to GET if URL is set but method is not + if (!data['http.method'] && data.url) { + data['http.method'] = 'GET'; + } + try { const urlStr = span.attributes[SemanticAttributes.HTTP_URL]; if (typeof urlStr === 'string') { diff --git a/packages/node-experimental/src/utils/getSpanKind.ts b/packages/node-experimental/src/utils/getSpanKind.ts new file mode 100644 index 000000000000..7769a1cd3290 --- /dev/null +++ b/packages/node-experimental/src/utils/getSpanKind.ts @@ -0,0 +1,18 @@ +import type { Span } from '@opentelemetry/api'; +import { SpanKind } from '@opentelemetry/api'; + +import { spanHasKind } from './spanTypes'; + +/** + * Get the span kind from a span. + * For whatever reason, this is not public API on the generic "Span" type, + * so we need to check if we actually have a `SDKTraceBaseSpan` where we can fetch this from. + * Otherwise, we fall back to `SpanKind.INTERNAL`. + */ +export function getSpanKind(span: Span): SpanKind { + if (spanHasKind(span)) { + return span.kind; + } + + return SpanKind.INTERNAL; +} diff --git a/packages/node-experimental/src/utils/groupSpansWithParents.ts b/packages/node-experimental/src/utils/groupSpansWithParents.ts new file mode 100644 index 000000000000..2af278d0bce2 --- /dev/null +++ b/packages/node-experimental/src/utils/groupSpansWithParents.ts @@ -0,0 +1,80 @@ +import type { ReadableSpan } from '@opentelemetry/sdk-trace-base'; + +import { getSpanParent } from '../opentelemetry/spanData'; + +export interface SpanNode { + id: string; + span?: ReadableSpan; + parentNode?: SpanNode | undefined; + children: SpanNode[]; +} + +type SpanMap = Map; + +/** + * This function runs through a list of OTEL Spans, and wraps them in an `SpanNode` + * where each node holds a reference to their parent node. + */ +export function groupSpansWithParents(spans: ReadableSpan[]): SpanNode[] { + const nodeMap: SpanMap = new Map(); + + for (const span of spans) { + createOrUpdateSpanNodeAndRefs(nodeMap, span); + } + + return Array.from(nodeMap, function ([_id, spanNode]) { + return spanNode; + }); +} + +function createOrUpdateSpanNodeAndRefs(nodeMap: SpanMap, span: ReadableSpan): void { + const parentSpan = getSpanParent(span); + const parentIsRemote = parentSpan ? !!parentSpan.spanContext().isRemote : false; + + const id = span.spanContext().spanId; + + // If the parentId is the trace parent ID, we pretend it's undefined + // As this means the parent exists somewhere else + const parentId = !parentIsRemote ? span.parentSpanId : undefined; + + if (!parentId) { + createOrUpdateNode(nodeMap, { id, span, children: [] }); + return; + } + + // Else make sure to create parent node as well + // Note that the parent may not know it's parent _yet_, this may be updated in a later pass + const parentNode = createOrGetParentNode(nodeMap, parentId); + const node = createOrUpdateNode(nodeMap, { id, span, parentNode, children: [] }); + parentNode.children.push(node); +} + +function createOrGetParentNode(nodeMap: SpanMap, id: string): SpanNode { + const existing = nodeMap.get(id); + + if (existing) { + return existing; + } + + return createOrUpdateNode(nodeMap, { id, children: [] }); +} + +function createOrUpdateNode(nodeMap: SpanMap, spanNode: SpanNode): SpanNode { + const existing = nodeMap.get(spanNode.id); + + // If span is already set, nothing to do here + if (existing && existing.span) { + return existing; + } + + // If it exists but span is not set yet, we update it + if (existing && !existing.span) { + existing.span = spanNode.span; + existing.parentNode = spanNode.parentNode; + return existing; + } + + // Else, we create a new one... + nodeMap.set(spanNode.id, spanNode); + return spanNode; +} diff --git a/packages/node-experimental/src/utils/setupEventContextTrace.ts b/packages/node-experimental/src/utils/setupEventContextTrace.ts new file mode 100644 index 000000000000..0e8dc7c23d7b --- /dev/null +++ b/packages/node-experimental/src/utils/setupEventContextTrace.ts @@ -0,0 +1,32 @@ +import type { Client } from '@sentry/types'; + +import { getActiveSpan } from './getActiveSpan'; +import { spanHasParentId } from './spanTypes'; + +/** Ensure the `trace` context is set on all events. */ +export function setupEventContextTrace(client: Client): void { + if (!client.addEventProcessor) { + return; + } + + client.addEventProcessor(event => { + const span = getActiveSpan(); + if (!span) { + return event; + } + + const spanContext = span.spanContext(); + + // If event has already set `trace` context, use that one. + event.contexts = { + trace: { + trace_id: spanContext.traceId, + span_id: spanContext.spanId, + parent_span_id: spanHasParentId(span) ? span.parentSpanId : undefined, + }, + ...event.contexts, + }; + + return event; + }); +} diff --git a/packages/node-experimental/src/utils/spanTypes.ts b/packages/node-experimental/src/utils/spanTypes.ts new file mode 100644 index 000000000000..3883a97f8004 --- /dev/null +++ b/packages/node-experimental/src/utils/spanTypes.ts @@ -0,0 +1,58 @@ +import type { SpanKind } from '@opentelemetry/api'; +import type { ReadableSpan, TimedEvent } from '@opentelemetry/sdk-trace-base'; +import { Span as SdkTraceBaseSpan } from '@opentelemetry/sdk-trace-base'; + +import type { AbstractSpan } from '../types'; + +/** + * Check if a given span has attributes. + * This is necessary because the base `Span` type does not have attributes, + * so in places where we are passed a generic span, we need to check if we want to access them. + */ +export function spanHasAttributes( + span: SpanType, +): span is SpanType & { attributes: ReadableSpan['attributes'] } { + const castSpan = span as ReadableSpan; + return !!castSpan.attributes && typeof castSpan.attributes === 'object'; +} + +/** + * Check if a given span has a kind. + * This is necessary because the base `Span` type does not have a kind, + * so in places where we are passed a generic span, we need to check if we want to access it. + */ +export function spanHasKind(span: SpanType): span is SpanType & { kind: SpanKind } { + const castSpan = span as ReadableSpan; + return !!castSpan.kind; +} + +/** + * Check if a given span has a kind. + * This is necessary because the base `Span` type does not have a kind, + * so in places where we are passed a generic span, we need to check if we want to access it. + */ +export function spanHasParentId( + span: SpanType, +): span is SpanType & { parentSpanId: string } { + const castSpan = span as ReadableSpan; + return !!castSpan.parentSpanId; +} + +/** + * Check if a given span has events. + * This is necessary because the base `Span` type does not have events, + * so in places where we are passed a generic span, we need to check if we want to access it. + */ +export function spanHasEvents( + span: SpanType, +): span is SpanType & { events: TimedEvent[] } { + const castSpan = span as ReadableSpan; + return Array.isArray(castSpan.events); +} + +/** + * If the span is a SDK trace base span, which has some additional fields. + */ +export function spanIsSdkTraceBaseSpan(span: AbstractSpan): span is SdkTraceBaseSpan { + return span instanceof SdkTraceBaseSpan; +} diff --git a/packages/node-experimental/test/helpers/createSpan.ts b/packages/node-experimental/test/helpers/createSpan.ts new file mode 100644 index 000000000000..38c4ed96f3a8 --- /dev/null +++ b/packages/node-experimental/test/helpers/createSpan.ts @@ -0,0 +1,30 @@ +import type { Context, SpanContext } from '@opentelemetry/api'; +import { SpanKind } from '@opentelemetry/api'; +import type { Tracer } from '@opentelemetry/sdk-trace-base'; +import { Span } from '@opentelemetry/sdk-trace-base'; +import { uuid4 } from '@sentry/utils'; + +export function createSpan( + name?: string, + { spanId, parentSpanId }: { spanId?: string; parentSpanId?: string } = {}, +): Span { + const spanProcessor = { + onStart: () => {}, + onEnd: () => {}, + }; + const tracer = { + resource: 'test-resource', + instrumentationLibrary: 'test-instrumentation-library', + getSpanLimits: () => ({}), + getActiveSpanProcessor: () => spanProcessor, + } as unknown as Tracer; + + const spanContext: SpanContext = { + spanId: spanId || uuid4(), + traceId: uuid4(), + traceFlags: 0, + }; + + // eslint-disable-next-line deprecation/deprecation + return new Span(tracer, {} as Context, name || 'test', spanContext, SpanKind.INTERNAL, parentSpanId); +} diff --git a/packages/node-experimental/test/helpers/getDefaultNodePreviewClientOptions.ts b/packages/node-experimental/test/helpers/getDefaultNodePreviewClientOptions.ts index c5f2b8b5cf82..00778e78582a 100644 --- a/packages/node-experimental/test/helpers/getDefaultNodePreviewClientOptions.ts +++ b/packages/node-experimental/test/helpers/getDefaultNodePreviewClientOptions.ts @@ -7,6 +7,7 @@ export function getDefaultNodeExperimentalClientOptions( options: Partial = {}, ): NodeExperimentalClientOptions { return { + tracesSampleRate: 1, integrations: [], transport: () => createTransport({ recordDroppedEvent: () => undefined }, _ => resolvedSyncPromise({})), stackParser: () => [], diff --git a/packages/node-experimental/test/helpers/mockSdkInit.ts b/packages/node-experimental/test/helpers/mockSdkInit.ts index f7bfb68f6bf6..3443f0608806 100644 --- a/packages/node-experimental/test/helpers/mockSdkInit.ts +++ b/packages/node-experimental/test/helpers/mockSdkInit.ts @@ -1,13 +1,49 @@ +import { context, propagation, ProxyTracerProvider, trace } from '@opentelemetry/api'; +import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; +import { GLOBAL_OBJ } from '@sentry/utils'; + import { init } from '../../src/sdk/init'; import type { NodeExperimentalClientOptions } from '../../src/types'; -// eslint-disable-next-line no-var -declare var global: any; - const PUBLIC_DSN = 'https://username@domain/123'; export function mockSdkInit(options?: Partial) { - global.__SENTRY__ = {}; + GLOBAL_OBJ.__SENTRY__ = { + extensions: {}, + hub: undefined, + globalEventProcessors: [], + logger: undefined, + }; init({ dsn: PUBLIC_DSN, defaultIntegrations: false, ...options }); } + +export function cleanupOtel(_provider?: BasicTracerProvider): void { + const provider = getProvider(_provider); + + if (!provider) { + return; + } + + void provider.forceFlush(); + void provider.shutdown(); + + // Disable all globally registered APIs + trace.disable(); + context.disable(); + propagation.disable(); +} + +export function getProvider(_provider?: BasicTracerProvider): BasicTracerProvider | undefined { + let provider = _provider || trace.getTracerProvider(); + + if (provider instanceof ProxyTracerProvider) { + provider = provider.getDelegate(); + } + + if (!(provider instanceof BasicTracerProvider)) { + return undefined; + } + + return provider; +} diff --git a/packages/node-experimental/test/integration/breadcrumbs.test.ts b/packages/node-experimental/test/integration/breadcrumbs.test.ts new file mode 100644 index 000000000000..fbd46a6bd466 --- /dev/null +++ b/packages/node-experimental/test/integration/breadcrumbs.test.ts @@ -0,0 +1,362 @@ +import { withScope } from '../../src/'; +import { NodeExperimentalClient } from '../../src/sdk/client'; +import { getCurrentHub, NodeExperimentalHub } from '../../src/sdk/hub'; +import { startSpan } from '../../src/sdk/trace'; +import { cleanupOtel, mockSdkInit } from '../helpers/mockSdkInit'; + +describe('Integration | breadcrumbs', () => { + const beforeSendTransaction = jest.fn(() => null); + + afterEach(() => { + cleanupOtel(); + }); + + describe('without tracing', () => { + it('correctly adds & retrieves breadcrumbs', async () => { + const beforeSend = jest.fn(() => null); + const beforeBreadcrumb = jest.fn(breadcrumb => breadcrumb); + + mockSdkInit({ beforeSend, beforeBreadcrumb }); + + const hub = getCurrentHub(); + const client = hub.getClient() as NodeExperimentalClient; + + expect(hub).toBeInstanceOf(NodeExperimentalHub); + expect(client).toBeInstanceOf(NodeExperimentalClient); + + hub.addBreadcrumb({ timestamp: 123456, message: 'test1' }); + hub.addBreadcrumb({ timestamp: 123457, message: 'test2', data: { nested: 'yes' } }); + hub.addBreadcrumb({ timestamp: 123455, message: 'test3' }); + + const error = new Error('test'); + hub.captureException(error); + + await client.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(1); + expect(beforeBreadcrumb).toHaveBeenCalledTimes(3); + + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + breadcrumbs: [ + { message: 'test1', timestamp: 123456 }, + { data: { nested: 'yes' }, message: 'test2', timestamp: 123457 }, + { message: 'test3', timestamp: 123455 }, + ], + }), + { + event_id: expect.any(String), + originalException: error, + syntheticException: expect.any(Error), + }, + ); + }); + + it('handles parallel scopes', async () => { + const beforeSend = jest.fn(() => null); + const beforeBreadcrumb = jest.fn(breadcrumb => breadcrumb); + + mockSdkInit({ beforeSend, beforeBreadcrumb }); + + const hub = getCurrentHub(); + const client = hub.getClient() as NodeExperimentalClient; + + expect(hub).toBeInstanceOf(NodeExperimentalHub); + expect(client).toBeInstanceOf(NodeExperimentalClient); + + const error = new Error('test'); + + hub.addBreadcrumb({ timestamp: 123456, message: 'test0' }); + + withScope(() => { + hub.addBreadcrumb({ timestamp: 123456, message: 'test1' }); + }); + + withScope(() => { + hub.addBreadcrumb({ timestamp: 123456, message: 'test2' }); + hub.captureException(error); + }); + + withScope(() => { + hub.addBreadcrumb({ timestamp: 123456, message: 'test3' }); + }); + + await client.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(1); + expect(beforeBreadcrumb).toHaveBeenCalledTimes(4); + + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + breadcrumbs: [ + { message: 'test0', timestamp: 123456 }, + { message: 'test2', timestamp: 123456 }, + ], + }), + { + event_id: expect.any(String), + originalException: error, + syntheticException: expect.any(Error), + }, + ); + }); + }); + + it('correctly adds & retrieves breadcrumbs', async () => { + const beforeSend = jest.fn(() => null); + const beforeBreadcrumb = jest.fn(breadcrumb => breadcrumb); + + mockSdkInit({ beforeSend, beforeBreadcrumb, beforeSendTransaction, enableTracing: true }); + + const hub = getCurrentHub(); + const client = hub.getClient() as NodeExperimentalClient; + + const error = new Error('test'); + + startSpan({ name: 'test' }, () => { + hub.addBreadcrumb({ timestamp: 123456, message: 'test1' }); + + startSpan({ name: 'inner1' }, () => { + hub.addBreadcrumb({ timestamp: 123457, message: 'test2', data: { nested: 'yes' } }); + }); + + startSpan({ name: 'inner2' }, () => { + hub.addBreadcrumb({ timestamp: 123455, message: 'test3' }); + }); + + hub.captureException(error); + }); + + await client.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(1); + expect(beforeBreadcrumb).toHaveBeenCalledTimes(3); + + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + breadcrumbs: [ + { message: 'test1', timestamp: 123456 }, + { data: { nested: 'yes' }, message: 'test2', timestamp: 123457 }, + { message: 'test3', timestamp: 123455 }, + ], + }), + { + event_id: expect.any(String), + originalException: error, + syntheticException: expect.any(Error), + }, + ); + }); + + it('correctly adds & retrieves breadcrumbs for the current root span only', async () => { + const beforeSend = jest.fn(() => null); + const beforeBreadcrumb = jest.fn(breadcrumb => breadcrumb); + + mockSdkInit({ beforeSend, beforeBreadcrumb, beforeSendTransaction, enableTracing: true }); + + const hub = getCurrentHub(); + const client = hub.getClient() as NodeExperimentalClient; + + const error = new Error('test'); + + startSpan({ name: 'test1' }, () => { + hub.addBreadcrumb({ timestamp: 123456, message: 'test1-a' }); + + startSpan({ name: 'inner1' }, () => { + hub.addBreadcrumb({ timestamp: 123457, message: 'test1-b' }); + }); + }); + + startSpan({ name: 'test2' }, () => { + hub.addBreadcrumb({ timestamp: 123456, message: 'test2-a' }); + + startSpan({ name: 'inner2' }, () => { + hub.addBreadcrumb({ timestamp: 123457, message: 'test2-b' }); + }); + + hub.captureException(error); + }); + + await client.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(1); + expect(beforeBreadcrumb).toHaveBeenCalledTimes(4); + + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + breadcrumbs: [ + { message: 'test2-a', timestamp: 123456 }, + { message: 'test2-b', timestamp: 123457 }, + ], + }), + { + event_id: expect.any(String), + originalException: error, + syntheticException: expect.any(Error), + }, + ); + }); + + it('ignores scopes inside of root span', async () => { + const beforeSend = jest.fn(() => null); + const beforeBreadcrumb = jest.fn(breadcrumb => breadcrumb); + + mockSdkInit({ beforeSend, beforeBreadcrumb, beforeSendTransaction, enableTracing: true }); + + const hub = getCurrentHub(); + const client = hub.getClient() as NodeExperimentalClient; + + const error = new Error('test'); + + startSpan({ name: 'test1' }, () => { + withScope(() => { + hub.addBreadcrumb({ timestamp: 123456, message: 'test1' }); + }); + startSpan({ name: 'inner1' }, () => { + hub.addBreadcrumb({ timestamp: 123457, message: 'test2' }); + }); + + hub.captureException(error); + }); + + await client.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(1); + expect(beforeBreadcrumb).toHaveBeenCalledTimes(2); + + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + breadcrumbs: [ + { message: 'test1', timestamp: 123456 }, + { message: 'test2', timestamp: 123457 }, + ], + }), + { + event_id: expect.any(String), + originalException: error, + syntheticException: expect.any(Error), + }, + ); + }); + + it('handles deep nesting of scopes', async () => { + const beforeSend = jest.fn(() => null); + const beforeBreadcrumb = jest.fn(breadcrumb => breadcrumb); + + mockSdkInit({ beforeSend, beforeBreadcrumb, beforeSendTransaction, enableTracing: true }); + + const hub = getCurrentHub(); + const client = hub.getClient() as NodeExperimentalClient; + + const error = new Error('test'); + + startSpan({ name: 'test1' }, () => { + withScope(() => { + hub.addBreadcrumb({ timestamp: 123456, message: 'test1' }); + }); + startSpan({ name: 'inner1' }, () => { + hub.addBreadcrumb({ timestamp: 123457, message: 'test2' }); + + startSpan({ name: 'inner2' }, () => { + hub.addBreadcrumb({ timestamp: 123457, message: 'test3' }); + + startSpan({ name: 'inner3' }, () => { + hub.addBreadcrumb({ timestamp: 123457, message: 'test4' }); + + hub.captureException(error); + + startSpan({ name: 'inner4' }, () => { + hub.addBreadcrumb({ timestamp: 123457, message: 'test5' }); + }); + + hub.addBreadcrumb({ timestamp: 123457, message: 'test6' }); + }); + }); + }); + + hub.addBreadcrumb({ timestamp: 123456, message: 'test99' }); + }); + + await client.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(1); + + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + breadcrumbs: [ + { message: 'test1', timestamp: 123456 }, + { message: 'test2', timestamp: 123457 }, + { message: 'test3', timestamp: 123457 }, + { message: 'test4', timestamp: 123457 }, + ], + }), + { + event_id: expect.any(String), + originalException: error, + syntheticException: expect.any(Error), + }, + ); + }); + + it('correctly adds & retrieves breadcrumbs in async spans', async () => { + const beforeSend = jest.fn(() => null); + const beforeBreadcrumb = jest.fn(breadcrumb => breadcrumb); + + mockSdkInit({ beforeSend, beforeBreadcrumb, beforeSendTransaction, enableTracing: true }); + + const hub = getCurrentHub(); + const client = hub.getClient() as NodeExperimentalClient; + + const error = new Error('test'); + + const promise1 = startSpan({ name: 'test' }, async () => { + hub.addBreadcrumb({ timestamp: 123456, message: 'test1' }); + + await startSpan({ name: 'inner1' }, async () => { + hub.addBreadcrumb({ timestamp: 123457, message: 'test2' }); + }); + + await startSpan({ name: 'inner2' }, async () => { + hub.addBreadcrumb({ timestamp: 123455, message: 'test3' }); + }); + + await new Promise(resolve => setTimeout(resolve, 10)); + + hub.captureException(error); + }); + + const promise2 = startSpan({ name: 'test-b' }, async () => { + hub.addBreadcrumb({ timestamp: 123456, message: 'test1-b' }); + + await startSpan({ name: 'inner1' }, async () => { + hub.addBreadcrumb({ timestamp: 123457, message: 'test2-b' }); + }); + + await startSpan({ name: 'inner2' }, async () => { + hub.addBreadcrumb({ timestamp: 123455, message: 'test3-b' }); + }); + }); + + await Promise.all([promise1, promise2]); + + await client.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(1); + expect(beforeBreadcrumb).toHaveBeenCalledTimes(6); + + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + breadcrumbs: [ + { message: 'test1', timestamp: 123456 }, + { message: 'test2', timestamp: 123457 }, + { message: 'test3', timestamp: 123455 }, + ], + }), + { + event_id: expect.any(String), + originalException: error, + syntheticException: expect.any(Error), + }, + ); + }); +}); diff --git a/packages/node-experimental/test/integration/otelTimedEvents.test.ts b/packages/node-experimental/test/integration/otelTimedEvents.test.ts new file mode 100644 index 000000000000..8bdaec750a15 --- /dev/null +++ b/packages/node-experimental/test/integration/otelTimedEvents.test.ts @@ -0,0 +1,57 @@ +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; + +import type { NodeExperimentalClient } from '../../src/sdk/client'; +import { getCurrentHub } from '../../src/sdk/hub'; +import { startSpan } from '../../src/sdk/trace'; +import { cleanupOtel, mockSdkInit } from '../helpers/mockSdkInit'; + +describe('Integration | OTEL TimedEvents', () => { + afterEach(() => { + cleanupOtel(); + }); + + it('captures TimedEvents with name `exception` as exceptions', async () => { + const beforeSend = jest.fn(() => null); + const beforeSendTransaction = jest.fn(() => null); + + mockSdkInit({ beforeSend, beforeSendTransaction, enableTracing: true }); + + const hub = getCurrentHub(); + const client = hub.getClient() as NodeExperimentalClient; + + startSpan({ name: 'test' }, span => { + span?.addEvent('exception', { + [SemanticAttributes.EXCEPTION_MESSAGE]: 'test-message', + 'test-span-event-attr': 'test-span-event-attr-value', + }); + + span?.addEvent('other', { + [SemanticAttributes.EXCEPTION_MESSAGE]: 'test-message-2', + 'test-span-event-attr': 'test-span-event-attr-value', + }); + }); + + await client.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(1); + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + exception: { + values: [ + { + mechanism: { handled: true, type: 'generic' }, + stacktrace: expect.any(Object), + type: 'Error', + value: 'test-message', + }, + ], + }, + }), + { + event_id: expect.any(String), + originalException: expect.any(Error), + syntheticException: expect.any(Error), + }, + ); + }); +}); diff --git a/packages/node-experimental/test/integration/scope.test.ts b/packages/node-experimental/test/integration/scope.test.ts new file mode 100644 index 000000000000..925047583f2e --- /dev/null +++ b/packages/node-experimental/test/integration/scope.test.ts @@ -0,0 +1,235 @@ +import * as Sentry from '../../src/'; +import { NodeExperimentalClient } from '../../src/sdk/client'; +import { getCurrentHub, NodeExperimentalHub } from '../../src/sdk/hub'; +import { NodeExperimentalScope } from '../../src/sdk/scope'; +import { cleanupOtel, mockSdkInit } from '../helpers/mockSdkInit'; + +describe('Integration | Scope', () => { + afterEach(() => { + cleanupOtel(); + }); + + describe.each([ + ['with tracing', true], + ['without tracing', false], + ])('%s', (_name, enableTracing) => { + it('correctly syncs OTEL context & Sentry hub/scope', async () => { + const beforeSend = jest.fn(() => null); + const beforeSendTransaction = jest.fn(() => null); + + mockSdkInit({ enableTracing, beforeSend, beforeSendTransaction }); + + const hub = getCurrentHub(); + const client = hub.getClient() as NodeExperimentalClient; + + const rootScope = hub.getScope(); + + expect(hub).toBeInstanceOf(NodeExperimentalHub); + expect(rootScope).toBeInstanceOf(NodeExperimentalScope); + expect(client).toBeInstanceOf(NodeExperimentalClient); + + const error = new Error('test error'); + let spanId: string | undefined; + let traceId: string | undefined; + + rootScope.setTag('tag1', 'val1'); + + Sentry.withScope(scope1 => { + scope1.setTag('tag2', 'val2'); + + Sentry.withScope(scope2b => { + scope2b.setTag('tag3-b', 'val3-b'); + }); + + Sentry.withScope(scope2 => { + scope2.setTag('tag3', 'val3'); + + Sentry.startSpan({ name: 'outer' }, span => { + spanId = span?.spanContext().spanId; + traceId = span?.spanContext().traceId; + + Sentry.setTag('tag4', 'val4'); + + Sentry.captureException(error); + }); + }); + }); + + await client.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(1); + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + contexts: expect.objectContaining({ + trace: spanId + ? { + span_id: spanId, + trace_id: traceId, + parent_span_id: undefined, + } + : expect.any(Object), + }), + tags: { + tag1: 'val1', + tag2: 'val2', + tag3: 'val3', + tag4: 'val4', + }, + }), + { + event_id: expect.any(String), + originalException: error, + syntheticException: expect.any(Error), + }, + ); + + if (enableTracing) { + expect(beforeSendTransaction).toHaveBeenCalledTimes(1); + // Note: Scope for transaction is taken at `start` time, not `finish` time + expect(beforeSendTransaction).toHaveBeenCalledWith( + expect.objectContaining({ + contexts: expect.objectContaining({ + trace: { + data: { 'otel.kind': 'INTERNAL' }, + span_id: spanId, + status: 'ok', + trace_id: traceId, + }, + }), + + spans: [], + start_timestamp: expect.any(Number), + tags: { + tag1: 'val1', + tag2: 'val2', + tag3: 'val3', + }, + timestamp: expect.any(Number), + transaction: 'outer', + transaction_info: { source: 'custom' }, + type: 'transaction', + }), + { + event_id: expect.any(String), + }, + ); + } + }); + + it('isolates parallel root scopes', async () => { + const beforeSend = jest.fn(() => null); + const beforeSendTransaction = jest.fn(() => null); + + mockSdkInit({ enableTracing, beforeSend, beforeSendTransaction }); + + const hub = getCurrentHub(); + const client = hub.getClient() as NodeExperimentalClient; + + const rootScope = hub.getScope(); + + expect(hub).toBeInstanceOf(NodeExperimentalHub); + expect(rootScope).toBeInstanceOf(NodeExperimentalScope); + expect(client).toBeInstanceOf(NodeExperimentalClient); + + const error1 = new Error('test error 1'); + const error2 = new Error('test error 2'); + let spanId1: string | undefined; + let spanId2: string | undefined; + let traceId1: string | undefined; + let traceId2: string | undefined; + + rootScope.setTag('tag1', 'val1'); + + Sentry.withScope(scope1 => { + scope1.setTag('tag2', 'val2a'); + + Sentry.withScope(scope2 => { + scope2.setTag('tag3', 'val3a'); + + Sentry.startSpan({ name: 'outer' }, span => { + spanId1 = span?.spanContext().spanId; + traceId1 = span?.spanContext().traceId; + + Sentry.setTag('tag4', 'val4a'); + + Sentry.captureException(error1); + }); + }); + }); + + Sentry.withScope(scope1 => { + scope1.setTag('tag2', 'val2b'); + + Sentry.withScope(scope2 => { + scope2.setTag('tag3', 'val3b'); + + Sentry.startSpan({ name: 'outer' }, span => { + spanId2 = span?.spanContext().spanId; + traceId2 = span?.spanContext().traceId; + + Sentry.setTag('tag4', 'val4b'); + + Sentry.captureException(error2); + }); + }); + }); + + await client.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(2); + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + contexts: expect.objectContaining({ + trace: spanId1 + ? { + span_id: spanId1, + trace_id: traceId1, + parent_span_id: undefined, + } + : expect.any(Object), + }), + tags: { + tag1: 'val1', + tag2: 'val2a', + tag3: 'val3a', + tag4: 'val4a', + }, + }), + { + event_id: expect.any(String), + originalException: error1, + syntheticException: expect.any(Error), + }, + ); + + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + contexts: expect.objectContaining({ + trace: spanId2 + ? { + span_id: spanId2, + trace_id: traceId2, + parent_span_id: undefined, + } + : expect.any(Object), + }), + tags: { + tag1: 'val1', + tag2: 'val2b', + tag3: 'val3b', + tag4: 'val4b', + }, + }), + { + event_id: expect.any(String), + originalException: error2, + syntheticException: expect.any(Error), + }, + ); + + if (enableTracing) { + expect(beforeSendTransaction).toHaveBeenCalledTimes(2); + } + }); + }); +}); diff --git a/packages/node-experimental/test/integration/transactions.test.ts b/packages/node-experimental/test/integration/transactions.test.ts new file mode 100644 index 000000000000..4d657fc4cbf5 --- /dev/null +++ b/packages/node-experimental/test/integration/transactions.test.ts @@ -0,0 +1,675 @@ +import { context, SpanKind, trace, TraceFlags } from '@opentelemetry/api'; +import type { SpanProcessor } from '@opentelemetry/sdk-trace-base'; +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; +import type { Integration, PropagationContext, TransactionEvent } from '@sentry/types'; +import { logger } from '@sentry/utils'; + +import * as Sentry from '../../src'; +import { startSpan } from '../../src'; +import { SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY } from '../../src/constants'; +import type { Http, NodeFetch } from '../../src/integrations'; +import { SentrySpanProcessor } from '../../src/opentelemetry/spanProcessor'; +import type { NodeExperimentalClient } from '../../src/sdk/client'; +import { getCurrentHub } from '../../src/sdk/hub'; +import { cleanupOtel, getProvider, mockSdkInit } from '../helpers/mockSdkInit'; + +describe('Integration | Transactions', () => { + afterEach(() => { + jest.restoreAllMocks(); + cleanupOtel(); + }); + + it('correctly creates transaction & spans', async () => { + const beforeSendTransaction = jest.fn(() => null); + + mockSdkInit({ enableTracing: true, beforeSendTransaction }); + + const hub = getCurrentHub(); + const client = hub.getClient() as NodeExperimentalClient; + + Sentry.addBreadcrumb({ message: 'test breadcrumb 1', timestamp: 123456 }); + Sentry.setTag('outer.tag', 'test value'); + + Sentry.startSpan( + { + op: 'test op', + name: 'test name', + source: 'task', + origin: 'auto.test', + metadata: { requestPath: 'test-path' }, + }, + span => { + if (!span) { + return; + } + + Sentry.addBreadcrumb({ message: 'test breadcrumb 2', timestamp: 123456 }); + + span.setAttributes({ + 'test.outer': 'test value', + }); + + const subSpan = Sentry.startInactiveSpan({ name: 'inner span 1' }); + subSpan?.end(); + + Sentry.setTag('test.tag', 'test value'); + + Sentry.startSpan({ name: 'inner span 2' }, innerSpan => { + if (!innerSpan) { + return; + } + + Sentry.addBreadcrumb({ message: 'test breadcrumb 3', timestamp: 123456 }); + + innerSpan.setAttributes({ + 'test.inner': 'test value', + }); + }); + }, + ); + + await client.flush(); + + expect(beforeSendTransaction).toHaveBeenCalledTimes(1); + expect(beforeSendTransaction).toHaveBeenLastCalledWith( + expect.objectContaining({ + breadcrumbs: [ + { message: 'test breadcrumb 1', timestamp: 123456 }, + { message: 'test breadcrumb 2', timestamp: 123456 }, + { message: 'test breadcrumb 3', timestamp: 123456 }, + ], + contexts: { + otel: { + attributes: { + 'test.outer': 'test value', + }, + resource: { + 'service.name': 'node-experimental', + 'service.namespace': 'sentry', + 'service.version': expect.any(String), + 'telemetry.sdk.language': 'nodejs', + 'telemetry.sdk.name': 'opentelemetry', + 'telemetry.sdk.version': expect.any(String), + }, + }, + runtime: { name: 'node', version: expect.any(String) }, + trace: { + data: { 'otel.kind': 'INTERNAL' }, + op: 'test op', + span_id: expect.any(String), + status: 'ok', + trace_id: expect.any(String), + }, + }, + environment: 'production', + event_id: expect.any(String), + platform: 'node', + sdkProcessingMetadata: { + dynamicSamplingContext: expect.objectContaining({ + environment: 'production', + public_key: expect.any(String), + sample_rate: '1', + sampled: 'true', + trace_id: expect.any(String), + transaction: 'test name', + }), + propagationContext: { + sampled: undefined, + spanId: expect.any(String), + traceId: expect.any(String), + }, + sampleRate: 1, + source: 'task', + spanMetadata: expect.any(Object), + requestPath: 'test-path', + }, + server_name: expect.any(String), + // spans are circular (they have a reference to the transaction), which leads to jest choking on this + // instead we compare them in detail below + spans: [ + expect.objectContaining({ + description: 'inner span 1', + }), + expect.objectContaining({ + description: 'inner span 2', + }), + ], + start_timestamp: expect.any(Number), + tags: { + 'outer.tag': 'test value', + }, + timestamp: expect.any(Number), + transaction: 'test name', + transaction_info: { source: 'task' }, + type: 'transaction', + }), + { + event_id: expect.any(String), + }, + ); + + // Checking the spans here, as they are circular to the transaction... + const runArgs = beforeSendTransaction.mock.calls[0] as unknown as [TransactionEvent, unknown]; + const spans = runArgs[0].spans || []; + + // note: Currently, spans do not have any context/span added to them + // This is the same behavior as for the "regular" SDKs + expect(spans.map(span => span.toJSON())).toEqual([ + { + data: { 'otel.kind': 'INTERNAL' }, + description: 'inner span 1', + origin: 'manual', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.any(String), + }, + { + data: { 'otel.kind': 'INTERNAL', 'test.inner': 'test value' }, + description: 'inner span 2', + origin: 'manual', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.any(String), + }, + ]); + }); + + it('correctly creates concurrent transaction & spans', async () => { + const beforeSendTransaction = jest.fn(() => null); + + mockSdkInit({ enableTracing: true, beforeSendTransaction }); + + const hub = getCurrentHub(); + const client = hub.getClient() as NodeExperimentalClient; + + Sentry.addBreadcrumb({ message: 'test breadcrumb 1', timestamp: 123456 }); + + Sentry.startSpan({ op: 'test op', name: 'test name', source: 'task', origin: 'auto.test' }, span => { + if (!span) { + return; + } + + Sentry.addBreadcrumb({ message: 'test breadcrumb 2', timestamp: 123456 }); + + span.setAttributes({ + 'test.outer': 'test value', + }); + + const subSpan = Sentry.startInactiveSpan({ name: 'inner span 1' }); + subSpan?.end(); + + Sentry.setTag('test.tag', 'test value'); + + Sentry.startSpan({ name: 'inner span 2' }, innerSpan => { + if (!innerSpan) { + return; + } + + Sentry.addBreadcrumb({ message: 'test breadcrumb 3', timestamp: 123456 }); + + innerSpan.setAttributes({ + 'test.inner': 'test value', + }); + }); + }); + + Sentry.startSpan({ op: 'test op b', name: 'test name b' }, span => { + if (!span) { + return; + } + + Sentry.addBreadcrumb({ message: 'test breadcrumb 2b', timestamp: 123456 }); + + span.setAttributes({ + 'test.outer': 'test value b', + }); + + const subSpan = Sentry.startInactiveSpan({ name: 'inner span 1b' }); + subSpan?.end(); + + Sentry.setTag('test.tag', 'test value b'); + + Sentry.startSpan({ name: 'inner span 2b' }, innerSpan => { + if (!innerSpan) { + return; + } + + Sentry.addBreadcrumb({ message: 'test breadcrumb 3b', timestamp: 123456 }); + + innerSpan.setAttributes({ + 'test.inner': 'test value b', + }); + }); + }); + + await client.flush(); + + expect(beforeSendTransaction).toHaveBeenCalledTimes(2); + expect(beforeSendTransaction).toHaveBeenCalledWith( + expect.objectContaining({ + breadcrumbs: [ + { message: 'test breadcrumb 1', timestamp: 123456 }, + { message: 'test breadcrumb 2', timestamp: 123456 }, + { message: 'test breadcrumb 3', timestamp: 123456 }, + ], + contexts: expect.objectContaining({ + otel: expect.objectContaining({ + attributes: { + 'test.outer': 'test value', + }, + }), + trace: { + data: { 'otel.kind': 'INTERNAL' }, + op: 'test op', + span_id: expect.any(String), + status: 'ok', + trace_id: expect.any(String), + }, + }), + spans: [ + expect.objectContaining({ + description: 'inner span 1', + }), + expect.objectContaining({ + description: 'inner span 2', + }), + ], + start_timestamp: expect.any(Number), + tags: {}, + timestamp: expect.any(Number), + transaction: 'test name', + transaction_info: { source: 'task' }, + type: 'transaction', + }), + { + event_id: expect.any(String), + }, + ); + + expect(beforeSendTransaction).toHaveBeenCalledWith( + expect.objectContaining({ + breadcrumbs: [ + { message: 'test breadcrumb 1', timestamp: 123456 }, + { message: 'test breadcrumb 2b', timestamp: 123456 }, + { message: 'test breadcrumb 3b', timestamp: 123456 }, + ], + contexts: expect.objectContaining({ + otel: expect.objectContaining({ + attributes: { + 'test.outer': 'test value b', + }, + }), + trace: { + data: { 'otel.kind': 'INTERNAL' }, + op: 'test op b', + span_id: expect.any(String), + status: 'ok', + trace_id: expect.any(String), + }, + }), + spans: [ + expect.objectContaining({ + description: 'inner span 1b', + }), + expect.objectContaining({ + description: 'inner span 2b', + }), + ], + start_timestamp: expect.any(Number), + tags: {}, + timestamp: expect.any(Number), + transaction: 'test name b', + transaction_info: { source: 'custom' }, + type: 'transaction', + }), + { + event_id: expect.any(String), + }, + ); + }); + + it('correctly creates transaction & spans with a trace header data', async () => { + const beforeSendTransaction = jest.fn(() => null); + + const traceId = 'd4cda95b652f4a1592b449d5929fda1b'; + const parentSpanId = '6e0c63257de34c92'; + + const spanContext = { + traceId, + spanId: parentSpanId, + sampled: true, + isRemote: true, + traceFlags: TraceFlags.SAMPLED, + }; + + const propagationContext: PropagationContext = { + traceId, + parentSpanId, + spanId: '6e0c63257de34c93', + sampled: true, + }; + + mockSdkInit({ enableTracing: true, beforeSendTransaction }); + + const hub = getCurrentHub(); + const client = hub.getClient() as NodeExperimentalClient; + + // We simulate the correct context we'd normally get from the SentryPropagator + context.with( + trace.setSpanContext( + context.active().setValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY, propagationContext), + spanContext, + ), + () => { + Sentry.startSpan({ op: 'test op', name: 'test name', source: 'task', origin: 'auto.test' }, span => { + if (!span) { + return; + } + + const subSpan = Sentry.startInactiveSpan({ name: 'inner span 1' }); + subSpan?.end(); + + Sentry.startSpan({ name: 'inner span 2' }, innerSpan => { + if (!innerSpan) { + return; + } + }); + }); + }, + ); + + await client.flush(); + + expect(beforeSendTransaction).toHaveBeenCalledTimes(1); + expect(beforeSendTransaction).toHaveBeenLastCalledWith( + expect.objectContaining({ + contexts: expect.objectContaining({ + otel: expect.objectContaining({ + attributes: {}, + }), + trace: { + data: { 'otel.kind': 'INTERNAL' }, + op: 'test op', + span_id: expect.any(String), + parent_span_id: parentSpanId, + status: 'ok', + trace_id: traceId, + }, + }), + // spans are circular (they have a reference to the transaction), which leads to jest choking on this + // instead we compare them in detail below + spans: [ + expect.objectContaining({ + description: 'inner span 1', + }), + expect.objectContaining({ + description: 'inner span 2', + }), + ], + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + transaction: 'test name', + transaction_info: { source: 'task' }, + type: 'transaction', + }), + { + event_id: expect.any(String), + }, + ); + + // Checking the spans here, as they are circular to the transaction... + const runArgs = beforeSendTransaction.mock.calls[0] as unknown as [TransactionEvent, unknown]; + const spans = runArgs[0].spans || []; + + // note: Currently, spans do not have any context/span added to them + // This is the same behavior as for the "regular" SDKs + expect(spans.map(span => span.toJSON())).toEqual([ + { + data: { 'otel.kind': 'INTERNAL' }, + description: 'inner span 1', + origin: 'manual', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: traceId, + }, + { + data: { 'otel.kind': 'INTERNAL' }, + description: 'inner span 2', + origin: 'manual', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: traceId, + }, + ]); + }); + + it('cleans up spans that are not flushed for over 5 mins', async () => { + const beforeSendTransaction = jest.fn(() => null); + + const now = Date.now(); + jest.useFakeTimers(); + jest.setSystemTime(now); + + const logs: unknown[] = []; + jest.spyOn(logger, 'log').mockImplementation(msg => logs.push(msg)); + + mockSdkInit({ enableTracing: true, beforeSendTransaction }); + + const hub = getCurrentHub(); + const client = hub.getClient() as NodeExperimentalClient; + const provider = getProvider(); + const multiSpanProcessor = provider?.activeSpanProcessor as + | (SpanProcessor & { _spanProcessors?: SpanProcessor[] }) + | undefined; + const spanProcessor = multiSpanProcessor?.['_spanProcessors']?.find( + spanProcessor => spanProcessor instanceof SentrySpanProcessor, + ) as SentrySpanProcessor | undefined; + + const exporter = spanProcessor ? spanProcessor['_exporter'] : undefined; + + if (!exporter) { + throw new Error('No exporter found, aborting test...'); + } + + let innerSpan1Id: string | undefined; + let innerSpan2Id: string | undefined; + + void Sentry.startSpan({ name: 'test name' }, async span => { + if (!span) { + return; + } + + const subSpan = Sentry.startInactiveSpan({ name: 'inner span 1' }); + innerSpan1Id = subSpan?.spanContext().spanId; + subSpan?.end(); + + Sentry.startSpan({ name: 'inner span 2' }, innerSpan => { + if (!innerSpan) { + return; + } + + innerSpan2Id = innerSpan.spanContext().spanId; + }); + + // Pretend this is pending for 10 minutes + await new Promise(resolve => setTimeout(resolve, 10 * 60 * 1000)); + }); + + // Nothing added to exporter yet + expect(exporter['_finishedSpans'].length).toBe(0); + + void client.flush(5_000); + jest.advanceTimersByTime(5_000); + + // Now the child-spans have been added to the exporter, but they are pending since they are waiting for their parant + expect(exporter['_finishedSpans'].length).toBe(2); + expect(beforeSendTransaction).toHaveBeenCalledTimes(0); + + // Now wait for 5 mins + jest.advanceTimersByTime(5 * 60 * 1_000); + + // Adding another span will trigger the cleanup + Sentry.startSpan({ name: 'other span' }, () => {}); + + void client.flush(5_000); + jest.advanceTimersByTime(5_000); + + // Old spans have been cleared away + expect(exporter['_finishedSpans'].length).toBe(0); + + // Called once for the 'other span' + expect(beforeSendTransaction).toHaveBeenCalledTimes(1); + + expect(logs).toEqual( + expect.arrayContaining([ + 'SpanExporter exported 0 spans, 2 unsent spans remaining', + 'SpanExporter exported 1 spans, 2 unsent spans remaining', + `SpanExporter dropping span inner span 1 (${innerSpan1Id}) because it is pending for more than 5 minutes.`, + `SpanExporter dropping span inner span 2 (${innerSpan2Id}) because it is pending for more than 5 minutes.`, + ]), + ); + }); + + it('does not create spans for http requests if disabled in http integration', async () => { + const beforeSendTransaction = jest.fn(() => null); + + mockSdkInit({ enableTracing: true, beforeSendTransaction }); + + jest.useFakeTimers(); + + const hub = getCurrentHub(); + const client = hub.getClient() as NodeExperimentalClient; + + jest.spyOn(client, 'getIntegration').mockImplementation(integrationClass => { + if (integrationClass.name === 'Http') { + return { + shouldCreateSpansForRequests: false, + } as Http; + } + + return {} as Integration; + }); + + client.tracer.startActiveSpan( + 'test op', + { + kind: SpanKind.CLIENT, + attributes: { + [SemanticAttributes.HTTP_METHOD]: 'GET', + [SemanticAttributes.HTTP_URL]: 'https://example.com', + }, + }, + span => { + startSpan({ name: 'inner 1' }, () => { + startSpan({ name: 'inner 2' }, () => {}); + }); + + span.end(); + }, + ); + + void client.flush(); + jest.advanceTimersByTime(5_000); + + expect(beforeSendTransaction).toHaveBeenCalledTimes(0); + + // Now try a non-HTTP span + client.tracer.startActiveSpan( + 'test op 2', + { + kind: SpanKind.CLIENT, + attributes: {}, + }, + span => { + startSpan({ name: 'inner 1' }, () => { + startSpan({ name: 'inner 2' }, () => {}); + }); + + span.end(); + }, + ); + + void client.flush(); + jest.advanceTimersByTime(5_000); + + expect(beforeSendTransaction).toHaveBeenCalledTimes(1); + }); + + it('does not create spans for fetch requests if disabled in fetch integration', async () => { + const beforeSendTransaction = jest.fn(() => null); + + mockSdkInit({ enableTracing: true, beforeSendTransaction }); + + jest.useFakeTimers(); + + const hub = getCurrentHub(); + const client = hub.getClient() as NodeExperimentalClient; + + jest.spyOn(client, 'getIntegration').mockImplementation(integrationClass => { + if (integrationClass.name === 'NodeFetch') { + return { + shouldCreateSpansForRequests: false, + } as NodeFetch; + } + + return {} as Integration; + }); + + client.tracer.startActiveSpan( + 'test op', + { + kind: SpanKind.CLIENT, + attributes: { + [SemanticAttributes.HTTP_METHOD]: 'GET', + [SemanticAttributes.HTTP_URL]: 'https://example.com', + 'http.client': 'fetch', + }, + }, + span => { + startSpan({ name: 'inner 1' }, () => { + startSpan({ name: 'inner 2' }, () => {}); + }); + + span.end(); + }, + ); + + void client.flush(); + jest.advanceTimersByTime(5_000); + + expect(beforeSendTransaction).toHaveBeenCalledTimes(0); + + // Now try a non-HTTP span + client.tracer.startActiveSpan( + 'test op 2', + { + kind: SpanKind.CLIENT, + attributes: {}, + }, + span => { + startSpan({ name: 'inner 1' }, () => { + startSpan({ name: 'inner 2' }, () => {}); + }); + + span.end(); + }, + ); + + void client.flush(); + jest.advanceTimersByTime(5_000); + + expect(beforeSendTransaction).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/node-experimental/test/opentelemetry/propagator.test.ts b/packages/node-experimental/test/opentelemetry/propagator.test.ts new file mode 100644 index 000000000000..80b027496428 --- /dev/null +++ b/packages/node-experimental/test/opentelemetry/propagator.test.ts @@ -0,0 +1,375 @@ +import type { Context } from '@opentelemetry/api'; +import { + defaultTextMapGetter, + defaultTextMapSetter, + propagation, + ROOT_CONTEXT, + trace, + TraceFlags, +} from '@opentelemetry/api'; +import { suppressTracing } from '@opentelemetry/core'; +import { addTracingExtensions, Hub, makeMain } from '@sentry/core'; +import type { PropagationContext } from '@sentry/types'; + +import { + SENTRY_BAGGAGE_HEADER, + SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY, + SENTRY_TRACE_HEADER, +} from '../../src/constants'; +import { SentryPropagator } from '../../src/opentelemetry/propagator'; + +beforeAll(() => { + addTracingExtensions(); +}); + +describe('SentryPropagator', () => { + const propagator = new SentryPropagator(); + let carrier: { [key: string]: unknown }; + + beforeEach(() => { + carrier = {}; + }); + + it('returns fields set', () => { + expect(propagator.fields()).toEqual([SENTRY_TRACE_HEADER, SENTRY_BAGGAGE_HEADER]); + }); + + describe('inject', () => { + const client = { + getOptions: () => ({ + environment: 'production', + release: '1.0.0', + }), + getDsn: () => ({ + publicKey: 'abc', + }), + }; + // @ts-expect-error Use mock client for unit tests + const hub: Hub = new Hub(client); + makeMain(hub); + + describe('with active span', () => { + it.each([ + [ + 'works with a sampled propagation context', + { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: '6e0c63257de34c92', + traceFlags: TraceFlags.SAMPLED, + }, + { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: '6e0c63257de34c94', + parentSpanId: '6e0c63257de34c93', + sampled: true, + dsc: { + transaction: 'sampled-transaction', + trace_id: 'd4cda95b652f4a1592b449d5929fda1b', + sampled: 'true', + public_key: 'abc', + environment: 'production', + release: '1.0.0', + }, + }, + [ + 'sentry-environment=production', + 'sentry-release=1.0.0', + 'sentry-public_key=abc', + 'sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b', + 'sentry-transaction=sampled-transaction', + 'sentry-sampled=true', + ], + 'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1', + ], + [ + 'works with an unsampled propagation context', + { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: '6e0c63257de34c92', + traceFlags: TraceFlags.NONE, + }, + { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: '6e0c63257de34c94', + parentSpanId: '6e0c63257de34c93', + sampled: false, + dsc: { + transaction: 'not-sampled-transaction', + trace_id: 'd4cda95b652f4a1592b449d5929fda1b', + sampled: 'false', + public_key: 'abc', + environment: 'production', + release: '1.0.0', + }, + }, + [ + 'sentry-environment=production', + 'sentry-release=1.0.0', + 'sentry-public_key=abc', + 'sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b', + 'sentry-transaction=not-sampled-transaction', + 'sentry-sampled=false', + ], + 'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-0', + ], + [ + 'creates a new DSC if none exists yet', + { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: '6e0c63257de34c92', + traceFlags: TraceFlags.SAMPLED, + }, + { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: '6e0c63257de34c94', + parentSpanId: '6e0c63257de34c93', + sampled: true, + dsc: undefined, + }, + [ + 'sentry-environment=production', + 'sentry-public_key=abc', + 'sentry-release=1.0.0', + 'sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b', + ], + 'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1', + ], + [ + 'works with a remote parent span', + { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: '6e0c63257de34c92', + traceFlags: TraceFlags.SAMPLED, + isRemote: true, + }, + { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: '6e0c63257de34c94', + parentSpanId: '6e0c63257de34c93', + sampled: true, + dsc: { + transaction: 'sampled-transaction', + trace_id: 'd4cda95b652f4a1592b449d5929fda1b', + sampled: 'true', + public_key: 'abc', + environment: 'production', + release: '1.0.0', + }, + }, + [ + 'sentry-environment=production', + 'sentry-release=1.0.0', + 'sentry-public_key=abc', + 'sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b', + 'sentry-transaction=sampled-transaction', + 'sentry-sampled=true', + ], + 'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c94-1', + ], + ])('%s', (_name, spanContext, propagationContext, baggage, sentryTrace) => { + const context = trace.setSpanContext(setPropagationContext(ROOT_CONTEXT, propagationContext), spanContext); + propagator.inject(context, carrier, defaultTextMapSetter); + expect(baggageToArray(carrier[SENTRY_BAGGAGE_HEADER])).toEqual(baggage.sort()); + expect(carrier[SENTRY_TRACE_HEADER]).toBe(sentryTrace); + }); + + it('should include existing baggage', () => { + const propagationContext: PropagationContext = { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: '6e0c63257de34c92', + parentSpanId: '6e0c63257de34c93', + sampled: true, + dsc: { + transaction: 'sampled-transaction', + trace_id: 'd4cda95b652f4a1592b449d5929fda1b', + sampled: 'true', + public_key: 'abc', + environment: 'production', + release: '1.0.0', + }, + }; + + const spanContext = { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: '6e0c63257de34c92', + traceFlags: TraceFlags.SAMPLED, + }; + const context = trace.setSpanContext(setPropagationContext(ROOT_CONTEXT, propagationContext), spanContext); + const baggage = propagation.createBaggage({ foo: { value: 'bar' } }); + propagator.inject(propagation.setBaggage(context, baggage), carrier, defaultTextMapSetter); + expect(baggageToArray(carrier[SENTRY_BAGGAGE_HEADER])).toEqual( + [ + 'foo=bar', + 'sentry-transaction=sampled-transaction', + 'sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b', + 'sentry-sampled=true', + 'sentry-public_key=abc', + 'sentry-environment=production', + 'sentry-release=1.0.0', + ].sort(), + ); + }); + + it('should create baggage without propagation context', () => { + const spanContext = { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: '6e0c63257de34c92', + traceFlags: TraceFlags.SAMPLED, + }; + const context = trace.setSpanContext(ROOT_CONTEXT, spanContext); + const baggage = propagation.createBaggage({ foo: { value: 'bar' } }); + propagator.inject(propagation.setBaggage(context, baggage), carrier, defaultTextMapSetter); + expect(carrier[SENTRY_BAGGAGE_HEADER]).toBe('foo=bar'); + }); + + it('should NOT set baggage and sentry-trace header if instrumentation is supressed', () => { + const spanContext = { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: '6e0c63257de34c92', + traceFlags: TraceFlags.SAMPLED, + }; + const propagationContext: PropagationContext = { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: '6e0c63257de34c92', + parentSpanId: '6e0c63257de34c93', + sampled: true, + dsc: { + transaction: 'sampled-transaction', + trace_id: 'd4cda95b652f4a1592b449d5929fda1b', + sampled: 'true', + public_key: 'abc', + environment: 'production', + release: '1.0.0', + }, + }; + const context = suppressTracing( + trace.setSpanContext(setPropagationContext(ROOT_CONTEXT, propagationContext), spanContext), + ); + propagator.inject(context, carrier, defaultTextMapSetter); + expect(carrier[SENTRY_TRACE_HEADER]).toBe(undefined); + expect(carrier[SENTRY_BAGGAGE_HEADER]).toBe(undefined); + }); + }); + + it('should take span from propagationContext id if no active span is found', () => { + const propagationContext: PropagationContext = { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + parentSpanId: '6e0c63257de34c93', + spanId: '6e0c63257de34c92', + sampled: true, + dsc: { + transaction: 'sampled-transaction', + trace_id: 'd4cda95b652f4a1592b449d5929fda1b', + sampled: 'true', + public_key: 'abc', + environment: 'production', + release: '1.0.0', + }, + }; + + const context = setPropagationContext(ROOT_CONTEXT, propagationContext); + propagator.inject(context, carrier, defaultTextMapSetter); + expect(baggageToArray(carrier[SENTRY_BAGGAGE_HEADER])).toEqual( + [ + 'sentry-transaction=sampled-transaction', + 'sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b', + 'sentry-sampled=true', + 'sentry-public_key=abc', + 'sentry-environment=production', + 'sentry-release=1.0.0', + ].sort(), + ); + expect(carrier[SENTRY_TRACE_HEADER]).toBe('d4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1'); + }); + }); + + describe('extract', () => { + it('sets sentry span context on the context', () => { + const sentryTraceHeader = 'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1'; + carrier[SENTRY_TRACE_HEADER] = sentryTraceHeader; + const context = propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter); + expect(trace.getSpanContext(context)).toEqual({ + isRemote: true, + spanId: '6e0c63257de34c92', + traceFlags: TraceFlags.SAMPLED, + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + }); + }); + + it('sets defined sentry trace header on context', () => { + const sentryTraceHeader = 'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1'; + carrier[SENTRY_TRACE_HEADER] = sentryTraceHeader; + const context = propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter); + + const propagationContext = context.getValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY) as PropagationContext; + expect(propagationContext).toEqual({ + sampled: true, + parentSpanId: '6e0c63257de34c92', + spanId: expect.any(String), + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + }); + + // Ensure spanId !== parentSpanId - it should be a new random ID + expect(propagationContext.spanId).not.toBe('6e0c63257de34c92'); + }); + + it('sets undefined sentry trace header on context', () => { + const sentryTraceHeader = undefined; + carrier[SENTRY_TRACE_HEADER] = sentryTraceHeader; + const context = propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter); + expect(context.getValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY)).toEqual({ + sampled: undefined, + spanId: expect.any(String), + traceId: expect.any(String), + }); + }); + + it('sets defined dynamic sampling context on context', () => { + const baggage = + 'sentry-environment=production,sentry-release=1.0.0,sentry-public_key=abc,sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b,sentry-transaction=dsc-transaction'; + carrier[SENTRY_BAGGAGE_HEADER] = baggage; + const context = propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter); + expect(context.getValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY)).toEqual({ + sampled: undefined, + spanId: expect.any(String), + traceId: expect.any(String), // Note: This is not automatically taken from the DSC (in reality, this should be aligned) + dsc: { + environment: 'production', + public_key: 'abc', + release: '1.0.0', + trace_id: 'd4cda95b652f4a1592b449d5929fda1b', + transaction: 'dsc-transaction', + }, + }); + }); + + it('sets undefined dynamic sampling context on context', () => { + const baggage = ''; + carrier[SENTRY_BAGGAGE_HEADER] = baggage; + const context = propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter); + expect(context.getValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY)).toEqual({ + sampled: undefined, + spanId: expect.any(String), + traceId: expect.any(String), + }); + }); + + it('handles when sentry-trace is an empty array', () => { + carrier[SENTRY_TRACE_HEADER] = []; + const context = propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter); + expect(context.getValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY)).toEqual({ + sampled: undefined, + spanId: expect.any(String), + traceId: expect.any(String), + }); + }); + }); +}); + +function setPropagationContext(context: Context, propagationContext: PropagationContext): Context { + return context.setValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY, propagationContext); +} + +function baggageToArray(baggage: unknown): string[] { + return typeof baggage === 'string' ? baggage.split(',').sort() : []; +} diff --git a/packages/node-experimental/test/sdk/client.test.ts b/packages/node-experimental/test/sdk/client.test.ts new file mode 100644 index 000000000000..b7db215a4cd8 --- /dev/null +++ b/packages/node-experimental/test/sdk/client.test.ts @@ -0,0 +1,48 @@ +import { ProxyTracer } from '@opentelemetry/api'; +import { SDK_VERSION } from '@sentry/core'; + +import { NodeExperimentalClient } from '../../src/sdk/client'; +import { getDefaultNodeExperimentalClientOptions } from '../helpers/getDefaultNodePreviewClientOptions'; + +describe('NodeExperimentalClient', () => { + it('sets correct metadata', () => { + const options = getDefaultNodeExperimentalClientOptions(); + const client = new NodeExperimentalClient(options); + + expect(client.getOptions()).toEqual({ + integrations: [], + transport: options.transport, + stackParser: options.stackParser, + _metadata: { + sdk: { + name: 'sentry.javascript.node-experimental', + packages: [ + { + name: 'npm:@sentry/node-experimental', + version: SDK_VERSION, + }, + ], + version: SDK_VERSION, + }, + }, + transportOptions: { textEncoder: expect.any(Object) }, + platform: 'node', + runtime: { name: 'node', version: expect.any(String) }, + serverName: expect.any(String), + tracesSampleRate: 1, + }); + }); + + it('exposes a tracer', () => { + const client = new NodeExperimentalClient(getDefaultNodeExperimentalClientOptions()); + + const tracer = client.tracer; + expect(tracer).toBeDefined(); + expect(tracer).toBeInstanceOf(ProxyTracer); + + // Ensure we always get the same tracer instance + const tracer2 = client.tracer; + + expect(tracer2).toBe(tracer); + }); +}); diff --git a/packages/node-experimental/test/sdk/hub.test.ts b/packages/node-experimental/test/sdk/hub.test.ts new file mode 100644 index 000000000000..a25de1565ad8 --- /dev/null +++ b/packages/node-experimental/test/sdk/hub.test.ts @@ -0,0 +1,43 @@ +import { getCurrentHub, NodeExperimentalHub } from '../../src/sdk/hub'; +import { NodeExperimentalScope } from '../../src/sdk/scope'; + +describe('NodeExperimentalHub', () => { + it('getCurrentHub() returns the correct hub', () => { + const hub = getCurrentHub(); + expect(hub).toBeDefined(); + expect(hub).toBeInstanceOf(NodeExperimentalHub); + + const hub2 = getCurrentHub(); + expect(hub2).toBe(hub); + + const scope = hub.getScope(); + expect(scope).toBeDefined(); + expect(scope).toBeInstanceOf(NodeExperimentalScope); + }); + + it('hub gets correct scope on initialization', () => { + const hub = new NodeExperimentalHub(); + + const scope = hub.getScope(); + expect(scope).toBeDefined(); + expect(scope).toBeInstanceOf(NodeExperimentalScope); + }); + + it('pushScope() creates correct scope', () => { + const hub = new NodeExperimentalHub(); + + const scope = hub.pushScope(); + expect(scope).toBeInstanceOf(NodeExperimentalScope); + + const scope2 = hub.getScope(); + expect(scope2).toBe(scope); + }); + + it('withScope() creates correct scope', () => { + const hub = new NodeExperimentalHub(); + + hub.withScope(scope => { + expect(scope).toBeInstanceOf(NodeExperimentalScope); + }); + }); +}); diff --git a/packages/node-experimental/test/sdk/hubextensions.test.ts b/packages/node-experimental/test/sdk/hubextensions.test.ts new file mode 100644 index 000000000000..c2fee6baabde --- /dev/null +++ b/packages/node-experimental/test/sdk/hubextensions.test.ts @@ -0,0 +1,26 @@ +import { NodeExperimentalClient } from '../../src/sdk/client'; +import { getCurrentHub } from '../../src/sdk/hub'; +import { addTracingExtensions } from '../../src/sdk/hubextensions'; +import { getDefaultNodeExperimentalClientOptions } from '../helpers/getDefaultNodePreviewClientOptions'; + +describe('hubextensions', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + it('startTransaction is noop', () => { + const client = new NodeExperimentalClient(getDefaultNodeExperimentalClientOptions()); + getCurrentHub().bindClient(client); + addTracingExtensions(); + + const mockConsole = jest.spyOn(console, 'warn').mockImplementation(() => {}); + + const transaction = getCurrentHub().startTransaction({ name: 'test' }); + expect(transaction).toEqual({}); + + expect(mockConsole).toHaveBeenCalledTimes(1); + expect(mockConsole).toHaveBeenCalledWith( + 'startTransaction is a noop in @sentry/node-experimental. Use `startSpan` instead.', + ); + }); +}); diff --git a/packages/node-experimental/test/sdk/init.test.ts b/packages/node-experimental/test/sdk/init.test.ts index a150d61f3bf5..e220bf7e6ecd 100644 --- a/packages/node-experimental/test/sdk/init.test.ts +++ b/packages/node-experimental/test/sdk/init.test.ts @@ -3,6 +3,7 @@ import type { Integration } from '@sentry/types'; import * as auto from '../../src/integrations/getAutoPerformanceIntegrations'; import * as sdk from '../../src/sdk/init'; import { init } from '../../src/sdk/init'; +import { cleanupOtel } from '../helpers/mockSdkInit'; // eslint-disable-next-line no-var declare var global: any; @@ -31,6 +32,8 @@ describe('init()', () => { afterEach(() => { // @ts-expect-error - Reset the default integrations of node sdk to original sdk.defaultIntegrations = defaultIntegrationsBackup; + + cleanupOtel(); }); it("doesn't install default integrations if told not to", () => { diff --git a/packages/node-experimental/test/sdk/otelAsyncContextStrategy.test.ts b/packages/node-experimental/test/sdk/otelAsyncContextStrategy.test.ts new file mode 100644 index 000000000000..518d61000fee --- /dev/null +++ b/packages/node-experimental/test/sdk/otelAsyncContextStrategy.test.ts @@ -0,0 +1,144 @@ +import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; +import type { Hub } from '@sentry/core'; +import { runWithAsyncContext, setAsyncContextStrategy } from '@sentry/core'; + +import { NodeExperimentalClient } from '../../src/sdk/client'; +import { getCurrentHub } from '../../src/sdk/hub'; +import { setupOtel } from '../../src/sdk/initOtel'; +import { setOtelContextAsyncContextStrategy } from '../../src/sdk/otelAsyncContextStrategy'; +import { getDefaultNodeExperimentalClientOptions } from '../helpers/getDefaultNodePreviewClientOptions'; +import { cleanupOtel } from '../helpers/mockSdkInit'; + +describe('otelAsyncContextStrategy', () => { + let provider: BasicTracerProvider | undefined; + + beforeEach(() => { + const options = getDefaultNodeExperimentalClientOptions(); + const client = new NodeExperimentalClient(options); + provider = setupOtel(client); + setOtelContextAsyncContextStrategy(); + }); + + afterEach(() => { + cleanupOtel(provider); + }); + + afterAll(() => { + // clear the strategy + setAsyncContextStrategy(undefined); + }); + + test('hub scope inheritance', () => { + const globalHub = getCurrentHub(); + globalHub.setExtra('a', 'b'); + + runWithAsyncContext(() => { + const hub1 = getCurrentHub(); + expect(hub1).toEqual(globalHub); + + hub1.setExtra('c', 'd'); + expect(hub1).not.toEqual(globalHub); + + runWithAsyncContext(() => { + const hub2 = getCurrentHub(); + expect(hub2).toEqual(hub1); + expect(hub2).not.toEqual(globalHub); + + hub2.setExtra('e', 'f'); + expect(hub2).not.toEqual(hub1); + }); + }); + }); + + test('async hub scope inheritance', async () => { + async function addRandomExtra(hub: Hub, key: string): Promise { + return new Promise(resolve => { + setTimeout(() => { + hub.setExtra(key, Math.random()); + resolve(); + }, 100); + }); + } + + const globalHub = getCurrentHub(); + await addRandomExtra(globalHub, 'a'); + + await runWithAsyncContext(async () => { + const hub1 = getCurrentHub(); + expect(hub1).toEqual(globalHub); + + await addRandomExtra(hub1, 'b'); + expect(hub1).not.toEqual(globalHub); + + await runWithAsyncContext(async () => { + const hub2 = getCurrentHub(); + expect(hub2).toEqual(hub1); + expect(hub2).not.toEqual(globalHub); + + await addRandomExtra(hub1, 'c'); + expect(hub2).not.toEqual(hub1); + }); + }); + }); + + test('context single instance', () => { + const globalHub = getCurrentHub(); + runWithAsyncContext(() => { + expect(globalHub).not.toBe(getCurrentHub()); + }); + }); + + test('context within a context not reused', () => { + runWithAsyncContext(() => { + const hub1 = getCurrentHub(); + runWithAsyncContext(() => { + const hub2 = getCurrentHub(); + expect(hub1).not.toBe(hub2); + }); + }); + }); + + test('context within a context reused when requested', () => { + runWithAsyncContext(() => { + const hub1 = getCurrentHub(); + runWithAsyncContext( + () => { + const hub2 = getCurrentHub(); + expect(hub1).toBe(hub2); + }, + { reuseExisting: true }, + ); + }); + }); + + test('concurrent hub contexts', done => { + let d1done = false; + let d2done = false; + + runWithAsyncContext(() => { + const hub = getCurrentHub(); + hub.getStack().push({ client: 'process' } as any); + expect(hub.getStack()[1]).toEqual({ client: 'process' }); + // Just in case so we don't have to worry which one finishes first + // (although it always should be d2) + setTimeout(() => { + d1done = true; + if (d2done) { + done(); + } + }); + }); + + runWithAsyncContext(() => { + const hub = getCurrentHub(); + hub.getStack().push({ client: 'local' } as any); + expect(hub.getStack()[1]).toEqual({ client: 'local' }); + setTimeout(() => { + d2done = true; + if (d1done) { + done(); + } + }); + }); + }); +}); diff --git a/packages/node-experimental/test/sdk/scope.test.ts b/packages/node-experimental/test/sdk/scope.test.ts new file mode 100644 index 000000000000..7d8d772abd8c --- /dev/null +++ b/packages/node-experimental/test/sdk/scope.test.ts @@ -0,0 +1,438 @@ +import { makeSession } from '@sentry/core'; +import type { Breadcrumb } from '@sentry/types'; + +import { + OTEL_ATTR_BREADCRUMB_CATEGORY, + OTEL_ATTR_BREADCRUMB_DATA, + OTEL_ATTR_BREADCRUMB_EVENT_ID, + OTEL_ATTR_BREADCRUMB_LEVEL, + OTEL_ATTR_BREADCRUMB_TYPE, +} from '../../src/constants'; +import { setSpanParent } from '../../src/opentelemetry/spanData'; +import { NodeExperimentalScope } from '../../src/sdk/scope'; +import { createSpan } from '../helpers/createSpan'; +import * as GetActiveSpan from './../../src/utils/getActiveSpan'; + +describe('NodeExperimentalScope', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + it('clone() correctly clones the scope', () => { + const scope = new NodeExperimentalScope(); + + scope['_breadcrumbs'] = [{ message: 'test' }]; + scope['_tags'] = { tag: 'bar' }; + scope['_extra'] = { extra: 'bar' }; + scope['_contexts'] = { os: { name: 'Linux' } }; + scope['_user'] = { id: '123' }; + scope['_level'] = 'warning'; + // we don't care about _span + scope['_session'] = makeSession({ sid: '123' }); + // we don't care about transactionName + scope['_fingerprint'] = ['foo']; + scope['_eventProcessors'] = [() => ({})]; + scope['_requestSession'] = { status: 'ok' }; + scope['_attachments'] = [{ data: '123', filename: 'test.txt' }]; + scope['_sdkProcessingMetadata'] = { sdk: 'bar' }; + + const scope2 = NodeExperimentalScope.clone(scope); + + expect(scope2).toBeInstanceOf(NodeExperimentalScope); + expect(scope2).not.toBe(scope); + + // Ensure everything is correctly cloned + expect(scope2['_breadcrumbs']).toEqual(scope['_breadcrumbs']); + expect(scope2['_tags']).toEqual(scope['_tags']); + expect(scope2['_extra']).toEqual(scope['_extra']); + expect(scope2['_contexts']).toEqual(scope['_contexts']); + expect(scope2['_user']).toEqual(scope['_user']); + expect(scope2['_level']).toEqual(scope['_level']); + expect(scope2['_session']).toEqual(scope['_session']); + expect(scope2['_fingerprint']).toEqual(scope['_fingerprint']); + expect(scope2['_eventProcessors']).toEqual(scope['_eventProcessors']); + expect(scope2['_requestSession']).toEqual(scope['_requestSession']); + expect(scope2['_attachments']).toEqual(scope['_attachments']); + expect(scope2['_sdkProcessingMetadata']).toEqual(scope['_sdkProcessingMetadata']); + expect(scope2['_propagationContext']).toEqual(scope['_propagationContext']); + + // Ensure things are not copied by reference + expect(scope2['_breadcrumbs']).not.toBe(scope['_breadcrumbs']); + expect(scope2['_tags']).not.toBe(scope['_tags']); + expect(scope2['_extra']).not.toBe(scope['_extra']); + expect(scope2['_contexts']).not.toBe(scope['_contexts']); + expect(scope2['_eventProcessors']).not.toBe(scope['_eventProcessors']); + expect(scope2['_attachments']).not.toBe(scope['_attachments']); + expect(scope2['_sdkProcessingMetadata']).not.toBe(scope['_sdkProcessingMetadata']); + expect(scope2['_propagationContext']).not.toBe(scope['_propagationContext']); + + // These are actually copied by reference + expect(scope2['_user']).toBe(scope['_user']); + expect(scope2['_session']).toBe(scope['_session']); + expect(scope2['_requestSession']).toBe(scope['_requestSession']); + expect(scope2['_fingerprint']).toBe(scope['_fingerprint']); + }); + + it('clone() works without existing scope', () => { + const scope = NodeExperimentalScope.clone(undefined); + + expect(scope).toBeInstanceOf(NodeExperimentalScope); + }); + + it('getSpan returns undefined', () => { + const scope = new NodeExperimentalScope(); + + // Pretend we have a _span set + scope['_span'] = {} as any; + + expect(scope.getSpan()).toBeUndefined(); + }); + + it('setSpan is a noop', () => { + const scope = new NodeExperimentalScope(); + + scope.setSpan({} as any); + + expect(scope['_span']).toBeUndefined(); + }); + + describe('addBreadcrumb', () => { + it('adds to scope if no root span is found', () => { + jest.spyOn(GetActiveSpan, 'getActiveSpan').mockReturnValue(undefined); + + const scope = new NodeExperimentalScope(); + const breadcrumb: Breadcrumb = { message: 'test' }; + + const now = Date.now(); + jest.useFakeTimers(); + jest.setSystemTime(now); + + scope.addBreadcrumb(breadcrumb); + + expect(scope['_breadcrumbs']).toEqual([{ message: 'test', timestamp: now / 1000 }]); + }); + + it('adds to scope if no root span is found & uses given timestamp', () => { + jest.spyOn(GetActiveSpan, 'getActiveSpan').mockReturnValue(undefined); + + const scope = new NodeExperimentalScope(); + const breadcrumb: Breadcrumb = { message: 'test', timestamp: 1234 }; + + scope.addBreadcrumb(breadcrumb); + + expect(scope['_breadcrumbs']).toEqual([breadcrumb]); + }); + + it('adds to root span if found', () => { + const span = createSpan(); + jest.spyOn(GetActiveSpan, 'getActiveSpan').mockReturnValue(span); + + const scope = new NodeExperimentalScope(); + const breadcrumb: Breadcrumb = { message: 'test' }; + + const now = Date.now(); + jest.useFakeTimers(); + jest.setSystemTime(now); + + scope.addBreadcrumb(breadcrumb); + + expect(scope['_breadcrumbs']).toEqual([]); + expect(span.events).toEqual([ + expect.objectContaining({ + name: 'test', + time: [Math.floor(now / 1000), (now % 1000) * 1_000_000], + attributes: {}, + }), + ]); + }); + + it('adds to root span if found & uses given timestamp', () => { + const span = createSpan(); + jest.spyOn(GetActiveSpan, 'getActiveSpan').mockReturnValue(span); + + const scope = new NodeExperimentalScope(); + const breadcrumb: Breadcrumb = { timestamp: 12345, message: 'test' }; + + scope.addBreadcrumb(breadcrumb); + + expect(scope['_breadcrumbs']).toEqual([]); + expect(span.events).toEqual([ + expect.objectContaining({ + name: 'test', + time: [12345, 0], + attributes: {}, + }), + ]); + }); + + it('adds many breadcrumbs to root span if found', () => { + const span = createSpan(); + jest.spyOn(GetActiveSpan, 'getActiveSpan').mockReturnValue(span); + + const scope = new NodeExperimentalScope(); + const breadcrumb1: Breadcrumb = { timestamp: 12345, message: 'test1' }; + const breadcrumb2: Breadcrumb = { timestamp: 5678, message: 'test2' }; + const breadcrumb3: Breadcrumb = { timestamp: 9101112, message: 'test3' }; + + scope.addBreadcrumb(breadcrumb1); + scope.addBreadcrumb(breadcrumb2); + scope.addBreadcrumb(breadcrumb3); + + expect(scope['_breadcrumbs']).toEqual([]); + expect(span.events).toEqual([ + expect.objectContaining({ + name: 'test1', + time: [12345, 0], + attributes: {}, + }), + expect.objectContaining({ + name: 'test2', + time: [5678, 0], + attributes: {}, + }), + expect.objectContaining({ + name: 'test3', + time: [9101112, 0], + attributes: {}, + }), + ]); + }); + + it('adds to root span if found & no message is given', () => { + const span = createSpan(); + jest.spyOn(GetActiveSpan, 'getActiveSpan').mockReturnValue(span); + + const scope = new NodeExperimentalScope(); + const breadcrumb: Breadcrumb = { timestamp: 12345 }; + + scope.addBreadcrumb(breadcrumb); + + expect(scope['_breadcrumbs']).toEqual([]); + expect(span.events).toEqual([ + expect.objectContaining({ + name: '', + time: [12345, 0], + attributes: {}, + }), + ]); + }); + + it('adds to root span with full attributes', () => { + const span = createSpan(); + jest.spyOn(GetActiveSpan, 'getActiveSpan').mockReturnValue(span); + + const scope = new NodeExperimentalScope(); + const breadcrumb: Breadcrumb = { + timestamp: 12345, + message: 'test', + data: { nested: { indeed: true } }, + level: 'info', + category: 'test-category', + type: 'test-type', + event_id: 'test-event-id', + }; + + scope.addBreadcrumb(breadcrumb); + + expect(scope['_breadcrumbs']).toEqual([]); + expect(span.events).toEqual([ + expect.objectContaining({ + name: 'test', + time: [12345, 0], + attributes: { + [OTEL_ATTR_BREADCRUMB_DATA]: JSON.stringify({ nested: { indeed: true } }), + [OTEL_ATTR_BREADCRUMB_TYPE]: 'test-type', + [OTEL_ATTR_BREADCRUMB_LEVEL]: 'info', + [OTEL_ATTR_BREADCRUMB_EVENT_ID]: 'test-event-id', + [OTEL_ATTR_BREADCRUMB_CATEGORY]: 'test-category', + }, + }), + ]); + }); + + it('adds to root span with empty data', () => { + const span = createSpan(); + jest.spyOn(GetActiveSpan, 'getActiveSpan').mockReturnValue(span); + + const scope = new NodeExperimentalScope(); + const breadcrumb: Breadcrumb = { timestamp: 12345, message: 'test', data: {} }; + + scope.addBreadcrumb(breadcrumb); + + expect(scope['_breadcrumbs']).toEqual([]); + expect(span.events).toEqual([ + expect.objectContaining({ + name: 'test', + time: [12345, 0], + attributes: {}, + }), + ]); + }); + }); + + describe('_getBreadcrumbs', () => { + it('gets from scope if no root span is found', () => { + jest.spyOn(GetActiveSpan, 'getActiveSpan').mockReturnValue(undefined); + + const scope = new NodeExperimentalScope(); + const breadcrumbs: Breadcrumb[] = [ + { message: 'test1', timestamp: 1234 }, + { message: 'test2', timestamp: 12345 }, + { message: 'test3', timestamp: 12346 }, + ]; + scope['_breadcrumbs'] = breadcrumbs; + + expect(scope['_getBreadcrumbs']()).toEqual(breadcrumbs); + }); + + it('gets from root span if found', () => { + const span = createSpan(); + jest.spyOn(GetActiveSpan, 'getActiveSpan').mockReturnValue(span); + + const scope = new NodeExperimentalScope(); + + const now = Date.now(); + + span.addEvent('basic event', now); + span.addEvent('breadcrumb event', {}, now + 1000); + span.addEvent( + 'breadcrumb event 2', + { + [OTEL_ATTR_BREADCRUMB_DATA]: JSON.stringify({ nested: { indeed: true } }), + [OTEL_ATTR_BREADCRUMB_TYPE]: 'test-type', + [OTEL_ATTR_BREADCRUMB_LEVEL]: 'info', + [OTEL_ATTR_BREADCRUMB_EVENT_ID]: 'test-event-id', + [OTEL_ATTR_BREADCRUMB_CATEGORY]: 'test-category', + }, + now + 3000, + ); + span.addEvent( + 'breadcrumb event invalid JSON data', + { + [OTEL_ATTR_BREADCRUMB_DATA]: 'this is not JSON...', + }, + now + 2000, + ); + + expect(scope['_getBreadcrumbs']()).toEqual([ + { message: 'basic event', timestamp: now / 1000 }, + { message: 'breadcrumb event', timestamp: now / 1000 + 1 }, + { + message: 'breadcrumb event 2', + timestamp: now / 1000 + 3, + data: { nested: { indeed: true } }, + level: 'info', + event_id: 'test-event-id', + category: 'test-category', + type: 'test-type', + }, + { message: 'breadcrumb event invalid JSON data', timestamp: now / 1000 + 2 }, + ]); + }); + + it('gets from spans up the parent chain if found', () => { + const span = createSpan(); + const parentSpan = createSpan(); + const rootSpan = createSpan(); + jest.spyOn(GetActiveSpan, 'getActiveSpan').mockReturnValue(span); + + setSpanParent(span, parentSpan); + setSpanParent(parentSpan, rootSpan); + + const scope = new NodeExperimentalScope(); + + const now = Date.now(); + + span.addEvent('basic event', now); + parentSpan.addEvent('parent breadcrumb event', {}, now + 1000); + span.addEvent( + 'breadcrumb event 2', + { + [OTEL_ATTR_BREADCRUMB_DATA]: JSON.stringify({ nested: true }), + }, + now + 3000, + ); + rootSpan.addEvent( + 'breadcrumb event invalid JSON data', + { + [OTEL_ATTR_BREADCRUMB_DATA]: 'this is not JSON...', + }, + now + 2000, + ); + + expect(scope['_getBreadcrumbs']()).toEqual([ + { message: 'basic event', timestamp: now / 1000 }, + { message: 'breadcrumb event 2', timestamp: now / 1000 + 3, data: { nested: true } }, + { message: 'parent breadcrumb event', timestamp: now / 1000 + 1 }, + { message: 'breadcrumb event invalid JSON data', timestamp: now / 1000 + 2 }, + ]); + }); + + it('combines scope & span breadcrumbs if both exist', () => { + const span = createSpan(); + jest.spyOn(GetActiveSpan, 'getActiveSpan').mockReturnValue(span); + + const scope = new NodeExperimentalScope(); + + const breadcrumbs: Breadcrumb[] = [ + { message: 'test1', timestamp: 1234 }, + { message: 'test2', timestamp: 12345 }, + { message: 'test3', timestamp: 12346 }, + ]; + scope['_breadcrumbs'] = breadcrumbs; + + const now = Date.now(); + + span.addEvent('basic event', now); + span.addEvent('breadcrumb event', {}, now + 1000); + + expect(scope['_getBreadcrumbs']()).toEqual([ + { message: 'test1', timestamp: 1234 }, + { message: 'test2', timestamp: 12345 }, + { message: 'test3', timestamp: 12346 }, + { message: 'basic event', timestamp: now / 1000 }, + { message: 'breadcrumb event', timestamp: now / 1000 + 1 }, + ]); + }); + + it('gets from activeSpan if defined', () => { + const span = createSpan(); + jest.spyOn(GetActiveSpan, 'getActiveSpan').mockReturnValue(span); + + const scope = new NodeExperimentalScope(); + + const now = Date.now(); + + span.addEvent('basic event', now); + span.addEvent('breadcrumb event', {}, now + 1000); + span.addEvent( + 'breadcrumb event 2', + { + [OTEL_ATTR_BREADCRUMB_DATA]: JSON.stringify({ nested: { indeed: true } }), + [OTEL_ATTR_BREADCRUMB_TYPE]: 'test-type', + [OTEL_ATTR_BREADCRUMB_LEVEL]: 'info', + [OTEL_ATTR_BREADCRUMB_EVENT_ID]: 'test-event-id', + [OTEL_ATTR_BREADCRUMB_CATEGORY]: 'test-category', + }, + now + 3000, + ); + span.addEvent( + 'breadcrumb event invalid JSON data', + { + [OTEL_ATTR_BREADCRUMB_DATA]: 'this is not JSON...', + }, + now + 2000, + ); + + const activeSpan = createSpan(); + activeSpan.addEvent('event 1', now); + activeSpan.addEvent('event 2', {}, now + 1000); + scope.activeSpan = activeSpan; + + expect(scope['_getBreadcrumbs']()).toEqual([ + { message: 'event 1', timestamp: now / 1000 }, + { message: 'event 2', timestamp: now / 1000 + 1 }, + ]); + }); + }); +}); diff --git a/packages/node-experimental/test/sdk/trace.test.ts b/packages/node-experimental/test/sdk/trace.test.ts index c53606140fa1..e141372552a6 100644 --- a/packages/node-experimental/test/sdk/trace.test.ts +++ b/packages/node-experimental/test/sdk/trace.test.ts @@ -1,63 +1,78 @@ -import { Span, Transaction } from '@sentry/core'; +import { context, trace, TraceFlags } from '@opentelemetry/api'; +import type { Span } from '@opentelemetry/sdk-trace-base'; +import type { PropagationContext } from '@sentry/types'; import * as Sentry from '../../src'; -import { mockSdkInit } from '../helpers/mockSdkInit'; +import { + OTEL_ATTR_OP, + OTEL_ATTR_ORIGIN, + OTEL_ATTR_SENTRY_SAMPLE_RATE, + OTEL_ATTR_SOURCE, + SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY, +} from '../../src/constants'; +import { getSpanMetadata } from '../../src/opentelemetry/spanData'; +import { getActiveSpan } from '../../src/utils/getActiveSpan'; +import { cleanupOtel, mockSdkInit } from '../helpers/mockSdkInit'; describe('trace', () => { beforeEach(() => { mockSdkInit({ enableTracing: true }); }); + afterEach(() => { + cleanupOtel(); + }); + describe('startSpan', () => { it('works with a sync callback', () => { const spans: Span[] = []; - expect(Sentry.getActiveSpan()).toEqual(undefined); + expect(getActiveSpan()).toEqual(undefined); - Sentry.startSpan({ name: 'outer' }, outerSpan => { + const res = Sentry.startSpan({ name: 'outer' }, outerSpan => { expect(outerSpan).toBeDefined(); spans.push(outerSpan!); expect(outerSpan?.name).toEqual('outer'); - expect(outerSpan).toBeInstanceOf(Transaction); - expect(Sentry.getActiveSpan()).toEqual(outerSpan); + expect(getActiveSpan()).toEqual(outerSpan); Sentry.startSpan({ name: 'inner' }, innerSpan => { expect(innerSpan).toBeDefined(); spans.push(innerSpan!); - expect(innerSpan?.description).toEqual('inner'); - expect(innerSpan).toBeInstanceOf(Span); - expect(innerSpan).not.toBeInstanceOf(Transaction); - expect(Sentry.getActiveSpan()).toEqual(innerSpan); + expect(innerSpan?.name).toEqual('inner'); + expect(getActiveSpan()).toEqual(innerSpan); }); + + return 'test value'; }); - expect(Sentry.getActiveSpan()).toEqual(undefined); + expect(res).toEqual('test value'); + + expect(getActiveSpan()).toEqual(undefined); expect(spans).toHaveLength(2); const [outerSpan, innerSpan] = spans; - expect((outerSpan as Transaction).name).toEqual('outer'); - expect(innerSpan.description).toEqual('inner'); + expect(outerSpan.name).toEqual('outer'); + expect(innerSpan.name).toEqual('inner'); - expect(outerSpan.endTimestamp).toEqual(expect.any(Number)); - expect(innerSpan.endTimestamp).toEqual(expect.any(Number)); + expect(outerSpan.endTime).not.toEqual([0, 0]); + expect(innerSpan.endTime).not.toEqual([0, 0]); }); it('works with an async callback', async () => { const spans: Span[] = []; - expect(Sentry.getActiveSpan()).toEqual(undefined); + expect(getActiveSpan()).toEqual(undefined); - await Sentry.startSpan({ name: 'outer' }, async outerSpan => { + const res = await Sentry.startSpan({ name: 'outer' }, async outerSpan => { expect(outerSpan).toBeDefined(); spans.push(outerSpan!); await new Promise(resolve => setTimeout(resolve, 10)); expect(outerSpan?.name).toEqual('outer'); - expect(outerSpan).toBeInstanceOf(Transaction); - expect(Sentry.getActiveSpan()).toEqual(outerSpan); + expect(getActiveSpan()).toEqual(outerSpan); await Sentry.startSpan({ name: 'inner' }, async innerSpan => { expect(innerSpan).toBeDefined(); @@ -65,46 +80,45 @@ describe('trace', () => { await new Promise(resolve => setTimeout(resolve, 10)); - expect(innerSpan?.description).toEqual('inner'); - expect(innerSpan).toBeInstanceOf(Span); - expect(innerSpan).not.toBeInstanceOf(Transaction); - expect(Sentry.getActiveSpan()).toEqual(innerSpan); + expect(innerSpan?.name).toEqual('inner'); + expect(getActiveSpan()).toEqual(innerSpan); }); + + return 'test value'; }); - expect(Sentry.getActiveSpan()).toEqual(undefined); + expect(res).toEqual('test value'); + + expect(getActiveSpan()).toEqual(undefined); expect(spans).toHaveLength(2); const [outerSpan, innerSpan] = spans; - expect((outerSpan as Transaction).name).toEqual('outer'); - expect(innerSpan.description).toEqual('inner'); + expect(outerSpan.name).toEqual('outer'); + expect(innerSpan.name).toEqual('inner'); - expect(outerSpan.endTimestamp).toEqual(expect.any(Number)); - expect(innerSpan.endTimestamp).toEqual(expect.any(Number)); + expect(outerSpan.endTime).not.toEqual([0, 0]); + expect(innerSpan.endTime).not.toEqual([0, 0]); }); it('works with multiple parallel calls', () => { const spans1: Span[] = []; const spans2: Span[] = []; - expect(Sentry.getActiveSpan()).toEqual(undefined); + expect(getActiveSpan()).toEqual(undefined); Sentry.startSpan({ name: 'outer' }, outerSpan => { expect(outerSpan).toBeDefined(); spans1.push(outerSpan!); expect(outerSpan?.name).toEqual('outer'); - expect(outerSpan).toBeInstanceOf(Transaction); - expect(Sentry.getActiveSpan()).toEqual(outerSpan); + expect(getActiveSpan()).toEqual(outerSpan); Sentry.startSpan({ name: 'inner' }, innerSpan => { expect(innerSpan).toBeDefined(); spans1.push(innerSpan!); - expect(innerSpan?.description).toEqual('inner'); - expect(innerSpan).toBeInstanceOf(Span); - expect(innerSpan).not.toBeInstanceOf(Transaction); - expect(Sentry.getActiveSpan()).toEqual(innerSpan); + expect(innerSpan?.name).toEqual('inner'); + expect(getActiveSpan()).toEqual(innerSpan); }); }); @@ -113,24 +127,58 @@ describe('trace', () => { spans2.push(outerSpan!); expect(outerSpan?.name).toEqual('outer2'); - expect(outerSpan).toBeInstanceOf(Transaction); - expect(Sentry.getActiveSpan()).toEqual(outerSpan); + expect(getActiveSpan()).toEqual(outerSpan); Sentry.startSpan({ name: 'inner2' }, innerSpan => { expect(innerSpan).toBeDefined(); spans2.push(innerSpan!); - expect(innerSpan?.description).toEqual('inner2'); - expect(innerSpan).toBeInstanceOf(Span); - expect(innerSpan).not.toBeInstanceOf(Transaction); - expect(Sentry.getActiveSpan()).toEqual(innerSpan); + expect(innerSpan?.name).toEqual('inner2'); + expect(getActiveSpan()).toEqual(innerSpan); }); }); - expect(Sentry.getActiveSpan()).toEqual(undefined); + expect(getActiveSpan()).toEqual(undefined); expect(spans1).toHaveLength(2); expect(spans2).toHaveLength(2); }); + + it('allows to pass context arguments', () => { + Sentry.startSpan( + { + name: 'outer', + }, + span => { + expect(span).toBeDefined(); + expect(span?.attributes).toEqual({ + [OTEL_ATTR_SENTRY_SAMPLE_RATE]: 1, + }); + + expect(getSpanMetadata(span!)).toEqual(undefined); + }, + ); + + Sentry.startSpan( + { + name: 'outer', + op: 'my-op', + origin: 'auto.test.origin', + source: 'task', + metadata: { requestPath: 'test-path' }, + }, + span => { + expect(span).toBeDefined(); + expect(span?.attributes).toEqual({ + [OTEL_ATTR_SOURCE]: 'task', + [OTEL_ATTR_ORIGIN]: 'auto.test.origin', + [OTEL_ATTR_OP]: 'my-op', + [OTEL_ATTR_SENTRY_SAMPLE_RATE]: 1, + }); + + expect(getSpanMetadata(span!)).toEqual({ requestPath: 'test-path' }); + }, + ); + }); }); describe('startInactiveSpan', () => { @@ -138,36 +186,389 @@ describe('trace', () => { const span = Sentry.startInactiveSpan({ name: 'test' }); expect(span).toBeDefined(); - expect(span).toBeInstanceOf(Transaction); expect(span?.name).toEqual('test'); - expect(span?.endTimestamp).toBeUndefined(); - expect(Sentry.getActiveSpan()).toBeUndefined(); + expect(span?.endTime).toEqual([0, 0]); + expect(getActiveSpan()).toBeUndefined(); - span?.finish(); + span?.end(); - expect(span?.endTimestamp).toEqual(expect.any(Number)); - expect(Sentry.getActiveSpan()).toBeUndefined(); + expect(span?.endTime).not.toEqual([0, 0]); + expect(getActiveSpan()).toBeUndefined(); }); it('works as a child span', () => { Sentry.startSpan({ name: 'outer' }, outerSpan => { expect(outerSpan).toBeDefined(); - expect(Sentry.getActiveSpan()).toEqual(outerSpan); + expect(getActiveSpan()).toEqual(outerSpan); const innerSpan = Sentry.startInactiveSpan({ name: 'test' }); expect(innerSpan).toBeDefined(); - expect(innerSpan).toBeInstanceOf(Span); - expect(innerSpan).not.toBeInstanceOf(Transaction); - expect(innerSpan?.description).toEqual('test'); - expect(innerSpan?.endTimestamp).toBeUndefined(); - expect(Sentry.getActiveSpan()).toEqual(outerSpan); + expect(innerSpan?.name).toEqual('test'); + expect(innerSpan?.endTime).toEqual([0, 0]); + expect(getActiveSpan()).toEqual(outerSpan); + + innerSpan?.end(); + + expect(innerSpan?.endTime).not.toEqual([0, 0]); + expect(getActiveSpan()).toEqual(outerSpan); + }); + }); + + it('allows to pass context arguments', () => { + const span = Sentry.startInactiveSpan({ + name: 'outer', + }); + + expect(span).toBeDefined(); + expect(span?.attributes).toEqual({ + [OTEL_ATTR_SENTRY_SAMPLE_RATE]: 1, + }); + + expect(getSpanMetadata(span!)).toEqual(undefined); + + const span2 = Sentry.startInactiveSpan({ + name: 'outer', + op: 'my-op', + origin: 'auto.test.origin', + source: 'task', + metadata: { requestPath: 'test-path' }, + }); + + expect(span2).toBeDefined(); + expect(span2?.attributes).toEqual({ + [OTEL_ATTR_SENTRY_SAMPLE_RATE]: 1, + [OTEL_ATTR_SOURCE]: 'task', + [OTEL_ATTR_ORIGIN]: 'auto.test.origin', + [OTEL_ATTR_OP]: 'my-op', + }); + + expect(getSpanMetadata(span2!)).toEqual({ requestPath: 'test-path' }); + }); + }); +}); + +describe('trace (tracing disabled)', () => { + beforeEach(() => { + mockSdkInit({ enableTracing: false }); + }); + + afterEach(() => { + cleanupOtel(); + }); + + it('startSpan calls callback without span', () => { + const val = Sentry.startSpan({ name: 'outer' }, outerSpan => { + expect(outerSpan).toBeUndefined(); + + return 'test value'; + }); + + expect(val).toEqual('test value'); + }); + + it('startInactiveSpan returns undefined', () => { + const span = Sentry.startInactiveSpan({ name: 'test' }); + + expect(span).toBeUndefined(); + }); +}); + +describe('trace (sampling)', () => { + afterEach(() => { + cleanupOtel(); + jest.clearAllMocks(); + }); + + it('samples with a tracesSampleRate, when Math.random() > tracesSampleRate', () => { + jest.spyOn(Math, 'random').mockImplementation(() => 0.6); + + mockSdkInit({ tracesSampleRate: 0.5 }); + + Sentry.startSpan({ name: 'outer' }, outerSpan => { + expect(outerSpan).toBeUndefined(); + + Sentry.startSpan({ name: 'inner' }, innerSpan => { + expect(innerSpan).toBeUndefined(); + }); + }); + }); + + it('samples with a tracesSampleRate, when Math.random() < tracesSampleRate', () => { + jest.spyOn(Math, 'random').mockImplementation(() => 0.4); + + mockSdkInit({ tracesSampleRate: 0.5 }); + + Sentry.startSpan({ name: 'outer' }, outerSpan => { + expect(outerSpan).toBeDefined(); + expect(outerSpan?.isRecording()).toBe(true); + // All fields are empty for NonRecordingSpan + expect(outerSpan?.name).toBe('outer'); + + Sentry.startSpan({ name: 'inner' }, innerSpan => { + expect(innerSpan).toBeDefined(); + expect(innerSpan?.isRecording()).toBe(true); + expect(innerSpan?.name).toBe('inner'); + }); + }); + }); + + it('positive parent sampling takes precedence over tracesSampleRate', () => { + jest.spyOn(Math, 'random').mockImplementation(() => 0.6); + + mockSdkInit({ tracesSampleRate: 1 }); + + // This will def. be sampled because of the tracesSampleRate + Sentry.startSpan({ name: 'outer' }, outerSpan => { + expect(outerSpan).toBeDefined(); + expect(outerSpan?.isRecording()).toBe(true); + expect(outerSpan?.name).toBe('outer'); + + // Now let's mutate the tracesSampleRate so that the next entry _should_ not be sampled + // but it will because of parent sampling + const client = Sentry.getCurrentHub().getClient(); + client!.getOptions().tracesSampleRate = 0.5; + + Sentry.startSpan({ name: 'inner' }, innerSpan => { + expect(innerSpan).toBeDefined(); + expect(innerSpan?.isRecording()).toBe(true); + expect(innerSpan?.name).toBe('inner'); + }); + }); + }); + + it('negative parent sampling takes precedence over tracesSampleRate', () => { + jest.spyOn(Math, 'random').mockImplementation(() => 0.6); + + mockSdkInit({ tracesSampleRate: 0.5 }); + + // This will def. be sampled because of the tracesSampleRate + Sentry.startSpan({ name: 'outer' }, outerSpan => { + expect(outerSpan).toBeUndefined(); + + // Now let's mutate the tracesSampleRate so that the next entry _should_ not be sampled + // but it will because of parent sampling + const client = Sentry.getCurrentHub().getClient(); + client!.getOptions().tracesSampleRate = 1; + + Sentry.startSpan({ name: 'inner' }, innerSpan => { + expect(innerSpan).toBeUndefined(); + }); + }); + }); + + it('positive remote parent sampling takes precedence over tracesSampleRate', () => { + jest.spyOn(Math, 'random').mockImplementation(() => 0.6); + + mockSdkInit({ tracesSampleRate: 0.5 }); + + const traceId = 'd4cda95b652f4a1592b449d5929fda1b'; + const parentSpanId = '6e0c63257de34c92'; + + const spanContext = { + traceId, + spanId: parentSpanId, + sampled: true, + isRemote: true, + traceFlags: TraceFlags.SAMPLED, + }; + + const propagationContext: PropagationContext = { + traceId, + sampled: true, + parentSpanId, + spanId: '6e0c63257de34c93', + }; + + // We simulate the correct context we'd normally get from the SentryPropagator + context.with( + trace.setSpanContext( + context.active().setValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY, propagationContext), + spanContext, + ), + () => { + // This will def. be sampled because of the tracesSampleRate + Sentry.startSpan({ name: 'outer' }, outerSpan => { + expect(outerSpan).toBeDefined(); + expect(outerSpan?.isRecording()).toBe(true); + expect(outerSpan?.name).toBe('outer'); + }); + }, + ); + }); + + it('negative remote parent sampling takes precedence over tracesSampleRate', () => { + jest.spyOn(Math, 'random').mockImplementation(() => 0.6); + + mockSdkInit({ tracesSampleRate: 0.5 }); + + const traceId = 'd4cda95b652f4a1592b449d5929fda1b'; + const parentSpanId = '6e0c63257de34c92'; + + const spanContext = { + traceId, + spanId: parentSpanId, + sampled: false, + isRemote: true, + traceFlags: TraceFlags.NONE, + }; + + const propagationContext: PropagationContext = { + traceId, + sampled: false, + parentSpanId, + spanId: '6e0c63257de34c93', + }; + + // We simulate the correct context we'd normally get from the SentryPropagator + context.with( + trace.setSpanContext( + context.active().setValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY, propagationContext), + spanContext, + ), + () => { + // This will def. be sampled because of the tracesSampleRate + Sentry.startSpan({ name: 'outer' }, outerSpan => { + expect(outerSpan).toBeUndefined(); + }); + }, + ); + }); + + it('samples with a tracesSampler returning a boolean', () => { + let tracesSamplerResponse: boolean = true; + + const tracesSampler = jest.fn(() => { + return tracesSamplerResponse; + }); + + mockSdkInit({ tracesSampler }); + + Sentry.startSpan({ name: 'outer' }, outerSpan => { + expect(outerSpan).toBeDefined(); + }); + + expect(tracesSampler).toBeCalledTimes(1); + expect(tracesSampler).toHaveBeenLastCalledWith({ + parentSampled: undefined, + transactionContext: { name: 'outer', parentSampled: undefined }, + }); + + // Now return `false`, it should not sample + tracesSamplerResponse = false; - innerSpan?.finish(); + Sentry.startSpan({ name: 'outer2' }, outerSpan => { + expect(outerSpan).toBeUndefined(); - expect(innerSpan?.endTimestamp).toEqual(expect.any(Number)); - expect(Sentry.getActiveSpan()).toEqual(outerSpan); + Sentry.startSpan({ name: 'inner2' }, outerSpan => { + expect(outerSpan).toBeUndefined(); }); }); + + expect(tracesSampler).toHaveBeenCalledTimes(3); + expect(tracesSampler).toHaveBeenLastCalledWith({ + parentSampled: false, + transactionContext: { name: 'inner2', parentSampled: false }, + }); + }); + + it('samples with a tracesSampler returning a number', () => { + jest.spyOn(Math, 'random').mockImplementation(() => 0.6); + + let tracesSamplerResponse: number = 1; + + const tracesSampler = jest.fn(() => { + return tracesSamplerResponse; + }); + + mockSdkInit({ tracesSampler }); + + Sentry.startSpan({ name: 'outer' }, outerSpan => { + expect(outerSpan).toBeDefined(); + }); + + expect(tracesSampler).toBeCalledTimes(1); + expect(tracesSampler).toHaveBeenLastCalledWith({ + parentSampled: undefined, + transactionContext: { name: 'outer', parentSampled: undefined }, + }); + + // Now return `0`, it should not sample + tracesSamplerResponse = 0; + + Sentry.startSpan({ name: 'outer2' }, outerSpan => { + expect(outerSpan).toBeUndefined(); + + Sentry.startSpan({ name: 'inner2' }, outerSpan => { + expect(outerSpan).toBeUndefined(); + }); + }); + + expect(tracesSampler).toHaveBeenCalledTimes(3); + expect(tracesSampler).toHaveBeenLastCalledWith({ + parentSampled: false, + transactionContext: { name: 'inner2', parentSampled: false }, + }); + + // Now return `0.4`, it should not sample + tracesSamplerResponse = 0.4; + + Sentry.startSpan({ name: 'outer3' }, outerSpan => { + expect(outerSpan).toBeUndefined(); + }); + + expect(tracesSampler).toHaveBeenCalledTimes(4); + expect(tracesSampler).toHaveBeenLastCalledWith({ + parentSampled: undefined, + transactionContext: { name: 'outer3', parentSampled: undefined }, + }); + }); + + it('samples with a tracesSampler even if parent is remotely sampled', () => { + const tracesSampler = jest.fn(() => { + return false; + }); + + mockSdkInit({ tracesSampler }); + const traceId = 'd4cda95b652f4a1592b449d5929fda1b'; + const parentSpanId = '6e0c63257de34c92'; + + const spanContext = { + traceId, + spanId: parentSpanId, + sampled: true, + isRemote: true, + traceFlags: TraceFlags.SAMPLED, + }; + + const propagationContext: PropagationContext = { + traceId, + sampled: true, + parentSpanId, + spanId: '6e0c63257de34c93', + }; + + // We simulate the correct context we'd normally get from the SentryPropagator + context.with( + trace.setSpanContext( + context.active().setValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY, propagationContext), + spanContext, + ), + () => { + // This will def. be sampled because of the tracesSampleRate + Sentry.startSpan({ name: 'outer' }, outerSpan => { + expect(outerSpan).toBeUndefined(); + }); + }, + ); + + expect(tracesSampler).toBeCalledTimes(1); + expect(tracesSampler).toHaveBeenLastCalledWith({ + parentSampled: true, + transactionContext: { + name: 'outer', + parentSampled: true, + }, + }); }); }); diff --git a/packages/node-experimental/test/sdk/transaction.test.ts b/packages/node-experimental/test/sdk/transaction.test.ts new file mode 100644 index 000000000000..132696655b09 --- /dev/null +++ b/packages/node-experimental/test/sdk/transaction.test.ts @@ -0,0 +1,197 @@ +import { NodeExperimentalClient } from '../../src/sdk/client'; +import { getCurrentHub } from '../../src/sdk/hub'; +import { NodeExperimentalScope } from '../../src/sdk/scope'; +import { NodeExperimentalTransaction, startTransaction } from '../../src/sdk/transaction'; +import { getDefaultNodeExperimentalClientOptions } from '../helpers/getDefaultNodePreviewClientOptions'; + +describe('NodeExperimentalTransaction', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + it('works with finishWithScope without arguments', () => { + const client = new NodeExperimentalClient(getDefaultNodeExperimentalClientOptions()); + + const mockSend = jest.spyOn(client, 'captureEvent').mockImplementation(() => 'mocked'); + + const hub = getCurrentHub(); + hub.bindClient(client); + + const transaction = new NodeExperimentalTransaction({ name: 'test' }, hub); + transaction.sampled = true; + + const res = transaction.finishWithScope(); + + expect(mockSend).toBeCalledTimes(1); + expect(mockSend).toBeCalledWith( + expect.objectContaining({ + contexts: { + trace: { + span_id: expect.any(String), + trace_id: expect.any(String), + }, + }, + spans: [], + start_timestamp: expect.any(Number), + tags: {}, + timestamp: expect.any(Number), + transaction: 'test', + type: 'transaction', + sdkProcessingMetadata: { + source: 'custom', + spanMetadata: {}, + dynamicSamplingContext: { + environment: 'production', + trace_id: expect.any(String), + transaction: 'test', + sampled: 'true', + }, + }, + transaction_info: { source: 'custom' }, + }), + { event_id: expect.any(String) }, + undefined, + ); + expect(res).toBe('mocked'); + }); + + it('works with finishWithScope with endTime', () => { + const client = new NodeExperimentalClient(getDefaultNodeExperimentalClientOptions()); + + const mockSend = jest.spyOn(client, 'captureEvent').mockImplementation(() => 'mocked'); + + const hub = getCurrentHub(); + hub.bindClient(client); + + const transaction = new NodeExperimentalTransaction({ name: 'test', startTimestamp: 123456 }, hub); + transaction.sampled = true; + + const res = transaction.finishWithScope(1234567); + + expect(mockSend).toBeCalledTimes(1); + expect(mockSend).toBeCalledWith( + expect.objectContaining({ + start_timestamp: 123456, + timestamp: 1234567, + }), + { event_id: expect.any(String) }, + undefined, + ); + expect(res).toBe('mocked'); + }); + + it('works with finishWithScope with endTime & scope', () => { + const client = new NodeExperimentalClient(getDefaultNodeExperimentalClientOptions()); + + const mockSend = jest.spyOn(client, 'captureEvent').mockImplementation(() => 'mocked'); + + const hub = getCurrentHub(); + hub.bindClient(client); + + const transaction = new NodeExperimentalTransaction({ name: 'test', startTimestamp: 123456 }, hub); + transaction.sampled = true; + + const scope = new NodeExperimentalScope(); + scope.setTags({ + tag1: 'yes', + tag2: 'no', + }); + scope.setContext('os', { name: 'Custom OS' }); + + const res = transaction.finishWithScope(1234567, scope); + + expect(mockSend).toBeCalledTimes(1); + expect(mockSend).toBeCalledWith( + expect.objectContaining({ + contexts: { + trace: { + span_id: expect.any(String), + trace_id: expect.any(String), + }, + }, + spans: [], + start_timestamp: 123456, + tags: {}, + timestamp: 1234567, + transaction: 'test', + type: 'transaction', + sdkProcessingMetadata: { + source: 'custom', + spanMetadata: {}, + dynamicSamplingContext: { + environment: 'production', + trace_id: expect.any(String), + transaction: 'test', + sampled: 'true', + }, + }, + transaction_info: { source: 'custom' }, + }), + { event_id: expect.any(String) }, + scope, + ); + expect(res).toBe('mocked'); + }); +}); + +describe('startTranscation', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + it('creates a NodeExperimentalTransaction', () => { + const client = new NodeExperimentalClient(getDefaultNodeExperimentalClientOptions({ tracesSampleRate: 0 })); + const hub = getCurrentHub(); + hub.bindClient(client); + + const transaction = startTransaction(hub, { name: 'test' }); + + expect(transaction).toBeInstanceOf(NodeExperimentalTransaction); + + expect(transaction.sampled).toBe(undefined); + expect(transaction.spanRecorder).toBeDefined(); + expect(transaction.spanRecorder?.spans).toHaveLength(1); + expect(transaction.metadata).toEqual({ + source: 'custom', + spanMetadata: {}, + }); + + expect(transaction.toJSON()).toEqual( + expect.objectContaining({ + origin: 'manual', + span_id: expect.any(String), + start_timestamp: expect.any(Number), + trace_id: expect.any(String), + }), + ); + }); + + it('allows to pass data to transaction', () => { + const client = new NodeExperimentalClient(getDefaultNodeExperimentalClientOptions()); + const hub = getCurrentHub(); + hub.bindClient(client); + + const transaction = startTransaction(hub, { + name: 'test', + startTimestamp: 1234, + spanId: 'span1', + traceId: 'trace1', + }); + + expect(transaction).toBeInstanceOf(NodeExperimentalTransaction); + + expect(transaction.metadata).toEqual({ + source: 'custom', + spanMetadata: {}, + }); + + expect(transaction.toJSON()).toEqual( + expect.objectContaining({ + origin: 'manual', + span_id: 'span1', + start_timestamp: 1234, + trace_id: 'trace1', + }), + ); + }); +}); diff --git a/packages/node-experimental/test/utils/convertOtelTimeToSeconds.test.ts b/packages/node-experimental/test/utils/convertOtelTimeToSeconds.test.ts new file mode 100644 index 000000000000..4f4911cee0cb --- /dev/null +++ b/packages/node-experimental/test/utils/convertOtelTimeToSeconds.test.ts @@ -0,0 +1,9 @@ +import { convertOtelTimeToSeconds } from '../../src/utils/convertOtelTimeToSeconds'; + +describe('convertOtelTimeToSeconds', () => { + it('works', () => { + expect(convertOtelTimeToSeconds([0, 0])).toEqual(0); + expect(convertOtelTimeToSeconds([1000, 50])).toEqual(1000.00000005); + expect(convertOtelTimeToSeconds([1000, 505])).toEqual(1000.000000505); + }); +}); diff --git a/packages/node-experimental/test/utils/getActiveSpan.test.ts b/packages/node-experimental/test/utils/getActiveSpan.test.ts new file mode 100644 index 000000000000..b97ced5bdbf8 --- /dev/null +++ b/packages/node-experimental/test/utils/getActiveSpan.test.ts @@ -0,0 +1,157 @@ +import { trace } from '@opentelemetry/api'; +import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; + +import { NodeExperimentalClient } from '../../src/sdk/client'; +import { setupOtel } from '../../src/sdk/initOtel'; +import { getActiveSpan, getRootSpan } from '../../src/utils/getActiveSpan'; +import { getDefaultNodeExperimentalClientOptions } from '../helpers/getDefaultNodePreviewClientOptions'; +import { cleanupOtel } from '../helpers/mockSdkInit'; + +describe('getActiveSpan', () => { + let provider: BasicTracerProvider | undefined; + + beforeEach(() => { + const options = getDefaultNodeExperimentalClientOptions(); + const client = new NodeExperimentalClient(options); + provider = setupOtel(client); + }); + + afterEach(() => { + cleanupOtel(provider); + }); + + it('returns undefined if no span is active', () => { + const span = getActiveSpan(); + expect(span).toBeUndefined(); + }); + + it('returns undefined if no provider is active', async () => { + await provider?.forceFlush(); + await provider?.shutdown(); + provider = undefined; + + const span = getActiveSpan(); + expect(span).toBeUndefined(); + }); + + it('returns currently active span', () => { + const tracer = trace.getTracer('test'); + + expect(getActiveSpan()).toBeUndefined(); + + tracer.startActiveSpan('test', span => { + expect(getActiveSpan()).toBe(span); + + const inner1 = tracer.startSpan('inner1'); + + expect(getActiveSpan()).toBe(span); + + inner1.end(); + + tracer.startActiveSpan('inner2', inner2 => { + expect(getActiveSpan()).toBe(inner2); + + inner2.end(); + }); + + expect(getActiveSpan()).toBe(span); + + span.end(); + }); + + expect(getActiveSpan()).toBeUndefined(); + }); + + it('returns currently active span in concurrent spans', () => { + const tracer = trace.getTracer('test'); + + expect(getActiveSpan()).toBeUndefined(); + + tracer.startActiveSpan('test1', span => { + expect(getActiveSpan()).toBe(span); + + tracer.startActiveSpan('inner1', inner1 => { + expect(getActiveSpan()).toBe(inner1); + inner1.end(); + }); + + span.end(); + }); + + tracer.startActiveSpan('test2', span => { + expect(getActiveSpan()).toBe(span); + + tracer.startActiveSpan('inner2', inner => { + expect(getActiveSpan()).toBe(inner); + inner.end(); + }); + + span.end(); + }); + + expect(getActiveSpan()).toBeUndefined(); + }); +}); + +describe('getRootSpan', () => { + let provider: BasicTracerProvider | undefined; + + beforeEach(() => { + const options = getDefaultNodeExperimentalClientOptions(); + const client = new NodeExperimentalClient(options); + provider = setupOtel(client); + }); + + afterEach(async () => { + await provider?.forceFlush(); + await provider?.shutdown(); + }); + + it('returns currently active root span', () => { + const tracer = trace.getTracer('test'); + + tracer.startActiveSpan('test', span => { + expect(getRootSpan(span)).toBe(span); + + const inner1 = tracer.startSpan('inner1'); + + expect(getRootSpan(inner1)).toBe(span); + + inner1.end(); + + tracer.startActiveSpan('inner2', inner2 => { + expect(getRootSpan(inner2)).toBe(span); + + inner2.end(); + }); + + span.end(); + }); + }); + + it('returns currently active root span in concurrent spans', () => { + const tracer = trace.getTracer('test'); + + tracer.startActiveSpan('test1', span => { + expect(getRootSpan(span)).toBe(span); + + tracer.startActiveSpan('inner1', inner1 => { + expect(getRootSpan(inner1)).toBe(span); + inner1.end(); + }); + + span.end(); + }); + + tracer.startActiveSpan('test2', span => { + expect(getRootSpan(span)).toBe(span); + + tracer.startActiveSpan('inner2', inner => { + expect(getRootSpan(inner)).toBe(span); + inner.end(); + }); + + span.end(); + }); + }); +}); diff --git a/packages/node-experimental/test/utils/getRequestSpanData.test.ts b/packages/node-experimental/test/utils/getRequestSpanData.test.ts new file mode 100644 index 000000000000..0edd2befea6c --- /dev/null +++ b/packages/node-experimental/test/utils/getRequestSpanData.test.ts @@ -0,0 +1,59 @@ +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; + +import { getRequestSpanData } from '../../src/utils/getRequestSpanData'; +import { createSpan } from '../helpers/createSpan'; + +describe('getRequestSpanData', () => { + it('works with basic span', () => { + const span = createSpan(); + const data = getRequestSpanData(span); + + expect(data).toEqual({}); + }); + + it('works with http span', () => { + const span = createSpan(); + span.setAttributes({ + [SemanticAttributes.HTTP_URL]: 'http://example.com?foo=bar#baz', + [SemanticAttributes.HTTP_METHOD]: 'GET', + }); + + const data = getRequestSpanData(span); + + expect(data).toEqual({ + url: 'http://example.com', + 'http.method': 'GET', + 'http.query': '?foo=bar', + 'http.fragment': '#baz', + }); + }); + + it('works without method', () => { + const span = createSpan(); + span.setAttributes({ + [SemanticAttributes.HTTP_URL]: 'http://example.com', + }); + + const data = getRequestSpanData(span); + + expect(data).toEqual({ + url: 'http://example.com', + 'http.method': 'GET', + }); + }); + + it('works with incorrect URL', () => { + const span = createSpan(); + span.setAttributes({ + [SemanticAttributes.HTTP_URL]: 'malformed-url-here', + [SemanticAttributes.HTTP_METHOD]: 'GET', + }); + + const data = getRequestSpanData(span); + + expect(data).toEqual({ + url: 'malformed-url-here', + 'http.method': 'GET', + }); + }); +}); diff --git a/packages/node-experimental/test/utils/groupSpansWithParents.test.ts b/packages/node-experimental/test/utils/groupSpansWithParents.test.ts new file mode 100644 index 000000000000..d9a3fa60cb97 --- /dev/null +++ b/packages/node-experimental/test/utils/groupSpansWithParents.test.ts @@ -0,0 +1,123 @@ +import { groupSpansWithParents } from '../../src/utils/groupSpansWithParents'; +import { createSpan } from '../helpers/createSpan'; + +describe('groupSpansWithParents', () => { + it('works with no spans', () => { + const actual = groupSpansWithParents([]); + expect(actual).toEqual([]); + }); + + it('works with a single root span & in-order spans', () => { + const rootSpan = createSpan('root', { spanId: 'rootId' }); + const parentSpan1 = createSpan('parent1', { spanId: 'parent1Id', parentSpanId: 'rootId' }); + const parentSpan2 = createSpan('parent2', { spanId: 'parent2Id', parentSpanId: 'rootId' }); + const child1 = createSpan('child1', { spanId: 'child1', parentSpanId: 'parent1Id' }); + + const actual = groupSpansWithParents([rootSpan, parentSpan1, parentSpan2, child1]); + expect(actual).toHaveLength(4); + + // Ensure parent & span is correctly set + const rootRef = actual.find(ref => ref.span === rootSpan); + const parent1Ref = actual.find(ref => ref.span === parentSpan1); + const parent2Ref = actual.find(ref => ref.span === parentSpan2); + const child1Ref = actual.find(ref => ref.span === child1); + + expect(rootRef).toBeDefined(); + expect(parent1Ref).toBeDefined(); + expect(parent2Ref).toBeDefined(); + expect(child1Ref).toBeDefined(); + + expect(rootRef?.parentNode).toBeUndefined(); + expect(rootRef?.children).toEqual([parent1Ref, parent2Ref]); + + expect(parent1Ref?.span).toBe(parentSpan1); + expect(parent2Ref?.span).toBe(parentSpan2); + + expect(parent1Ref?.parentNode).toBe(rootRef); + expect(parent2Ref?.parentNode).toBe(rootRef); + + expect(parent1Ref?.children).toEqual([child1Ref]); + expect(parent2Ref?.children).toEqual([]); + + expect(child1Ref?.parentNode).toBe(parent1Ref); + expect(child1Ref?.children).toEqual([]); + }); + + it('works with a spans with missing root span', () => { + const parentSpan1 = createSpan('parent1', { spanId: 'parent1Id', parentSpanId: 'rootId' }); + const parentSpan2 = createSpan('parent2', { spanId: 'parent2Id', parentSpanId: 'rootId' }); + const child1 = createSpan('child1', { spanId: 'child1', parentSpanId: 'parent1Id' }); + + const actual = groupSpansWithParents([parentSpan1, parentSpan2, child1]); + expect(actual).toHaveLength(4); + + // Ensure parent & span is correctly set + const rootRef = actual.find(ref => ref.id === 'rootId'); + const parent1Ref = actual.find(ref => ref.span === parentSpan1); + const parent2Ref = actual.find(ref => ref.span === parentSpan2); + const child1Ref = actual.find(ref => ref.span === child1); + + expect(rootRef).toBeDefined(); + expect(parent1Ref).toBeDefined(); + expect(parent2Ref).toBeDefined(); + expect(child1Ref).toBeDefined(); + + expect(rootRef?.parentNode).toBeUndefined(); + expect(rootRef?.span).toBeUndefined(); + expect(rootRef?.children).toEqual([parent1Ref, parent2Ref]); + + expect(parent1Ref?.span).toBe(parentSpan1); + expect(parent2Ref?.span).toBe(parentSpan2); + + expect(parent1Ref?.parentNode).toBe(rootRef); + expect(parent2Ref?.parentNode).toBe(rootRef); + + expect(parent1Ref?.children).toEqual([child1Ref]); + expect(parent2Ref?.children).toEqual([]); + + expect(child1Ref?.parentNode).toBe(parent1Ref); + expect(child1Ref?.children).toEqual([]); + }); + + it('works with multiple root spans & out-of-order spans', () => { + const rootSpan1 = createSpan('root1', { spanId: 'root1Id' }); + const rootSpan2 = createSpan('root2', { spanId: 'root2Id' }); + const parentSpan1 = createSpan('parent1', { spanId: 'parent1Id', parentSpanId: 'root1Id' }); + const parentSpan2 = createSpan('parent2', { spanId: 'parent2Id', parentSpanId: 'root2Id' }); + const childSpan1 = createSpan('child1', { spanId: 'child1Id', parentSpanId: 'parent1Id' }); + + const actual = groupSpansWithParents([childSpan1, parentSpan1, parentSpan2, rootSpan2, rootSpan1]); + expect(actual).toHaveLength(5); + + // Ensure parent & span is correctly set + const root1Ref = actual.find(ref => ref.span === rootSpan1); + const root2Ref = actual.find(ref => ref.span === rootSpan2); + const parent1Ref = actual.find(ref => ref.span === parentSpan1); + const parent2Ref = actual.find(ref => ref.span === parentSpan2); + const child1Ref = actual.find(ref => ref.span === childSpan1); + + expect(root1Ref).toBeDefined(); + expect(root2Ref).toBeDefined(); + expect(parent1Ref).toBeDefined(); + expect(parent2Ref).toBeDefined(); + expect(child1Ref).toBeDefined(); + + expect(root1Ref?.parentNode).toBeUndefined(); + expect(root1Ref?.children).toEqual([parent1Ref]); + + expect(root2Ref?.parentNode).toBeUndefined(); + expect(root2Ref?.children).toEqual([parent2Ref]); + + expect(parent1Ref?.span).toBe(parentSpan1); + expect(parent2Ref?.span).toBe(parentSpan2); + + expect(parent1Ref?.parentNode).toBe(root1Ref); + expect(parent2Ref?.parentNode).toBe(root2Ref); + + expect(parent1Ref?.children).toEqual([child1Ref]); + expect(parent2Ref?.children).toEqual([]); + + expect(child1Ref?.parentNode).toBe(parent1Ref); + expect(child1Ref?.children).toEqual([]); + }); +}); diff --git a/packages/node-experimental/test/utils/setupEventContextTrace.test.ts b/packages/node-experimental/test/utils/setupEventContextTrace.test.ts new file mode 100644 index 000000000000..15d7f0976b9e --- /dev/null +++ b/packages/node-experimental/test/utils/setupEventContextTrace.test.ts @@ -0,0 +1,111 @@ +import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; +import { makeMain } from '@sentry/core'; + +import { NodeExperimentalClient } from '../../src/sdk/client'; +import { NodeExperimentalHub } from '../../src/sdk/hub'; +import { setupOtel } from '../../src/sdk/initOtel'; +import { startSpan } from '../../src/sdk/trace'; +import { setupEventContextTrace } from '../../src/utils/setupEventContextTrace'; +import { getDefaultNodeExperimentalClientOptions } from '../helpers/getDefaultNodePreviewClientOptions'; +import { cleanupOtel } from '../helpers/mockSdkInit'; + +const PUBLIC_DSN = 'https://username@domain/123'; + +describe('setupEventContextTrace', () => { + const beforeSend = jest.fn(() => null); + let client: NodeExperimentalClient; + let hub: NodeExperimentalHub; + let provider: BasicTracerProvider | undefined; + + beforeEach(() => { + client = new NodeExperimentalClient( + getDefaultNodeExperimentalClientOptions({ + sampleRate: 1, + enableTracing: true, + beforeSend, + debug: true, + dsn: PUBLIC_DSN, + }), + ); + + hub = new NodeExperimentalHub(client); + makeMain(hub); + + setupEventContextTrace(client); + provider = setupOtel(client); + }); + + afterEach(() => { + beforeSend.mockReset(); + cleanupOtel(provider); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('works with no active span', async () => { + const error = new Error('test'); + hub.captureException(error); + await client.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(1); + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + contexts: expect.objectContaining({ + trace: { + span_id: expect.any(String), + trace_id: expect.any(String), + }, + }), + }), + expect.objectContaining({ + event_id: expect.any(String), + originalException: error, + syntheticException: expect.any(Error), + }), + ); + }); + + it('works with active span', async () => { + const error = new Error('test'); + + let outerId: string | undefined; + let innerId: string | undefined; + let traceId: string | undefined; + + startSpan({ name: 'outer' }, outerSpan => { + outerId = outerSpan?.spanContext().spanId; + traceId = outerSpan?.spanContext().traceId; + + startSpan({ name: 'inner' }, innerSpan => { + innerId = innerSpan?.spanContext().spanId; + hub.captureException(error); + }); + }); + + await client.flush(); + + expect(outerId).toBeDefined(); + expect(innerId).toBeDefined(); + expect(traceId).toBeDefined(); + + expect(beforeSend).toHaveBeenCalledTimes(1); + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + contexts: expect.objectContaining({ + trace: { + span_id: innerId, + parent_span_id: outerId, + trace_id: traceId, + }, + }), + }), + expect.objectContaining({ + event_id: expect.any(String), + originalException: error, + syntheticException: expect.any(Error), + }), + ); + }); +}); diff --git a/packages/node-experimental/test/utils/spanTypes.test.ts b/packages/node-experimental/test/utils/spanTypes.test.ts new file mode 100644 index 000000000000..fcd4703db9ce --- /dev/null +++ b/packages/node-experimental/test/utils/spanTypes.test.ts @@ -0,0 +1,98 @@ +import type { Span } from '@opentelemetry/api'; + +import { + spanHasAttributes, + spanHasEvents, + spanHasKind, + spanHasParentId, + spanIsSdkTraceBaseSpan, +} from '../../src/utils/spanTypes'; +import { createSpan } from '../helpers/createSpan'; + +describe('spanTypes', () => { + describe('spanHasAttributes', () => { + it.each([ + [{}, false], + [{ attributes: null }, false], + [{ attributes: {} }, true], + ])('works with %p', (span, expected) => { + const castSpan = span as unknown as Span; + const actual = spanHasAttributes(castSpan); + + expect(actual).toBe(expected); + + if (actual) { + expect(castSpan.attributes).toBeDefined(); + } + }); + }); + + describe('spanHasKind', () => { + it.each([ + [{}, false], + [{ kind: null }, false], + [{ kind: 'TEST_KIND' }, true], + ])('works with %p', (span, expected) => { + const castSpan = span as unknown as Span; + const actual = spanHasKind(castSpan); + + expect(actual).toBe(expected); + + if (actual) { + expect(castSpan.kind).toBeDefined(); + } + }); + }); + + describe('spanHasParentId', () => { + it.each([ + [{}, false], + [{ parentSpanId: null }, false], + [{ parentSpanId: 'TEST_PARENT_ID' }, true], + ])('works with %p', (span, expected) => { + const castSpan = span as unknown as Span; + const actual = spanHasParentId(castSpan); + + expect(actual).toBe(expected); + + if (actual) { + expect(castSpan.parentSpanId).toBeDefined(); + } + }); + }); + + describe('spanHasEvents', () => { + it.each([ + [{}, false], + [{ events: null }, false], + [{ events: [] }, true], + ])('works with %p', (span, expected) => { + const castSpan = span as unknown as Span; + const actual = spanHasEvents(castSpan); + + expect(actual).toBe(expected); + + if (actual) { + expect(castSpan.events).toBeDefined(); + } + }); + }); + + describe('spanIsSdkTraceBaseSpan', () => { + it.each([ + [{}, false], + [createSpan(), true], + ])('works with %p', (span, expected) => { + const castSpan = span as unknown as Span; + const actual = spanIsSdkTraceBaseSpan(castSpan); + + expect(actual).toBe(expected); + + if (actual) { + expect(castSpan.events).toBeDefined(); + expect(castSpan.attributes).toBeDefined(); + expect(castSpan.kind).toBeDefined(); + } + }); + }); +}); diff --git a/packages/node-integration-tests/suites/anr/basic.js b/packages/node-integration-tests/suites/anr/basic.js index 3abadc09b9c3..45a324e507c5 100644 --- a/packages/node-integration-tests/suites/anr/basic.js +++ b/packages/node-integration-tests/suites/anr/basic.js @@ -10,13 +10,14 @@ setTimeout(() => { Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', + debug: true, beforeSend: event => { // eslint-disable-next-line no-console console.log(JSON.stringify(event)); }, }); -Sentry.enableAnrDetection({ captureStackTrace: true, anrThreshold: 200, debug: true }).then(() => { +Sentry.enableAnrDetection({ captureStackTrace: true, anrThreshold: 200 }).then(() => { function longWork() { for (let i = 0; i < 100; i++) { const salt = crypto.randomBytes(128).toString('base64'); diff --git a/packages/node-integration-tests/suites/anr/basic.mjs b/packages/node-integration-tests/suites/anr/basic.mjs index ba9c8623da7e..1d89ac1b3989 100644 --- a/packages/node-integration-tests/suites/anr/basic.mjs +++ b/packages/node-integration-tests/suites/anr/basic.mjs @@ -10,13 +10,14 @@ setTimeout(() => { Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', + debug: true, beforeSend: event => { // eslint-disable-next-line no-console console.log(JSON.stringify(event)); }, }); -await Sentry.enableAnrDetection({ captureStackTrace: true, anrThreshold: 200, debug: true }); +await Sentry.enableAnrDetection({ captureStackTrace: true, anrThreshold: 200 }); function longWork() { for (let i = 0; i < 100; i++) { diff --git a/packages/node-integration-tests/suites/anr/forked.js b/packages/node-integration-tests/suites/anr/forked.js index 3abadc09b9c3..45a324e507c5 100644 --- a/packages/node-integration-tests/suites/anr/forked.js +++ b/packages/node-integration-tests/suites/anr/forked.js @@ -10,13 +10,14 @@ setTimeout(() => { Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', + debug: true, beforeSend: event => { // eslint-disable-next-line no-console console.log(JSON.stringify(event)); }, }); -Sentry.enableAnrDetection({ captureStackTrace: true, anrThreshold: 200, debug: true }).then(() => { +Sentry.enableAnrDetection({ captureStackTrace: true, anrThreshold: 200 }).then(() => { function longWork() { for (let i = 0; i < 100; i++) { const salt = crypto.randomBytes(128).toString('base64'); diff --git a/packages/node-integration-tests/suites/anr/test.ts b/packages/node-integration-tests/suites/anr/test.ts index 4fd83c0b3205..96d83c64a6a7 100644 --- a/packages/node-integration-tests/suites/anr/test.ts +++ b/packages/node-integration-tests/suites/anr/test.ts @@ -5,6 +5,22 @@ import * as path from 'path'; const NODE_VERSION = parseSemver(process.versions.node).major || 0; +/** The output will contain logging so we need to find the line that parses as JSON */ +function parseJsonLine(input: string): T { + return ( + input + .split('\n') + .map(line => { + try { + return JSON.parse(line) as T; + } catch { + return undefined; + } + }) + .filter(a => a) as T[] + )[0]; +} + describe('should report ANR when event loop blocked', () => { test('CJS', done => { // The stack trace is different when node < 12 @@ -15,7 +31,7 @@ describe('should report ANR when event loop blocked', () => { const testScriptPath = path.resolve(__dirname, 'basic.js'); childProcess.exec(`node ${testScriptPath}`, { encoding: 'utf8' }, (_, stdout) => { - const event = JSON.parse(stdout) as Event; + const event = parseJsonLine(stdout); expect(event.exception?.values?.[0].mechanism).toEqual({ type: 'ANR' }); expect(event.exception?.values?.[0].type).toEqual('ApplicationNotResponding'); @@ -42,7 +58,7 @@ describe('should report ANR when event loop blocked', () => { const testScriptPath = path.resolve(__dirname, 'basic.mjs'); childProcess.exec(`node ${testScriptPath}`, { encoding: 'utf8' }, (_, stdout) => { - const event = JSON.parse(stdout) as Event; + const event = parseJsonLine(stdout); expect(event.exception?.values?.[0].mechanism).toEqual({ type: 'ANR' }); expect(event.exception?.values?.[0].type).toEqual('ApplicationNotResponding'); @@ -64,7 +80,7 @@ describe('should report ANR when event loop blocked', () => { const testScriptPath = path.resolve(__dirname, 'forker.js'); childProcess.exec(`node ${testScriptPath}`, { encoding: 'utf8' }, (_, stdout) => { - const event = JSON.parse(stdout) as Event; + const event = parseJsonLine(stdout); expect(event.exception?.values?.[0].mechanism).toEqual({ type: 'ANR' }); expect(event.exception?.values?.[0].type).toEqual('ApplicationNotResponding'); diff --git a/packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutCallback/scenario.ts b/packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutCallback/scenario.ts index a47f05203d35..ac1d6421dec8 100644 --- a/packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutCallback/scenario.ts +++ b/packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutCallback/scenario.ts @@ -13,6 +13,12 @@ const connection = mysql.createConnection({ password: 'docker', }); +connection.connect(function (err: unknown) { + if (err) { + return; + } +}); + const transaction = Sentry.startTransaction({ op: 'transaction', name: 'Test Transaction', @@ -22,10 +28,18 @@ Sentry.configureScope(scope => { scope.setSpan(transaction); }); -connection.query('SELECT 1 + 1 AS solution'); -connection.query('SELECT NOW()', ['1', '2']); +const query = connection.query('SELECT 1 + 1 AS solution'); +const query2 = connection.query('SELECT NOW()', ['1', '2']); + +query.on('end', () => { + transaction.setTag('result_done', 'yes'); -// Wait a bit to ensure the queries completed -setTimeout(() => { - transaction.finish(); -}, 500); + query2.on('end', () => { + transaction.setTag('result_done2', 'yes'); + + // Wait a bit to ensure the queries completed + setTimeout(() => { + transaction.finish(); + }, 500); + }); +}); diff --git a/packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutCallback/test.ts b/packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutCallback/test.ts index 9e58b59fecad..ccc5df1c4739 100644 --- a/packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutCallback/test.ts +++ b/packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutCallback/test.ts @@ -8,6 +8,10 @@ test('should auto-instrument `mysql` package when using query without callback', assertSentryTransaction(envelope[2], { transaction: 'Test Transaction', + tags: { + result_done: 'yes', + result_done2: 'yes', + }, spans: [ { description: 'SELECT 1 + 1 AS solution', diff --git a/packages/node/src/anr/debugger.ts b/packages/node/src/anr/debugger.ts index 4d4a2799fa64..01b2a90fe0f9 100644 --- a/packages/node/src/anr/debugger.ts +++ b/packages/node/src/anr/debugger.ts @@ -1,33 +1,10 @@ import type { StackFrame } from '@sentry/types'; -import { dropUndefinedKeys, filenameIsInApp } from '@sentry/utils'; +import { createDebugPauseMessageHandler } from '@sentry/utils'; import type { Debugger } from 'inspector'; import { getModuleFromFilename } from '../module'; import { createWebSocketClient } from './websocket'; -/** - * Converts Debugger.CallFrame to Sentry StackFrame - */ -function callFrameToStackFrame( - frame: Debugger.CallFrame, - filenameFromScriptId: (id: string) => string | undefined, -): StackFrame { - const filename = filenameFromScriptId(frame.location.scriptId)?.replace(/^file:\/\//, ''); - - // CallFrame row/col are 0 based, whereas StackFrame are 1 based - const colno = frame.location.columnNumber ? frame.location.columnNumber + 1 : undefined; - const lineno = frame.location.lineNumber ? frame.location.lineNumber + 1 : undefined; - - return dropUndefinedKeys({ - filename, - module: getModuleFromFilename(filename), - function: frame.functionName || '?', - colno, - lineno, - in_app: filename ? filenameIsInApp(filename) : undefined, - }); -} - // The only messages we care about type DebugMessage = | { @@ -45,7 +22,7 @@ type DebugMessage = async function webSocketDebugger( url: string, onMessage: (message: DebugMessage) => void, -): Promise<(method: string, params?: unknown) => void> { +): Promise<(method: string) => void> { let id = 0; const webSocket = await createWebSocketClient(url); @@ -54,8 +31,8 @@ async function webSocketDebugger( onMessage(message); }); - return (method: string, params?: unknown) => { - webSocket.send(JSON.stringify({ id: id++, method, params })); + return (method: string) => { + webSocket.send(JSON.stringify({ id: id++, method })); }; } @@ -66,27 +43,10 @@ async function webSocketDebugger( * @returns A function that triggers the debugger to pause and capture a stack trace */ export async function captureStackTrace(url: string, callback: (frames: StackFrame[]) => void): Promise<() => void> { - // Collect scriptId -> url map so we can look up the filenames later - const scripts = new Map(); - - const sendCommand = await webSocketDebugger(url, message => { - if (message.method === 'Debugger.scriptParsed') { - scripts.set(message.params.scriptId, message.params.url); - } else if (message.method === 'Debugger.paused') { - // copy the frames - const callFrames = [...message.params.callFrames]; - // and resume immediately! - sendCommand('Debugger.resume'); - sendCommand('Debugger.disable'); - - const frames = callFrames - .map(frame => callFrameToStackFrame(frame, id => scripts.get(id))) - // Sentry expects the frames to be in the opposite order - .reverse(); - - callback(frames); - } - }); + const sendCommand: (method: string) => void = await webSocketDebugger( + url, + createDebugPauseMessageHandler(cmd => sendCommand(cmd), getModuleFromFilename, callback), + ); return () => { sendCommand('Debugger.enable'); diff --git a/packages/node/src/anr/index.ts b/packages/node/src/anr/index.ts index 5c4edd808aa3..1d4d61b78b55 100644 --- a/packages/node/src/anr/index.ts +++ b/packages/node/src/anr/index.ts @@ -1,7 +1,6 @@ import type { Event, StackFrame } from '@sentry/types'; -import { logger } from '@sentry/utils'; +import { logger, watchdogTimer } from '@sentry/utils'; import { spawn } from 'child_process'; -import * as inspector from 'inspector'; import { addGlobalEventProcessor, captureEvent, flush } from '..'; import { captureStackTrace } from './debugger'; @@ -9,36 +8,6 @@ import { captureStackTrace } from './debugger'; const DEFAULT_INTERVAL = 50; const DEFAULT_HANG_THRESHOLD = 5000; -/** - * A node.js watchdog timer - * @param pollInterval The interval that we expect to get polled at - * @param anrThreshold The threshold for when we consider ANR - * @param callback The callback to call for ANR - * @returns A function to call to reset the timer - */ -function watchdogTimer(pollInterval: number, anrThreshold: number, callback: () => void): () => void { - let lastPoll = process.hrtime(); - let triggered = false; - - setInterval(() => { - const [seconds, nanoSeconds] = process.hrtime(lastPoll); - const diffMs = Math.floor(seconds * 1e3 + nanoSeconds / 1e6); - - if (triggered === false && diffMs > pollInterval + anrThreshold) { - triggered = true; - callback(); - } - - if (diffMs < pollInterval + anrThreshold) { - triggered = false; - } - }, 20); - - return () => { - lastPoll = process.hrtime(); - }; -} - interface Options { /** * The app entry script. This is used to run the same script as the child process. @@ -67,7 +36,7 @@ interface Options { */ captureStackTrace: boolean; /** - * Log debug information. + * @deprecated Use 'init' debug option instead */ debug: boolean; } @@ -98,12 +67,19 @@ function sendEvent(blockedMs: number, frames?: StackFrame[]): void { }); } +interface InspectorApi { + open: (port: number) => void; + url: () => string | undefined; +} + /** * Starts the node debugger and returns the inspector url. * * When inspector.url() returns undefined, it means the port is already in use so we try the next port. */ function startInspector(startPort: number = 9229): string | undefined { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const inspector: InspectorApi = require('inspector'); let inspectorUrl: string | undefined = undefined; let port = startPort; @@ -118,9 +94,7 @@ function startInspector(startPort: number = 9229): string | undefined { function startChildProcess(options: Options): void { function log(message: string, ...args: unknown[]): void { - if (options.debug) { - logger.log(`[ANR] ${message}`, ...args); - } + logger.log(`[ANR] ${message}`, ...args); } try { @@ -135,7 +109,7 @@ function startChildProcess(options: Options): void { const child = spawn(process.execPath, [options.entryScript], { env, - stdio: options.debug ? ['inherit', 'inherit', 'inherit', 'ipc'] : ['ignore', 'ignore', 'ignore', 'ipc'], + stdio: logger.isEnabled() ? ['inherit', 'inherit', 'inherit', 'ipc'] : ['ignore', 'ignore', 'ignore', 'ipc'], }); // The child process should not keep the main process alive child.unref(); @@ -166,9 +140,7 @@ function startChildProcess(options: Options): void { function handleChildProcess(options: Options): void { function log(message: string): void { - if (options.debug) { - logger.log(`[ANR child process] ${message}`); - } + logger.log(`[ANR child process] ${message}`); } process.title = 'sentry-anr'; @@ -210,10 +182,10 @@ function handleChildProcess(options: Options): void { } } - const ping = watchdogTimer(options.pollInterval, options.anrThreshold, watchdogTimeout); + const { poll } = watchdogTimer(options.pollInterval, options.anrThreshold, watchdogTimeout); process.on('message', () => { - ping(); + poll(); }); } @@ -257,6 +229,7 @@ export function enableAnrDetection(options: Partial): Promise { pollInterval: options.pollInterval || DEFAULT_INTERVAL, anrThreshold: options.anrThreshold || DEFAULT_HANG_THRESHOLD, captureStackTrace: !!options.captureStackTrace, + // eslint-disable-next-line deprecation/deprecation debug: !!options.debug, }; diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 503f2749ea29..b0ab9dffefcb 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -26,6 +26,7 @@ export type { NodeOptions } from './types'; export { addGlobalEventProcessor, addBreadcrumb, + addIntegration, captureException, captureEvent, captureMessage, @@ -61,6 +62,7 @@ export { startActiveSpan, startInactiveSpan, startSpanManual, + continueTrace, } from '@sentry/core'; export type { SpanStatusType } from '@sentry/core'; export { autoDiscoverNodePerformanceMonitoringIntegrations } from './tracing'; diff --git a/packages/node/src/module.ts b/packages/node/src/module.ts index 44bff87a02d2..6085813e6035 100644 --- a/packages/node/src/module.ts +++ b/packages/node/src/module.ts @@ -30,8 +30,8 @@ export function getModuleFromFilename( // It's specifically a module let file = basename; - if (ext === '.js') { - file = file.slice(0, file.length - '.js'.length); + if (ext === '.js' || ext === '.mjs' || ext === '.cjs') { + file = file.slice(0, ext.length * -1); } if (!root && !dir) { diff --git a/packages/node/test/module.test.ts b/packages/node/test/module.test.ts index e27f1482ff90..89c8878a433e 100644 --- a/packages/node/test/module.test.ts +++ b/packages/node/test/module.test.ts @@ -27,4 +27,16 @@ describe('getModuleFromFilename', () => { expect(getModuleFromFilename('/Users/users/Tim/Desktop/node_modules/module.js')).toEqual('module'); }, '/Users/Tim/app.js'); }); + + test('POSIX .mjs', () => { + withFilename(() => { + expect(getModuleFromFilename('/Users/users/Tim/Desktop/node_modules/module.mjs')).toEqual('module'); + }, '/Users/Tim/app.js'); + }); + + test('POSIX .cjs', () => { + withFilename(() => { + expect(getModuleFromFilename('/Users/users/Tim/Desktop/node_modules/module.cjs')).toEqual('module'); + }, '/Users/Tim/app.js'); + }); }); diff --git a/packages/opentelemetry-node/src/index.ts b/packages/opentelemetry-node/src/index.ts index 630acd960059..7ed84c517b45 100644 --- a/packages/opentelemetry-node/src/index.ts +++ b/packages/opentelemetry-node/src/index.ts @@ -1,19 +1,10 @@ -import { getSentrySpan } from './utils/spanMap'; - export { SentrySpanProcessor } from './spanprocessor'; export { SentryPropagator } from './propagator'; +export { maybeCaptureExceptionForTimedEvent } from './utils/captureExceptionForTimedEvent'; +export { parseOtelSpanDescription } from './utils/parseOtelSpanDescription'; +export { mapOtelStatus } from './utils/mapOtelStatus'; /* eslint-disable deprecation/deprecation */ export { addOtelSpanData, getOtelSpanData, clearOtelSpanData } from './utils/spanData'; export type { AdditionalOtelSpanData } from './utils/spanData'; /* eslint-enable deprecation/deprecation */ - -/** - * This is only exported for internal use. - * Semver etc. does not apply here, this is subject to change at any time! - * This is explicitly _NOT_ public because we may have to change the underlying way we store/handle spans, - * which may make this API unusable without further notice. - * - * @private - */ -export { getSentrySpan as _INTERNAL_getSentrySpan }; diff --git a/packages/opentelemetry-node/src/spanprocessor.ts b/packages/opentelemetry-node/src/spanprocessor.ts index 012ead8b9d3d..671cdbb7894a 100644 --- a/packages/opentelemetry-node/src/spanprocessor.ts +++ b/packages/opentelemetry-node/src/spanprocessor.ts @@ -10,7 +10,7 @@ import { SENTRY_DYNAMIC_SAMPLING_CONTEXT_KEY, SENTRY_TRACE_PARENT_CONTEXT_KEY } import { maybeCaptureExceptionForTimedEvent } from './utils/captureExceptionForTimedEvent'; import { isSentryRequestSpan } from './utils/isSentryRequest'; import { mapOtelStatus } from './utils/mapOtelStatus'; -import { parseSpanDescription } from './utils/parseOtelSpanDescription'; +import { parseOtelSpanDescription } from './utils/parseOtelSpanDescription'; import { clearSpan, getSentrySpan, setSentrySpan } from './utils/spanMap'; /** @@ -182,7 +182,7 @@ function getTraceData(otelSpan: OtelSpan, parentContext: Context): Partial { + errorHandler: (err: Error & { __rrweb__?: boolean }) => { try { - // @ts-expect-error Set this so that replay SDK can ignore errors originating from rrweb err.__rrweb__ = true; - } catch { - // avoid any potential hazards here + } catch (error) { + // ignore errors here + // this can happen if the error is frozen or does not allow mutation for other reasons } - // return true to suppress throwing the error inside of rrweb - return true; }, }; diff --git a/packages/serverless/src/awslambda.ts b/packages/serverless/src/awslambda.ts index e8847fc7b212..e29f77aaf879 100644 --- a/packages/serverless/src/awslambda.ts +++ b/packages/serverless/src/awslambda.ts @@ -2,7 +2,7 @@ import type { Scope } from '@sentry/node'; import * as Sentry from '@sentry/node'; import { captureException, captureMessage, flush, getCurrentHub, withScope } from '@sentry/node'; -import type { Integration } from '@sentry/types'; +import type { Integration, SdkMetadata } from '@sentry/types'; import { isString, logger, tracingContextFromHeaders } from '@sentry/utils'; // NOTE: I have no idea how to fix this right now, and don't want to waste more time, as it builds just fine — Kamil // eslint-disable-next-line import/no-unresolved @@ -34,7 +34,9 @@ export type AsyncHandler = ( export interface WrapperOptions { flushTimeout: number; - // TODO: DEPRECATED - remove `rethrowAfterCapture` in v7 + /** + * @deprecated This option is unused since v6 and will be removed in v8. + */ rethrowAfterCapture?: boolean; callbackWaitsForEmptyEventLoop: boolean; captureTimeoutWarning: boolean; @@ -45,6 +47,12 @@ export interface WrapperOptions { * @default false */ captureAllSettledReasons: boolean; + /** + * Automatically trace all handler invocations. + * You may want to disable this if you use express within Lambda (use tracingHandler instead). + * @default true + */ + startTrace: boolean; } export const defaultIntegrations: Integration[] = [...Sentry.defaultIntegrations, new AWSServices({ optional: true })]; @@ -61,12 +69,13 @@ interface AWSLambdaOptions extends Sentry.NodeOptions { * @see {@link Sentry.init} */ export function init(options: AWSLambdaOptions = {}): void { - if (options.defaultIntegrations === undefined) { - options.defaultIntegrations = defaultIntegrations; - } + const opts = { + _metadata: {} as SdkMetadata, + defaultIntegrations, + ...options, + }; - options._metadata = options._metadata || {}; - options._metadata.sdk = { + opts._metadata.sdk = opts._metadata.sdk || { name: 'sentry.javascript.serverless', integrations: ['AWSLambda'], packages: [ @@ -78,7 +87,7 @@ export function init(options: AWSLambdaOptions = {}): void { version: Sentry.SDK_VERSION, }; - Sentry.init(options); + Sentry.init(opts); Sentry.addGlobalEventProcessor(serverlessEventProcessor); } @@ -175,11 +184,6 @@ function tryGetRemainingTimeInMillis(context: Context): number { * @param startTime performance.now() when wrapHandler was invoked */ function enhanceScopeWithEnvironmentData(scope: Scope, context: Context, startTime: number): void { - scope.setTransactionName(context.functionName); - - scope.setTag('server_name', process.env._AWS_XRAY_DAEMON_ADDRESS || process.env.SENTRY_NAME || hostname()); - scope.setTag('url', `awslambda:///${context.functionName}`); - scope.setContext('aws.lambda', { aws_request_id: context.awsRequestId, function_name: context.functionName, @@ -201,6 +205,18 @@ function enhanceScopeWithEnvironmentData(scope: Scope, context: Context, startTi }); } +/** + * Adds additional transaction-related information from the environment and AWS Context to the Sentry Scope. + * + * @param scope Scope that should be enhanced + * @param context AWS Lambda context that will be used to extract some part of the data + */ +function enhanceScopeWithTransactionData(scope: Scope, context: Context): void { + scope.setTransactionName(context.functionName); + scope.setTag('server_name', process.env._AWS_XRAY_DAEMON_ADDRESS || process.env.SENTRY_NAME || hostname()); + scope.setTag('url', `awslambda:///${context.functionName}`); +} + /** * Wraps a lambda handler adding it error capture and tracing capabilities. * @@ -219,6 +235,7 @@ export function wrapHandler( captureTimeoutWarning: true, timeoutWarningLimit: 500, captureAllSettledReasons: false, + startTrace: true, ...wrapOptions, }; let timeoutWarningTimer: NodeJS.Timeout; @@ -276,36 +293,42 @@ export function wrapHandler( const hub = getCurrentHub(); - const eventWithHeaders = event as { headers?: { [key: string]: string } }; - - const sentryTrace = - eventWithHeaders.headers && isString(eventWithHeaders.headers['sentry-trace']) - ? eventWithHeaders.headers['sentry-trace'] - : undefined; - const baggage = eventWithHeaders.headers?.baggage; - const { traceparentData, dynamicSamplingContext, propagationContext } = tracingContextFromHeaders( - sentryTrace, - baggage, - ); - hub.getScope().setPropagationContext(propagationContext); - - const transaction = hub.startTransaction({ - name: context.functionName, - op: 'function.aws.lambda', - origin: 'auto.function.serverless', - ...traceparentData, - metadata: { - dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext, - source: 'component', - }, - }) as Sentry.Transaction | undefined; + let transaction: Sentry.Transaction | undefined; + if (options.startTrace) { + const eventWithHeaders = event as { headers?: { [key: string]: string } }; + + const sentryTrace = + eventWithHeaders.headers && isString(eventWithHeaders.headers['sentry-trace']) + ? eventWithHeaders.headers['sentry-trace'] + : undefined; + const baggage = eventWithHeaders.headers?.baggage; + const { traceparentData, dynamicSamplingContext, propagationContext } = tracingContextFromHeaders( + sentryTrace, + baggage, + ); + hub.getScope().setPropagationContext(propagationContext); + + transaction = hub.startTransaction({ + name: context.functionName, + op: 'function.aws.lambda', + origin: 'auto.function.serverless', + ...traceparentData, + metadata: { + dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext, + source: 'component', + }, + }); + } const scope = hub.pushScope(); let rv: TResult; try { enhanceScopeWithEnvironmentData(scope, context, START_TIME); - // We put the transaction on the scope so users can attach children to it - scope.setSpan(transaction); + if (options.startTrace) { + enhanceScopeWithTransactionData(scope, context); + // We put the transaction on the scope so users can attach children to it + scope.setSpan(transaction); + } rv = await asyncHandler(event, context); // We manage lambdas that use Promise.allSettled by capturing the errors of failed promises diff --git a/packages/serverless/src/gcpfunction/index.ts b/packages/serverless/src/gcpfunction/index.ts index 12e912d45b77..aa8f800d0d52 100644 --- a/packages/serverless/src/gcpfunction/index.ts +++ b/packages/serverless/src/gcpfunction/index.ts @@ -1,5 +1,5 @@ import * as Sentry from '@sentry/node'; -import type { Integration } from '@sentry/types'; +import type { Integration, SdkMetadata } from '@sentry/types'; import { GoogleCloudGrpc } from '../google-cloud-grpc'; import { GoogleCloudHttp } from '../google-cloud-http'; @@ -19,12 +19,13 @@ export const defaultIntegrations: Integration[] = [ * @see {@link Sentry.init} */ export function init(options: Sentry.NodeOptions = {}): void { - if (options.defaultIntegrations === undefined) { - options.defaultIntegrations = defaultIntegrations; - } + const opts = { + _metadata: {} as SdkMetadata, + defaultIntegrations, + ...options, + }; - options._metadata = options._metadata || {}; - options._metadata.sdk = { + opts._metadata.sdk = opts._metadata.sdk || { name: 'sentry.javascript.serverless', integrations: ['GCPFunction'], packages: [ @@ -36,6 +37,6 @@ export function init(options: Sentry.NodeOptions = {}): void { version: Sentry.SDK_VERSION, }; - Sentry.init(options); + Sentry.init(opts); Sentry.addGlobalEventProcessor(serverlessEventProcessor); } diff --git a/packages/serverless/src/index.ts b/packages/serverless/src/index.ts index a17d0463202d..62fc55012719 100644 --- a/packages/serverless/src/index.ts +++ b/packages/serverless/src/index.ts @@ -15,6 +15,7 @@ export { Scope, addBreadcrumb, addGlobalEventProcessor, + addIntegration, autoDiscoverNodePerformanceMonitoringIntegrations, captureEvent, captureException, @@ -56,4 +57,5 @@ export { startActiveSpan, startInactiveSpan, startSpanManual, + continueTrace, } from '@sentry/node'; diff --git a/packages/serverless/test/__mocks__/@sentry/node.ts b/packages/serverless/test/__mocks__/@sentry/node.ts index 6da20c091780..c29f8f78dd0a 100644 --- a/packages/serverless/test/__mocks__/@sentry/node.ts +++ b/packages/serverless/test/__mocks__/@sentry/node.ts @@ -50,6 +50,7 @@ export const resetMocks = (): void => { fakeHub.pushScope.mockClear(); fakeHub.popScope.mockClear(); fakeHub.getScope.mockClear(); + fakeHub.startTransaction.mockClear(); fakeScope.addEventProcessor.mockClear(); fakeScope.setTransactionName.mockClear(); diff --git a/packages/serverless/test/awslambda.test.ts b/packages/serverless/test/awslambda.test.ts index 454b36296adb..199d0ac295ab 100644 --- a/packages/serverless/test/awslambda.test.ts +++ b/packages/serverless/test/awslambda.test.ts @@ -45,6 +45,8 @@ function expectScopeSettings(fakeTransactionContext: any) { // @ts-expect-error see "Why @ts-expect-error" note const fakeTransaction = { ...SentryNode.fakeTransaction, ...fakeTransactionContext }; // @ts-expect-error see "Why @ts-expect-error" note + expect(SentryNode.fakeScope.setTransactionName).toBeCalledWith('functionName'); + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeScope.setSpan).toBeCalledWith(fakeTransaction); // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeScope.setTag).toBeCalledWith('server_name', expect.anything()); @@ -180,11 +182,27 @@ describe('AWSLambda', () => { expect(SentryNode.captureException).toHaveBeenNthCalledWith(2, error2, expect.any(Function)); expect(SentryNode.captureException).toBeCalledTimes(2); }); + + // "wrapHandler() ... successful execution" tests the default of startTrace enabled + test('startTrace disabled', async () => { + expect.assertions(3); + + const handler: Handler = async (_event, _context) => 42; + const wrappedHandler = wrapHandler(handler, { startTrace: false }); + await wrappedHandler(fakeEvent, fakeContext, fakeCallback); + + // @ts-expect-error see "Why @ts-expect-error" note + expect(SentryNode.fakeScope.setTransactionName).toBeCalledTimes(0); + // @ts-expect-error see "Why @ts-expect-error" note + expect(SentryNode.fakeScope.setTag).toBeCalledTimes(0); + // @ts-expect-error see "Why @ts-expect-error" note + expect(SentryNode.fakeHub.startTransaction).toBeCalledTimes(0); + }); }); describe('wrapHandler() on sync handler', () => { test('successful execution', async () => { - expect.assertions(9); + expect.assertions(10); const handler: Handler = (_event, _context, callback) => { callback(null, 42); @@ -209,7 +227,7 @@ describe('AWSLambda', () => { }); test('unsuccessful execution', async () => { - expect.assertions(9); + expect.assertions(10); const error = new Error('sorry'); const handler: Handler = (_event, _context, callback) => { @@ -284,7 +302,7 @@ describe('AWSLambda', () => { }); test('capture error', async () => { - expect.assertions(9); + expect.assertions(10); const error = new Error('wat'); const handler: Handler = (_event, _context, _callback) => { @@ -319,7 +337,7 @@ describe('AWSLambda', () => { describe('wrapHandler() on async handler', () => { test('successful execution', async () => { - expect.assertions(9); + expect.assertions(10); const handler: Handler = async (_event, _context) => { return 42; @@ -355,7 +373,7 @@ describe('AWSLambda', () => { }); test('capture error', async () => { - expect.assertions(9); + expect.assertions(10); const error = new Error('wat'); const handler: Handler = async (_event, _context) => { @@ -401,7 +419,7 @@ describe('AWSLambda', () => { describe('wrapHandler() on async handler with a callback method (aka incorrect usage)', () => { test('successful execution', async () => { - expect.assertions(9); + expect.assertions(10); const handler: Handler = async (_event, _context, _callback) => { return 42; @@ -437,7 +455,7 @@ describe('AWSLambda', () => { }); test('capture error', async () => { - expect.assertions(9); + expect.assertions(10); const error = new Error('wat'); const handler: Handler = async (_event, _context, _callback) => { diff --git a/packages/svelte/src/sdk.ts b/packages/svelte/src/sdk.ts index 3a7c671a7d1d..c09e101a72c4 100644 --- a/packages/svelte/src/sdk.ts +++ b/packages/svelte/src/sdk.ts @@ -1,13 +1,17 @@ import type { BrowserOptions } from '@sentry/browser'; import { addGlobalEventProcessor, init as browserInit, SDK_VERSION } from '@sentry/browser'; -import type { EventProcessor } from '@sentry/types'; +import type { EventProcessor, SdkMetadata } from '@sentry/types'; import { getDomElement } from '@sentry/utils'; /** * Inits the Svelte SDK */ export function init(options: BrowserOptions): void { - options._metadata = options._metadata || {}; - options._metadata.sdk = options._metadata.sdk || { + const opts = { + _metadata: {} as SdkMetadata, + ...options, + }; + + opts._metadata.sdk = opts._metadata.sdk || { name: 'sentry.javascript.svelte', packages: [ { @@ -17,8 +21,7 @@ export function init(options: BrowserOptions): void { ], version: SDK_VERSION, }; - - browserInit(options); + browserInit(opts); detectAndReportSvelteKit(); } diff --git a/packages/sveltekit/README.md b/packages/sveltekit/README.md index 5ca2cff3e73d..477d9181b8a5 100644 --- a/packages/sveltekit/README.md +++ b/packages/sveltekit/README.md @@ -194,7 +194,7 @@ export default { ### Configuring Source maps upload Under `sourceMapsUploadOptions`, you can also specify all additional options supported by the -[Sentry Vite Plugin](https://github.com/getsentry/sentry-javascript-bundler-plugins/blob/main/packages/vite-plugin/README.md#configuration). +[Sentry Vite Plugin](https://www.npmjs.com/package/@sentry/vite-plugin). This might be useful if you're using adapters other than the Node adapter or have a more customized build setup. ```js diff --git a/packages/sveltekit/src/common/metadata.ts b/packages/sveltekit/src/common/metadata.ts index 76a9642ee36b..d6acf72510cb 100644 --- a/packages/sveltekit/src/common/metadata.ts +++ b/packages/sveltekit/src/common/metadata.ts @@ -8,7 +8,7 @@ const PACKAGE_NAME_PREFIX = 'npm:@sentry/'; * * Note: This function is identical to `buildMetadata` in Remix and NextJS. * We don't extract it for bundle size reasons. - * If you make changes to this function consider updating the othera as well. + * If you make changes to this function consider updating the others as well. * * @param options SDK options object that gets mutated * @param names list of package names diff --git a/packages/sveltekit/src/server/handle.ts b/packages/sveltekit/src/server/handle.ts index 245fcee9658e..5076710970a8 100644 --- a/packages/sveltekit/src/server/handle.ts +++ b/packages/sveltekit/src/server/handle.ts @@ -1,12 +1,12 @@ /* eslint-disable @sentry-internal/sdk/no-optional-chaining */ import type { Span } from '@sentry/core'; -import { getActiveTransaction, getCurrentHub, runWithAsyncContext, trace } from '@sentry/core'; +import { getActiveTransaction, getCurrentHub, runWithAsyncContext, startSpan } from '@sentry/core'; import { captureException } from '@sentry/node'; import { addExceptionMechanism, dynamicSamplingContextToSentryBaggageHeader, objectify } from '@sentry/utils'; import type { Handle, ResolveOptions } from '@sveltejs/kit'; import { isHttpError, isRedirect } from '../common/utils'; -import { getTracePropagationData } from './utils'; +import { flushIfServerless, getTracePropagationData } from './utils'; export type SentryHandleOptions = { /** @@ -118,7 +118,10 @@ export function sentryHandle(handlerOptions?: SentryHandleOptions): Handle { return sentryRequestHandler; } -function instrumentHandle({ event, resolve }: Parameters[0], options: SentryHandleOptions): ReturnType { +async function instrumentHandle( + { event, resolve }: Parameters[0], + options: SentryHandleOptions, +): Promise { if (!event.route?.id && !options.handleUnknownRoutes) { return resolve(event); } @@ -126,25 +129,32 @@ function instrumentHandle({ event, resolve }: Parameters[0], options: Se const { dynamicSamplingContext, traceparentData, propagationContext } = getTracePropagationData(event); getCurrentHub().getScope().setPropagationContext(propagationContext); - return trace( - { - op: 'http.server', - origin: 'auto.http.sveltekit', - name: `${event.request.method} ${event.route?.id || event.url.pathname}`, - status: 'ok', - ...traceparentData, - metadata: { - source: event.route?.id ? 'route' : 'url', - dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext, + try { + const resolveResult = await startSpan( + { + op: 'http.server', + origin: 'auto.http.sveltekit', + name: `${event.request.method} ${event.route?.id || event.url.pathname}`, + status: 'ok', + ...traceparentData, + metadata: { + source: event.route?.id ? 'route' : 'url', + dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext, + }, + }, + async (span?: Span) => { + const res = await resolve(event, { transformPageChunk }); + if (span) { + span.setHttpStatus(res.status); + } + return res; }, - }, - async (span?: Span) => { - const res = await resolve(event, { transformPageChunk }); - if (span) { - span.setHttpStatus(res.status); - } - return res; - }, - sendErrorToSentry, - ); + ); + return resolveResult; + } catch (e: unknown) { + sendErrorToSentry(e); + throw e; + } finally { + await flushIfServerless(); + } } diff --git a/packages/sveltekit/src/server/handleError.ts b/packages/sveltekit/src/server/handleError.ts index 022c1c814930..938cbf612e2f 100644 --- a/packages/sveltekit/src/server/handleError.ts +++ b/packages/sveltekit/src/server/handleError.ts @@ -6,6 +6,8 @@ import { addExceptionMechanism } from '@sentry/utils'; // eslint-disable-next-line import/no-unresolved import type { HandleServerError, RequestEvent } from '@sveltejs/kit'; +import { flushIfServerless } from './utils'; + // The SvelteKit default error handler just logs the error's stack trace to the console // see: https://github.com/sveltejs/kit/blob/369e7d6851f543a40c947e033bfc4a9506fdc0a8/packages/kit/src/runtime/server/index.js#L43 function defaultErrorHandler({ error }: Parameters[0]): ReturnType { @@ -20,7 +22,7 @@ function defaultErrorHandler({ error }: Parameters[0]): Retur * @param handleError The original SvelteKit error handler. */ export function handleErrorWithSentry(handleError: HandleServerError = defaultErrorHandler): HandleServerError { - return (input: { error: unknown; event: RequestEvent }): ReturnType => { + return async (input: { error: unknown; event: RequestEvent }): Promise => { if (isNotFoundError(input)) { return handleError(input); } @@ -36,6 +38,8 @@ export function handleErrorWithSentry(handleError: HandleServerError = defaultEr return scope; }); + await flushIfServerless(); + return handleError(input); }; } diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index 90c651a41175..f81cedd8444b 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -6,6 +6,7 @@ export { addGlobalEventProcessor, addBreadcrumb, + addIntegration, captureException, captureEvent, captureMessage, @@ -51,6 +52,7 @@ export { startActiveSpan, startInactiveSpan, startSpanManual, + continueTrace, } from '@sentry/node'; // We can still leave this for the carrier init and type exports diff --git a/packages/sveltekit/src/server/load.ts b/packages/sveltekit/src/server/load.ts index c286ad5e3834..e819c434e81b 100644 --- a/packages/sveltekit/src/server/load.ts +++ b/packages/sveltekit/src/server/load.ts @@ -1,5 +1,5 @@ /* eslint-disable @sentry-internal/sdk/no-optional-chaining */ -import { getCurrentHub, trace } from '@sentry/core'; +import { getCurrentHub, startSpan } from '@sentry/core'; import { captureException } from '@sentry/node'; import type { TransactionContext } from '@sentry/types'; import { addExceptionMechanism, addNonEnumerableProperty, objectify } from '@sentry/utils'; @@ -7,7 +7,7 @@ import type { LoadEvent, ServerLoadEvent } from '@sveltejs/kit'; import type { SentryWrappedFlag } from '../common/utils'; import { isHttpError, isRedirect } from '../common/utils'; -import { getTracePropagationData } from './utils'; +import { flushIfServerless, getTracePropagationData } from './utils'; type PatchedLoadEvent = LoadEvent & SentryWrappedFlag; type PatchedServerLoadEvent = ServerLoadEvent & SentryWrappedFlag; @@ -57,7 +57,7 @@ function sendErrorToSentry(e: unknown): unknown { // eslint-disable-next-line @typescript-eslint/no-explicit-any export function wrapLoadWithSentry any>(origLoad: T): T { return new Proxy(origLoad, { - apply: (wrappingTarget, thisArg, args: Parameters) => { + apply: async (wrappingTarget, thisArg, args: Parameters) => { // Type casting here because `T` cannot extend `Load` (see comment above function signature) // Also, this event possibly already has a sentry wrapped flag attached const event = args[0] as PatchedLoadEvent; @@ -80,7 +80,15 @@ export function wrapLoadWithSentry any>(origLoad: T) }, }; - return trace(traceLoadContext, () => wrappingTarget.apply(thisArg, args), sendErrorToSentry); + try { + // We need to await before returning, otherwise we won't catch any errors thrown by the load function + return await startSpan(traceLoadContext, () => wrappingTarget.apply(thisArg, args)); + } catch (e) { + sendErrorToSentry(e); + throw e; + } finally { + await flushIfServerless(); + } }, }); } @@ -109,7 +117,7 @@ export function wrapLoadWithSentry any>(origLoad: T) // eslint-disable-next-line @typescript-eslint/no-explicit-any export function wrapServerLoadWithSentry any>(origServerLoad: T): T { return new Proxy(origServerLoad, { - apply: (wrappingTarget, thisArg, args: Parameters) => { + apply: async (wrappingTarget, thisArg, args: Parameters) => { // Type casting here because `T` cannot extend `ServerLoad` (see comment above function signature) // Also, this event possibly already has a sentry wrapped flag attached const event = args[0] as PatchedServerLoadEvent; @@ -144,7 +152,15 @@ export function wrapServerLoadWithSentry any>(origSe ...traceparentData, }; - return trace(traceLoadContext, () => wrappingTarget.apply(thisArg, args), sendErrorToSentry); + try { + // We need to await before returning, otherwise we won't catch any errors thrown by the load function + return await startSpan(traceLoadContext, () => wrappingTarget.apply(thisArg, args)); + } catch (e: unknown) { + sendErrorToSentry(e); + throw e; + } finally { + await flushIfServerless(); + } }, }); } diff --git a/packages/sveltekit/src/server/utils.ts b/packages/sveltekit/src/server/utils.ts index 1a9e1781643c..cf591568486b 100644 --- a/packages/sveltekit/src/server/utils.ts +++ b/packages/sveltekit/src/server/utils.ts @@ -1,5 +1,6 @@ +import { flush } from '@sentry/node'; import type { StackFrame } from '@sentry/types'; -import { basename, escapeStringForRegex, GLOBAL_OBJ, join, tracingContextFromHeaders } from '@sentry/utils'; +import { basename, escapeStringForRegex, GLOBAL_OBJ, join, logger, tracingContextFromHeaders } from '@sentry/utils'; import type { RequestEvent } from '@sveltejs/kit'; import { WRAPPED_MODULE_SUFFIX } from '../vite/autoInstrument'; @@ -68,3 +69,18 @@ export function rewriteFramesIteratee(frame: StackFrame): StackFrame { return frame; } + +/** Flush the event queue to ensure that events get sent to Sentry before the response is finished and the lambda ends */ +export async function flushIfServerless(): Promise { + const platformSupportsStreaming = !process.env.LAMBDA_TASK_ROOT && !process.env.VERCEL; + + if (!platformSupportsStreaming) { + try { + __DEBUG_BUILD__ && logger.log('Flushing events...'); + await flush(2000); + __DEBUG_BUILD__ && logger.log('Done flushing events'); + } catch (e) { + __DEBUG_BUILD__ && logger.log('Error while flushing events:\n', e); + } + } +} diff --git a/packages/sveltekit/test/server/load.test.ts b/packages/sveltekit/test/server/load.test.ts index c2b35bb7d2e9..e68e075c7ebd 100644 --- a/packages/sveltekit/test/server/load.test.ts +++ b/packages/sveltekit/test/server/load.test.ts @@ -21,26 +21,28 @@ vi.mock('@sentry/node', async () => { }; }); -const mockTrace = vi.fn(); +const mockStartSpan = vi.fn(); vi.mock('@sentry/core', async () => { const original = (await vi.importActual('@sentry/core')) as any; return { ...original, - trace: (...args: unknown[]) => { - mockTrace(...args); - return original.trace(...args); + startSpan: (...args: unknown[]) => { + mockStartSpan(...args); + return original.startSpan(...args); }, }; }); -const mockAddExceptionMechanism = vi.fn(); +const mockAddExceptionMechanism = vi.fn((_e, _m) => {}); vi.mock('@sentry/utils', async () => { const original = (await vi.importActual('@sentry/utils')) as any; return { ...original, - addExceptionMechanism: (...args: unknown[]) => mockAddExceptionMechanism(...args), + addExceptionMechanism: (...args: unknown[]) => { + return mockAddExceptionMechanism(args[0], args[1]); + }, }; }); @@ -127,10 +129,10 @@ beforeAll(() => { addTracingExtensions(); }); -beforeEach(() => { +afterEach(() => { mockCaptureException.mockClear(); mockAddExceptionMechanism.mockClear(); - mockTrace.mockClear(); + mockStartSpan.mockClear(); mockScope = new Scope(); }); @@ -203,11 +205,11 @@ describe.each([ }; } - const wrappedLoad = sentryLoadWrapperFn.call(this, load); + const wrappedLoad = sentryLoadWrapperFn(load); const res = wrappedLoad(getServerOnlyArgs()); await expect(res).rejects.toThrow(); - expect(addEventProcessorSpy).toBeCalledTimes(1); + expect(addEventProcessorSpy).toHaveBeenCalledTimes(1); expect(mockAddExceptionMechanism).toBeCalledTimes(1); expect(mockAddExceptionMechanism).toBeCalledWith( {}, @@ -226,8 +228,8 @@ describe('wrapLoadWithSentry calls trace', () => { const wrappedLoad = wrapLoadWithSentry(load); await wrappedLoad(getLoadArgs()); - expect(mockTrace).toHaveBeenCalledTimes(1); - expect(mockTrace).toHaveBeenCalledWith( + expect(mockStartSpan).toHaveBeenCalledTimes(1); + expect(mockStartSpan).toHaveBeenCalledWith( { op: 'function.sveltekit.load', origin: 'auto.function.sveltekit', @@ -238,7 +240,6 @@ describe('wrapLoadWithSentry calls trace', () => { }, }, expect.any(Function), - expect.any(Function), ); }); @@ -246,8 +247,8 @@ describe('wrapLoadWithSentry calls trace', () => { const wrappedLoad = wrapLoadWithSentry(load); await wrappedLoad(getLoadArgsWithoutRoute()); - expect(mockTrace).toHaveBeenCalledTimes(1); - expect(mockTrace).toHaveBeenCalledWith( + expect(mockStartSpan).toHaveBeenCalledTimes(1); + expect(mockStartSpan).toHaveBeenCalledWith( { op: 'function.sveltekit.load', origin: 'auto.function.sveltekit', @@ -258,7 +259,6 @@ describe('wrapLoadWithSentry calls trace', () => { }, }, expect.any(Function), - expect.any(Function), ); }); @@ -266,7 +266,7 @@ describe('wrapLoadWithSentry calls trace', () => { const wrappedLoad = wrapLoadWithSentry(wrapLoadWithSentry(wrapLoadWithSentry(load))); await wrappedLoad(getLoadArgs()); - expect(mockTrace).toHaveBeenCalledTimes(1); + expect(mockStartSpan).toHaveBeenCalledTimes(1); }); }); @@ -281,8 +281,8 @@ describe('wrapServerLoadWithSentry calls trace', () => { const wrappedLoad = wrapServerLoadWithSentry(serverLoad); await wrappedLoad(getServerOnlyArgs()); - expect(mockTrace).toHaveBeenCalledTimes(1); - expect(mockTrace).toHaveBeenCalledWith( + expect(mockStartSpan).toHaveBeenCalledTimes(1); + expect(mockStartSpan).toHaveBeenCalledWith( { op: 'function.sveltekit.server.load', origin: 'auto.function.sveltekit', @@ -308,7 +308,6 @@ describe('wrapServerLoadWithSentry calls trace', () => { }, }, expect.any(Function), - expect.any(Function), ); }); @@ -316,8 +315,8 @@ describe('wrapServerLoadWithSentry calls trace', () => { const wrappedLoad = wrapServerLoadWithSentry(serverLoad); await wrappedLoad(getServerArgsWithoutTracingHeaders()); - expect(mockTrace).toHaveBeenCalledTimes(1); - expect(mockTrace).toHaveBeenCalledWith( + expect(mockStartSpan).toHaveBeenCalledTimes(1); + expect(mockStartSpan).toHaveBeenCalledWith( { op: 'function.sveltekit.server.load', origin: 'auto.function.sveltekit', @@ -331,7 +330,6 @@ describe('wrapServerLoadWithSentry calls trace', () => { }, }, expect.any(Function), - expect.any(Function), ); }); @@ -339,8 +337,8 @@ describe('wrapServerLoadWithSentry calls trace', () => { const wrappedLoad = wrapServerLoadWithSentry(serverLoad); await wrappedLoad(getServerArgsWithoutBaggageHeader()); - expect(mockTrace).toHaveBeenCalledTimes(1); - expect(mockTrace).toHaveBeenCalledWith( + expect(mockStartSpan).toHaveBeenCalledTimes(1); + expect(mockStartSpan).toHaveBeenCalledWith( { op: 'function.sveltekit.server.load', origin: 'auto.function.sveltekit', @@ -358,7 +356,6 @@ describe('wrapServerLoadWithSentry calls trace', () => { }, }, expect.any(Function), - expect.any(Function), ); }); @@ -369,8 +366,8 @@ describe('wrapServerLoadWithSentry calls trace', () => { const wrappedLoad = wrapServerLoadWithSentry(serverLoad); await wrappedLoad(event); - expect(mockTrace).toHaveBeenCalledTimes(1); - expect(mockTrace).toHaveBeenCalledWith( + expect(mockStartSpan).toHaveBeenCalledTimes(1); + expect(mockStartSpan).toHaveBeenCalledWith( { op: 'function.sveltekit.server.load', origin: 'auto.function.sveltekit', @@ -396,7 +393,6 @@ describe('wrapServerLoadWithSentry calls trace', () => { }, }, expect.any(Function), - expect.any(Function), ); }); @@ -404,7 +400,7 @@ describe('wrapServerLoadWithSentry calls trace', () => { const wrappedLoad = wrapServerLoadWithSentry(wrapServerLoadWithSentry(serverLoad)); await wrappedLoad(getServerOnlyArgs()); - expect(mockTrace).toHaveBeenCalledTimes(1); + expect(mockStartSpan).toHaveBeenCalledTimes(1); }); it("doesn't invoke the proxy set on `event.route`", async () => { @@ -423,14 +419,13 @@ describe('wrapServerLoadWithSentry calls trace', () => { const wrappedLoad = wrapServerLoadWithSentry(serverLoad); await wrappedLoad(event); - expect(mockTrace).toHaveBeenCalledTimes(1); - expect(mockTrace).toHaveBeenCalledWith( + expect(mockStartSpan).toHaveBeenCalledTimes(1); + expect(mockStartSpan).toHaveBeenCalledWith( expect.objectContaining({ op: 'function.sveltekit.server.load', name: '/users/[id]', // <-- this shows that the route was still accessed }), expect.any(Function), - expect.any(Function), ); expect(proxyFn).not.toHaveBeenCalled(); diff --git a/packages/tracing-internal/package.json b/packages/tracing-internal/package.json index 4818702c81bb..266a5cce9183 100644 --- a/packages/tracing-internal/package.json +++ b/packages/tracing-internal/package.json @@ -43,7 +43,7 @@ "build:transpile:watch": "rollup -c rollup.npm.config.js --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", "build:tarball": "ts-node ../../scripts/prepack.ts && npm pack ./build", - "clean": "rimraf build coverage sentry-tracing-*.tgz", + "clean": "rimraf build coverage sentry-internal-tracing-*.tgz", "fix": "run-s fix:eslint fix:prettier", "fix:eslint": "eslint . --format stylish --fix", "fix:prettier": "prettier --write \"{src,test,scripts}/**/**.ts\"", diff --git a/packages/tracing-internal/src/node/integrations/mysql.ts b/packages/tracing-internal/src/node/integrations/mysql.ts index 748c43ec51b2..d176730f5847 100644 --- a/packages/tracing-internal/src/node/integrations/mysql.ts +++ b/packages/tracing-internal/src/node/integrations/mysql.ts @@ -84,7 +84,7 @@ export class Mysql implements LazyLoadedIntegration { } function finishSpan(span: Span | undefined): void { - if (!span) { + if (!span || span.endTimestamp) { return; } @@ -128,9 +128,14 @@ export class Mysql implements LazyLoadedIntegration { }); } - return orig.call(this, options, values, function () { + // streaming, no callback! + const query = orig.call(this, options, values) as { on: (event: string, callback: () => void) => void }; + + query.on('end', () => { finishSpan(span); }); + + return query; }; }); } diff --git a/packages/tracing-internal/src/node/integrations/postgres.ts b/packages/tracing-internal/src/node/integrations/postgres.ts index 4f3e2a94fc11..a3d94653477f 100644 --- a/packages/tracing-internal/src/node/integrations/postgres.ts +++ b/packages/tracing-internal/src/node/integrations/postgres.ts @@ -5,9 +5,15 @@ import { fill, isThenable, loadModule, logger } from '@sentry/utils'; import type { LazyLoadedIntegration } from './lazy'; import { shouldDisableAutoInstrumentation } from './utils/node-utils'; +type PgClientQuery = ( + config: unknown, + values?: unknown, + callback?: (err: unknown, result: unknown) => void, +) => void | Promise; + interface PgClient { prototype: { - query: () => void | Promise; + query: PgClientQuery; }; } @@ -20,9 +26,23 @@ interface PgClientThis { interface PgOptions { usePgNative?: boolean; + /** + * Supply your postgres module directly, instead of having Sentry attempt automatic resolution. + * Use this if you (a) use a module that's not `pg`, or (b) use a bundler that breaks resolution (e.g. esbuild). + * + * Usage: + * ``` + * import pg from 'pg'; + * + * Sentry.init({ + * integrations: [new Sentry.Integrations.Postgres({ module: pg })], + * }); + * ``` + */ + module?: PGModule; } -type PGModule = { Client: PgClient; native: { Client: PgClient } }; +type PGModule = { Client: PgClient; native: { Client: PgClient } | null }; /** Tracing integration for node-postgres package */ export class Postgres implements LazyLoadedIntegration { @@ -43,6 +63,7 @@ export class Postgres implements LazyLoadedIntegration { public constructor(options: PgOptions = {}) { this.name = Postgres.id; this._usePgNative = !!options.usePgNative; + this._module = options.module; } /** @inheritdoc */ @@ -66,13 +87,13 @@ export class Postgres implements LazyLoadedIntegration { return; } - if (this._usePgNative && !pkg.native?.Client) { + const Client = this._usePgNative ? pkg.native?.Client : pkg.Client; + + if (!Client) { __DEBUG_BUILD__ && logger.error("Postgres Integration was unable to access 'pg-native' bindings."); return; } - const { Client } = this._usePgNative ? pkg.native : pkg; - /** * function (query, callback) => void * function (query, params, callback) => void @@ -80,7 +101,7 @@ export class Postgres implements LazyLoadedIntegration { * function (query, params) => Promise * function (pg.Cursor) => pg.Cursor */ - fill(Client.prototype, 'query', function (orig: () => void | Promise) { + fill(Client.prototype, 'query', function (orig: PgClientQuery) { return function (this: PgClientThis, config: unknown, values: unknown, callback: unknown) { const scope = getCurrentHub().getScope(); const parentSpan = scope.getSpan(); diff --git a/packages/tracing-internal/test/browser/backgroundtab.test.ts b/packages/tracing-internal/test/browser/backgroundtab.test.ts index 3903f2eb2406..031d68d01d78 100644 --- a/packages/tracing-internal/test/browser/backgroundtab.test.ts +++ b/packages/tracing-internal/test/browser/backgroundtab.test.ts @@ -33,7 +33,7 @@ conditionalTest({ min: 10 })('registerBackgroundTabDetection', () => { hub.configureScope(scope => scope.setSpan(undefined)); }); - it('does not creates an event listener if global document is undefined', () => { + it('does not create an event listener if global document is undefined', () => { // @ts-expect-error need to override global document global.document = undefined; registerBackgroundTabDetection(); diff --git a/packages/tracing/test/integrations/node/postgres.test.ts b/packages/tracing/test/integrations/node/postgres.test.ts index bb735ca40d8b..446d837d22d7 100644 --- a/packages/tracing/test/integrations/node/postgres.test.ts +++ b/packages/tracing/test/integrations/node/postgres.test.ts @@ -1,16 +1,17 @@ /* eslint-disable deprecation/deprecation */ /* eslint-disable @typescript-eslint/unbound-method */ import { Hub, Scope } from '@sentry/core'; -import { logger } from '@sentry/utils'; +import { loadModule, logger } from '@sentry/utils'; +import pg from 'pg'; import { Integrations, Span } from '../../../src'; import { getTestClient } from '../../testutils'; class PgClient { // https://node-postgres.com/api/client#clientquery - public query(_text: unknown, values: unknown, callback?: () => void) { + public query(_text: unknown, values: unknown, callback?: (err: unknown, result: unknown) => void) { if (typeof callback === 'function') { - callback(); + callback(null, null); return; } @@ -25,25 +26,28 @@ class PgClient { // Jest mocks get hoisted. vars starting with `mock` are hoisted before imports. /* eslint-disable no-var */ -var mockClient = PgClient; +var mockModule = { + Client: PgClient, + native: { + Client: PgClient, + }, +}; // mock for 'pg' / 'pg-native' package jest.mock('@sentry/utils', () => { const actual = jest.requireActual('@sentry/utils'); return { ...actual, - loadModule() { - return { - Client: mockClient, - native: { - Client: mockClient, - }, - }; - }, + loadModule: jest.fn(() => mockModule), }; }); describe('setupOnce', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.resetAllMocks(); + }); + ['pg', 'pg-native'].forEach(pgApi => { const Client: PgClient = new PgClient(); let scope = new Scope(); @@ -127,4 +131,23 @@ describe('setupOnce', () => { expect(loggerLogSpy).toBeCalledWith('Postgres Integration is skipped because of instrumenter configuration.'); }); + + it('does not attempt resolution when module is passed directly', async () => { + const scope = new Scope(); + jest.spyOn(scope, 'getSpan').mockReturnValueOnce(new Span()); + + new Integrations.Postgres({ module: mockModule }).setupOnce( + () => undefined, + () => new Hub(undefined, scope), + ); + + await new PgClient().query('SELECT NOW()', null); + + expect(loadModule).not.toBeCalled(); + expect(scope.getSpan).toBeCalled(); + }); + + it('has valid module type', () => { + expect(() => new Integrations.Postgres({ module: pg })).not.toThrow(); + }); }); diff --git a/packages/types/src/integration.ts b/packages/types/src/integration.ts index 19df0b9e67c2..0e60b7a530ee 100644 --- a/packages/types/src/integration.ts +++ b/packages/types/src/integration.ts @@ -36,5 +36,5 @@ export interface Integration { * Return `null` to drop the event, or mutate the event & return it. * This receives the client that the integration was installed for as third argument. */ - processEvent?(event: Event, hint: EventHint | undefined, client: Client): Event | null | PromiseLike; + processEvent?(event: Event, hint: EventHint, client: Client): Event | null | PromiseLike; } diff --git a/packages/utils/src/anr.ts b/packages/utils/src/anr.ts new file mode 100644 index 000000000000..b007c1355cb7 --- /dev/null +++ b/packages/utils/src/anr.ts @@ -0,0 +1,133 @@ +import type { StackFrame } from '@sentry/types'; + +import { dropUndefinedKeys } from './object'; +import { filenameIsInApp, stripSentryFramesAndReverse } from './stacktrace'; + +type WatchdogReturn = { + /** Resets the watchdog timer */ + poll: () => void; + /** Enables or disables the watchdog timer */ + enabled: (state: boolean) => void; +}; + +/** + * A node.js watchdog timer + * @param pollInterval The interval that we expect to get polled at + * @param anrThreshold The threshold for when we consider ANR + * @param callback The callback to call for ANR + * @returns An object with `poll` and `enabled` functions {@link WatchdogReturn} + */ +export function watchdogTimer(pollInterval: number, anrThreshold: number, callback: () => void): WatchdogReturn { + let lastPoll = process.hrtime(); + let triggered = false; + let enabled = true; + + setInterval(() => { + const [seconds, nanoSeconds] = process.hrtime(lastPoll); + const diffMs = Math.floor(seconds * 1e3 + nanoSeconds / 1e6); + + if (triggered === false && diffMs > pollInterval + anrThreshold) { + triggered = true; + if (enabled) { + callback(); + } + } + + if (diffMs < pollInterval + anrThreshold) { + triggered = false; + } + }, 20); + + return { + poll: () => { + lastPoll = process.hrtime(); + }, + enabled: (state: boolean) => { + enabled = state; + }, + }; +} + +// types copied from inspector.d.ts +interface Location { + scriptId: string; + lineNumber: number; + columnNumber?: number; +} + +interface CallFrame { + functionName: string; + location: Location; + url: string; +} + +interface ScriptParsedEventDataType { + scriptId: string; + url: string; +} + +interface PausedEventDataType { + callFrames: CallFrame[]; + reason: string; +} + +/** + * Converts Debugger.CallFrame to Sentry StackFrame + */ +function callFrameToStackFrame( + frame: CallFrame, + url: string | undefined, + getModuleFromFilename: (filename: string | undefined) => string | undefined, +): StackFrame { + const filename = url ? url.replace(/^file:\/\//, '') : undefined; + + // CallFrame row/col are 0 based, whereas StackFrame are 1 based + const colno = frame.location.columnNumber ? frame.location.columnNumber + 1 : undefined; + const lineno = frame.location.lineNumber ? frame.location.lineNumber + 1 : undefined; + + return dropUndefinedKeys({ + filename, + module: getModuleFromFilename(filename), + function: frame.functionName || '?', + colno, + lineno, + in_app: filename ? filenameIsInApp(filename) : undefined, + }); +} + +// The only messages we care about +type DebugMessage = + | { method: 'Debugger.scriptParsed'; params: ScriptParsedEventDataType } + | { method: 'Debugger.paused'; params: PausedEventDataType }; + +/** + * Creates a message handler from the v8 debugger protocol and passed stack frames to the callback when paused. + */ +export function createDebugPauseMessageHandler( + sendCommand: (message: string) => void, + getModuleFromFilename: (filename?: string) => string | undefined, + pausedStackFrames: (frames: StackFrame[]) => void, +): (message: DebugMessage) => void { + // Collect scriptId -> url map so we can look up the filenames later + const scripts = new Map(); + + return message => { + if (message.method === 'Debugger.scriptParsed') { + scripts.set(message.params.scriptId, message.params.url); + } else if (message.method === 'Debugger.paused') { + // copy the frames + const callFrames = [...message.params.callFrames]; + // and resume immediately + sendCommand('Debugger.resume'); + sendCommand('Debugger.disable'); + + const stackFrames = stripSentryFramesAndReverse( + callFrames.map(frame => + callFrameToStackFrame(frame, scripts.get(frame.location.scriptId), getModuleFromFilename), + ), + ); + + pausedStackFrames(stackFrames); + } + }; +} diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 8de4941f6b96..81f4d947cd0d 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -31,3 +31,4 @@ export * from './url'; export * from './userIntegrations'; export * from './cache'; export * from './eventbuilder'; +export * from './anr'; diff --git a/packages/utils/src/instrument.ts b/packages/utils/src/instrument.ts index f3364ba92b9c..99ddc3aaefc8 100644 --- a/packages/utils/src/instrument.ts +++ b/packages/utils/src/instrument.ts @@ -12,7 +12,7 @@ import type { import { isString } from './is'; import type { ConsoleLevel } from './logger'; import { CONSOLE_LEVELS, logger, originalConsoleMethods } from './logger'; -import { fill } from './object'; +import { addNonEnumerableProperty, fill } from './object'; import { getFunctionName } from './stacktrace'; import { supportsHistory, supportsNativeFetch } from './supports'; import { getGlobalObject, GLOBAL_OBJ } from './worldwide'; @@ -400,31 +400,24 @@ function instrumentHistory(): void { fill(WINDOW.history, 'replaceState', historyReplacementFunction); } -const debounceDuration = 1000; +const DEBOUNCE_DURATION = 1000; let debounceTimerID: number | undefined; let lastCapturedEvent: Event | undefined; /** - * Decide whether the current event should finish the debounce of previously captured one. - * @param previous previously captured event - * @param current event to be captured + * Check whether two DOM events are similar to eachother. For example, two click events on the same button. */ -function shouldShortcircuitPreviousDebounce(previous: Event | undefined, current: Event): boolean { - // If there was no previous event, it should always be swapped for the new one. - if (!previous) { - return true; - } - +function areSimilarDomEvents(a: Event, b: Event): boolean { // If both events have different type, then user definitely performed two separate actions. e.g. click + keypress. - if (previous.type !== current.type) { - return true; + if (a.type !== b.type) { + return false; } try { // If both events have the same type, it's still possible that actions were performed on different targets. // e.g. 2 clicks on different buttons. - if (previous.target !== current.target) { - return true; + if (a.target !== b.target) { + return false; } } catch (e) { // just accessing `target` property can throw an exception in some rare circumstances @@ -434,7 +427,7 @@ function shouldShortcircuitPreviousDebounce(previous: Event | undefined, current // If both events have the same type _and_ same `target` (an element which triggered an event, _not necessarily_ // to which an event listener was attached), we treat them as the same action, as we want to capture // only one breadcrumb. e.g. multiple clicks on the same button, or typing inside a user input box. - return false; + return true; } /** @@ -475,11 +468,11 @@ function shouldSkipDOMEvent(event: Event): boolean { * @hidden */ function makeDOMEventHandler(handler: Function, globalListener: boolean = false): (event: Event) => void { - return (event: Event): void => { + return (event: Event & { _sentryCaptured?: true }): void => { // It's possible this handler might trigger multiple times for the same // event (e.g. event propagation through node ancestors). // Ignore if we've already captured that event. - if (!event || lastCapturedEvent === event) { + if (!event || event['_sentryCaptured']) { return; } @@ -488,20 +481,15 @@ function makeDOMEventHandler(handler: Function, globalListener: boolean = false) return; } + // Mark event as "seen" + addNonEnumerableProperty(event, '_sentryCaptured', true); + const name = event.type === 'keypress' ? 'input' : event.type; - // If there is no debounce timer, it means that we can safely capture the new event and store it for future comparisons. - if (debounceTimerID === undefined) { - handler({ - event: event, - name, - global: globalListener, - }); - lastCapturedEvent = event; - } - // If there is a debounce awaiting, see if the new event is different enough to treat it as a unique one. + // If there is no last captured event, it means that we can safely capture the new event and store it for future comparisons. + // If there is a last captured event, see if the new event is different enough to treat it as a unique one. // If that's the case, emit the previous event and store locally the newly-captured DOM event. - else if (shouldShortcircuitPreviousDebounce(lastCapturedEvent, event)) { + if (lastCapturedEvent === undefined || !areSimilarDomEvents(lastCapturedEvent, event)) { handler({ event: event, name, @@ -513,8 +501,8 @@ function makeDOMEventHandler(handler: Function, globalListener: boolean = false) // Start a new debounce timer that will prevent us from capturing multiple events that should be grouped together. clearTimeout(debounceTimerID); debounceTimerID = WINDOW.setTimeout(() => { - debounceTimerID = undefined; - }, debounceDuration); + lastCapturedEvent = undefined; + }, DEBOUNCE_DURATION); }; } diff --git a/packages/utils/src/logger.ts b/packages/utils/src/logger.ts index 07642b8c4f04..9d37855e9114 100644 --- a/packages/utils/src/logger.ts +++ b/packages/utils/src/logger.ts @@ -19,6 +19,7 @@ export const originalConsoleMethods: { interface Logger extends LoggerConsoleMethods { disable(): void; enable(): void; + isEnabled(): boolean; } /** @@ -63,6 +64,7 @@ function makeLogger(): Logger { disable: () => { enabled = false; }, + isEnabled: () => enabled, }; if (__DEBUG_BUILD__) { diff --git a/packages/utils/src/normalize.ts b/packages/utils/src/normalize.ts index 7c1adaa32ccc..5445cc33ba58 100644 --- a/packages/utils/src/normalize.ts +++ b/packages/utils/src/normalize.ts @@ -169,7 +169,9 @@ function visit( return normalized; } -// TODO remove this in v7 (this means the method will no longer be exported, under any name) +/** + * @deprecated This export will be removed in v8. + */ export { visit as walk }; /* eslint-disable complexity */ diff --git a/packages/utils/src/object.ts b/packages/utils/src/object.ts index e705214f950d..e4e866bf2763 100644 --- a/packages/utils/src/object.ts +++ b/packages/utils/src/object.ts @@ -42,7 +42,7 @@ export function fill(source: { [key: string]: any }, name: string, replacementFa * @param name The name of the property to be set * @param value The value to which to set the property */ -export function addNonEnumerableProperty(obj: { [key: string]: unknown }, name: string, value: unknown): void { +export function addNonEnumerableProperty(obj: object, name: string, value: unknown): void { try { Object.defineProperty(obj, name, { // enumerable: false, // the default, so we can save on bundle size by not explicitly setting it diff --git a/packages/vercel-edge/package.json b/packages/vercel-edge/package.json index bd72bbb9a0ab..b3d945ca9818 100644 --- a/packages/vercel-edge/package.json +++ b/packages/vercel-edge/package.json @@ -45,7 +45,7 @@ "build:types:watch": "tsc -p tsconfig.types.json --watch", "build:tarball": "ts-node ../../scripts/prepack.ts && npm pack ./build", "circularDepCheck": "madge --circular src/index.ts", - "clean": "rimraf build coverage sentry-core-*.tgz", + "clean": "rimraf build coverage sentry-vercel-edge-*.tgz", "fix": "run-s fix:eslint fix:prettier", "fix:eslint": "eslint . --format stylish --fix", "fix:prettier": "prettier --write \"{src,test,scripts}/**/**.ts\"", diff --git a/packages/vercel-edge/src/index.ts b/packages/vercel-edge/src/index.ts index cd596269a36f..bba58f568db0 100644 --- a/packages/vercel-edge/src/index.ts +++ b/packages/vercel-edge/src/index.ts @@ -25,6 +25,7 @@ export type { VercelEdgeOptions } from './types'; export { addGlobalEventProcessor, addBreadcrumb, + addIntegration, captureException, captureEvent, captureMessage, @@ -58,6 +59,7 @@ export { startSpan, startInactiveSpan, startSpanManual, + continueTrace, } from '@sentry/core'; export type { SpanStatusType } from '@sentry/core'; diff --git a/packages/vercel-edge/src/transports/index.ts b/packages/vercel-edge/src/transports/index.ts index a479425f96e6..d73e7fd4341b 100644 --- a/packages/vercel-edge/src/transports/index.ts +++ b/packages/vercel-edge/src/transports/index.ts @@ -83,7 +83,6 @@ export function makeEdgeTransport(options: VercelEdgeTransportOptions): Transpor const requestOptions: RequestInit = { body: request.body, method: 'POST', - referrerPolicy: 'origin', headers: options.headers, ...options.fetchOptions, }; diff --git a/packages/vercel-edge/test/transports/index.test.ts b/packages/vercel-edge/test/transports/index.test.ts index cab31eca5bf2..da0ab1389325 100644 --- a/packages/vercel-edge/test/transports/index.test.ts +++ b/packages/vercel-edge/test/transports/index.test.ts @@ -57,7 +57,6 @@ describe('Edge Transport', () => { expect(mockFetch).toHaveBeenLastCalledWith(DEFAULT_EDGE_TRANSPORT_OPTIONS.url, { body: serializeEnvelope(ERROR_ENVELOPE, new TextEncoder()), method: 'POST', - referrerPolicy: 'origin', }); }); diff --git a/packages/vue/src/errorhandler.ts b/packages/vue/src/errorhandler.ts index 542d341c322f..900bed5a5074 100644 --- a/packages/vue/src/errorhandler.ts +++ b/packages/vue/src/errorhandler.ts @@ -1,12 +1,12 @@ import { getCurrentHub } from '@sentry/browser'; import { addExceptionMechanism } from '@sentry/utils'; -import type { Options, ViewModel, Vue } from './types'; +import type { ViewModel, Vue, VueOptions } from './types'; import { formatComponentName, generateComponentTrace } from './vendor/components'; type UnknownFunc = (...args: unknown[]) => void; -export const attachErrorHandler = (app: Vue, options: Options): void => { +export const attachErrorHandler = (app: Vue, options: VueOptions): void => { const { errorHandler, warnHandler, silent } = app.config; app.config.errorHandler = (error: Error, vm: ViewModel, lifecycleHook: string): void => { diff --git a/packages/vue/src/index.ts b/packages/vue/src/index.ts index 24a352aba99a..6afcc0f60ae8 100644 --- a/packages/vue/src/index.ts +++ b/packages/vue/src/index.ts @@ -4,3 +4,4 @@ export { init } from './sdk'; export { vueRouterInstrumentation } from './router'; export { attachErrorHandler } from './errorhandler'; export { createTracingMixins } from './tracing'; +export { VueIntegration } from './integration'; diff --git a/packages/vue/src/integration.ts b/packages/vue/src/integration.ts new file mode 100644 index 000000000000..9a3969f5c415 --- /dev/null +++ b/packages/vue/src/integration.ts @@ -0,0 +1,99 @@ +import { hasTracingEnabled } from '@sentry/core'; +import type { Hub, Integration } from '@sentry/types'; +import { arrayify, GLOBAL_OBJ } from '@sentry/utils'; + +import { DEFAULT_HOOKS } from './constants'; +import { attachErrorHandler } from './errorhandler'; +import { createTracingMixins } from './tracing'; +import type { Options, Vue, VueOptions } from './types'; + +const globalWithVue = GLOBAL_OBJ as typeof GLOBAL_OBJ & { Vue: Vue }; + +const DEFAULT_CONFIG: VueOptions = { + Vue: globalWithVue.Vue, + attachProps: true, + logErrors: true, + hooks: DEFAULT_HOOKS, + timeout: 2000, + trackComponents: false, +}; + +/** + * Initialize Vue error & performance tracking. + */ +export class VueIntegration implements Integration { + /** + * @inheritDoc + */ + public static id: string = 'Vue'; + + /** + * @inheritDoc + */ + public name: string; + + private readonly _options: Partial; + + public constructor(options: Partial = {}) { + this.name = VueIntegration.id; + this._options = options; + } + + /** @inheritDoc */ + public setupOnce(_addGlobaleventProcessor: unknown, getCurrentHub: () => Hub): void { + this._setupIntegration(getCurrentHub()); + } + + /** Just here for easier testing */ + protected _setupIntegration(hub: Hub): void { + const client = hub.getClient(); + const options: Options = { ...DEFAULT_CONFIG, ...(client && client.getOptions()), ...this._options }; + + if (!options.Vue && !options.app) { + // eslint-disable-next-line no-console + console.warn( + `[@sentry/vue]: Misconfigured SDK. Vue specific errors will not be captured. +Update your \`Sentry.init\` call with an appropriate config option: +\`app\` (Application Instance - Vue 3) or \`Vue\` (Vue Constructor - Vue 2).`, + ); + return; + } + + if (options.app) { + const apps = arrayify(options.app); + apps.forEach(app => vueInit(app, options)); + } else if (options.Vue) { + vueInit(options.Vue, options); + } + } +} + +const vueInit = (app: Vue, options: Options): void => { + // Check app is not mounted yet - should be mounted _after_ init()! + // This is _somewhat_ private, but in the case that this doesn't exist we simply ignore it + // See: https://github.com/vuejs/core/blob/eb2a83283caa9de0a45881d860a3cbd9d0bdd279/packages/runtime-core/src/component.ts#L394 + const appWithInstance = app as Vue & { + _instance?: { + isMounted?: boolean; + }; + }; + + const isMounted = appWithInstance._instance && appWithInstance._instance.isMounted; + if (isMounted === true) { + // eslint-disable-next-line no-console + console.warn( + '[@sentry/vue]: Misconfigured SDK. Vue app is already mounted. Make sure to call `app.mount()` after `Sentry.init()`.', + ); + } + + attachErrorHandler(app, options); + + if (hasTracingEnabled(options)) { + app.mixin( + createTracingMixins({ + ...options, + ...options.tracingOptions, + }), + ); + } +}; diff --git a/packages/vue/src/sdk.ts b/packages/vue/src/sdk.ts index ecc879bccbd7..21d7246f503c 100644 --- a/packages/vue/src/sdk.ts +++ b/packages/vue/src/sdk.ts @@ -1,34 +1,7 @@ -import { init as browserInit, SDK_VERSION } from '@sentry/browser'; -import { hasTracingEnabled } from '@sentry/core'; -import { arrayify, GLOBAL_OBJ } from '@sentry/utils'; +import { defaultIntegrations, init as browserInit, SDK_VERSION } from '@sentry/browser'; -import { DEFAULT_HOOKS } from './constants'; -import { attachErrorHandler } from './errorhandler'; -import { createTracingMixins } from './tracing'; -import type { Options, TracingOptions, Vue } from './types'; - -const globalWithVue = GLOBAL_OBJ as typeof GLOBAL_OBJ & { Vue: Vue }; - -const DEFAULT_CONFIG: Options = { - Vue: globalWithVue.Vue, - attachProps: true, - logErrors: true, - hooks: DEFAULT_HOOKS, - timeout: 2000, - trackComponents: false, - _metadata: { - sdk: { - name: 'sentry.javascript.vue', - packages: [ - { - name: 'npm:@sentry/vue', - version: SDK_VERSION, - }, - ], - version: SDK_VERSION, - }, - }, -}; +import { VueIntegration } from './integration'; +import type { Options, TracingOptions } from './types'; /** * Inits the Vue SDK @@ -37,56 +10,21 @@ export function init( config: Partial & { tracingOptions: Partial }> = {}, ): void { const options = { - ...DEFAULT_CONFIG, + _metadata: { + sdk: { + name: 'sentry.javascript.vue', + packages: [ + { + name: 'npm:@sentry/vue', + version: SDK_VERSION, + }, + ], + version: SDK_VERSION, + }, + }, + defaultIntegrations: [...defaultIntegrations, new VueIntegration()], ...config, }; browserInit(options); - - if (!options.Vue && !options.app) { - // eslint-disable-next-line no-console - console.warn( - `[@sentry/vue]: Misconfigured SDK. Vue specific errors will not be captured. -Update your \`Sentry.init\` call with an appropriate config option: -\`app\` (Application Instance - Vue 3) or \`Vue\` (Vue Constructor - Vue 2).`, - ); - return; - } - - if (options.app) { - const apps = arrayify(options.app); - apps.forEach(app => vueInit(app, options)); - } else if (options.Vue) { - vueInit(options.Vue, options); - } } - -const vueInit = (app: Vue, options: Options): void => { - // Check app is not mounted yet - should be mounted _after_ init()! - // This is _somewhat_ private, but in the case that this doesn't exist we simply ignore it - // See: https://github.com/vuejs/core/blob/eb2a83283caa9de0a45881d860a3cbd9d0bdd279/packages/runtime-core/src/component.ts#L394 - const appWithInstance = app as Vue & { - _instance?: { - isMounted?: boolean; - }; - }; - - const isMounted = appWithInstance._instance && appWithInstance._instance.isMounted; - if (isMounted === true) { - // eslint-disable-next-line no-console - console.warn( - '[@sentry/vue]: Misconfigured SDK. Vue app is already mounted. Make sure to call `app.mount()` after `Sentry.init()`.', - ); - } - - attachErrorHandler(app, options); - - if (hasTracingEnabled(options)) { - app.mixin( - createTracingMixins({ - ...options, - ...options.tracingOptions, - }), - ); - } -}; diff --git a/packages/vue/src/types.ts b/packages/vue/src/types.ts index 1cc39b97b887..2a1ee6d89046 100644 --- a/packages/vue/src/types.ts +++ b/packages/vue/src/types.ts @@ -25,11 +25,13 @@ export type ViewModel = { }; }; -export interface Options extends TracingOptions, BrowserOptions { +export interface VueOptions extends TracingOptions { /** Vue constructor to be used inside the integration (as imported by `import Vue from 'vue'` in Vue2) */ Vue?: Vue; - /** Vue app instance(s) to be used inside the integration (as generated by `createApp` in Vue3 ) */ + /** + * Vue app instance(s) to be used inside the integration (as generated by `createApp` in Vue3). + */ app?: Vue | Vue[]; /** @@ -48,6 +50,8 @@ export interface Options extends TracingOptions, BrowserOptions { tracingOptions?: Partial; } +export interface Options extends BrowserOptions, VueOptions {} + /** Vue specific configuration for Tracing Integration */ export interface TracingOptions { /** diff --git a/packages/vue/test/integration/VueIntegration.test.ts b/packages/vue/test/integration/VueIntegration.test.ts new file mode 100644 index 000000000000..22f53df4c498 --- /dev/null +++ b/packages/vue/test/integration/VueIntegration.test.ts @@ -0,0 +1,68 @@ +import { logger } from '@sentry/utils'; +import { createApp } from 'vue'; + +import * as Sentry from '../../src'; + +const PUBLIC_DSN = 'https://username@domain/123'; + +describe('Sentry.VueIntegration', () => { + let loggerWarnings: unknown[] = []; + let warnings: unknown[] = []; + + beforeEach(() => { + warnings = []; + loggerWarnings = []; + + jest.spyOn(logger, 'warn').mockImplementation((message: unknown) => { + loggerWarnings.push(message); + }); + + jest.spyOn(console, 'warn').mockImplementation((message: unknown) => { + warnings.push(message); + }); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('allows to initialize integration later', () => { + Sentry.init({ dsn: PUBLIC_DSN, defaultIntegrations: false, autoSessionTracking: false }); + + const el = document.createElement('div'); + const app = createApp({ + template: '
hello
', + }); + + // This would normally happen through client.addIntegration() + const integration = new Sentry.VueIntegration({ app }); + integration['_setupIntegration'](Sentry.getCurrentHub()); + + app.mount(el); + + expect(warnings).toEqual([]); + expect(loggerWarnings).toEqual([]); + + expect(app.config.errorHandler).toBeDefined(); + }); + + it('warns when mounting before SDK.VueIntegration', () => { + Sentry.init({ dsn: PUBLIC_DSN, defaultIntegrations: false, autoSessionTracking: false }); + + const el = document.createElement('div'); + const app = createApp({ + template: '
hello
', + }); + + app.mount(el); + + // This would normally happen through client.addIntegration() + const integration = new Sentry.VueIntegration({ app }); + integration['_setupIntegration'](Sentry.getCurrentHub()); + + expect(warnings).toEqual([ + '[@sentry/vue]: Misconfigured SDK. Vue app is already mounted. Make sure to call `app.mount()` after `Sentry.init()`.', + ]); + expect(loggerWarnings).toEqual([]); + }); +}); diff --git a/packages/vue/test/integration/init.test.ts b/packages/vue/test/integration/init.test.ts index a9936c97bc89..e176e5b1691c 100644 --- a/packages/vue/test/integration/init.test.ts +++ b/packages/vue/test/integration/init.test.ts @@ -1,24 +1,23 @@ import { createApp } from 'vue'; +import { VueIntegration } from '../../src/integration'; +import type { Options } from '../../src/types'; import * as Sentry from './../../src'; +const PUBLIC_DSN = 'https://username@domain/123'; + describe('Sentry.init', () => { - let _consoleWarn: any; - let warnings: string[] = []; + let warnings: unknown[] = []; beforeEach(() => { warnings = []; - // eslint-disable-next-line no-console - _consoleWarn = console.warn; - // eslint-disable-next-line no-console - console.warn = jest.fn((message: string) => { + jest.spyOn(console, 'warn').mockImplementation((message: unknown) => { warnings.push(message); }); }); afterEach(() => { - // eslint-disable-next-line no-console - console.warn = _consoleWarn; + jest.clearAllMocks(); }); it('does not warn when correctly setup (Vue 3)', () => { @@ -27,9 +26,8 @@ describe('Sentry.init', () => { template: '
hello
', }); - Sentry.init({ + runInit({ app, - defaultIntegrations: false, }); app.mount(el); @@ -43,10 +41,9 @@ describe('Sentry.init', () => { template: '
hello
', }); - Sentry.init({ + runInit({ // this is a bit "hacky", but good enough to test what we want Vue: app, - defaultIntegrations: false, }); app.mount(el); @@ -62,9 +59,8 @@ describe('Sentry.init', () => { app.mount(el); - Sentry.init({ + runInit({ app, - defaultIntegrations: false, }); expect(warnings).toEqual([ @@ -78,9 +74,7 @@ describe('Sentry.init', () => { template: '
hello
', }); - Sentry.init({ - defaultIntegrations: false, - }); + runInit({}); app.mount(el); @@ -90,4 +84,41 @@ Update your \`Sentry.init\` call with an appropriate config option: \`app\` (Application Instance - Vue 3) or \`Vue\` (Vue Constructor - Vue 2).`, ]); }); + + it('does not warn when skipping Vue integration', () => { + const el = document.createElement('div'); + const app = createApp({ + template: '
hello
', + }); + + Sentry.init({ + dsn: PUBLIC_DSN, + defaultIntegrations: false, + integrations: [], + }); + + app.mount(el); + + expect(warnings).toEqual([]); + }); }); + +function runInit(options: Partial): void { + const hasRunBefore = Sentry.getCurrentHub().getIntegration(VueIntegration); + + const integration = new VueIntegration(); + + Sentry.init({ + dsn: PUBLIC_DSN, + defaultIntegrations: false, + integrations: [integration], + ...options, + }); + + // Because our integrations API is terrible to test, we need to make sure to check + // If we've already had this integration registered before + // if that's the case, `setup()` will not be run, so we need to manually run it :( + if (hasRunBefore) { + integration['_setupIntegration'](Sentry.getCurrentHub()); + } +} diff --git a/rollup/npmHelpers.js b/rollup/npmHelpers.js index be6a900b2115..fe2a55543e59 100644 --- a/rollup/npmHelpers.js +++ b/rollup/npmHelpers.js @@ -25,10 +25,11 @@ export function makeBaseNPMConfig(options = {}) { esModuleInterop = false, hasBundles = false, packageSpecificConfig = {}, + addPolyfills = true, } = options; const nodeResolvePlugin = makeNodeResolvePlugin(); - const sucrasePlugin = makeSucrasePlugin(); + const sucrasePlugin = makeSucrasePlugin({ disableESTransforms: !addPolyfills }); const debugBuildStatementReplacePlugin = makeDebugBuildStatementReplacePlugin(); const cleanupPlugin = makeCleanupPlugin(); const extractPolyfillsPlugin = makeExtractPolyfillsPlugin(); @@ -83,14 +84,7 @@ export function makeBaseNPMConfig(options = {}) { interop: esModuleInterop ? 'auto' : 'esModule', }, - plugins: [ - nodeResolvePlugin, - setSdkSourcePlugin, - sucrasePlugin, - debugBuildStatementReplacePlugin, - cleanupPlugin, - extractPolyfillsPlugin, - ], + plugins: [nodeResolvePlugin, setSdkSourcePlugin, sucrasePlugin, debugBuildStatementReplacePlugin, cleanupPlugin], // don't include imported modules from outside the package in the final output external: [ @@ -100,6 +94,10 @@ export function makeBaseNPMConfig(options = {}) { ], }; + if (addPolyfills) { + defaultBaseConfig.plugins.push(extractPolyfillsPlugin); + } + return deepMerge(defaultBaseConfig, packageSpecificConfig, { // Plugins have to be in the correct order or everything breaks, so when merging we have to manually re-order them customMerge: key => (key === 'plugins' ? mergePlugins : undefined), diff --git a/rollup/plugins/npmPlugins.js b/rollup/plugins/npmPlugins.js index ec162615623f..5265f5007755 100644 --- a/rollup/plugins/npmPlugins.js +++ b/rollup/plugins/npmPlugins.js @@ -16,9 +16,10 @@ import sucrase from '@rollup/plugin-sucrase'; * * @returns An instance of the `@rollup/plugin-sucrase` plugin */ -export function makeSucrasePlugin() { +export function makeSucrasePlugin(options = {}) { return sucrase({ transforms: ['typescript', 'jsx'], + ...options, }); } diff --git a/scripts/node-unit-tests.ts b/scripts/node-unit-tests.ts index 28167e15d557..8824cee77d66 100644 --- a/scripts/node-unit-tests.ts +++ b/scripts/node-unit-tests.ts @@ -22,6 +22,7 @@ const DEFAULT_SKIP_TESTS_PACKAGES = [ '@sentry/replay', '@sentry/wasm', '@sentry/bun', + '@sentry/deno', ]; const SKIP_TEST_PACKAGES: Record = { @@ -35,6 +36,7 @@ const SKIP_TEST_PACKAGES: Record = { '@sentry-internal/replay-worker', '@sentry/node-experimental', '@sentry/vercel-edge', + '@sentry/astro', ], legacyDeps: [ 'jsdom@15.x', @@ -53,22 +55,29 @@ const SKIP_TEST_PACKAGES: Record = { '@sentry-internal/replay-worker', '@sentry/node-experimental', '@sentry/vercel-edge', + '@sentry/astro', ], legacyDeps: ['jsdom@16.x', 'lerna@3.13.4'], shouldES6Utils: true, }, '12': { - ignoredPackages: ['@sentry/remix', '@sentry/sveltekit', '@sentry/node-experimental', '@sentry/vercel-edge'], + ignoredPackages: [ + '@sentry/remix', + '@sentry/sveltekit', + '@sentry/node-experimental', + '@sentry/vercel-edge', + '@sentry/astro', + ], legacyDeps: ['lerna@3.13.4'], shouldES6Utils: true, }, '14': { - ignoredPackages: ['@sentry/sveltekit', '@sentry/vercel-edge'], + ignoredPackages: ['@sentry/sveltekit', '@sentry/vercel-edge', '@sentry/astro'], legacyDeps: [], shouldES6Utils: false, }, '16': { - ignoredPackages: ['@sentry/vercel-edge'], + ignoredPackages: ['@sentry/vercel-edge', '@sentry/astro'], legacyDeps: [], shouldES6Utils: false, }, diff --git a/scripts/prepack.ts b/scripts/prepack.ts index 0c810f3e9030..ed280c45d088 100644 --- a/scripts/prepack.ts +++ b/scripts/prepack.ts @@ -21,13 +21,13 @@ const TYPES_VERSIONS_ENTRY_POINT = 'typesVersions'; const packageWithBundles = process.argv.includes('--bundles'); const buildDir = packageWithBundles ? NPM_BUILD_DIR : BUILD_DIR; -type PackageJsonEntryPoints = Record; +type PackageJsonEntryPoints = Record<(typeof ENTRY_POINTS)[number], string>; interface TypeVersions { [key: string]: { [key: string]: string[]; }; -}; +} interface PackageJson extends Record, PackageJsonEntryPoints { [EXPORT_MAP_ENTRY_POINT]: { @@ -35,6 +35,9 @@ interface PackageJson extends Record, PackageJsonEntryPoints { import: string; require: string; types: string; + node: string; + browser: string; + default: string; }; }; [TYPES_VERSIONS_ENTRY_POINT]: TypeVersions; diff --git a/yarn.lock b/yarn.lock index bc849269e256..f64756e09026 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17,6 +17,14 @@ dependencies: "@jridgewell/trace-mapping" "^0.3.0" +"@ampproject/remapping@^2.2.0": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630" + integrity sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg== + dependencies: + "@jridgewell/gen-mapping" "^0.3.0" + "@jridgewell/trace-mapping" "^0.3.9" + "@angular-devkit/architect@0.1002.4": version "0.1002.4" resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.1002.4.tgz#2e1fa9c7a4718a4d0d101516ab0cc9cb653c5c57" @@ -540,6 +548,56 @@ resolved "https://registry.yarnpkg.com/@assemblyscript/loader/-/loader-0.10.1.tgz#70e45678f06c72fa2e350e8553ec4a4d72b92e06" integrity sha512-H71nDOOL8Y7kWRLqf6Sums+01Q5msqBW2KhDUTemh1tvY04eSkSXrK0uj/4mmY0Xr16/3zyZmsrxN7CKuRbNRg== +"@astrojs/compiler@^2.1.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@astrojs/compiler/-/compiler-2.2.0.tgz#a6b106b7878b461e3d55715d90810a7df5df3ca2" + integrity sha512-JvmckEJgg8uXUw8Rs6VZDvN7LcweCHOdcxsCXpC+4KMDC9FaB5t9EH/NooSE+hu/rnACEhsXA3FKmf9wnhb7hA== + +"@astrojs/internal-helpers@0.2.1": + version "0.2.1" + resolved "https://registry.yarnpkg.com/@astrojs/internal-helpers/-/internal-helpers-0.2.1.tgz#4e2e6aabaa9819f17119aa10f413c4d6122c94cf" + integrity sha512-06DD2ZnItMwUnH81LBLco3tWjcZ1lGU9rLCCBaeUCGYe9cI0wKyY2W3kDyoW1I6GmcWgt1fu+D1CTvz+FIKf8A== + +"@astrojs/markdown-remark@3.2.1": + version "3.2.1" + resolved "https://registry.yarnpkg.com/@astrojs/markdown-remark/-/markdown-remark-3.2.1.tgz#0014c9c2d8666af4b2fee0cbd4185201eb328d76" + integrity sha512-Z4YNMRtgFZeHhB29uCZl0B9MbMZddW9ZKCNucapoysbvygbDFF1gGtqpVnf+Lyv3rUBHwM/J5qWB2MSZuTuz1g== + dependencies: + "@astrojs/prism" "^3.0.0" + github-slugger "^2.0.0" + import-meta-resolve "^3.0.0" + mdast-util-definitions "^6.0.0" + rehype-raw "^6.1.1" + rehype-stringify "^9.0.4" + remark-gfm "^3.0.1" + remark-parse "^10.0.2" + remark-rehype "^10.1.0" + remark-smartypants "^2.0.0" + shiki "^0.14.3" + unified "^10.1.2" + unist-util-visit "^4.1.2" + vfile "^5.3.7" + +"@astrojs/prism@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@astrojs/prism/-/prism-3.0.0.tgz#c9443e4cbf435acf0b5adc2c627d9789991514e7" + integrity sha512-g61lZupWq1bYbcBnYZqdjndShr/J3l/oFobBKPA3+qMat146zce3nz2kdO4giGbhYDt4gYdhmoBz0vZJ4sIurQ== + dependencies: + prismjs "^1.29.0" + +"@astrojs/telemetry@3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@astrojs/telemetry/-/telemetry-3.0.3.tgz#a7a87a40de74bfeaae78fc4cbec1f6ec1cbf1c36" + integrity sha512-j19Cf5mfyLt9hxgJ9W/FMdAA5Lovfp7/CINNB/7V71GqvygnL7KXhRC3TzfB+PsVQcBtgWZzCXhUWRbmJ64Raw== + dependencies: + ci-info "^3.8.0" + debug "^4.3.4" + dlv "^1.1.3" + dset "^3.1.2" + is-docker "^3.0.0" + is-wsl "^3.0.0" + which-pm-runs "^1.1.0" + "@babel/code-frame@7.12.11": version "7.12.11" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" @@ -554,6 +612,14 @@ dependencies: "@babel/highlight" "^7.18.6" +"@babel/code-frame@^7.22.13": + version "7.22.13" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e" + integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w== + dependencies: + "@babel/highlight" "^7.22.13" + chalk "^2.4.2" + "@babel/compat-data@^7.11.0", "@babel/compat-data@^7.13.0", "@babel/compat-data@^7.17.7", "@babel/compat-data@^7.19.4", "@babel/compat-data@^7.20.0": version "7.20.1" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.20.1.tgz#f2e6ef7790d8c8dbf03d379502dcc246dcce0b30" @@ -564,6 +630,11 @@ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.20.14.tgz#4106fc8b755f3e3ee0a0a7c27dde5de1d2b2baf8" integrity sha512-0YpKHD6ImkWMEINCyDAD0HLLUH/lPCefG8ld9it8DJB2wnApraKuhgYTvTY1z7UFIfBTGy5LwncZ+5HWWGbhFw== +"@babel/compat-data@^7.22.9": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.22.20.tgz#8df6e96661209623f1975d66c35ffca66f3306d0" + integrity sha512-BQYjKbpXjoXwFW5jGqiizJQQT/aC7pFm9Ok1OWssonuguICi264lbgMzRp2ZMmRSlfkX6DsWDDcsrctK8Rwfiw== + "@babel/core@7.11.1": version "7.11.1" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.11.1.tgz#2c55b604e73a40dc21b0e52650b11c65cf276643" @@ -628,6 +699,27 @@ json5 "^2.2.1" semver "^6.3.0" +"@babel/core@^7.22.10": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.23.0.tgz#f8259ae0e52a123eb40f552551e647b506a94d83" + integrity sha512-97z/ju/Jy1rZmDxybphrBuI+jtJjFVoz7Mr9yUQVVVi+DNZE333uFQeMOqcCIy1x3WYBIbWftUSLmbNXNT7qFQ== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.22.13" + "@babel/generator" "^7.23.0" + "@babel/helper-compilation-targets" "^7.22.15" + "@babel/helper-module-transforms" "^7.23.0" + "@babel/helpers" "^7.23.0" + "@babel/parser" "^7.23.0" + "@babel/template" "^7.22.15" + "@babel/traverse" "^7.23.0" + "@babel/types" "^7.23.0" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + "@babel/core@^7.8.6": version "7.20.12" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.20.12.tgz#7930db57443c6714ad216953d1356dac0eb8496d" @@ -685,6 +777,16 @@ "@jridgewell/gen-mapping" "^0.3.2" jsesc "^2.5.1" +"@babel/generator@^7.22.10", "@babel/generator@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.0.tgz#df5c386e2218be505b34837acbcb874d7a983420" + integrity sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g== + dependencies: + "@babel/types" "^7.23.0" + "@jridgewell/gen-mapping" "^0.3.2" + "@jridgewell/trace-mapping" "^0.3.17" + jsesc "^2.5.1" + "@babel/helper-annotate-as-pure@7.14.5": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.14.5.tgz#7bf478ec3b71726d56a8ca5775b046fc29879e61" @@ -699,6 +801,13 @@ dependencies: "@babel/types" "^7.18.6" +"@babel/helper-annotate-as-pure@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz#e7f06737b197d580a01edf75d97e2c8be99d3882" + integrity sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg== + dependencies: + "@babel/types" "^7.22.5" + "@babel/helper-builder-binary-assignment-operator-visitor@^7.18.6": version "7.18.9" resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.18.9.tgz#acd4edfd7a566d1d51ea975dff38fd52906981bb" @@ -728,6 +837,17 @@ lru-cache "^5.1.1" semver "^6.3.0" +"@babel/helper-compilation-targets@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz#0698fc44551a26cf29f18d4662d5bf545a6cfc52" + integrity sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw== + dependencies: + "@babel/compat-data" "^7.22.9" + "@babel/helper-validator-option" "^7.22.15" + browserslist "^4.21.9" + lru-cache "^5.1.1" + semver "^6.3.1" + "@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.19.0", "@babel/helper-create-class-features-plugin@^7.5.5": version "7.19.0" resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.19.0.tgz#bfd6904620df4e46470bae4850d66be1054c404b" @@ -816,6 +936,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be" integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg== +"@babel/helper-environment-visitor@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167" + integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA== + "@babel/helper-explode-assignable-expression@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz#41f8228ef0a6f1a036b8dfdfec7ce94f9a6bc096" @@ -831,6 +956,14 @@ "@babel/template" "^7.18.10" "@babel/types" "^7.19.0" +"@babel/helper-function-name@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759" + integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw== + dependencies: + "@babel/template" "^7.22.15" + "@babel/types" "^7.23.0" + "@babel/helper-hoist-variables@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz#d4d2c8fb4baeaa5c68b99cc8245c56554f926678" @@ -838,6 +971,13 @@ dependencies: "@babel/types" "^7.18.6" +"@babel/helper-hoist-variables@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb" + integrity sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw== + dependencies: + "@babel/types" "^7.22.5" + "@babel/helper-member-expression-to-functions@^7.18.9": version "7.18.9" resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.18.9.tgz#1531661e8375af843ad37ac692c132841e2fd815" @@ -859,6 +999,13 @@ dependencies: "@babel/types" "^7.18.6" +"@babel/helper-module-imports@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz#16146307acdc40cc00c3b2c647713076464bdbf0" + integrity sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w== + dependencies: + "@babel/types" "^7.22.15" + "@babel/helper-module-transforms@^7.11.0", "@babel/helper-module-transforms@^7.18.6", "@babel/helper-module-transforms@^7.19.6", "@babel/helper-module-transforms@^7.20.2": version "7.20.2" resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.20.2.tgz#ac53da669501edd37e658602a21ba14c08748712" @@ -887,6 +1034,17 @@ "@babel/traverse" "^7.20.10" "@babel/types" "^7.20.7" +"@babel/helper-module-transforms@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.23.0.tgz#3ec246457f6c842c0aee62a01f60739906f7047e" + integrity sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw== + dependencies: + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-module-imports" "^7.22.15" + "@babel/helper-simple-access" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/helper-validator-identifier" "^7.22.20" + "@babel/helper-optimise-call-expression@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz#9369aa943ee7da47edab2cb4e838acf09d290ffe" @@ -904,6 +1062,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz#d1b9000752b18d0877cff85a5c376ce5c3121629" integrity sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ== +"@babel/helper-plugin-utils@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz#dd7ee3735e8a313b9f7b05a773d892e88e6d7295" + integrity sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg== + "@babel/helper-remap-async-to-generator@^7.14.5", "@babel/helper-remap-async-to-generator@^7.18.6", "@babel/helper-remap-async-to-generator@^7.18.9": version "7.18.9" resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz#997458a0e3357080e54e1d79ec347f8a8cd28519" @@ -944,6 +1107,13 @@ dependencies: "@babel/types" "^7.20.2" +"@babel/helper-simple-access@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz#4938357dc7d782b80ed6dbb03a0fba3d22b1d5de" + integrity sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w== + dependencies: + "@babel/types" "^7.22.5" + "@babel/helper-skip-transparent-expression-wrappers@^7.18.9": version "7.18.9" resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.18.9.tgz#778d87b3a758d90b471e7b9918f34a9a02eb5818" @@ -965,6 +1135,13 @@ dependencies: "@babel/types" "^7.18.6" +"@babel/helper-split-export-declaration@^7.22.6": + version "7.22.6" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c" + integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g== + dependencies: + "@babel/types" "^7.22.5" + "@babel/helper-string-parser@^7.19.4": version "7.19.4" resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz#38d3acb654b4701a9b77fb0615a96f775c3a9e63" @@ -975,16 +1152,31 @@ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.21.5.tgz#2b3eea65443c6bdc31c22d037c65f6d323b6b2bd" integrity sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w== +"@babel/helper-string-parser@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f" + integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw== + "@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1": version "7.19.1" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== +"@babel/helper-validator-identifier@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" + integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== + "@babel/helper-validator-option@^7.14.5", "@babel/helper-validator-option@^7.16.7", "@babel/helper-validator-option@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz#bf0d2b5a509b1f336099e4ff36e1a63aa5db4db8" integrity sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw== +"@babel/helper-validator-option@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz#694c30dfa1d09a6534cdfcafbe56789d36aba040" + integrity sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA== + "@babel/helper-wrap-function@^7.18.9": version "7.19.0" resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.19.0.tgz#89f18335cff1152373222f76a4b37799636ae8b1" @@ -1013,6 +1205,15 @@ "@babel/traverse" "^7.20.13" "@babel/types" "^7.20.7" +"@babel/helpers@^7.23.0": + version "7.23.1" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.23.1.tgz#44e981e8ce2b9e99f8f0b703f3326a4636c16d15" + integrity sha512-chNpneuK18yW5Oxsr+t553UZzzAs3aZnFm4bxhebsNTeshrC95yA7l5yl7GBAG+JG1rF0F7zzD2EixK9mWSDoA== + dependencies: + "@babel/template" "^7.22.15" + "@babel/traverse" "^7.23.0" + "@babel/types" "^7.23.0" + "@babel/highlight@^7.10.4", "@babel/highlight@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" @@ -1022,6 +1223,15 @@ chalk "^2.0.0" js-tokens "^4.0.0" +"@babel/highlight@^7.22.13": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.20.tgz#4ca92b71d80554b01427815e06f2df965b9c1f54" + integrity sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg== + dependencies: + "@babel/helper-validator-identifier" "^7.22.20" + chalk "^2.4.2" + js-tokens "^4.0.0" + "@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.10.4", "@babel/parser@^7.11.1", "@babel/parser@^7.14.7", "@babel/parser@^7.16.4", "@babel/parser@^7.18.10", "@babel/parser@^7.20.1", "@babel/parser@^7.20.2", "@babel/parser@^7.4.5", "@babel/parser@^7.7.0": version "7.20.3" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.20.3.tgz#5358cf62e380cf69efcb87a7bb922ff88bfac6e2" @@ -1037,6 +1247,11 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.4.tgz#a770e98fd785c231af9d93f6459d36770993fb32" integrity sha512-VLLsx06XkEYqBtE5YGPwfSGwfrjnyPP5oiGty3S8pQLFDFLaS8VwWSIxkTXpcvr5zeYLE6+MBNl2npl/YnfofA== +"@babel/parser@^7.22.10", "@babel/parser@^7.22.15", "@babel/parser@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.0.tgz#da950e622420bf96ca0d0f2909cdddac3acd8719" + integrity sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw== + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz#da5b8f9a580acdfbe53494dba45ea389fb09a4d2" @@ -1338,6 +1553,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" +"@babel/plugin-syntax-jsx@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz#a6b68e84fb76e759fc3b93e901876ffabbe1d918" + integrity sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-logical-assignment-operators@^7.10.4", "@babel/plugin-syntax-logical-assignment-operators@^7.8.3": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" @@ -1689,6 +1911,17 @@ dependencies: "@babel/helper-plugin-utils" "^7.18.6" +"@babel/plugin-transform-react-jsx@^7.22.5": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.22.15.tgz#7e6266d88705d7c49f11c98db8b9464531289cd6" + integrity sha512-oKckg2eZFa8771O/5vi7XeTvmM6+O9cxZu+kanTU7tD4sin5nO/G8jGJhq8Hvt2Z0kUoEDRayuZLaUlYl8QuGA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-module-imports" "^7.22.15" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-jsx" "^7.22.5" + "@babel/types" "^7.22.15" + "@babel/plugin-transform-regenerator@^7.10.4", "@babel/plugin-transform-regenerator@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.18.6.tgz#585c66cb84d4b4bf72519a34cfce761b8676ca73" @@ -2169,6 +2402,15 @@ "@babel/parser" "^7.20.7" "@babel/types" "^7.20.7" +"@babel/template@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" + integrity sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w== + dependencies: + "@babel/code-frame" "^7.22.13" + "@babel/parser" "^7.22.15" + "@babel/types" "^7.22.15" + "@babel/traverse@^7.11.0", "@babel/traverse@^7.13.0", "@babel/traverse@^7.19.0", "@babel/traverse@^7.19.1", "@babel/traverse@^7.20.1", "@babel/traverse@^7.4.5", "@babel/traverse@^7.7.0", "@babel/traverse@^7.7.2": version "7.20.1" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.20.1.tgz#9b15ccbf882f6d107eeeecf263fbcdd208777ec8" @@ -2201,6 +2443,22 @@ debug "^4.1.0" globals "^11.1.0" +"@babel/traverse@^7.22.10", "@babel/traverse@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.0.tgz#18196ddfbcf4ccea324b7f6d3ada00d8c5a99c53" + integrity sha512-t/QaEvyIoIkwzpiZ7aoSKK8kObQYeF7T2v+dazAYCb8SXtp58zEVkWW7zAnju8FNKNdr4ScAOEDmMItbyOmEYw== + dependencies: + "@babel/code-frame" "^7.22.13" + "@babel/generator" "^7.23.0" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-function-name" "^7.23.0" + "@babel/helper-hoist-variables" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/parser" "^7.23.0" + "@babel/types" "^7.23.0" + debug "^4.1.0" + globals "^11.1.0" + "@babel/types@7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.8.3.tgz#5a383dffa5416db1b73dedffd311ffd0788fb31c" @@ -2237,6 +2495,15 @@ "@babel/helper-validator-identifier" "^7.19.1" to-fast-properties "^2.0.0" +"@babel/types@^7.22.10", "@babel/types@^7.22.15", "@babel/types@^7.22.5", "@babel/types@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.0.tgz#8c1f020c9df0e737e4e247c0619f58c68458aaeb" + integrity sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg== + dependencies: + "@babel/helper-string-parser" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.20" + to-fast-properties "^2.0.0" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -2457,111 +2724,331 @@ resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.16.17.tgz#cf91e86df127aa3d141744edafcba0abdc577d23" integrity sha512-MIGl6p5sc3RDTLLkYL1MyL8BMRN4tLMRCn+yRJJmEDvYZ2M7tmAf80hx1kbNEUX2KJ50RRtxZ4JHLvCfuB6kBg== +"@esbuild/android-arm64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz#984b4f9c8d0377443cc2dfcef266d02244593622" + integrity sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ== + +"@esbuild/android-arm64@0.19.4": + version "0.19.4" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.19.4.tgz#74752a09301b8c6b9a415fbda9fb71406a62a7b7" + integrity sha512-mRsi2vJsk4Bx/AFsNBqOH2fqedxn5L/moT58xgg51DjX1la64Z3Npicut2VbhvDFO26qjWtPMsVxCd80YTFVeg== + "@esbuild/android-arm@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.16.17.tgz#025b6246d3f68b7bbaa97069144fb5fb70f2fff2" integrity sha512-N9x1CMXVhtWEAMS7pNNONyA14f71VPQN9Cnavj1XQh6T7bskqiLLrSca4O0Vr8Wdcga943eThxnVp3JLnBMYtw== +"@esbuild/android-arm@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.18.20.tgz#fedb265bc3a589c84cc11f810804f234947c3682" + integrity sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw== + +"@esbuild/android-arm@0.19.4": + version "0.19.4" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.19.4.tgz#c27363e1e280e577d9b5c8fa7c7a3be2a8d79bf5" + integrity sha512-uBIbiYMeSsy2U0XQoOGVVcpIktjLMEKa7ryz2RLr7L/vTnANNEsPVAh4xOv7ondGz6ac1zVb0F8Jx20rQikffQ== + "@esbuild/android-x64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.16.17.tgz#c820e0fef982f99a85c4b8bfdd582835f04cd96e" integrity sha512-a3kTv3m0Ghh4z1DaFEuEDfz3OLONKuFvI4Xqczqx4BqLyuFaFkuaG4j2MtA6fuWEFeC5x9IvqnX7drmRq/fyAQ== +"@esbuild/android-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.18.20.tgz#35cf419c4cfc8babe8893d296cd990e9e9f756f2" + integrity sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg== + +"@esbuild/android-x64@0.19.4": + version "0.19.4" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.19.4.tgz#6c9ee03d1488973d928618100048b75b147e0426" + integrity sha512-4iPufZ1TMOD3oBlGFqHXBpa3KFT46aLl6Vy7gwed0ZSYgHaZ/mihbYb4t7Z9etjkC9Al3ZYIoOaHrU60gcMy7g== + "@esbuild/darwin-arm64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.16.17.tgz#edef4487af6b21afabba7be5132c26d22379b220" integrity sha512-/2agbUEfmxWHi9ARTX6OQ/KgXnOWfsNlTeLcoV7HSuSTv63E4DqtAc+2XqGw1KHxKMHGZgbVCZge7HXWX9Vn+w== +"@esbuild/darwin-arm64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz#08172cbeccf95fbc383399a7f39cfbddaeb0d7c1" + integrity sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA== + +"@esbuild/darwin-arm64@0.19.4": + version "0.19.4" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.19.4.tgz#64e2ee945e5932cd49812caa80e8896e937e2f8b" + integrity sha512-Lviw8EzxsVQKpbS+rSt6/6zjn9ashUZ7Tbuvc2YENgRl0yZTktGlachZ9KMJUsVjZEGFVu336kl5lBgDN6PmpA== + "@esbuild/darwin-x64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.16.17.tgz#42829168730071c41ef0d028d8319eea0e2904b4" integrity sha512-2By45OBHulkd9Svy5IOCZt376Aa2oOkiE9QWUK9fe6Tb+WDr8hXL3dpqi+DeLiMed8tVXspzsTAvd0jUl96wmg== +"@esbuild/darwin-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz#d70d5790d8bf475556b67d0f8b7c5bdff053d85d" + integrity sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ== + +"@esbuild/darwin-x64@0.19.4": + version "0.19.4" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.19.4.tgz#d8e26e1b965df284692e4d1263ba69a49b39ac7a" + integrity sha512-YHbSFlLgDwglFn0lAO3Zsdrife9jcQXQhgRp77YiTDja23FrC2uwnhXMNkAucthsf+Psr7sTwYEryxz6FPAVqw== + "@esbuild/freebsd-arm64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.16.17.tgz#1f4af488bfc7e9ced04207034d398e793b570a27" integrity sha512-mt+cxZe1tVx489VTb4mBAOo2aKSnJ33L9fr25JXpqQqzbUIw/yzIzi+NHwAXK2qYV1lEFp4OoVeThGjUbmWmdw== +"@esbuild/freebsd-arm64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz#98755cd12707f93f210e2494d6a4b51b96977f54" + integrity sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw== + +"@esbuild/freebsd-arm64@0.19.4": + version "0.19.4" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.4.tgz#29751a41b242e0a456d89713b228f1da4f45582f" + integrity sha512-vz59ijyrTG22Hshaj620e5yhs2dU1WJy723ofc+KUgxVCM6zxQESmWdMuVmUzxtGqtj5heHyB44PjV/HKsEmuQ== + "@esbuild/freebsd-x64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.16.17.tgz#636306f19e9bc981e06aa1d777302dad8fddaf72" integrity sha512-8ScTdNJl5idAKjH8zGAsN7RuWcyHG3BAvMNpKOBaqqR7EbUhhVHOqXRdL7oZvz8WNHL2pr5+eIT5c65kA6NHug== +"@esbuild/freebsd-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz#c1eb2bff03915f87c29cece4c1a7fa1f423b066e" + integrity sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ== + +"@esbuild/freebsd-x64@0.19.4": + version "0.19.4" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.19.4.tgz#873edc0f73e83a82432460ea59bf568c1e90b268" + integrity sha512-3sRbQ6W5kAiVQRBWREGJNd1YE7OgzS0AmOGjDmX/qZZecq8NFlQsQH0IfXjjmD0XtUYqr64e0EKNFjMUlPL3Cw== + "@esbuild/linux-arm64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.16.17.tgz#a003f7ff237c501e095d4f3a09e58fc7b25a4aca" integrity sha512-7S8gJnSlqKGVJunnMCrXHU9Q8Q/tQIxk/xL8BqAP64wchPCTzuM6W3Ra8cIa1HIflAvDnNOt2jaL17vaW+1V0g== +"@esbuild/linux-arm64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz#bad4238bd8f4fc25b5a021280c770ab5fc3a02a0" + integrity sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA== + +"@esbuild/linux-arm64@0.19.4": + version "0.19.4" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.19.4.tgz#659f2fa988d448dbf5010b5cc583be757cc1b914" + integrity sha512-ZWmWORaPbsPwmyu7eIEATFlaqm0QGt+joRE9sKcnVUG3oBbr/KYdNE2TnkzdQwX6EDRdg/x8Q4EZQTXoClUqqA== + "@esbuild/linux-arm@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.16.17.tgz#b591e6a59d9c4fe0eeadd4874b157ab78cf5f196" integrity sha512-iihzrWbD4gIT7j3caMzKb/RsFFHCwqqbrbH9SqUSRrdXkXaygSZCZg1FybsZz57Ju7N/SHEgPyaR0LZ8Zbe9gQ== +"@esbuild/linux-arm@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz#3e617c61f33508a27150ee417543c8ab5acc73b0" + integrity sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg== + +"@esbuild/linux-arm@0.19.4": + version "0.19.4" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.19.4.tgz#d5b13a7ec1f1c655ce05c8d319b3950797baee55" + integrity sha512-z/4ArqOo9EImzTi4b6Vq+pthLnepFzJ92BnofU1jgNlcVb+UqynVFdoXMCFreTK7FdhqAzH0vmdwW5373Hm9pg== + "@esbuild/linux-ia32@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.16.17.tgz#24333a11027ef46a18f57019450a5188918e2a54" integrity sha512-kiX69+wcPAdgl3Lonh1VI7MBr16nktEvOfViszBSxygRQqSpzv7BffMKRPMFwzeJGPxcio0pdD3kYQGpqQ2SSg== +"@esbuild/linux-ia32@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz#699391cccba9aee6019b7f9892eb99219f1570a7" + integrity sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA== + +"@esbuild/linux-ia32@0.19.4": + version "0.19.4" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.19.4.tgz#878cd8bf24c9847c77acdb5dd1b2ef6e4fa27a82" + integrity sha512-EGc4vYM7i1GRUIMqRZNCTzJh25MHePYsnQfKDexD8uPTCm9mK56NIL04LUfX2aaJ+C9vyEp2fJ7jbqFEYgO9lQ== + "@esbuild/linux-loong64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.16.17.tgz#d5ad459d41ed42bbd4d005256b31882ec52227d8" integrity sha512-dTzNnQwembNDhd654cA4QhbS9uDdXC3TKqMJjgOWsC0yNCbpzfWoXdZvp0mY7HU6nzk5E0zpRGGx3qoQg8T2DQ== +"@esbuild/linux-loong64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz#e6fccb7aac178dd2ffb9860465ac89d7f23b977d" + integrity sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg== + +"@esbuild/linux-loong64@0.19.4": + version "0.19.4" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.19.4.tgz#df890499f6e566b7de3aa2361be6df2b8d5fa015" + integrity sha512-WVhIKO26kmm8lPmNrUikxSpXcgd6HDog0cx12BUfA2PkmURHSgx9G6vA19lrlQOMw+UjMZ+l3PpbtzffCxFDRg== + "@esbuild/linux-mips64el@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.16.17.tgz#4e5967a665c38360b0a8205594377d4dcf9c3726" integrity sha512-ezbDkp2nDl0PfIUn0CsQ30kxfcLTlcx4Foz2kYv8qdC6ia2oX5Q3E/8m6lq84Dj/6b0FrkgD582fJMIfHhJfSw== +"@esbuild/linux-mips64el@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz#eeff3a937de9c2310de30622a957ad1bd9183231" + integrity sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ== + +"@esbuild/linux-mips64el@0.19.4": + version "0.19.4" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.19.4.tgz#76eae4e88d2ce9f4f1b457e93892e802851b6807" + integrity sha512-keYY+Hlj5w86hNp5JJPuZNbvW4jql7c1eXdBUHIJGTeN/+0QFutU3GrS+c27L+NTmzi73yhtojHk+lr2+502Mw== + "@esbuild/linux-ppc64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.16.17.tgz#206443a02eb568f9fdf0b438fbd47d26e735afc8" integrity sha512-dzS678gYD1lJsW73zrFhDApLVdM3cUF2MvAa1D8K8KtcSKdLBPP4zZSLy6LFZ0jYqQdQ29bjAHJDgz0rVbLB3g== +"@esbuild/linux-ppc64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz#2f7156bde20b01527993e6881435ad79ba9599fb" + integrity sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA== + +"@esbuild/linux-ppc64@0.19.4": + version "0.19.4" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.19.4.tgz#c49032f4abbcfa3f747b543a106931fe3dce41ff" + integrity sha512-tQ92n0WMXyEsCH4m32S21fND8VxNiVazUbU4IUGVXQpWiaAxOBvtOtbEt3cXIV3GEBydYsY8pyeRMJx9kn3rvw== + "@esbuild/linux-riscv64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.16.17.tgz#c351e433d009bf256e798ad048152c8d76da2fc9" integrity sha512-ylNlVsxuFjZK8DQtNUwiMskh6nT0vI7kYl/4fZgV1llP5d6+HIeL/vmmm3jpuoo8+NuXjQVZxmKuhDApK0/cKw== +"@esbuild/linux-riscv64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz#6628389f210123d8b4743045af8caa7d4ddfc7a6" + integrity sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A== + +"@esbuild/linux-riscv64@0.19.4": + version "0.19.4" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.19.4.tgz#0f815a090772138503ee0465a747e16865bf94b1" + integrity sha512-tRRBey6fG9tqGH6V75xH3lFPpj9E8BH+N+zjSUCnFOX93kEzqS0WdyJHkta/mmJHn7MBaa++9P4ARiU4ykjhig== + "@esbuild/linux-s390x@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.16.17.tgz#661f271e5d59615b84b6801d1c2123ad13d9bd87" integrity sha512-gzy7nUTO4UA4oZ2wAMXPNBGTzZFP7mss3aKR2hH+/4UUkCOyqmjXiKpzGrY2TlEUhbbejzXVKKGazYcQTZWA/w== +"@esbuild/linux-s390x@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz#255e81fb289b101026131858ab99fba63dcf0071" + integrity sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ== + +"@esbuild/linux-s390x@0.19.4": + version "0.19.4" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.19.4.tgz#8d2cca20cd4e7c311fde8701d9f1042664f8b92b" + integrity sha512-152aLpQqKZYhThiJ+uAM4PcuLCAOxDsCekIbnGzPKVBRUDlgaaAfaUl5NYkB1hgY6WN4sPkejxKlANgVcGl9Qg== + "@esbuild/linux-x64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.16.17.tgz#e4ba18e8b149a89c982351443a377c723762b85f" integrity sha512-mdPjPxfnmoqhgpiEArqi4egmBAMYvaObgn4poorpUaqmvzzbvqbowRllQ+ZgzGVMGKaPkqUmPDOOFQRUFDmeUw== +"@esbuild/linux-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz#c7690b3417af318a9b6f96df3031a8865176d338" + integrity sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w== + +"@esbuild/linux-x64@0.19.4": + version "0.19.4" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.19.4.tgz#f618bec2655de49bff91c588777e37b5e3169d4a" + integrity sha512-Mi4aNA3rz1BNFtB7aGadMD0MavmzuuXNTaYL6/uiYIs08U7YMPETpgNn5oue3ICr+inKwItOwSsJDYkrE9ekVg== + "@esbuild/netbsd-x64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.16.17.tgz#7d4f4041e30c5c07dd24ffa295c73f06038ec775" integrity sha512-/PzmzD/zyAeTUsduZa32bn0ORug+Jd1EGGAUJvqfeixoEISYpGnAezN6lnJoskauoai0Jrs+XSyvDhppCPoKOA== +"@esbuild/netbsd-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz#30e8cd8a3dded63975e2df2438ca109601ebe0d1" + integrity sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A== + +"@esbuild/netbsd-x64@0.19.4": + version "0.19.4" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.19.4.tgz#7889744ca4d60f1538d62382b95e90a49687cef2" + integrity sha512-9+Wxx1i5N/CYo505CTT7T+ix4lVzEdz0uCoYGxM5JDVlP2YdDC1Bdz+Khv6IbqmisT0Si928eAxbmGkcbiuM/A== + "@esbuild/openbsd-x64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.16.17.tgz#970fa7f8470681f3e6b1db0cc421a4af8060ec35" integrity sha512-2yaWJhvxGEz2RiftSk0UObqJa/b+rIAjnODJgv2GbGGpRwAfpgzyrg1WLK8rqA24mfZa9GvpjLcBBg8JHkoodg== +"@esbuild/openbsd-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz#7812af31b205055874c8082ea9cf9ab0da6217ae" + integrity sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg== + +"@esbuild/openbsd-x64@0.19.4": + version "0.19.4" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.19.4.tgz#c3e436eb9271a423d2e8436fcb120e3fd90e2b01" + integrity sha512-MFsHleM5/rWRW9EivFssop+OulYVUoVcqkyOkjiynKBCGBj9Lihl7kh9IzrreDyXa4sNkquei5/DTP4uCk25xw== + "@esbuild/sunos-x64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.16.17.tgz#abc60e7c4abf8b89fb7a4fe69a1484132238022c" integrity sha512-xtVUiev38tN0R3g8VhRfN7Zl42YCJvyBhRKw1RJjwE1d2emWTVToPLNEQj/5Qxc6lVFATDiy6LjVHYhIPrLxzw== +"@esbuild/sunos-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz#d5c275c3b4e73c9b0ecd38d1ca62c020f887ab9d" + integrity sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ== + +"@esbuild/sunos-x64@0.19.4": + version "0.19.4" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.19.4.tgz#f63f5841ba8c8c1a1c840d073afc99b53e8ce740" + integrity sha512-6Xq8SpK46yLvrGxjp6HftkDwPP49puU4OF0hEL4dTxqCbfx09LyrbUj/D7tmIRMj5D5FCUPksBbxyQhp8tmHzw== + "@esbuild/win32-arm64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.16.17.tgz#7b0ff9e8c3265537a7a7b1fd9a24e7bd39fcd87a" integrity sha512-ga8+JqBDHY4b6fQAmOgtJJue36scANy4l/rL97W+0wYmijhxKetzZdKOJI7olaBaMhWt8Pac2McJdZLxXWUEQw== +"@esbuild/win32-arm64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz#73bc7f5a9f8a77805f357fab97f290d0e4820ac9" + integrity sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg== + +"@esbuild/win32-arm64@0.19.4": + version "0.19.4" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.19.4.tgz#80be69cec92da4da7781cf7a8351b95cc5a236b0" + integrity sha512-PkIl7Jq4mP6ke7QKwyg4fD4Xvn8PXisagV/+HntWoDEdmerB2LTukRZg728Yd1Fj+LuEX75t/hKXE2Ppk8Hh1w== + "@esbuild/win32-ia32@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.16.17.tgz#e90fe5267d71a7b7567afdc403dfd198c292eb09" integrity sha512-WnsKaf46uSSF/sZhwnqE4L/F89AYNMiD4YtEcYekBt9Q7nj0DiId2XH2Ng2PHM54qi5oPrQ8luuzGszqi/veig== +"@esbuild/win32-ia32@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz#ec93cbf0ef1085cc12e71e0d661d20569ff42102" + integrity sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g== + +"@esbuild/win32-ia32@0.19.4": + version "0.19.4" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.19.4.tgz#15dc0ed83d2794872b05d8edc4a358fecf97eb54" + integrity sha512-ga676Hnvw7/ycdKB53qPusvsKdwrWzEyJ+AtItHGoARszIqvjffTwaaW3b2L6l90i7MO9i+dlAW415INuRhSGg== + "@esbuild/win32-x64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.16.17.tgz#c5a1a4bfe1b57f0c3e61b29883525c6da3e5c091" integrity sha512-y+EHuSchhL7FjHgvQL/0fnnFmO4T1bhvWANX6gcnqTjtnKWbTvUMCpGnv2+t+31d7RzyEAYAd4u2fnIhHL6N/Q== +"@esbuild/win32-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz#786c5f41f043b07afb1af37683d7c33668858f6d" + integrity sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ== + +"@esbuild/win32-x64@0.19.4": + version "0.19.4" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.4.tgz#d46a6e220a717f31f39ae80f49477cc3220be0f0" + integrity sha512-HP0GDNla1T3ZL8Ko/SHAS2GgtjOg+VmWnnYLhuTksr++EnduYB0f3Y2LzHsUwb2iQ13JGoY6G3R8h6Du/WG6uA== + "@eslint-community/eslint-utils@^4.1.2", "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" @@ -3241,6 +3728,11 @@ resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" + integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== + "@jridgewell/set-array@^1.0.1": version "1.1.2" resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" @@ -3259,7 +3751,7 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== -"@jridgewell/sourcemap-codec@^1.4.14": +"@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.4.15": version "1.4.15" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== @@ -3280,6 +3772,14 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" +"@jridgewell/trace-mapping@^0.3.17": + version "0.3.19" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz#f8a3249862f91be48d3127c3cfe992f79b4b8811" + integrity sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + "@jsdevtools/coverage-istanbul-loader@3.0.5": version "3.0.5" resolved "https://registry.yarnpkg.com/@jsdevtools/coverage-istanbul-loader/-/coverage-istanbul-loader-3.0.5.tgz#2a4bc65d0271df8d4435982db4af35d81754ee26" @@ -3841,7 +4341,7 @@ dependencies: "@opentelemetry/semantic-conventions" "1.15.2" -"@opentelemetry/core@1.17.0", "@opentelemetry/core@^1.1.0", "@opentelemetry/core@^1.7.0", "@opentelemetry/core@^1.8.0": +"@opentelemetry/core@1.17.0", "@opentelemetry/core@^1.1.0", "@opentelemetry/core@^1.7.0", "@opentelemetry/core@^1.8.0", "@opentelemetry/core@~1.17.0": version "1.17.0" resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-1.17.0.tgz#6a72425f5f953dc68b4c7c66d947c018173d30d2" integrity sha512-tfnl3h+UefCgx1aeN2xtrmr6BmdWGKXypk0pflQR0urFS40aE88trnkOMc2HTJZbMrqEEl4HsaBeFhwLVXsrJg== @@ -3867,13 +4367,13 @@ "@opentelemetry/semantic-conventions" "^1.0.0" "@types/express" "4.17.17" -"@opentelemetry/instrumentation-fastify@~0.32.2": - version "0.32.2" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-fastify/-/instrumentation-fastify-0.32.2.tgz#4af882938d3c05f7c7f5f860095e568728a2d838" - integrity sha512-DKa7SgxTtZ0O1ngGtAdwr/g8XguYw6KvLNME+J8rt6QpWQM+xytS0bg4atZAyt6aeYr/kO1sMrGXSlHEEYWIhg== +"@opentelemetry/instrumentation-fastify@~0.32.3": + version "0.32.3" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-fastify/-/instrumentation-fastify-0.32.3.tgz#2c0640c986018d1a41dfff3d9c3bfe3b5b1cf62d" + integrity sha512-vRFVoEJXcu6nNpJ61H5syDb84PirOd4b3u8yl8Bcorrr6firGYBQH4pEIVB4PkQWlmi3sLOifqS3VAO2VRloEQ== dependencies: "@opentelemetry/core" "^1.8.0" - "@opentelemetry/instrumentation" "^0.41.2" + "@opentelemetry/instrumentation" "^0.44.0" "@opentelemetry/semantic-conventions" "^1.0.0" "@opentelemetry/instrumentation-graphql@~0.35.1": @@ -3960,7 +4460,7 @@ semver "^7.5.1" shimmer "^1.2.1" -"@opentelemetry/instrumentation@0.43.0", "@opentelemetry/instrumentation@~0.43.0": +"@opentelemetry/instrumentation@0.43.0", "@opentelemetry/instrumentation@^0.43.0", "@opentelemetry/instrumentation@~0.43.0": version "0.43.0" resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.43.0.tgz#749521415df03396f969bf42341fcb4acd2e9c7b" integrity sha512-S1uHE+sxaepgp+t8lvIDuRgyjJWisAb733198kwQTUc9ZtYQ2V2gmyCtR1x21ePGVLoMiX/NWY7WA290hwkjJQ== @@ -3971,6 +4471,17 @@ semver "^7.5.2" shimmer "^1.2.1" +"@opentelemetry/instrumentation@^0.44.0": + version "0.44.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.44.0.tgz#194f16fc96671575b6bd73d3fadffb5aa4497e67" + integrity sha512-B6OxJTRRCceAhhnPDBshyQO7K07/ltX3quOLu0icEvPK9QZ7r9P1y0RQX8O5DxB4vTv4URRkxkg+aFU/plNtQw== + dependencies: + "@types/shimmer" "^1.0.2" + import-in-the-middle "1.4.2" + require-in-the-middle "^7.1.1" + semver "^7.5.2" + shimmer "^1.2.1" + "@opentelemetry/propagator-b3@1.17.0": version "1.17.0" resolved "https://registry.yarnpkg.com/@opentelemetry/propagator-b3/-/propagator-b3-1.17.0.tgz#32509a8214b7ced7709fd06c0ee5a0d86adcc51f" @@ -4321,6 +4832,18 @@ magic-string "^0.25.7" resolve "^1.17.0" +"@rollup/plugin-commonjs@^25.0.5": + version "25.0.5" + resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.5.tgz#0bac8f985a5de151b4b09338847f8c7f20a28a29" + integrity sha512-xY8r/A9oisSeSuLCTfhssyDjo9Vp/eDiRLXkg1MXCcEEgEjPmLU+ZyDB20OOD0NlyDa/8SGbK5uIggF5XTx77w== + dependencies: + "@rollup/pluginutils" "^5.0.1" + commondir "^1.0.1" + estree-walker "^2.0.2" + glob "^8.0.3" + is-reference "1.2.1" + magic-string "^0.27.0" + "@rollup/plugin-json@^4.0.0", "@rollup/plugin-json@^4.1.0": version "4.1.0" resolved "https://registry.yarnpkg.com/@rollup/plugin-json/-/plugin-json-4.1.0.tgz#54e09867ae6963c593844d8bd7a9c718294496f3" @@ -4380,6 +4903,14 @@ "@rollup/pluginutils" "^4.1.1" sucrase "^3.20.0" +"@rollup/plugin-typescript@^11.1.5": + version "11.1.5" + resolved "https://registry.yarnpkg.com/@rollup/plugin-typescript/-/plugin-typescript-11.1.5.tgz#039c763bf943a5921f3f42be255895e75764cb91" + integrity sha512-rnMHrGBB0IUEv69Q8/JGRD/n4/n6b3nfpufUu26axhUcboUzv/twfZU8fIBbTOphRAe0v8EyxzeDpKXqGHfyDA== + dependencies: + "@rollup/pluginutils" "^5.0.1" + resolve "^1.22.1" + "@rollup/plugin-typescript@^8.3.1": version "8.5.0" resolved "https://registry.yarnpkg.com/@rollup/plugin-typescript/-/plugin-typescript-8.5.0.tgz#7ea11599a15b0a30fa7ea69ce3b791d41b862515" @@ -4446,33 +4977,33 @@ semver "7.3.2" semver-intersect "1.4.0" -"@sentry-internal/rrdom@2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrdom/-/rrdom-2.0.0.tgz#326b5f26c76d2077874db7edffd5be3aa72848fb" - integrity sha512-PLSw54GWCmxOmJWJ2NGDfz9b+/76IBpGsWnIjBiW7L3NDVuTo705/7+DmKTrDADO7xXAZZRpbuQjqBjV8Mu+yQ== +"@sentry-internal/rrdom@2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrdom/-/rrdom-2.0.1.tgz#5d41892ff26462bb5e2412c2f2c646ef2dcfe0b5" + integrity sha512-uPQyq/ANoXSS5HpYkv9qupRSYh/tfbX4xBgM7XZDlApsnD3t6LxAqdAUP//zQO/z+kOHzJVUX5H5uiauqA96Yg== dependencies: - "@sentry-internal/rrweb-snapshot" "2.0.0" + "@sentry-internal/rrweb-snapshot" "2.0.1" -"@sentry-internal/rrweb-snapshot@2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-snapshot/-/rrweb-snapshot-2.0.0.tgz#6d034f4f65736e990e279842cf1c2868fc9f47dd" - integrity sha512-MFpUw2Kuq4OVQn1dv6l/oSPgbHdy8N0oWBeVeHQlBzxugje4i2KU9tf6K7KH2RAce7Bi9r5UgHvCsNG3PNi/XQ== +"@sentry-internal/rrweb-snapshot@2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-snapshot/-/rrweb-snapshot-2.0.1.tgz#5467041c33815d7c07ec0e484a85418d31857ddc" + integrity sha512-C4fIzcpreOzDXkyPOBwGir9YvLiT9jeTa2WQ96U1RVRiLBvXhEyPKgMxWXQcyYTpzYtGwX9dLfHR29uOejzzxQ== -"@sentry-internal/rrweb-types@2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-types/-/rrweb-types-2.0.0.tgz#8606e47d98e14580f46f98d5dc5d95bc9ebc8b59" - integrity sha512-3dgoh4sbqgY8XwsKh6ofA8WRtUE+qWLHPDMzipp1XefKfEhr6qTtw0riurnJBrO5lD6dJuewK5BWwjcrFb3Gag== +"@sentry-internal/rrweb-types@2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-types/-/rrweb-types-2.0.1.tgz#4f465715df2959cde486fe77fdda528d85a3c7f7" + integrity sha512-MQRdjsKm/kypHqumsWN+cmFhU0OWWoJSPNxOEG1efbUxZPvZL64tZSrgWimfisIId9TPDn0tr58sBhIgpqgNuw== dependencies: - "@sentry-internal/rrweb-snapshot" "2.0.0" + "@sentry-internal/rrweb-snapshot" "2.0.1" -"@sentry-internal/rrweb@2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb/-/rrweb-2.0.0.tgz#180e2763b77f83aa24bae964dd2f8c8065ddfc49" - integrity sha512-SOyIGjCi1q9ocMOHAAU6DhO2vecRkLk9/zQ6YbIJsUz1vB1ZoF0L1xDlwuL+fGw3HjZ6Wn8RoZWSSiQRokL7lg== +"@sentry-internal/rrweb@2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb/-/rrweb-2.0.1.tgz#fa3a60d1e01362ba2ce58583f87bfa076d77ee3b" + integrity sha512-X33eL2CioQn0vOgkFVgu9L8LV4D4H48LFz7cqAofnWC5h6n36zsf7eIBpdDJKZ8JCj1z52h9gL5X+X4W2i/yXQ== dependencies: - "@sentry-internal/rrdom" "2.0.0" - "@sentry-internal/rrweb-snapshot" "2.0.0" - "@sentry-internal/rrweb-types" "2.0.0" + "@sentry-internal/rrdom" "2.0.1" + "@sentry-internal/rrweb-snapshot" "2.0.1" + "@sentry-internal/rrweb-types" "2.0.1" "@types/css-font-loading-module" "0.0.7" "@xstate/fsm" "^1.4.0" base64-arraybuffer "^1.0.1" @@ -4493,6 +5024,20 @@ unplugin "1.0.1" webpack-sources "3.2.3" +"@sentry/bundler-plugin-core@2.8.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-2.8.0.tgz#e01df24d7f909277f453844132b856ed997c182b" + integrity sha512-DsTUgeKPqck3DkGzKjRduhPpEn0pez+/THF3gpwQBEfbPnKGr0EYugDvfungZwBFenckIvQBDTOZw0STvbgChA== + dependencies: + "@sentry/cli" "^2.21.2" + "@sentry/node" "^7.60.0" + "@sentry/utils" "^7.60.0" + dotenv "^16.3.1" + find-up "5.0.0" + glob "9.3.2" + magic-string "0.27.0" + unplugin "1.0.1" + "@sentry/cli@2.20.5": version "2.20.5" resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.20.5.tgz#255a5388ca24c211a0eae01dcc4ad813a7ff335a" @@ -4528,6 +5073,17 @@ proxy-from-env "^1.1.0" which "^2.0.2" +"@sentry/cli@^2.21.2": + version "2.21.2" + resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.21.2.tgz#89e5633ff48a83d078c76c6997fffd4b68b2da1c" + integrity sha512-X1nye89zl+QV3FSuQDGItfM51tW9PQ7ce0TtV/12DgGgTVEgnVp5uvO3wX5XauHvulQzRPzwUL3ZK+yS5bAwCw== + dependencies: + https-proxy-agent "^5.0.0" + node-fetch "^2.6.7" + progress "^2.0.3" + proxy-from-env "^1.1.0" + which "^2.0.2" + "@sentry/vite-plugin@^0.6.1": version "0.6.1" resolved "https://registry.yarnpkg.com/@sentry/vite-plugin/-/vite-plugin-0.6.1.tgz#31eb744e8d87b1528eed8d41433647727a62e7c0" @@ -4535,6 +5091,14 @@ dependencies: "@sentry/bundler-plugin-core" "0.6.1" +"@sentry/vite-plugin@^2.8.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@sentry/vite-plugin/-/vite-plugin-2.8.0.tgz#d19d2ebf07fcbf09bb585033d803b9967717e5a6" + integrity sha512-17++vXjfn0xEfE7W4FWdwoXdNNqGjXnuTvIgSLlhJvDCTcqWONDpA/TGXGLjbhQEmQ58wL4wQqmlyxoqMPlokQ== + dependencies: + "@sentry/bundler-plugin-core" "2.8.0" + unplugin "1.0.1" + "@sentry/webpack-plugin@1.19.0": version "1.19.0" resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-1.19.0.tgz#2b134318f1552ba7f3e3f9c83c71a202095f7a44" @@ -4864,6 +5428,17 @@ "@types/babel__template" "*" "@types/babel__traverse" "*" +"@types/babel__core@^7.20.1": + version "7.20.2" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.2.tgz#215db4f4a35d710256579784a548907237728756" + integrity sha512-pNpr1T1xLUc2l3xJKuPtsEky3ybxN3m4fJkknfIpTCTfIZCDW57oAg+EfCgIIp2rvCe0Wn++/FfodDS4YXxBwA== + dependencies: + "@babel/parser" "^7.20.7" + "@babel/types" "^7.20.7" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + "@types/babel__generator@*": version "7.6.2" resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.2.tgz#f3d71178e187858f7c45e30380f8f1b7415a12d8" @@ -4957,6 +5532,13 @@ resolved "https://registry.yarnpkg.com/@types/css-font-loading-module/-/css-font-loading-module-0.0.7.tgz#2f98ede46acc0975de85c0b7b0ebe06041d24601" integrity sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q== +"@types/debug@^4.0.0": + version "4.1.9" + resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.9.tgz#906996938bc672aaf2fb8c0d3733ae1dda05b005" + integrity sha512-8Hz50m2eoS56ldRlepxSBa6PWEVCtzUo/92HgLc2qTMnotJNIm7xP+UZhyWoYsyOdd5dxZ+NZLb24rsKyFs2ow== + dependencies: + "@types/ms" "*" + "@types/duplexify@^3.6.0": version "3.6.0" resolved "https://registry.yarnpkg.com/@types/duplexify/-/duplexify-3.6.0.tgz#dfc82b64bd3a2168f5bd26444af165bf0237dcd8" @@ -5274,6 +5856,13 @@ dependencies: "@types/node" "*" +"@types/hast@^2.0.0": + version "2.3.6" + resolved "https://registry.yarnpkg.com/@types/hast/-/hast-2.3.6.tgz#bb8b05602112a26d22868acb70c4b20984ec7086" + integrity sha512-47rJE80oqPmFdVDCD7IheXBrVdwuBgsYwoczFvKmwfo2Mzsnt+V9OONsYauFmICb6lQPpCuXYJWejBNs4pDJRg== + dependencies: + "@types/unist" "^2" + "@types/history-4@npm:@types/history@4.7.8", "@types/history-5@npm:@types/history@4.7.8", "@types/history@*": name "@types/history-4" version "4.7.8" @@ -5346,16 +5935,26 @@ "@types/parse5" "*" "@types/tough-cookie" "*" -"@types/json-schema@*", "@types/json-schema@^7.0.3", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": +"@types/json-schema@*", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": version "7.0.11" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== +"@types/json-schema@^7.0.12": + version "7.0.13" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.13.tgz#02c24f4363176d2d18fc8b70b9f3c54aba178a85" + integrity sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ== + "@types/json5@^0.0.29": version "0.0.29" resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= +"@types/json5@^0.0.30": + version "0.0.30" + resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.30.tgz#44cb52f32a809734ca562e685c6473b5754a7818" + integrity sha512-sqm9g7mHlPY/43fcSNrCYfOeX9zkTTK+euO5E6+CVijSMm5tTjkVdwdqRkY3ljjIAf8679vps5jKUoJBCLsMDA== + "@types/long@^4.0.0", "@types/long@^4.0.1": version "4.0.2" resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a" @@ -5373,6 +5972,20 @@ dependencies: "@types/node" "*" +"@types/mdast@^3.0.0": + version "3.0.13" + resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.13.tgz#b7ba6e52d0faeb9c493e32c205f3831022be4e1b" + integrity sha512-HjiGiWedR0DVFkeNljpa6Lv4/IZU1+30VY5d747K7lBudFc3R0Ibr6yJ9lN3BE28VnZyDfLF/VB1Ql1ZIbKrmg== + dependencies: + "@types/unist" "^2" + +"@types/mdast@^4.0.0": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-4.0.1.tgz#9c45e60a04e79f160dcefe6545d28ae536a6ed22" + integrity sha512-IlKct1rUTJ1T81d8OHzyop15kGv9A/ff7Gz7IJgrk6jDb4Udw77pCJ+vq8oxZf4Ghpm+616+i1s/LNg/Vh7d+g== + dependencies: + "@types/unist" "*" + "@types/mime@^1": version "1.3.2" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" @@ -5401,6 +6014,11 @@ "@types/bson" "*" "@types/node" "*" +"@types/ms@*": + version "0.7.32" + resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.32.tgz#f6cd08939ae3ad886fcc92ef7f0109dacddf61ab" + integrity sha512-xPSg0jm4mqgEkNhowKgZFBNtwoEwF6gJ4Dhww+GFpm3IgtNseHQZ5IqdNwnquZEoANxyDAKDRAdVo4Z72VvD/g== + "@types/mysql@2.15.21", "@types/mysql@^2.15.21": version "2.15.21" resolved "https://registry.yarnpkg.com/@types/mysql/-/mysql-2.15.21.tgz#7516cba7f9d077f980100c85fd500c8210bd5e45" @@ -5408,6 +6026,13 @@ dependencies: "@types/node" "*" +"@types/nlcst@^1.0.0": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@types/nlcst/-/nlcst-1.0.2.tgz#dfcc9ef164e2d2a76ce7d249a9b909b7d0b7b616" + integrity sha512-ykxL/GDDUhqikjU0LIywZvEwb1NTYXTEWf+XgMSS2o6IXIakafPccxZmxgZcvJPZ3yFl2kdL1gJZz3U3iZF3QA== + dependencies: + "@types/unist" "^2" + "@types/node-fetch@^2.6.0": version "2.6.2" resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.2.tgz#d1a9c5fd049d9415dce61571557104dec3ec81da" @@ -5421,6 +6046,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.38.tgz#f8bb07c371ccb1903f3752872c89f44006132947" integrity sha512-5jY9RhV7c0Z4Jy09G+NIDTsCZ5G0L5n+Z+p+Y7t5VJHM30bgwzSjVtlcBxqAj+6L/swIlvtOSzr8rBk/aNyV2g== +"@types/node@20.8.2": + version "20.8.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.8.2.tgz#d76fb80d87d0d8abfe334fc6d292e83e5524efc4" + integrity sha512-Vvycsc9FQdwhxE3y3DzeIxuEJbWGDsnrxvMADzTDF/lcdR9/K+AQIeAghTQsHtotg/q0j3WEOYS/jQgSdWue3w== + "@types/node@^10.1.0", "@types/node@~10.17.0": version "10.17.60" resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.60.tgz#35f3d6213daed95da7f0f73e75bcc6980e90597b" @@ -5456,6 +6086,11 @@ resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-6.0.0.tgz#38590dc2c3cf5717154064e3ee9b6947ee21b299" integrity sha512-oPwPSj4a1wu9rsXTEGIJz91ISU725t0BmSnUhb57sI+M8XEmvUop84lzuiYdq0Y5M6xLY8DBPg0C2xEQKLyvBA== +"@types/parse5@^6.0.0": + version "6.0.3" + resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-6.0.3.tgz#705bb349e789efa06f43f128cef51240753424cb" + integrity sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g== + "@types/pg-pool@2.0.3": version "2.0.3" resolved "https://registry.yarnpkg.com/@types/pg-pool/-/pg-pool-2.0.3.tgz#3eb8df2933f617f219a53091ad4080c94ba1c959" @@ -5571,6 +6206,11 @@ dependencies: "@types/node" "*" +"@types/resolve@^1.17.0": + version "1.20.3" + resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.3.tgz#066742d69a0bbba8c5d7d517f82e1140ddeb3c3c" + integrity sha512-NH5oErHOtHZYcjCtg69t26aXEk4BN2zLWqf7wnDZ+dpe0iR7Rds1SPGEItl3fca21oOe0n3OCnZ4W7jBxu7FOw== + "@types/rimraf@^2.0.2", "@types/rimraf@^2.0.3": version "2.0.4" resolved "https://registry.yarnpkg.com/@types/rimraf/-/rimraf-2.0.4.tgz#403887b0b53c6100a6c35d2ab24f6ccc042fec46" @@ -5602,6 +6242,11 @@ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.13.tgz#da4bfd73f49bd541d28920ab0e2bf0ee80f71c91" integrity sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw== +"@types/semver@^7.5.0": + version "7.5.3" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.3.tgz#9a726e116beb26c24f1ccd6850201e1246122e04" + integrity sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw== + "@types/send@*": version "0.17.1" resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.1.tgz#ed4932b8a2a805f1fe362a70f4e62d0ac994e301" @@ -5687,6 +6332,16 @@ dependencies: source-map "^0.6.1" +"@types/unist@*", "@types/unist@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.0.tgz#988ae8af1e5239e89f9fbb1ade4c935f4eeedf9a" + integrity sha512-MFETx3tbTjE7Uk6vvnWINA/1iJ7LuMdO4fcq8UfF0pRbj01aGLduVvQcRyswuACJdpnHgg8E3rQLhaRdNEJS0w== + +"@types/unist@^2", "@types/unist@^2.0.0": + version "2.0.8" + resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.8.tgz#bb197b9639aa1a04cf464a617fe800cccd92ad5c" + integrity sha512-d0XxK3YTObnWVp6rZuev3c49+j4Lo8g4L1ZRm9z5L0xpoZycUPshHgczK5gsUMaZOstjVYYi09p5gYvUtfChYw== + "@types/webpack-sources@*": version "3.2.0" resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-3.2.0.tgz#16d759ba096c289034b26553d2df1bf45248d38b" @@ -5758,17 +6413,6 @@ semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/experimental-utils@^2.19.2 || ^3.0.0": - version "3.10.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-3.10.1.tgz#e179ffc81a80ebcae2ea04e0332f8b251345a686" - integrity sha512-DewqIgscDzmAfd5nOGe4zm6Bl7PKtMG2Ad0KG8CUZAHlXfAKTF9Ol5PXhiMh39yRL2ChRH1cuuUGOcVyyrhQIw== - dependencies: - "@types/json-schema" "^7.0.3" - "@typescript-eslint/types" "3.10.1" - "@typescript-eslint/typescript-estree" "3.10.1" - eslint-scope "^5.0.0" - eslint-utils "^2.0.0" - "@typescript-eslint/parser@^5.48.0": version "5.48.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.48.0.tgz#02803355b23884a83e543755349809a50b7ed9ba" @@ -5795,6 +6439,14 @@ "@typescript-eslint/types" "5.62.0" "@typescript-eslint/visitor-keys" "5.62.0" +"@typescript-eslint/scope-manager@6.7.4": + version "6.7.4" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.7.4.tgz#a484a17aa219e96044db40813429eb7214d7b386" + integrity sha512-SdGqSLUPTXAXi7c3Ob7peAGVnmMoGzZ361VswK2Mqf8UOYcODiYvs8rs5ILqEdfvX1lE7wEZbLyELCW+Yrql1A== + dependencies: + "@typescript-eslint/types" "6.7.4" + "@typescript-eslint/visitor-keys" "6.7.4" + "@typescript-eslint/type-utils@5.48.0": version "5.48.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.48.0.tgz#40496dccfdc2daa14a565f8be80ad1ae3882d6d6" @@ -5805,11 +6457,6 @@ debug "^4.3.4" tsutils "^3.21.0" -"@typescript-eslint/types@3.10.1": - version "3.10.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-3.10.1.tgz#1d7463fa7c32d8a23ab508a803ca2fe26e758727" - integrity sha512-+3+FCUJIahE9q0lDi1WleYzjCwJs5hIsbugIgnbB+dSCYUxl8L6PwmsyOPFZde2hc1DlTo/xnkOgiTLSyAbHiQ== - "@typescript-eslint/types@4.23.0": version "4.23.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.23.0.tgz#da1654c8a5332f4d1645b2d9a1c64193cae3aa3b" @@ -5825,19 +6472,10 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f" integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ== -"@typescript-eslint/typescript-estree@3.10.1": - version "3.10.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-3.10.1.tgz#fd0061cc38add4fad45136d654408569f365b853" - integrity sha512-QbcXOuq6WYvnB3XPsZpIwztBoquEYLXh2MtwVU+kO8jgYCiv4G5xrSP/1wg4tkvrEE+esZVquIPX/dxPlePk1w== - dependencies: - "@typescript-eslint/types" "3.10.1" - "@typescript-eslint/visitor-keys" "3.10.1" - debug "^4.1.1" - glob "^7.1.6" - is-glob "^4.0.1" - lodash "^4.17.15" - semver "^7.3.2" - tsutils "^3.17.1" +"@typescript-eslint/types@6.7.4": + version "6.7.4" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.7.4.tgz#5d358484d2be986980c039de68e9f1eb62ea7897" + integrity sha512-o9XWK2FLW6eSS/0r/tgjAGsYasLAnOWg7hvZ/dGYSSNjCh+49k5ocPN8OmG5aZcSJ8pclSOyVKP2x03Sj+RrCA== "@typescript-eslint/typescript-estree@5.48.0": version "5.48.0" @@ -5865,6 +6503,19 @@ semver "^7.3.7" tsutils "^3.21.0" +"@typescript-eslint/typescript-estree@6.7.4": + version "6.7.4" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.7.4.tgz#f2baece09f7bb1df9296e32638b2e1130014ef1a" + integrity sha512-ty8b5qHKatlNYd9vmpHooQz3Vki3gG+3PchmtsA4TgrZBKWHNjWfkQid7K7xQogBqqc7/BhGazxMD5vr6Ha+iQ== + dependencies: + "@typescript-eslint/types" "6.7.4" + "@typescript-eslint/visitor-keys" "6.7.4" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + semver "^7.5.4" + ts-api-utils "^1.0.1" + "@typescript-eslint/typescript-estree@^4.8.2": version "4.23.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.23.0.tgz#0753b292097523852428a6f5a1aa8ccc1aae6cd9" @@ -5906,12 +6557,18 @@ eslint-scope "^5.1.1" semver "^7.3.7" -"@typescript-eslint/visitor-keys@3.10.1": - version "3.10.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-3.10.1.tgz#cd4274773e3eb63b2e870ac602274487ecd1e931" - integrity sha512-9JgC82AaQeglebjZMgYR5wgmfUdUc+EitGUUMW8u2nDckaeimzW+VsoLV6FoimPv2id3VQzfjwBxEMVz08ameQ== +"@typescript-eslint/utils@^6.0.0": + version "6.7.4" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.7.4.tgz#2236f72b10e38277ee05ef06142522e1de470ff2" + integrity sha512-PRQAs+HUn85Qdk+khAxsVV+oULy3VkbH3hQ8hxLRJXWBEd7iI+GbQxH5SEUSH7kbEoTp6oT1bOwyga24ELALTA== dependencies: - eslint-visitor-keys "^1.1.0" + "@eslint-community/eslint-utils" "^4.4.0" + "@types/json-schema" "^7.0.12" + "@types/semver" "^7.5.0" + "@typescript-eslint/scope-manager" "6.7.4" + "@typescript-eslint/types" "6.7.4" + "@typescript-eslint/typescript-estree" "6.7.4" + semver "^7.5.4" "@typescript-eslint/visitor-keys@4.23.0": version "4.23.0" @@ -5937,6 +6594,14 @@ "@typescript-eslint/types" "5.62.0" eslint-visitor-keys "^3.3.0" +"@typescript-eslint/visitor-keys@6.7.4": + version "6.7.4" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.7.4.tgz#80dfecf820fc67574012375859085f91a4dff043" + integrity sha512-pOW37DUhlTZbvph50x5zZCkFn3xzwkGtNoJHzIM3svpiSkJzwOYr/kVBaXmf+RAQiUDs1AHEZVNPg6UJCJpwRA== + dependencies: + "@typescript-eslint/types" "6.7.4" + eslint-visitor-keys "^3.4.1" + "@vitest/coverage-c8@^0.29.2": version "0.29.2" resolved "https://registry.yarnpkg.com/@vitest/coverage-c8/-/coverage-c8-0.29.2.tgz#30b81e32ff11c20e2f3ab78c84e21b4c6c08190c" @@ -6484,6 +7149,11 @@ acorn@^8.0.4, acorn@^8.2.4, acorn@^8.4.1, acorn@^8.5.0, acorn@^8.7.0, acorn@^8.7 resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8" integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w== +acorn@^8.10.0: + version "8.10.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" + integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== + add-stream@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/add-stream/-/add-stream-1.0.0.tgz#6a7990437ca736d5e1288db92bd3266d5f5cb2aa" @@ -6628,7 +7298,7 @@ anser@1.4.9: resolved "https://registry.yarnpkg.com/anser/-/anser-1.4.9.tgz#1f85423a5dcf8da4631a341665ff675b96845760" integrity sha512-AI+BjTeGt2+WFk4eWcqbQ7snZpDBt8SaLlj0RT2h5xfdWaiy51OjYvqwMrNzJLGy8iOAL6nKDITWO+rd4MkYEA== -ansi-align@^3.0.0: +ansi-align@^3.0.0, ansi-align@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.1.tgz#0cdf12e111ace773a86e9a1fad1225c43cb19a59" integrity sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w== @@ -6704,6 +7374,11 @@ ansi-regex@^6.0.1: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== +ansi-sequence-parser@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ansi-sequence-parser/-/ansi-sequence-parser-1.1.1.tgz#e0aa1cdcbc8f8bb0b5bca625aac41f5f056973cf" + integrity sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg== + ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" @@ -7024,6 +7699,11 @@ array-includes@^3.1.1, array-includes@^3.1.2, array-includes@^3.1.6: get-intrinsic "^1.1.3" is-string "^1.0.7" +array-iterate@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/array-iterate/-/array-iterate-2.0.1.tgz#6efd43f8295b3fee06251d3d62ead4bd9805dd24" + integrity sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg== + array-to-error@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/array-to-error/-/array-to-error-1.1.1.tgz#d68812926d14097a205579a667eeaf1856a44c07" @@ -7193,6 +7873,69 @@ astral-regex@^2.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== +astro@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/astro/-/astro-3.2.3.tgz#a6f14bf946555683ee1537a345e4a819a3aeff9b" + integrity sha512-1epnxQhTbfzgdmLP1yu51E8zjIOKYxZyA8hMTD4S2E+F5gMp/D81H4hekPbbq89GDxNJiHDRNZDHtS5vrU5E5w== + dependencies: + "@astrojs/compiler" "^2.1.0" + "@astrojs/internal-helpers" "0.2.1" + "@astrojs/markdown-remark" "3.2.1" + "@astrojs/telemetry" "3.0.3" + "@babel/core" "^7.22.10" + "@babel/generator" "^7.22.10" + "@babel/parser" "^7.22.10" + "@babel/plugin-transform-react-jsx" "^7.22.5" + "@babel/traverse" "^7.22.10" + "@babel/types" "^7.22.10" + "@types/babel__core" "^7.20.1" + acorn "^8.10.0" + boxen "^7.1.1" + chokidar "^3.5.3" + ci-info "^3.8.0" + clsx "^2.0.0" + common-ancestor-path "^1.0.1" + cookie "^0.5.0" + debug "^4.3.4" + devalue "^4.3.2" + diff "^5.1.0" + es-module-lexer "^1.3.0" + esbuild "^0.19.2" + estree-walker "^3.0.3" + execa "^8.0.1" + fast-glob "^3.3.1" + github-slugger "^2.0.0" + gray-matter "^4.0.3" + html-escaper "^3.0.3" + http-cache-semantics "^4.1.1" + js-yaml "^4.1.0" + kleur "^4.1.4" + magic-string "^0.30.3" + mime "^3.0.0" + ora "^7.0.1" + p-limit "^4.0.0" + path-to-regexp "^6.2.1" + preferred-pm "^3.1.2" + probe-image-size "^7.2.3" + prompts "^2.4.2" + rehype "^12.0.1" + resolve "^1.22.4" + semver "^7.5.4" + server-destroy "^1.0.1" + shiki "^0.14.3" + string-width "^6.1.0" + strip-ansi "^7.1.0" + tsconfig-resolver "^3.0.1" + unist-util-visit "^4.1.2" + vfile "^5.3.7" + vite "^4.4.9" + vitefu "^0.2.4" + which-pm "^2.1.1" + yargs-parser "^21.1.1" + zod "3.21.1" + optionalDependencies: + sharp "^0.32.5" + async-disk-cache@^1.2.1: version "1.3.5" resolved "https://registry.yarnpkg.com/async-disk-cache/-/async-disk-cache-1.3.5.tgz#cc6206ed79bb6982b878fc52e0505e4f52b62a02" @@ -7372,6 +8115,11 @@ axios@^1.0.0: form-data "^4.0.0" proxy-from-env "^1.1.0" +b4a@^1.6.4: + version "1.6.4" + resolved "https://registry.yarnpkg.com/b4a/-/b4a-1.6.4.tgz#ef1c1422cae5ce6535ec191baeed7567443f36c9" + integrity sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw== + babel-code-frame@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" @@ -8178,6 +8926,11 @@ backbone@^1.1.2: dependencies: underscore ">=1.8.3" +bail@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/bail/-/bail-2.0.2.tgz#d26f5cd8fe5d6f832a31517b9f7c356040ba6d5d" + integrity sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw== + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -8299,6 +9052,15 @@ bl@^4.0.3, bl@^4.1.0: inherits "^2.0.4" readable-stream "^3.4.0" +bl@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-5.1.0.tgz#183715f678c7188ecef9fe475d90209400624273" + integrity sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ== + dependencies: + buffer "^6.0.3" + inherits "^2.0.4" + readable-stream "^3.4.0" + blank-object@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/blank-object/-/blank-object-1.0.2.tgz#f990793fbe9a8c8dd013fb3219420bec81d5f4b9" @@ -8400,6 +9162,20 @@ boxen@^5.0.0: widest-line "^3.1.0" wrap-ansi "^7.0.0" +boxen@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/boxen/-/boxen-7.1.1.tgz#f9ba525413c2fec9cdb88987d835c4f7cad9c8f4" + integrity sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog== + dependencies: + ansi-align "^3.0.1" + camelcase "^7.0.1" + chalk "^5.2.0" + cli-boxes "^3.0.0" + string-width "^5.1.2" + type-fest "^2.13.0" + widest-line "^4.0.1" + wrap-ansi "^8.1.0" + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -9020,6 +9796,16 @@ browserslist@^4.16.1, browserslist@^4.21.5, browserslist@^4.6.4: node-releases "^2.0.8" update-browserslist-db "^1.0.10" +browserslist@^4.21.9: + version "4.22.1" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.22.1.tgz#ba91958d1a59b87dab6fed8dfbcb3da5e2e9c619" + integrity sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ== + dependencies: + caniuse-lite "^1.0.30001541" + electron-to-chromium "^1.4.535" + node-releases "^2.0.13" + update-browserslist-db "^1.0.13" + browserstack-local@^1.3.7: version "1.4.8" resolved "https://registry.yarnpkg.com/browserstack-local/-/browserstack-local-1.4.8.tgz#07f74a19b324cf2de69ffe65f9c2baa3a2dd9a0e" @@ -9123,8 +9909,16 @@ buffer@^5.2.1, buffer@^5.5.0, buffer@^5.6.0: base64-js "^1.3.1" ieee754 "^1.1.13" -builtin-modules@^3.1.0: - version "3.2.0" +buffer@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + +builtin-modules@^3.1.0: + version "3.2.0" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.2.0.tgz#45d5db99e7ee5e6bc4f362e008bf917ab5049887" integrity sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA== @@ -9463,6 +10257,11 @@ camelcase@^6.0.0, camelcase@^6.1.0, camelcase@^6.2.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== +camelcase@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-7.0.1.tgz#f02e50af9fd7782bc8b88a3558c32fd3a388f048" + integrity sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw== + can-symlink@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/can-symlink/-/can-symlink-1.0.0.tgz#97b607d8a84bb6c6e228b902d864ecb594b9d219" @@ -9490,6 +10289,11 @@ caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001449: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001453.tgz#6d3a1501622bf424a3cee5ad9550e640b0de3de8" integrity sha512-R9o/uySW38VViaTrOtwfbFEiBFUh7ST3uIG4OEymIG3/uKdHDO4xk/FaqfUw0d+irSUyFPy3dZszf9VvSTPnsA== +caniuse-lite@^1.0.30001541: + version "1.0.30001546" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001546.tgz#10fdad03436cfe3cc632d3af7a99a0fb497407f0" + integrity sha512-zvtSJwuQFpewSyRrI3AsftF6rM0X80mZkChIt1spBGEvRglCrjTniXvinc8JKRoqTwXAgvqTImaN9igfSMtUBw== + canonical-path@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/canonical-path/-/canonical-path-1.0.0.tgz#fcb470c23958def85081856be7a86e904f180d1d" @@ -9515,6 +10319,11 @@ caseless@~0.12.0: resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= +ccount@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5" + integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg== + chai@^4.1.2: version "4.3.4" resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.4.tgz#b55e655b31e1eac7099be4c08c21964fce2e6c49" @@ -9592,11 +10401,31 @@ chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2: ansi-styles "^4.1.0" supports-color "^7.1.0" +chalk@^5.0.0, chalk@^5.2.0, chalk@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385" + integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== + char-regex@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== +character-entities-html4@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-2.1.0.tgz#1f1adb940c971a4b22ba39ddca6b618dc6e56b2b" + integrity sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA== + +character-entities-legacy@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz#76bc83a90738901d7bc223a9e93759fdd560125b" + integrity sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ== + +character-entities@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-2.0.2.tgz#2d09c2e72cd9523076ccb21157dff66ad43fcc22" + integrity sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ== + chardet@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" @@ -9695,6 +10524,11 @@ ci-info@^3.6.1: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.8.0.tgz#81408265a5380c929f0bc665d62256628ce9ef91" integrity sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw== +ci-info@^3.8.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" + integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== + ci-job-number@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/ci-job-number/-/ci-job-number-1.2.2.tgz#f4e5918fcaeeda95b604f214be7d7d4a961fe0c0" @@ -9787,6 +10621,11 @@ cli-boxes@^2.2.1: resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f" integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw== +cli-boxes@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-3.0.0.tgz#71a10c716feeba005e4504f36329ef0b17cf3145" + integrity sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g== + cli-cursor@3.1.0, cli-cursor@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" @@ -9801,11 +10640,23 @@ cli-cursor@^2.1.0: dependencies: restore-cursor "^2.0.0" +cli-cursor@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-4.0.0.tgz#3cecfe3734bf4fe02a8361cbdc0f6fe28c6a57ea" + integrity sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg== + dependencies: + restore-cursor "^4.0.0" + cli-spinners@2.6.1, cli-spinners@^2.0.0, cli-spinners@^2.4.0, cli-spinners@^2.5.0: version "2.6.1" resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.6.1.tgz#adc954ebe281c37a6319bfa401e6dd2488ffb70d" integrity sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g== +cli-spinners@^2.9.0: + version "2.9.1" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.9.1.tgz#9c0b9dad69a6d47cbb4333c14319b060ed395a35" + integrity sha512-jHgecW0pxkonBJdrKsqxgRX9AcG+u/5k0Q7WPDfi8AogLAdwxEkyYYNWwZ5GvVFoFx2uiY1eNcSK00fh+1+FyQ== + cli-table3@^0.6.0: version "0.6.3" resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.3.tgz#61ab765aac156b52f222954ffc607a6f01dbeeb2" @@ -9902,6 +10753,11 @@ clone@^2.1.2: resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18= +clsx@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.0.0.tgz#12658f3fd98fafe62075595a5c30e43d18f3d00b" + integrity sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q== + cmd-shim@6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/cmd-shim/-/cmd-shim-6.0.1.tgz#a65878080548e1dca760b3aea1e21ed05194da9d" @@ -9982,7 +10838,7 @@ color-string@^1.5.4: color-name "^1.0.0" simple-swizzle "^0.2.2" -color-string@^1.6.0: +color-string@^1.6.0, color-string@^1.9.0: version "1.9.1" resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4" integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg== @@ -10011,6 +10867,14 @@ color@^3.1.3: color-convert "^1.9.3" color-string "^1.6.0" +color@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/color/-/color-4.2.3.tgz#d781ecb5e57224ee43ea9627560107c0e0c6463a" + integrity sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A== + dependencies: + color-convert "^2.0.1" + color-string "^1.9.0" + colord@^2.9.1: version "2.9.3" resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.3.tgz#4f8ce919de456f1d5c1c368c307fe20f3e59fb43" @@ -10064,6 +10928,11 @@ combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" +comma-separated-tokens@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee" + integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg== + commander@10.0.0: version "10.0.0" resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.0.tgz#71797971162cd3cf65f0b9d24eb28f8d303acdf1" @@ -10111,6 +10980,11 @@ commenting@1.1.0: resolved "https://registry.yarnpkg.com/commenting/-/commenting-1.1.0.tgz#fae14345c6437b8554f30bc6aa6c1e1633033590" integrity sha512-YeNK4tavZwtH7jEgK1ZINXzLKm6DZdEMfsaaieOsCAN0S8vsY7UeuO3Q7d/M018EFgE+IeUAuBOKkFccBZsUZA== +common-ancestor-path@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/common-ancestor-path/-/common-ancestor-path-1.0.1.tgz#4f7d2d1394d91b7abdf51871c62f71eadb0182a7" + integrity sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w== + common-tags@^1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.0.tgz#8e3153e542d4a39e9b10554434afaaf98956a937" @@ -10353,6 +11227,11 @@ convert-source-map@^0.3.3: resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-0.3.5.tgz#f1d802950af7dd2631a1febe0596550c86ab3190" integrity sha1-8dgClQr33SYxof6+BZZVDIarMZA= +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== + convert-source-map@~1.1.0: version "1.1.3" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.1.3.tgz#4829c877e9fe49b3161f3bf3673888e204699860" @@ -11186,6 +12065,13 @@ decimal.js@^10.2.1, decimal.js@^10.3.1: resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783" integrity sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ== +decode-named-character-reference@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz#daabac9690874c394c81e4162a0304b35d824f0e" + integrity sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg== + dependencies: + character-entities "^2.0.0" + decode-uri-component@^0.2.0: version "0.2.2" resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9" @@ -11205,6 +12091,13 @@ decompress-response@^3.3.0: dependencies: mimic-response "^1.0.0" +decompress-response@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" + integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== + dependencies: + mimic-response "^3.1.0" + dedent@0.7.0, dedent@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" @@ -11380,6 +12273,11 @@ deprecation@^2.0.0, deprecation@^2.3.1: resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919" integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ== +dequal@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" + integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== + des.js@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843" @@ -11415,6 +12313,11 @@ detect-indent@^6.0.0: resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.0.0.tgz#0abd0f549f69fc6659a254fe96786186b6f528fd" integrity sha512-oSyFlqaTHCItVRGK5RmrmjB+CmaMOW7IaNA/kdxqhoa6d17j/5ce9O9eWXmV/KEdRwqpQA+Vqe8a8Bsybu4YnA== +detect-libc@^2.0.0, detect-libc@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.2.tgz#8ccf2ba9315350e1241b88d0ac3b0e1fbd99605d" + integrity sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw== + detect-newline@3.1.0, detect-newline@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" @@ -11517,6 +12420,11 @@ devalue@^4.3.0: resolved "https://registry.yarnpkg.com/devalue/-/devalue-4.3.0.tgz#d86db8fee63a70317c2355be0d3d1b4d8f89a44e" integrity sha512-n94yQo4LI3w7erwf84mhRUkUJfhLoCZiLyoOZ/QFsDbcWNZePrLwbQpvZBUG2TNxwV3VjCKPxkiiQA6pe3TrTA== +devalue@^4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/devalue/-/devalue-4.3.2.tgz#cc44e4cf3872ac5a78229fbce3b77e57032727b5" + integrity sha512-KqFl6pOgOW+Y6wJgu80rHpo2/3H07vr8ntR9rkkFIRETewbf5GaYYcakYfiKz89K+sLsuPkQIZaXDMjUObZwWg== + dezalgo@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.3.tgz#7f742de066fc748bc8db820569dddce49bf0d456" @@ -11550,7 +12458,7 @@ diff@^4.0.1: resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== -diff@^5.1.0: +diff@^5.0.0, diff@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/diff/-/diff-5.1.0.tgz#bc52d298c5ea8df9194800224445ed43ffc87e40" integrity sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw== @@ -11571,6 +12479,11 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" +dlv@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79" + integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA== + dns-equal@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d" @@ -11737,6 +12650,11 @@ dotenv@16.0.3: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.3.tgz#115aec42bac5053db3c456db30cc243a5a836a07" integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ== +dotenv@^16.3.1: + version "16.3.1" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e" + integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ== + dotenv@~10.0.0: version "10.0.0" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81" @@ -11751,6 +12669,11 @@ downlevel-dts@~0.11.0: shelljs "^0.8.3" typescript next +dset@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/dset/-/dset-3.1.2.tgz#89c436ca6450398396dc6538ea00abc0c54cd45a" + integrity sha512-g/M9sqy3oHe477Ar4voQxWtaPIFw1jTdKZuomOjhCcBx9nHUNn0pu6NopuFFrTh/TRZIKEj+76vLWFu9BNKk+Q== + duplexer3@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" @@ -11836,6 +12759,11 @@ electron-to-chromium@^1.4.284: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.299.tgz#faa2069cd4879a73e540e533178db5c618768d41" integrity sha512-lQ7ijJghH6pCGbfWXr6EY+KYCMaRSjgsY925r1p/TlpSfVM1VjHTcn1gAc15VM4uwti283X6QtjPTXdpoSGiZQ== +electron-to-chromium@^1.4.535: + version "1.4.543" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.543.tgz#51116ffc9fba1ee93514d6a40d34676aa6d7d1c4" + integrity sha512-t2ZP4AcGE0iKCCQCBx/K2426crYdxD3YU6l0uK2EO3FZH0pbC4pFz/sZm2ruZsND6hQBTcDWWlo/MLpiOdif5g== + elliptic@^6.5.3: version "6.5.4" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" @@ -12536,6 +13464,11 @@ emittery@^0.8.1: resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.8.1.tgz#bb23cc86d03b30aa75a7f734819dee2e1ba70860" integrity sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg== +emoji-regex@^10.2.1: + version "10.2.1" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.2.1.tgz#a41c330d957191efd3d9dfe6e1e8e1e9ab048b3f" + integrity sha512-97g6QgOk8zlDRdgq1WxwgTMgEWGVAQvB5Fdpgc1MkNy56la5SKP9GsMXKDOdqwn90/41a8yPwIGk1Y6WVbeMQA== + emoji-regex@^7.0.1: version "7.0.3" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" @@ -12771,6 +13704,11 @@ es-module-lexer@^0.9.0: resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.9.3.tgz#6f13db00cc38417137daf74366f535c8eb438f19" integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ== +es-module-lexer@^1.3.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.3.1.tgz#c1b0dd5ada807a3b3155315911f364dc4e909db1" + integrity sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q== + es-shim-unscopables@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz#702e632193201e3edf8713635d083d378e510241" @@ -12964,6 +13902,62 @@ esbuild@^0.16.14, esbuild@^0.16.3: "@esbuild/win32-ia32" "0.16.17" "@esbuild/win32-x64" "0.16.17" +esbuild@^0.18.10: + version "0.18.20" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.18.20.tgz#4709f5a34801b43b799ab7d6d82f7284a9b7a7a6" + integrity sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA== + optionalDependencies: + "@esbuild/android-arm" "0.18.20" + "@esbuild/android-arm64" "0.18.20" + "@esbuild/android-x64" "0.18.20" + "@esbuild/darwin-arm64" "0.18.20" + "@esbuild/darwin-x64" "0.18.20" + "@esbuild/freebsd-arm64" "0.18.20" + "@esbuild/freebsd-x64" "0.18.20" + "@esbuild/linux-arm" "0.18.20" + "@esbuild/linux-arm64" "0.18.20" + "@esbuild/linux-ia32" "0.18.20" + "@esbuild/linux-loong64" "0.18.20" + "@esbuild/linux-mips64el" "0.18.20" + "@esbuild/linux-ppc64" "0.18.20" + "@esbuild/linux-riscv64" "0.18.20" + "@esbuild/linux-s390x" "0.18.20" + "@esbuild/linux-x64" "0.18.20" + "@esbuild/netbsd-x64" "0.18.20" + "@esbuild/openbsd-x64" "0.18.20" + "@esbuild/sunos-x64" "0.18.20" + "@esbuild/win32-arm64" "0.18.20" + "@esbuild/win32-ia32" "0.18.20" + "@esbuild/win32-x64" "0.18.20" + +esbuild@^0.19.2: + version "0.19.4" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.19.4.tgz#cdf5c4c684956d550bc3c6d0c01dac7fef6c75b1" + integrity sha512-x7jL0tbRRpv4QUyuDMjONtWFciygUxWaUM1kMX2zWxI0X2YWOt7MSA0g4UdeSiHM8fcYVzpQhKYOycZwxTdZkA== + optionalDependencies: + "@esbuild/android-arm" "0.19.4" + "@esbuild/android-arm64" "0.19.4" + "@esbuild/android-x64" "0.19.4" + "@esbuild/darwin-arm64" "0.19.4" + "@esbuild/darwin-x64" "0.19.4" + "@esbuild/freebsd-arm64" "0.19.4" + "@esbuild/freebsd-x64" "0.19.4" + "@esbuild/linux-arm" "0.19.4" + "@esbuild/linux-arm64" "0.19.4" + "@esbuild/linux-ia32" "0.19.4" + "@esbuild/linux-loong64" "0.19.4" + "@esbuild/linux-mips64el" "0.19.4" + "@esbuild/linux-ppc64" "0.19.4" + "@esbuild/linux-riscv64" "0.19.4" + "@esbuild/linux-s390x" "0.19.4" + "@esbuild/linux-x64" "0.19.4" + "@esbuild/netbsd-x64" "0.19.4" + "@esbuild/openbsd-x64" "0.19.4" + "@esbuild/sunos-x64" "0.19.4" + "@esbuild/win32-arm64" "0.19.4" + "@esbuild/win32-ia32" "0.19.4" + "@esbuild/win32-x64" "0.19.4" + escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" @@ -12994,6 +13988,11 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== +escape-string-regexp@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8" + integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw== + escodegen@1.8.x: version "1.8.1" resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.8.1.tgz#5a5b53af4693110bebb0867aa3430dd3b70a1018" @@ -13041,14 +14040,14 @@ eslint-module-utils@^2.6.0: debug "^2.6.9" pkg-dir "^2.0.0" -eslint-plugin-deprecation@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-deprecation/-/eslint-plugin-deprecation-1.2.0.tgz#e12333a857986fc87fa2eff44c7425eba9653070" - integrity sha512-SrZqomFYofRbxJ9dlAcu526/tiZoWoZgHdZWKHjrRT/uLfTtTTjdVf0gdy0AZxK8nH5ri0fukgwS28llUueitA== +eslint-plugin-deprecation@^1.5.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-deprecation/-/eslint-plugin-deprecation-1.6.0.tgz#b12d0c5a9baf3bcde0752ff6337703c059a4ae23" + integrity sha512-rld+Vrneh/NXRtDB0vQifOvgUy0HJYoejaxWlVnsk/LK7iij2tCWQIFcCKG4uzQb+Ef86bDke39w1lh4wnon4Q== dependencies: - "@typescript-eslint/experimental-utils" "^2.19.2 || ^3.0.0" - tslib "^1.10.0" - tsutils "^3.0.0" + "@typescript-eslint/utils" "^6.0.0" + tslib "^2.3.1" + tsutils "^3.21.0" eslint-plugin-ember@11.9.0: version "11.9.0" @@ -13169,7 +14168,7 @@ eslint-plugin-simple-import-sort@^5.0.3: resolved "https://registry.yarnpkg.com/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-5.0.3.tgz#9ae258ddada6efffc55e47a134afbd279eb31fc6" integrity sha512-1rf3AWiHeWNCQdAq0iXNnlccnH1UDnelGgrPbjBBHE8d2hXVtOudcmy0vTF4hri3iJ0MKz8jBhmH6lJ0ZWZLHQ== -eslint-scope@5.1.1, eslint-scope@^5.0.0, eslint-scope@^5.1.1: +eslint-scope@5.1.1, eslint-scope@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== @@ -13185,7 +14184,7 @@ eslint-scope@^4.0.3: esrecurse "^4.1.0" estraverse "^4.1.1" -eslint-utils@^2.0.0, eslint-utils@^2.1.0: +eslint-utils@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27" integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg== @@ -13214,6 +14213,11 @@ eslint-visitor-keys@^3.3.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== +eslint-visitor-keys@^3.4.1: + version "3.4.3" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + eslint@7.32.0: version "7.32.0" resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.32.0.tgz#c6d328a14be3fb08c8d1d21e12c02fdb7a2a812d" @@ -13338,6 +14342,13 @@ estree-walker@^2.0.1, estree-walker@^2.0.2: resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== +estree-walker@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-3.0.3.tgz#67c3e549ec402a487b4fc193d1953a524752340d" + integrity sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g== + dependencies: + "@types/estree" "^1.0.0" + esutils@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" @@ -13489,6 +14500,21 @@ execa@^5.0.0, execa@^5.1.1: signal-exit "^3.0.3" strip-final-newline "^2.0.0" +execa@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-8.0.1.tgz#51f6a5943b580f963c3ca9c6321796db8cc39b8c" + integrity sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^8.0.1" + human-signals "^5.0.0" + is-stream "^3.0.0" + merge-stream "^2.0.0" + npm-run-path "^5.1.0" + onetime "^6.0.0" + signal-exit "^4.1.0" + strip-final-newline "^3.0.0" + exists-sync@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/exists-sync/-/exists-sync-0.1.0.tgz#318d545213d2b2a31499e92c35f74c94196a22f7" @@ -13512,6 +14538,11 @@ expand-brackets@^2.1.4: snapdragon "^0.8.1" to-regex "^3.0.1" +expand-template@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" + integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== + expand-tilde@^2.0.0, expand-tilde@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502" @@ -13654,6 +14685,11 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== +fast-fifo@^1.1.0, fast-fifo@^1.2.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.3.2.tgz#286e31de96eb96d38a97899815740ba2a4f3640c" + integrity sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ== + fast-glob@3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.7.tgz#fd6cb7a2d7e9aa7a7846111e85a196d6b2f766a1" @@ -13676,6 +14712,17 @@ fast-glob@^3.0.3, fast-glob@^3.2.11, fast-glob@^3.2.12, fast-glob@^3.2.4, fast-g merge2 "^1.3.0" micromatch "^4.0.4" +fast-glob@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4" + integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + fast-json-stable-stringify@2.1.0, fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" @@ -14004,6 +15051,14 @@ find-up@^6.3.0: locate-path "^7.1.0" path-exists "^5.0.0" +find-yarn-workspace-root2@1.2.16: + version "1.2.16" + resolved "https://registry.yarnpkg.com/find-yarn-workspace-root2/-/find-yarn-workspace-root2-1.2.16.tgz#60287009dd2f324f59646bdb4b7610a6b301c2a9" + integrity sha512-hr6hb1w8ePMpPVUK39S4RlwJzi+xPLuVuG8XlwXU3KD5Yn3qgBWVfy3AzNlDhWvE1EORCE65/Qm26rFQt3VLVA== + dependencies: + micromatch "^4.0.2" + pkg-dir "^4.2.0" + find-yarn-workspace-root@^1.1.0: version "1.2.1" resolved "https://registry.yarnpkg.com/find-yarn-workspace-root/-/find-yarn-workspace-root-1.2.1.tgz#40eb8e6e7c2502ddfaa2577c176f221422f860db" @@ -14619,6 +15674,11 @@ get-stream@^5.0.0, get-stream@^5.1.0: dependencies: pump "^3.0.0" +get-stream@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-8.0.1.tgz#def9dfd71742cd7754a7761ed43749a27d02eca2" + integrity sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA== + get-symbol-description@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6" @@ -14696,6 +15756,16 @@ gitconfiglocal@^1.0.0: dependencies: ini "^1.3.2" +github-from-package@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" + integrity sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw== + +github-slugger@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/github-slugger/-/github-slugger-2.0.0.tgz#52cf2f9279a21eb6c59dd385b410f0c0adda8f1a" + integrity sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw== + glob-parent@5.1.2, glob-parent@^5.1.1, glob-parent@^5.1.2, glob-parent@~5.1.0, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" @@ -15069,7 +16139,7 @@ got@^9.6.0: to-readable-stream "^1.0.0" url-parse-lax "^3.0.0" -graceful-fs@4.2.11: +graceful-fs@4.2.11, graceful-fs@^4.1.5: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -15103,6 +16173,16 @@ graphviz@0.0.9: dependencies: temp "~0.4.0" +gray-matter@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/gray-matter/-/gray-matter-4.0.3.tgz#e893c064825de73ea1f5f7d88c7a9f7274288798" + integrity sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q== + dependencies: + js-yaml "^3.13.1" + kind-of "^6.0.2" + section-matter "^1.0.0" + strip-bom-string "^1.0.0" + growl@1.10.5: version "1.10.5" resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" @@ -15311,6 +16391,88 @@ hash.js@^1.0.0, hash.js@^1.0.3: inherits "^2.0.3" minimalistic-assert "^1.0.1" +hast-util-from-parse5@^7.0.0: + version "7.1.2" + resolved "https://registry.yarnpkg.com/hast-util-from-parse5/-/hast-util-from-parse5-7.1.2.tgz#aecfef73e3ceafdfa4550716443e4eb7b02e22b0" + integrity sha512-Nz7FfPBuljzsN3tCQ4kCBKqdNhQE2l0Tn+X1ubgKBPRoiDIu1mL08Cfw4k7q71+Duyaw7DXDN+VTAp4Vh3oCOw== + dependencies: + "@types/hast" "^2.0.0" + "@types/unist" "^2.0.0" + hastscript "^7.0.0" + property-information "^6.0.0" + vfile "^5.0.0" + vfile-location "^4.0.0" + web-namespaces "^2.0.0" + +hast-util-parse-selector@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-3.1.1.tgz#25ab00ae9e75cbc62cf7a901f68a247eade659e2" + integrity sha512-jdlwBjEexy1oGz0aJ2f4GKMaVKkA9jwjr4MjAAI22E5fM/TXVZHuS5OpONtdeIkRKqAaryQ2E9xNQxijoThSZA== + dependencies: + "@types/hast" "^2.0.0" + +hast-util-raw@^7.0.0, hast-util-raw@^7.2.0: + version "7.2.3" + resolved "https://registry.yarnpkg.com/hast-util-raw/-/hast-util-raw-7.2.3.tgz#dcb5b22a22073436dbdc4aa09660a644f4991d99" + integrity sha512-RujVQfVsOrxzPOPSzZFiwofMArbQke6DJjnFfceiEbFh7S05CbPt0cYN+A5YeD3pso0JQk6O1aHBnx9+Pm2uqg== + dependencies: + "@types/hast" "^2.0.0" + "@types/parse5" "^6.0.0" + hast-util-from-parse5 "^7.0.0" + hast-util-to-parse5 "^7.0.0" + html-void-elements "^2.0.0" + parse5 "^6.0.0" + unist-util-position "^4.0.0" + unist-util-visit "^4.0.0" + vfile "^5.0.0" + web-namespaces "^2.0.0" + zwitch "^2.0.0" + +hast-util-to-html@^8.0.0: + version "8.0.4" + resolved "https://registry.yarnpkg.com/hast-util-to-html/-/hast-util-to-html-8.0.4.tgz#0269ef33fa3f6599b260a8dc94f733b8e39e41fc" + integrity sha512-4tpQTUOr9BMjtYyNlt0P50mH7xj0Ks2xpo8M943Vykljf99HW6EzulIoJP1N3eKOSScEHzyzi9dm7/cn0RfGwA== + dependencies: + "@types/hast" "^2.0.0" + "@types/unist" "^2.0.0" + ccount "^2.0.0" + comma-separated-tokens "^2.0.0" + hast-util-raw "^7.0.0" + hast-util-whitespace "^2.0.0" + html-void-elements "^2.0.0" + property-information "^6.0.0" + space-separated-tokens "^2.0.0" + stringify-entities "^4.0.0" + zwitch "^2.0.4" + +hast-util-to-parse5@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/hast-util-to-parse5/-/hast-util-to-parse5-7.1.0.tgz#c49391bf8f151973e0c9adcd116b561e8daf29f3" + integrity sha512-YNRgAJkH2Jky5ySkIqFXTQiaqcAtJyVE+D5lkN6CdtOqrnkLfGYYrEcKuHOJZlp+MwjSwuD3fZuawI+sic/RBw== + dependencies: + "@types/hast" "^2.0.0" + comma-separated-tokens "^2.0.0" + property-information "^6.0.0" + space-separated-tokens "^2.0.0" + web-namespaces "^2.0.0" + zwitch "^2.0.0" + +hast-util-whitespace@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-2.0.1.tgz#0ec64e257e6fc216c7d14c8a1b74d27d650b4557" + integrity sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng== + +hastscript@^7.0.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-7.2.0.tgz#0eafb7afb153d047077fa2a833dc9b7ec604d10b" + integrity sha512-TtYPq24IldU8iKoJQqvZOuhi5CyCQRAbvDOX0x1eW6rsHSxa/1i2CCiptNTotGHJ3VoHRGmqiv6/D3q113ikkw== + dependencies: + "@types/hast" "^2.0.0" + comma-separated-tokens "^2.0.0" + hast-util-parse-selector "^3.0.0" + property-information "^6.0.0" + space-separated-tokens "^2.0.0" + hdr-histogram-js@^2.0.1: version "2.0.3" resolved "https://registry.yarnpkg.com/hdr-histogram-js/-/hdr-histogram-js-2.0.3.tgz#0b860534655722b6e3f3e7dca7b78867cf43dcb5" @@ -15546,6 +16708,11 @@ html-escaper@^2.0.0: resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== +html-escaper@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-3.0.3.tgz#4d336674652beb1dcbc29ef6b6ba7f6be6fdfed6" + integrity sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ== + html-minifier-terser@^6.0.2: version "6.1.0" resolved "https://registry.yarnpkg.com/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#bfc818934cc07918f6b3669f5774ecdfd48f32ab" @@ -15559,6 +16726,11 @@ html-minifier-terser@^6.0.2: relateurl "^0.2.7" terser "^5.10.0" +html-void-elements@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-2.0.1.tgz#29459b8b05c200b6c5ee98743c41b979d577549f" + integrity sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A== + html-webpack-plugin@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.5.0.tgz#c3911936f57681c1f9f4d8b68c158cd9dfe52f50" @@ -15745,6 +16917,11 @@ human-signals@^2.1.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== +human-signals@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-5.0.0.tgz#42665a284f9ae0dade3ba41ebc37eb4b852f3a28" + integrity sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ== + humanize-ms@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" @@ -15783,7 +16960,7 @@ ieee754@1.1.13: resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== -ieee754@^1.1.13, ieee754@^1.1.4: +ieee754@^1.1.13, ieee754@^1.1.4, ieee754@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== @@ -15917,6 +17094,11 @@ import-local@^2.0.0: pkg-dir "^3.0.0" resolve-cwd "^2.0.0" +import-meta-resolve@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/import-meta-resolve/-/import-meta-resolve-3.0.0.tgz#94a6aabc623874fbc2f3525ec1300db71c6cbc11" + integrity sha512-4IwhLhNNA8yy445rPjD/lWh++7hMDOml2eHtd58eG7h+qK3EryMuuRbsHGPikCoAgIkkDnckKfWSk2iDla/ejg== + imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" @@ -16233,7 +17415,7 @@ is-buffer@^1.1.5: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== -is-buffer@~2.0.3: +is-buffer@^2.0.0, is-buffer@~2.0.3: version "2.0.5" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== @@ -16283,6 +17465,13 @@ is-core-module@^2.11.0, is-core-module@^2.12.1, is-core-module@^2.5.0: dependencies: has "^1.0.3" +is-core-module@^2.13.0: + version "2.13.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.0.tgz#bb52aa6e2cbd49a30c2ba68c42bf3435ba6072db" + integrity sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ== + dependencies: + has "^1.0.3" + is-core-module@^2.2.0, is-core-module@^2.8.1, is-core-module@^2.9.0: version "2.11.0" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144" @@ -16337,6 +17526,11 @@ is-docker@^2.0.0, is-docker@^2.1.1: resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== +is-docker@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-3.0.0.tgz#90093aa3106277d8a77a5910dbae71747e15a200" + integrity sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ== + is-extendable@^0.1.0, is-extendable@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" @@ -16410,6 +17604,13 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" +is-inside-container@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-inside-container/-/is-inside-container-1.0.0.tgz#e81fba699662eb31dbdaf26766a61d4814717ea4" + integrity sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA== + dependencies: + is-docker "^3.0.0" + is-installed-globally@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.4.0.tgz#9a0fd407949c30f86eb6959ef1b7994ed0b7b520" @@ -16423,6 +17624,11 @@ is-interactive@^1.0.0: resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== +is-interactive@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-2.0.0.tgz#40c57614593826da1100ade6059778d597f16e90" + integrity sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ== + is-lambda@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5" @@ -16524,6 +17730,11 @@ is-plain-obj@^1.0.0, is-plain-obj@^1.1.0: resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= +is-plain-obj@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0" + integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg== + is-plain-object@^2.0.3, is-plain-object@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" @@ -16610,6 +17821,11 @@ is-stream@^1.0.1, is-stream@^1.1.0: resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= +is-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-3.0.0.tgz#e6bfd7aa6bef69f4f472ce9bb681e3e57b4319ac" + integrity sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA== + is-string@^1.0.5, is-string@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" @@ -16666,6 +17882,11 @@ is-unicode-supported@^0.1.0: resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== +is-unicode-supported@^1.1.0, is-unicode-supported@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz#d824984b616c292a2e198207d4a609983842f714" + integrity sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ== + is-url@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/is-url/-/is-url-1.2.4.tgz#04a4df46d28c4cff3d73d01ff06abeb318a1aa52" @@ -16705,6 +17926,13 @@ is-wsl@^2.1.0, is-wsl@^2.1.1, is-wsl@^2.2.0: dependencies: is-docker "^2.0.0" +is-wsl@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-3.1.0.tgz#e1c657e39c10090afcbedec61720f6b924c3cbd2" + integrity sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw== + dependencies: + is-inside-container "^1.0.0" + is-yarn-global@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/is-yarn-global/-/is-yarn-global-0.3.0.tgz#d502d3382590ea3004893746754c89139973e232" @@ -17439,7 +18667,7 @@ js-yaml@3.14.0: argparse "^1.0.7" esprima "^4.0.0" -js-yaml@3.x, js-yaml@^3.10.0, js-yaml@^3.13.1, js-yaml@^3.2.5, js-yaml@^3.2.7: +js-yaml@3.x, js-yaml@^3.10.0, js-yaml@^3.13.0, js-yaml@^3.13.1, js-yaml@^3.2.5, js-yaml@^3.2.7: version "3.14.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== @@ -17644,7 +18872,7 @@ json5@^1.0.1: dependencies: minimist "^1.2.0" -json5@^2.2.2: +json5@^2.1.3, json5@^2.2.2, json5@^2.2.3: version "2.2.3" resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== @@ -17965,7 +19193,7 @@ kleur@^3.0.3: resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== -kleur@^4.1.5: +kleur@^4.0.3, kleur@^4.1.4, kleur@^4.1.5: version "4.1.5" resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780" integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ== @@ -18329,6 +19557,16 @@ load-json-file@^4.0.0: pify "^3.0.0" strip-bom "^3.0.0" +load-yaml-file@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/load-yaml-file/-/load-yaml-file-0.2.0.tgz#af854edaf2bea89346c07549122753c07372f64d" + integrity sha512-OfCBkGEw4nN6JLtgRidPX6QxjBQGQf72q3si2uvqyFEMbycSFFHwAZeXx6cJgFM9wmLrf9zBwCP3Ivqa+LLZPw== + dependencies: + graceful-fs "^4.1.5" + js-yaml "^3.13.0" + pify "^4.0.1" + strip-bom "^3.0.0" + loader-runner@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357" @@ -18658,6 +19896,14 @@ log-symbols@^4.0.0, log-symbols@^4.1.0: chalk "^4.1.0" is-unicode-supported "^0.1.0" +log-symbols@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-5.1.0.tgz#a20e3b9a5f53fac6aeb8e2bb22c07cf2c8f16d93" + integrity sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA== + dependencies: + chalk "^5.0.0" + is-unicode-supported "^1.1.0" + log4js@^4.0.1: version "4.5.1" resolved "https://registry.yarnpkg.com/log4js/-/log4js-4.5.1.tgz#e543625e97d9e6f3e6e7c9fc196dd6ab2cae30b5" @@ -18714,6 +19960,11 @@ long@^4.0.0: resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== +longest-streak@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-3.1.0.tgz#62fa67cd958742a1574af9f39866364102d90cd4" + integrity sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g== + loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" @@ -18865,6 +20116,13 @@ magic-string@^0.30.0: dependencies: "@jridgewell/sourcemap-codec" "^1.4.13" +magic-string@^0.30.3, magic-string@^0.30.4: + version "0.30.4" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.4.tgz#c2c683265fc18dda49b56fc7318d33ca0332c98c" + integrity sha512-Q/TKtsC5BPm0kGqgBIF9oXAs/xEf2vRKiIB4wCRQTJOQIByZ1d+NnUOotvJOvNpi5RNIgVOMC3pOuaP1ZTDlVg== + dependencies: + "@jridgewell/sourcemap-codec" "^1.4.15" + magicast@0.2.8: version "0.2.8" resolved "https://registry.yarnpkg.com/magicast/-/magicast-0.2.8.tgz#02b298c65fbc5b7d1fce52ef779c59caf68cc9cf" @@ -19050,6 +20308,11 @@ markdown-it@^8.3.1: mdurl "^1.0.1" uc.micro "^1.0.5" +markdown-table@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-3.0.3.tgz#e6331d30e493127e031dd385488b5bd326e4a6bd" + integrity sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw== + marked@^1.1.1: version "1.2.9" resolved "https://registry.yarnpkg.com/marked/-/marked-1.2.9.tgz#53786f8b05d4c01a2a5a76b7d1ec9943d29d72dc" @@ -19084,6 +20347,153 @@ md5.js@^1.3.4: inherits "^2.0.1" safe-buffer "^5.1.2" +mdast-util-definitions@^5.0.0: + version "5.1.2" + resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-5.1.2.tgz#9910abb60ac5d7115d6819b57ae0bcef07a3f7a7" + integrity sha512-8SVPMuHqlPME/z3gqVwWY4zVXn8lqKv/pAhC57FuJ40ImXyBpmO5ukh98zB2v7Blql2FiHjHv9LVztSIqjY+MA== + dependencies: + "@types/mdast" "^3.0.0" + "@types/unist" "^2.0.0" + unist-util-visit "^4.0.0" + +mdast-util-definitions@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-6.0.0.tgz#c1bb706e5e76bb93f9a09dd7af174002ae69ac24" + integrity sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ== + dependencies: + "@types/mdast" "^4.0.0" + "@types/unist" "^3.0.0" + unist-util-visit "^5.0.0" + +mdast-util-find-and-replace@^2.0.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/mdast-util-find-and-replace/-/mdast-util-find-and-replace-2.2.2.tgz#cc2b774f7f3630da4bd592f61966fecade8b99b1" + integrity sha512-MTtdFRz/eMDHXzeK6W3dO7mXUlF82Gom4y0oOgvHhh/HXZAGvIQDUvQ0SuUx+j2tv44b8xTHOm8K/9OoRFnXKw== + dependencies: + "@types/mdast" "^3.0.0" + escape-string-regexp "^5.0.0" + unist-util-is "^5.0.0" + unist-util-visit-parents "^5.0.0" + +mdast-util-from-markdown@^1.0.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz#9421a5a247f10d31d2faed2a30df5ec89ceafcf0" + integrity sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww== + dependencies: + "@types/mdast" "^3.0.0" + "@types/unist" "^2.0.0" + decode-named-character-reference "^1.0.0" + mdast-util-to-string "^3.1.0" + micromark "^3.0.0" + micromark-util-decode-numeric-character-reference "^1.0.0" + micromark-util-decode-string "^1.0.0" + micromark-util-normalize-identifier "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + unist-util-stringify-position "^3.0.0" + uvu "^0.5.0" + +mdast-util-gfm-autolink-literal@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-1.0.3.tgz#67a13abe813d7eba350453a5333ae1bc0ec05c06" + integrity sha512-My8KJ57FYEy2W2LyNom4n3E7hKTuQk/0SES0u16tjA9Z3oFkF4RrC/hPAPgjlSpezsOvI8ObcXcElo92wn5IGA== + dependencies: + "@types/mdast" "^3.0.0" + ccount "^2.0.0" + mdast-util-find-and-replace "^2.0.0" + micromark-util-character "^1.0.0" + +mdast-util-gfm-footnote@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-1.0.2.tgz#ce5e49b639c44de68d5bf5399877a14d5020424e" + integrity sha512-56D19KOGbE00uKVj3sgIykpwKL179QsVFwx/DCW0u/0+URsryacI4MAdNJl0dh+u2PSsD9FtxPFbHCzJ78qJFQ== + dependencies: + "@types/mdast" "^3.0.0" + mdast-util-to-markdown "^1.3.0" + micromark-util-normalize-identifier "^1.0.0" + +mdast-util-gfm-strikethrough@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-1.0.3.tgz#5470eb105b483f7746b8805b9b989342085795b7" + integrity sha512-DAPhYzTYrRcXdMjUtUjKvW9z/FNAMTdU0ORyMcbmkwYNbKocDpdk+PX1L1dQgOID/+vVs1uBQ7ElrBQfZ0cuiQ== + dependencies: + "@types/mdast" "^3.0.0" + mdast-util-to-markdown "^1.3.0" + +mdast-util-gfm-table@^1.0.0: + version "1.0.7" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-table/-/mdast-util-gfm-table-1.0.7.tgz#3552153a146379f0f9c4c1101b071d70bbed1a46" + integrity sha512-jjcpmNnQvrmN5Vx7y7lEc2iIOEytYv7rTvu+MeyAsSHTASGCCRA79Igg2uKssgOs1i1po8s3plW0sTu1wkkLGg== + dependencies: + "@types/mdast" "^3.0.0" + markdown-table "^3.0.0" + mdast-util-from-markdown "^1.0.0" + mdast-util-to-markdown "^1.3.0" + +mdast-util-gfm-task-list-item@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-1.0.2.tgz#b280fcf3b7be6fd0cc012bbe67a59831eb34097b" + integrity sha512-PFTA1gzfp1B1UaiJVyhJZA1rm0+Tzn690frc/L8vNX1Jop4STZgOE6bxUhnzdVSB+vm2GU1tIsuQcA9bxTQpMQ== + dependencies: + "@types/mdast" "^3.0.0" + mdast-util-to-markdown "^1.3.0" + +mdast-util-gfm@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/mdast-util-gfm/-/mdast-util-gfm-2.0.2.tgz#e92f4d8717d74bdba6de57ed21cc8b9552e2d0b6" + integrity sha512-qvZ608nBppZ4icQlhQQIAdc6S3Ffj9RGmzwUKUWuEICFnd1LVkN3EktF7ZHAgfcEdvZB5owU9tQgt99e2TlLjg== + dependencies: + mdast-util-from-markdown "^1.0.0" + mdast-util-gfm-autolink-literal "^1.0.0" + mdast-util-gfm-footnote "^1.0.0" + mdast-util-gfm-strikethrough "^1.0.0" + mdast-util-gfm-table "^1.0.0" + mdast-util-gfm-task-list-item "^1.0.0" + mdast-util-to-markdown "^1.0.0" + +mdast-util-phrasing@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/mdast-util-phrasing/-/mdast-util-phrasing-3.0.1.tgz#c7c21d0d435d7fb90956038f02e8702781f95463" + integrity sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg== + dependencies: + "@types/mdast" "^3.0.0" + unist-util-is "^5.0.0" + +mdast-util-to-hast@^12.1.0: + version "12.3.0" + resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-12.3.0.tgz#045d2825fb04374e59970f5b3f279b5700f6fb49" + integrity sha512-pits93r8PhnIoU4Vy9bjW39M2jJ6/tdHyja9rrot9uujkN7UTU9SDnE6WNJz/IGyQk3XHX6yNNtrBH6cQzm8Hw== + dependencies: + "@types/hast" "^2.0.0" + "@types/mdast" "^3.0.0" + mdast-util-definitions "^5.0.0" + micromark-util-sanitize-uri "^1.1.0" + trim-lines "^3.0.0" + unist-util-generated "^2.0.0" + unist-util-position "^4.0.0" + unist-util-visit "^4.0.0" + +mdast-util-to-markdown@^1.0.0, mdast-util-to-markdown@^1.3.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/mdast-util-to-markdown/-/mdast-util-to-markdown-1.5.0.tgz#c13343cb3fc98621911d33b5cd42e7d0731171c6" + integrity sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A== + dependencies: + "@types/mdast" "^3.0.0" + "@types/unist" "^2.0.0" + longest-streak "^3.0.0" + mdast-util-phrasing "^3.0.0" + mdast-util-to-string "^3.0.0" + micromark-util-decode-string "^1.0.0" + unist-util-visit "^4.0.0" + zwitch "^2.0.0" + +mdast-util-to-string@^3.0.0, mdast-util-to-string@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz#66f7bb6324756741c5f47a53557f0cbf16b6f789" + integrity sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg== + dependencies: + "@types/mdast" "^3.0.0" + mdn-data@2.0.14: version "2.0.14" resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" @@ -19225,64 +20635,337 @@ methods@~1.1.2: resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= -micromatch@^3.1.10, micromatch@^3.1.4: - version "3.1.10" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" - integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== +micromark-core-commonmark@^1.0.0, micromark-core-commonmark@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz#1386628df59946b2d39fb2edfd10f3e8e0a75bb8" + integrity sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw== + dependencies: + decode-named-character-reference "^1.0.0" + micromark-factory-destination "^1.0.0" + micromark-factory-label "^1.0.0" + micromark-factory-space "^1.0.0" + micromark-factory-title "^1.0.0" + micromark-factory-whitespace "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-chunked "^1.0.0" + micromark-util-classify-character "^1.0.0" + micromark-util-html-tag-name "^1.0.0" + micromark-util-normalize-identifier "^1.0.0" + micromark-util-resolve-all "^1.0.0" + micromark-util-subtokenize "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.1" + uvu "^0.5.0" + +micromark-extension-gfm-autolink-literal@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-1.0.5.tgz#5853f0e579bbd8ef9e39a7c0f0f27c5a063a66e7" + integrity sha512-z3wJSLrDf8kRDOh2qBtoTRD53vJ+CWIyo7uyZuxf/JAbNJjiHsOpG1y5wxk8drtv3ETAHutCu6N3thkOOgueWg== dependencies: - arr-diff "^4.0.0" - array-unique "^0.3.2" - braces "^2.3.1" - define-property "^2.0.2" - extend-shallow "^3.0.2" - extglob "^2.0.4" - fragment-cache "^0.2.1" - kind-of "^6.0.2" - nanomatch "^1.2.9" - object.pick "^1.3.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.2" + micromark-util-character "^1.0.0" + micromark-util-sanitize-uri "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" -micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5: - version "4.0.5" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" - integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== +micromark-extension-gfm-footnote@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-1.1.2.tgz#05e13034d68f95ca53c99679040bc88a6f92fe2e" + integrity sha512-Yxn7z7SxgyGWRNa4wzf8AhYYWNrwl5q1Z8ii+CSTTIqVkmGZF1CElX2JI8g5yGoM3GAman9/PVCUFUSJ0kB/8Q== + dependencies: + micromark-core-commonmark "^1.0.0" + micromark-factory-space "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-normalize-identifier "^1.0.0" + micromark-util-sanitize-uri "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + uvu "^0.5.0" + +micromark-extension-gfm-strikethrough@^1.0.0: + version "1.0.7" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-1.0.7.tgz#c8212c9a616fa3bf47cb5c711da77f4fdc2f80af" + integrity sha512-sX0FawVE1o3abGk3vRjOH50L5TTLr3b5XMqnP9YDRb34M0v5OoZhG+OHFz1OffZ9dlwgpTBKaT4XW/AsUVnSDw== dependencies: - braces "^3.0.2" - picomatch "^2.3.1" + micromark-util-chunked "^1.0.0" + micromark-util-classify-character "^1.0.0" + micromark-util-resolve-all "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + uvu "^0.5.0" -miller-rabin@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" - integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA== +micromark-extension-gfm-table@^1.0.0: + version "1.0.7" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-table/-/micromark-extension-gfm-table-1.0.7.tgz#dcb46074b0c6254c3fc9cc1f6f5002c162968008" + integrity sha512-3ZORTHtcSnMQEKtAOsBQ9/oHp9096pI/UvdPtN7ehKvrmZZ2+bbWhi0ln+I9drmwXMt5boocn6OlwQzNXeVeqw== dependencies: - bn.js "^4.0.0" - brorand "^1.0.1" + micromark-factory-space "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + uvu "^0.5.0" -mime-db@1.52.0, "mime-db@>= 1.43.0 < 2": - version "1.52.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" - integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== +micromark-extension-gfm-tagfilter@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-1.0.2.tgz#aa7c4dd92dabbcb80f313ebaaa8eb3dac05f13a7" + integrity sha512-5XWB9GbAUSHTn8VPU8/1DBXMuKYT5uOgEjJb8gN3mW0PNW5OPHpSdojoqf+iq1xo7vWzw/P8bAHY0n6ijpXF7g== + dependencies: + micromark-util-types "^1.0.0" -mime-types@^2.1.12, mime-types@^2.1.18, mime-types@^2.1.26, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24, mime-types@~2.1.34: - version "2.1.35" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" - integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== +micromark-extension-gfm-task-list-item@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-1.0.5.tgz#b52ce498dc4c69b6a9975abafc18f275b9dde9f4" + integrity sha512-RMFXl2uQ0pNQy6Lun2YBYT9g9INXtWJULgbt01D/x8/6yJ2qpKyzdZD3pi6UIkzF++Da49xAelVKUeUMqd5eIQ== dependencies: - mime-db "1.52.0" + micromark-factory-space "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + uvu "^0.5.0" -mime@1.6.0, mime@^1.4.1: - version "1.6.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" - integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== +micromark-extension-gfm@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm/-/micromark-extension-gfm-2.0.3.tgz#e517e8579949a5024a493e49204e884aa74f5acf" + integrity sha512-vb9OoHqrhCmbRidQv/2+Bc6pkP0FrtlhurxZofvOEy5o8RtuuvTq+RQ1Vw5ZDNrVraQZu3HixESqbG+0iKk/MQ== + dependencies: + micromark-extension-gfm-autolink-literal "^1.0.0" + micromark-extension-gfm-footnote "^1.0.0" + micromark-extension-gfm-strikethrough "^1.0.0" + micromark-extension-gfm-table "^1.0.0" + micromark-extension-gfm-tagfilter "^1.0.0" + micromark-extension-gfm-task-list-item "^1.0.0" + micromark-util-combine-extensions "^1.0.0" + micromark-util-types "^1.0.0" + +micromark-factory-destination@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz#eb815957d83e6d44479b3df640f010edad667b9f" + integrity sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg== + dependencies: + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" -mime@^2.3.1, mime@^2.4.4, mime@^2.5.2: - version "2.6.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" - integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== +micromark-factory-label@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz#cc95d5478269085cfa2a7282b3de26eb2e2dec68" + integrity sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w== + dependencies: + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + uvu "^0.5.0" -mime@^3.0.0: +micromark-factory-space@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz#c8f40b0640a0150751d3345ed885a080b0d15faf" + integrity sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ== + dependencies: + micromark-util-character "^1.0.0" + micromark-util-types "^1.0.0" + +micromark-factory-title@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz#dd0fe951d7a0ac71bdc5ee13e5d1465ad7f50ea1" + integrity sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ== + dependencies: + micromark-factory-space "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + +micromark-factory-whitespace@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz#798fb7489f4c8abafa7ca77eed6b5745853c9705" + integrity sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ== + dependencies: + micromark-factory-space "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + +micromark-util-character@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-1.2.0.tgz#4fedaa3646db249bc58caeb000eb3549a8ca5dcc" + integrity sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg== + dependencies: + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + +micromark-util-chunked@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz#37a24d33333c8c69a74ba12a14651fd9ea8a368b" + integrity sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ== + dependencies: + micromark-util-symbol "^1.0.0" + +micromark-util-classify-character@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz#6a7f8c8838e8a120c8e3c4f2ae97a2bff9190e9d" + integrity sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw== + dependencies: + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + +micromark-util-combine-extensions@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz#192e2b3d6567660a85f735e54d8ea6e3952dbe84" + integrity sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA== + dependencies: + micromark-util-chunked "^1.0.0" + micromark-util-types "^1.0.0" + +micromark-util-decode-numeric-character-reference@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz#b1e6e17009b1f20bc652a521309c5f22c85eb1c6" + integrity sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw== + dependencies: + micromark-util-symbol "^1.0.0" + +micromark-util-decode-string@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz#dc12b078cba7a3ff690d0203f95b5d5537f2809c" + integrity sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ== + dependencies: + decode-named-character-reference "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-decode-numeric-character-reference "^1.0.0" + micromark-util-symbol "^1.0.0" + +micromark-util-encode@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz#92e4f565fd4ccb19e0dcae1afab9a173bbeb19a5" + integrity sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw== + +micromark-util-html-tag-name@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz#48fd7a25826f29d2f71479d3b4e83e94829b3588" + integrity sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q== + +micromark-util-normalize-identifier@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz#7a73f824eb9f10d442b4d7f120fecb9b38ebf8b7" + integrity sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q== + dependencies: + micromark-util-symbol "^1.0.0" + +micromark-util-resolve-all@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz#4652a591ee8c8fa06714c9b54cd6c8e693671188" + integrity sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA== + dependencies: + micromark-util-types "^1.0.0" + +micromark-util-sanitize-uri@^1.0.0, micromark-util-sanitize-uri@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz#613f738e4400c6eedbc53590c67b197e30d7f90d" + integrity sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A== + dependencies: + micromark-util-character "^1.0.0" + micromark-util-encode "^1.0.0" + micromark-util-symbol "^1.0.0" + +micromark-util-subtokenize@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz#941c74f93a93eaf687b9054aeb94642b0e92edb1" + integrity sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A== + dependencies: + micromark-util-chunked "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + uvu "^0.5.0" + +micromark-util-symbol@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz#813cd17837bdb912d069a12ebe3a44b6f7063142" + integrity sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag== + +micromark-util-types@^1.0.0, micromark-util-types@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-1.1.0.tgz#e6676a8cae0bb86a2171c498167971886cb7e283" + integrity sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg== + +micromark@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/micromark/-/micromark-3.2.0.tgz#1af9fef3f995ea1ea4ac9c7e2f19c48fd5c006e9" + integrity sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA== + dependencies: + "@types/debug" "^4.0.0" + debug "^4.0.0" + decode-named-character-reference "^1.0.0" + micromark-core-commonmark "^1.0.1" + micromark-factory-space "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-chunked "^1.0.0" + micromark-util-combine-extensions "^1.0.0" + micromark-util-decode-numeric-character-reference "^1.0.0" + micromark-util-encode "^1.0.0" + micromark-util-normalize-identifier "^1.0.0" + micromark-util-resolve-all "^1.0.0" + micromark-util-sanitize-uri "^1.0.0" + micromark-util-subtokenize "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.1" + uvu "^0.5.0" + +micromatch@^3.1.10, micromatch@^3.1.4: + version "3.1.10" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" + integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + braces "^2.3.1" + define-property "^2.0.2" + extend-shallow "^3.0.2" + extglob "^2.0.4" + fragment-cache "^0.2.1" + kind-of "^6.0.2" + nanomatch "^1.2.9" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.2" + +micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5: + version "4.0.5" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + dependencies: + braces "^3.0.2" + picomatch "^2.3.1" + +miller-rabin@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" + integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA== + dependencies: + bn.js "^4.0.0" + brorand "^1.0.1" + +mime-db@1.52.0, "mime-db@>= 1.43.0 < 2": + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12, mime-types@^2.1.18, mime-types@^2.1.26, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0, mime@^1.4.1: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +mime@^2.3.1, mime@^2.4.4, mime@^2.5.2: + version "2.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" + integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== + +mime@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7" integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A== @@ -19307,11 +20990,21 @@ mimic-fn@^3.1.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-3.1.0.tgz#65755145bbf3e36954b949c16450427451d5ca74" integrity sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ== +mimic-fn@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc" + integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw== + mimic-response@^1.0.0, mimic-response@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== +mimic-response@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" + integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== + min-indent@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" @@ -19431,6 +21124,11 @@ minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5, minimist@^1. resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== +minimist@^1.2.3: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + minipass-collect@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617" @@ -19589,6 +21287,11 @@ mixin-deep@^1.2.0: for-in "^1.0.2" is-extendable "^1.0.1" +mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" + integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== + mkdirp@0.5.4: version "0.5.4" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.4.tgz#fd01504a6797ec5c9be81ff43d204961ed64a512" @@ -19884,6 +21587,11 @@ nanomatch@^1.2.9: snapdragon "^0.8.1" to-regex "^3.0.1" +napi-build-utils@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806" + integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg== + native-request@^1.0.5: version "1.1.0" resolved "https://registry.yarnpkg.com/native-request/-/native-request-1.1.0.tgz#acdb30fe2eefa3e1bc8c54b3a6852e9c5c0d3cb0" @@ -20103,6 +21811,13 @@ nise@^5.1.4: just-extend "^4.0.2" path-to-regexp "^1.7.0" +nlcst-to-string@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/nlcst-to-string/-/nlcst-to-string-3.1.1.tgz#83b90f2e1ee2081e14701317efc26d3bbadc806e" + integrity sha512-63mVyqaqt0cmn2VcI2aH6kxe1rLAmSROqHMA0i4qqg1tidkfExgpb0FGMikMCn86mw5dFtBtEANfmSSK7TjNHw== + dependencies: + "@types/nlcst" "^1.0.0" + no-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" @@ -20121,6 +21836,13 @@ nock@^13.0.4, nock@^13.0.5, nock@^13.1.0: lodash.set "^4.3.2" propagate "^2.0.0" +node-abi@^3.3.0: + version "3.47.0" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.47.0.tgz#6cbfa2916805ae25c2b7156ca640131632eb05e8" + integrity sha512-2s6B2CWZM//kPgwnuI0KrYwNjfdByE25zvAaEpq9IH4zcNsarH8Ihu/UuX6XMPEogDAxkuUFeZn60pXNHAqn3A== + dependencies: + semver "^7.3.5" + node-abort-controller@^3.0.1: version "3.1.1" resolved "https://registry.yarnpkg.com/node-abort-controller/-/node-abort-controller-3.1.1.tgz#a94377e964a9a37ac3976d848cb5c765833b8548" @@ -20131,6 +21853,11 @@ node-addon-api@^3.0.0, node-addon-api@^3.2.1: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== +node-addon-api@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-6.1.0.tgz#ac8470034e58e67d0c6f1204a18ae6995d9c0d76" + integrity sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA== + node-environment-flags@1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/node-environment-flags/-/node-environment-flags-1.0.5.tgz#fa930275f5bf5dae188d6192b24b4c8bbac3d76a" @@ -20278,6 +22005,11 @@ node-releases@^1.1.69: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.77.tgz#50b0cfede855dd374e7585bf228ff34e57c1c32e" integrity sha512-rB1DUFUNAN4Gn9keO2K1efO35IDK7yKHCdCaIMvFO7yUYmmZYeDjnGKle26G4rwj+LKRQpjyUUvMkPglwGCYNQ== +node-releases@^2.0.13: + version "2.0.13" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.13.tgz#d5ed1627c23e3461e819b02e57b75e4899b1c81d" + integrity sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ== + node-releases@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503" @@ -20712,6 +22444,13 @@ npm-run-path@^4.0.0, npm-run-path@^4.0.1: dependencies: path-key "^3.0.0" +npm-run-path@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-5.1.0.tgz#bc62f7f3f6952d9894bd08944ba011a6ee7b7e00" + integrity sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q== + dependencies: + path-key "^4.0.0" + npmlog@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" @@ -20989,6 +22728,13 @@ onetime@^5.1.0, onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" +onetime@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-6.0.0.tgz#7c24c18ed1fd2e9bca4bd26806a33613c77d34b4" + integrity sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ== + dependencies: + mimic-fn "^4.0.0" + open@7.2.0: version "7.2.0" resolved "https://registry.yarnpkg.com/open/-/open-7.2.0.tgz#212959bd7b0ce2e8e3676adc76e3cf2f0a2498b4" @@ -21020,6 +22766,15 @@ opener@^1.5.2: resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A== +opentelemetry-instrumentation-fetch-node@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/opentelemetry-instrumentation-fetch-node/-/opentelemetry-instrumentation-fetch-node-1.1.0.tgz#f51d79862390f3a694fa91c35c4383e037a04c11" + integrity sha512-mSEpyRfwv6t1L+VvqTw5rCzNr3bVTsGE4/dcZruhFWivXFKl8pqm6W0LWPxHrEvwufw1eK9VmUgalfY0jjMl8Q== + dependencies: + "@opentelemetry/api" "^1.6.0" + "@opentelemetry/instrumentation" "^0.43.0" + "@opentelemetry/semantic-conventions" "^1.17.0" + opn@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/opn/-/opn-5.5.0.tgz#fc7164fab56d235904c51c3b27da6758ca3b9bfc" @@ -21107,6 +22862,21 @@ ora@^3.4.0: strip-ansi "^5.2.0" wcwidth "^1.0.1" +ora@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/ora/-/ora-7.0.1.tgz#cdd530ecd865fe39e451a0e7697865669cb11930" + integrity sha512-0TUxTiFJWv+JnjWm4o9yvuskpEJLXTcng8MJuKd+SzAzp2o+OP3HWqNhB4OdJRt1Vsd9/mR0oyaEYlOnL7XIRw== + dependencies: + chalk "^5.3.0" + cli-cursor "^4.0.0" + cli-spinners "^2.9.0" + is-interactive "^2.0.0" + is-unicode-supported "^1.3.0" + log-symbols "^5.1.0" + stdin-discarder "^0.1.0" + string-width "^6.1.0" + strip-ansi "^7.1.0" + original@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/original/-/original-1.0.2.tgz#e442a61cffe1c5fd20a65f3261c26663b303f25f" @@ -21509,6 +23279,15 @@ parse-json@^5.0.0, parse-json@^5.2.0: json-parse-even-better-errors "^2.3.0" lines-and-columns "^1.1.6" +parse-latin@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/parse-latin/-/parse-latin-5.0.1.tgz#f3b4fac54d06f6a0501cf8b8ecfafa4cbb4f2f47" + integrity sha512-b/K8ExXaWC9t34kKeDV8kGXBkXZ1HCSAZRYE7HR14eA1GlXX5L8iWhs8USJNhQU9q5ci413jCKF0gOyovvyRBg== + dependencies: + nlcst-to-string "^3.0.0" + unist-util-modify-children "^3.0.0" + unist-util-visit-children "^2.0.0" + parse-ms@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/parse-ms/-/parse-ms-2.1.0.tgz#348565a753d4391fa524029956b172cb7753097d" @@ -21565,7 +23344,7 @@ parse5-sax-parser@^6.0.1: dependencies: parse5 "^6.0.1" -parse5@6.0.1, parse5@^6.0.1: +parse5@6.0.1, parse5@^6.0.0, parse5@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== @@ -21645,6 +23424,11 @@ path-key@^3.0.0, path-key@^3.1.0: resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== +path-key@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-4.0.0.tgz#295588dc3aee64154f877adb9d780b81c554bf18" + integrity sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ== + path-parse@^1.0.6, path-parse@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" @@ -21703,6 +23487,11 @@ path-to-regexp@^1.5.3, path-to-regexp@^1.7.0: dependencies: isarray "0.0.1" +path-to-regexp@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.2.1.tgz#d54934d6798eb9e5ef14e7af7962c945906918e5" + integrity sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw== + path-type@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" @@ -23001,6 +24790,15 @@ postcss@^8.1.10, postcss@^8.1.7, postcss@^8.2.15, postcss@^8.2.4, postcss@^8.3.5 picocolors "^1.0.0" source-map-js "^1.0.2" +postcss@^8.4.27: + version "8.4.31" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d" + integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ== + dependencies: + nanoid "^3.3.6" + picocolors "^1.0.0" + source-map-js "^1.0.2" + postgres-array@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e" @@ -23050,6 +24848,24 @@ postgres-range@^1.1.1: resolved "https://registry.yarnpkg.com/postgres-range/-/postgres-range-1.1.3.tgz#9ccd7b01ca2789eb3c2e0888b3184225fa859f76" integrity sha512-VdlZoocy5lCP0c/t66xAfclglEapXPCIVhqqJRncYpvbCgImF0w67aPKfbqUMr72tO2k5q0TdTZwCLjPTI6C9g== +prebuild-install@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.1.tgz#de97d5b34a70a0c81334fd24641f2a1702352e45" + integrity sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw== + dependencies: + detect-libc "^2.0.0" + expand-template "^2.0.3" + github-from-package "0.0.0" + minimist "^1.2.3" + mkdirp-classic "^0.5.3" + napi-build-utils "^1.0.1" + node-abi "^3.3.0" + pump "^3.0.0" + rc "^1.2.7" + simple-get "^4.0.0" + tar-fs "^2.0.0" + tunnel-agent "^0.6.0" + precinct@^7.0.0: version "7.1.0" resolved "https://registry.yarnpkg.com/precinct/-/precinct-7.1.0.tgz#a0311e0b59029647eaf57c2d30b8efa9c85d129a" @@ -23069,6 +24885,16 @@ precinct@^7.0.0: module-definition "^3.3.1" node-source-walk "^4.2.0" +preferred-pm@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/preferred-pm/-/preferred-pm-3.1.2.tgz#aedb70550734a574dffcbf2ce82642bd1753bdd6" + integrity sha512-nk7dKrcW8hfCZ4H6klWcdRknBOXWzNQByJ0oJyX97BOupsYD+FzLS4hflgEu/uPUEHZCuRfMxzCBsuWd7OzT8Q== + dependencies: + find-up "^5.0.0" + find-yarn-workspace-root2 "1.2.16" + path-exists "^4.0.0" + which-pm "2.0.0" + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -23151,11 +24977,25 @@ printf@^0.6.1: resolved "https://registry.yarnpkg.com/printf/-/printf-0.6.1.tgz#b9afa3d3b55b7f2e8b1715272479fc756ed88650" integrity sha512-is0ctgGdPJ5951KulgfzvHGwJtZ5ck8l042vRkV6jrkpBzTmb/lueTqguWHy2JfVA+RY6gFVlaZgUS0j7S/dsw== +prismjs@^1.29.0: + version "1.29.0" + resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.29.0.tgz#f113555a8fa9b57c35e637bba27509dcf802dd12" + integrity sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q== + private@^0.1.6, private@^0.1.8: version "0.1.8" resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" integrity sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg== +probe-image-size@^7.2.3: + version "7.2.3" + resolved "https://registry.yarnpkg.com/probe-image-size/-/probe-image-size-7.2.3.tgz#d49c64be540ec8edea538f6f585f65a9b3ab4309" + integrity sha512-HubhG4Rb2UH8YtV4ba0Vp5bQ7L78RTONYu/ujmCu5nBI8wGv24s4E9xSKBi0N1MowRpxk76pFCpJtW0KPzOK0w== + dependencies: + lodash.merge "^4.6.2" + needle "^2.5.2" + stream-parser "~0.3.1" + proc-log@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/proc-log/-/proc-log-2.0.1.tgz#8f3f69a1f608de27878f91f5c688b225391cb685" @@ -23241,6 +25081,14 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" +prompts@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" + integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== + dependencies: + kleur "^3.0.3" + sisteransi "^1.0.5" + promzard@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/promzard/-/promzard-1.0.0.tgz#3246f8e6c9895a77c0549cefb65828ac0f6c006b" @@ -23280,6 +25128,11 @@ proper-lockfile@^4.1.2: retry "^0.12.0" signal-exit "^3.0.2" +property-information@^6.0.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.3.0.tgz#ba4a06ec6b4e1e90577df9931286953cdf4282c3" + integrity sha512-gVNZ74nqhRMiIUYWGQdosYetaKc83x8oT41a0LlV3AAFCAZwCpg4vmGkq8t34+cUhp3cnM4XDiU/7xlgK7HGrg== + protobufjs@^6.10.2, protobufjs@^6.8.6: version "6.11.4" resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.11.4.tgz#29a412c38bf70d89e537b6d02d904a6f448173aa" @@ -23469,6 +25322,11 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== +queue-tick@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/queue-tick/-/queue-tick-1.0.1.tgz#f6f07ac82c1fd60f82e098b417a80e52f1f4c142" + integrity sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag== + quick-lru@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" @@ -24123,6 +25981,44 @@ regjsparser@^0.9.1: dependencies: jsesc "~0.5.0" +rehype-parse@^8.0.0: + version "8.0.5" + resolved "https://registry.yarnpkg.com/rehype-parse/-/rehype-parse-8.0.5.tgz#ccffc21e08e288c7846614f8dc1dc23d603a4a80" + integrity sha512-Ds3RglaY/+clEX2U2mHflt7NlMA72KspZ0JLUJgBBLpRddBcEw3H8uYZQliQriku22NZpYMfjDdSgHcjxue24A== + dependencies: + "@types/hast" "^2.0.0" + hast-util-from-parse5 "^7.0.0" + parse5 "^6.0.0" + unified "^10.0.0" + +rehype-raw@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/rehype-raw/-/rehype-raw-6.1.1.tgz#81bbef3793bd7abacc6bf8335879d1b6c868c9d4" + integrity sha512-d6AKtisSRtDRX4aSPsJGTfnzrX2ZkHQLE5kiUuGOeEoLpbEulFF4hj0mLPbsa+7vmguDKOVVEQdHKDSwoaIDsQ== + dependencies: + "@types/hast" "^2.0.0" + hast-util-raw "^7.2.0" + unified "^10.0.0" + +rehype-stringify@^9.0.0, rehype-stringify@^9.0.4: + version "9.0.4" + resolved "https://registry.yarnpkg.com/rehype-stringify/-/rehype-stringify-9.0.4.tgz#31dbb9de6f5034c6964760a1b1083218059c4343" + integrity sha512-Uk5xu1YKdqobe5XpSskwPvo1XeHUUucWEQSl8hTrXt5selvca1e8K1EZ37E6YoZ4BT8BCqCdVfQW7OfHfthtVQ== + dependencies: + "@types/hast" "^2.0.0" + hast-util-to-html "^8.0.0" + unified "^10.0.0" + +rehype@^12.0.1: + version "12.0.1" + resolved "https://registry.yarnpkg.com/rehype/-/rehype-12.0.1.tgz#68a317662576dcaa2565a3952e149d6900096bf6" + integrity sha512-ey6kAqwLM3X6QnMDILJthGvG1m1ULROS9NT4uG9IDCuv08SFyLlreSuvOa//DgEvbXx62DS6elGVqusWhRUbgw== + dependencies: + "@types/hast" "^2.0.0" + rehype-parse "^8.0.0" + rehype-stringify "^9.0.0" + unified "^10.0.0" + relateurl@^0.2.7: version "0.2.7" resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" @@ -24139,6 +26035,44 @@ remap-istanbul@^0.13.0: source-map "0.6.1" through2 "3.0.0" +remark-gfm@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/remark-gfm/-/remark-gfm-3.0.1.tgz#0b180f095e3036545e9dddac0e8df3fa5cfee54f" + integrity sha512-lEFDoi2PICJyNrACFOfDD3JlLkuSbOa5Wd8EPt06HUdptv8Gn0bxYTdbU/XXQ3swAPkEaGxxPN9cbnMHvVu1Ig== + dependencies: + "@types/mdast" "^3.0.0" + mdast-util-gfm "^2.0.0" + micromark-extension-gfm "^2.0.0" + unified "^10.0.0" + +remark-parse@^10.0.2: + version "10.0.2" + resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-10.0.2.tgz#ca241fde8751c2158933f031a4e3efbaeb8bc262" + integrity sha512-3ydxgHa/ZQzG8LvC7jTXccARYDcRld3VfcgIIFs7bI6vbRSxJJmzgLEIIoYKyrfhaY+ujuWaf/PJiMZXoiCXgw== + dependencies: + "@types/mdast" "^3.0.0" + mdast-util-from-markdown "^1.0.0" + unified "^10.0.0" + +remark-rehype@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/remark-rehype/-/remark-rehype-10.1.0.tgz#32dc99d2034c27ecaf2e0150d22a6dcccd9a6279" + integrity sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw== + dependencies: + "@types/hast" "^2.0.0" + "@types/mdast" "^3.0.0" + mdast-util-to-hast "^12.1.0" + unified "^10.0.0" + +remark-smartypants@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/remark-smartypants/-/remark-smartypants-2.0.0.tgz#836cff43ec139b2e5ec9e488d80596ed677d1cb2" + integrity sha512-Rc0VDmr/yhnMQIz8n2ACYXlfw/P/XZev884QU1I5u+5DgJls32o97Vc1RbK3pfumLsJomS2yy8eT4Fxj/2MDVA== + dependencies: + retext "^8.1.0" + retext-smartypants "^5.1.0" + unist-util-visit "^4.1.0" + remote-git-tags@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/remote-git-tags/-/remote-git-tags-3.0.0.tgz#424f8ec2cdea00bb5af1784a49190f25e16983c3" @@ -24448,6 +26382,15 @@ resolve@^1.22.2: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +resolve@^1.22.4: + version "1.22.6" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.6.tgz#dd209739eca3aef739c626fea1b4f3c506195362" + integrity sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + resolve@^2.0.0-next.3: version "2.0.0-next.3" resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.3.tgz#d41016293d4a8586a39ca5d9b5f15cbea1f55e46" @@ -24479,11 +26422,58 @@ restore-cursor@^3.1.0: onetime "^5.1.0" signal-exit "^3.0.2" +restore-cursor@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-4.0.0.tgz#519560a4318975096def6e609d44100edaa4ccb9" + integrity sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + ret@~0.1.10: version "0.1.15" resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== +retext-latin@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/retext-latin/-/retext-latin-3.1.0.tgz#72b0176af2c69a373fd0d37eadd3924418bb3a89" + integrity sha512-5MrD1tuebzO8ppsja5eEu+ZbBeUNCjoEarn70tkXOS7Bdsdf6tNahsv2bY0Z8VooFF6cw7/6S+d3yI/TMlMVVQ== + dependencies: + "@types/nlcst" "^1.0.0" + parse-latin "^5.0.0" + unherit "^3.0.0" + unified "^10.0.0" + +retext-smartypants@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/retext-smartypants/-/retext-smartypants-5.2.0.tgz#da9cb79cc60f36aa33a20a462dfc663bec0068b4" + integrity sha512-Do8oM+SsjrbzT2UNIKgheP0hgUQTDDQYyZaIY3kfq0pdFzoPk+ZClYJ+OERNXveog4xf1pZL4PfRxNoVL7a/jw== + dependencies: + "@types/nlcst" "^1.0.0" + nlcst-to-string "^3.0.0" + unified "^10.0.0" + unist-util-visit "^4.0.0" + +retext-stringify@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/retext-stringify/-/retext-stringify-3.1.0.tgz#46ed45e077bfc4a8334977f6c2d6611e1d36263a" + integrity sha512-767TLOaoXFXyOnjx/EggXlb37ZD2u4P1n0GJqVdpipqACsQP+20W+BNpMYrlJkq7hxffnFk+jc6mAK9qrbuB8w== + dependencies: + "@types/nlcst" "^1.0.0" + nlcst-to-string "^3.0.0" + unified "^10.0.0" + +retext@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/retext/-/retext-8.1.0.tgz#c43437fb84cd46285ad240a9279142e239bada8d" + integrity sha512-N9/Kq7YTn6ZpzfiGW45WfEGJqFf1IM1q8OsRa1CGzIebCJBNCANDRmOrholiDRGKo/We7ofKR4SEvcGAWEMD3Q== + dependencies: + "@types/nlcst" "^1.0.0" + retext-latin "^3.0.0" + retext-stringify "^3.0.0" + unified "^10.0.0" + retry-request@^4.0.0, retry-request@^4.1.1: version "4.1.3" resolved "https://registry.yarnpkg.com/retry-request/-/retry-request-4.1.3.tgz#d5f74daf261372cff58d08b0a1979b4d7cab0fde" @@ -24602,6 +26592,15 @@ rollup-plugin-cleanup@3.2.1: js-cleanup "^1.2.0" rollup-pluginutils "^2.8.2" +rollup-plugin-dts@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/rollup-plugin-dts/-/rollup-plugin-dts-6.1.0.tgz#56e9c5548dac717213c6a4aa9df523faf04f75ae" + integrity sha512-ijSCPICkRMDKDLBK9torss07+8dl9UpY9z1N/zTeA1cIqdzMlpkV3MOOC7zukyvQfDyxa1s3Dl2+DeiP/G6DOw== + dependencies: + magic-string "^0.30.4" + optionalDependencies: + "@babel/code-frame" "^7.22.13" + rollup-plugin-license@^2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/rollup-plugin-license/-/rollup-plugin-license-2.6.1.tgz#20f15cc37950f362f8eefdc6e3a2e659d0cad9eb" @@ -24670,6 +26669,13 @@ rollup@^3.10.0, rollup@^3.20.2, rollup@^3.7.0: optionalDependencies: fsevents "~2.3.2" +rollup@^3.27.1: + version "3.29.4" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.29.4.tgz#4d70c0f9834146df8705bfb69a9a19c9e1109981" + integrity sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw== + optionalDependencies: + fsevents "~2.3.2" + rsvp@^3.0.14, rsvp@^3.0.17, rsvp@^3.0.18, rsvp@^3.0.21, rsvp@^3.0.6, rsvp@^3.1.0: version "3.6.2" resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.6.2.tgz#2e96491599a96cde1b515d5674a8f7a91452926a" @@ -24739,7 +26745,7 @@ rxjs@^7.5.5: dependencies: tslib "^2.1.0" -sade@^1.8.1: +sade@^1.7.3, sade@^1.8.1: version "1.8.1" resolved "https://registry.yarnpkg.com/sade/-/sade-1.8.1.tgz#0a78e81d658d394887be57d2a409bf703a3b2701" integrity sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A== @@ -24958,6 +26964,14 @@ schema-utils@^4.0.0: ajv-formats "^2.1.1" ajv-keywords "^5.0.0" +section-matter@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/section-matter/-/section-matter-1.0.0.tgz#e9041953506780ec01d59f292a19c7b850b84167" + integrity sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA== + dependencies: + extend-shallow "^2.0.1" + kind-of "^6.0.0" + select-hose@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" @@ -25020,14 +27034,14 @@ semver@7.5.3: dependencies: lru-cache "^6.0.0" -semver@7.x, semver@^7.0.0, semver@^7.1.1, semver@^7.1.3, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.1, semver@^7.5.2, semver@^7.5.3: +semver@7.x, semver@^7.0.0, semver@^7.1.1, semver@^7.1.3, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.1, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4: version "7.5.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== dependencies: lru-cache "^6.0.0" -semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0: +semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0, semver@^6.3.1: version "6.3.1" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== @@ -25088,6 +27102,11 @@ serve-static@1.15.0: parseurl "~1.3.3" send "0.18.0" +server-destroy@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/server-destroy/-/server-destroy-1.0.1.tgz#f13bf928e42b9c3e79383e61cc3998b5d14e6cdd" + integrity sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ== + set-blocking@^2.0.0, set-blocking@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" @@ -25143,6 +27162,20 @@ shallow-clone@^3.0.0: dependencies: kind-of "^6.0.2" +sharp@^0.32.5: + version "0.32.6" + resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.32.6.tgz#6ad30c0b7cd910df65d5f355f774aa4fce45732a" + integrity sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w== + dependencies: + color "^4.2.3" + detect-libc "^2.0.2" + node-addon-api "^6.1.0" + prebuild-install "^7.1.1" + semver "^7.5.4" + simple-get "^4.0.1" + tar-fs "^3.0.4" + tunnel-agent "^0.6.0" + shebang-command@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" @@ -25186,6 +27219,16 @@ shellwords@^0.1.1: resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== +shiki@^0.14.3: + version "0.14.4" + resolved "https://registry.yarnpkg.com/shiki/-/shiki-0.14.4.tgz#2454969b466a5f75067d0f2fa0d7426d32881b20" + integrity sha512-IXCRip2IQzKwxArNNq1S+On4KPML3Yyn8Zzs/xRgcgOWIr8ntIK3IKzjFPfjy/7kt9ZMjc+FItfqHRBg8b6tNQ== + dependencies: + ansi-sequence-parser "^1.1.0" + jsonc-parser "^3.2.0" + vscode-oniguruma "^1.7.0" + vscode-textmate "^8.0.0" + shimmer@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/shimmer/-/shimmer-1.2.1.tgz#610859f7de327b587efebf501fb43117f9aff337" @@ -25215,6 +27258,11 @@ signal-exit@^4.0.1: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.0.2.tgz#ff55bb1d9ff2114c13b400688fa544ac63c36967" integrity sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q== +signal-exit@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + sigstore@^1.3.0, sigstore@^1.4.0: version "1.6.0" resolved "https://registry.yarnpkg.com/sigstore/-/sigstore-1.6.0.tgz#887a4007c6ee83f3ef3fd844be1a0840e849c301" @@ -25232,6 +27280,20 @@ silent-error@^1.0.0, silent-error@^1.0.1, silent-error@^1.1.0, silent-error@^1.1 dependencies: debug "^2.2.0" +simple-concat@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f" + integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== + +simple-get@^4.0.0, simple-get@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-4.0.1.tgz#4a39db549287c979d352112fa03fd99fd6bc3543" + integrity sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA== + dependencies: + decompress-response "^6.0.0" + once "^1.3.1" + simple-concat "^1.0.0" + simple-git@^3.16.0: version "3.16.1" resolved "https://registry.yarnpkg.com/simple-git/-/simple-git-3.16.1.tgz#b67f18cbd3c68bbc4b9177ed49256afe51f12d47" @@ -25648,11 +27710,16 @@ source-map@0.6.1, source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, sourc resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== -source-map@0.7.3, source-map@^0.7.3, source-map@~0.7.2: +source-map@0.7.3, source-map@^0.7.3: version "0.7.3" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== +source-map@0.7.4, source-map@~0.7.2: + version "0.7.4" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" + integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== + source-map@0.8.0-beta.0: version "0.8.0-beta.0" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.8.0-beta.0.tgz#d4c1bb42c3f7ee925f005927ba10709e0d1d1f11" @@ -25694,6 +27761,11 @@ sourcemap-validator@^1.1.0: lodash.template "^4.5.0" source-map "~0.1.x" +space-separated-tokens@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz#1ecd9d2350a3844572c3f4a312bceb018348859f" + integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q== + sparse-bitfield@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz#ff4ae6e68656056ba4b3e792ab3334d38273ca11" @@ -25947,6 +28019,13 @@ std-env@^3.3.1: resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.3.2.tgz#af27343b001616015534292178327b202b9ee955" integrity sha512-uUZI65yrV2Qva5gqE0+A7uVAvO40iPo6jGhs7s8keRfHCmtg+uB2X6EiLGCI9IgL1J17xGhvoOqSz79lzICPTA== +stdin-discarder@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/stdin-discarder/-/stdin-discarder-0.1.0.tgz#22b3e400393a8e28ebf53f9958f3880622efde21" + integrity sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ== + dependencies: + bl "^5.0.0" + stream-browserify@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-3.0.0.tgz#22b0a2850cdf6503e73085da1fc7b7d0c2122f2f" @@ -26006,7 +28085,7 @@ stream-http@^2.7.2: to-arraybuffer "^1.0.0" xtend "^4.0.0" -stream-parser@^0.3.1: +stream-parser@^0.3.1, stream-parser@~0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/stream-parser/-/stream-parser-0.3.1.tgz#1618548694420021a1182ff0af1911c129761773" integrity sha1-FhhUhpRCACGhGC/wrxkRwSl2F3M= @@ -26048,6 +28127,14 @@ streamsearch@^1.1.0: resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== +streamx@^2.15.0: + version "2.15.1" + resolved "https://registry.yarnpkg.com/streamx/-/streamx-2.15.1.tgz#396ad286d8bc3eeef8f5cea3f029e81237c024c6" + integrity sha512-fQMzy2O/Q47rgwErk/eGeLu/roaFWV0jVsogDmrszM9uIw8L5OA+t+V93MgYlufNptfjmYR1tOMWhei/Eh7TQA== + dependencies: + fast-fifo "^1.1.0" + queue-tick "^1.0.1" + strict-uri-encode@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" @@ -26120,6 +28207,15 @@ string-width@^5.0.0, string-width@^5.0.1, string-width@^5.1.2: emoji-regex "^9.2.2" strip-ansi "^7.0.1" +string-width@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-6.1.0.tgz#96488d6ed23f9ad5d82d13522af9e4c4c3fd7518" + integrity sha512-k01swCJAgQmuADB0YIc+7TuatfNvTBVOoaUWJjTB9R4VJzR5vNWzf5t42ESVZFPS8xTySF7CAdV4t/aaIm3UnQ== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^10.2.1" + strip-ansi "^7.0.1" + string.prototype.matchall@^4.0.5, string.prototype.matchall@^4.0.6, string.prototype.matchall@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz#3bf85722021816dcd1bf38bb714915887ca79fd3" @@ -26180,6 +28276,14 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" +stringify-entities@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/stringify-entities/-/stringify-entities-4.0.3.tgz#cfabd7039d22ad30f3cc435b0ca2c1574fc88ef8" + integrity sha512-BP9nNHMhhfcMbiuQKCqMjhDP5yBCAxsPu4pHFFzJ6Alo9dZgY4VLDPutXqIjpRiMoKdp7Av85Gr73Q5uH9k7+g== + dependencies: + character-entities-html4 "^2.0.0" + character-entities-legacy "^3.0.0" + stringify-object@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.3.0.tgz#703065aefca19300d3ce88af4f5b3956d7556629" @@ -26231,6 +28335,18 @@ strip-ansi@^7.0.1: dependencies: ansi-regex "^6.0.1" +strip-ansi@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" + integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== + dependencies: + ansi-regex "^6.0.1" + +strip-bom-string@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-bom-string/-/strip-bom-string-1.0.0.tgz#e5211e9224369fbb81d633a2f00044dc8cedad92" + integrity sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g== + strip-bom@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" @@ -26258,6 +28374,11 @@ strip-final-newline@^2.0.0: resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== +strip-final-newline@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz#52894c313fbff318835280aed60ff71ebf12b8fd" + integrity sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw== + strip-indent@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" @@ -26614,6 +28735,25 @@ tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0: resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== +tar-fs@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" + integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng== + dependencies: + chownr "^1.1.1" + mkdirp-classic "^0.5.2" + pump "^3.0.0" + tar-stream "^2.1.4" + +tar-fs@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-3.0.4.tgz#a21dc60a2d5d9f55e0089ccd78124f1d3771dbbf" + integrity sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w== + dependencies: + mkdirp-classic "^0.5.2" + pump "^3.0.0" + tar-stream "^3.1.5" + tar-stream@^2.1.4, tar-stream@~2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" @@ -26625,6 +28765,15 @@ tar-stream@^2.1.4, tar-stream@~2.2.0: inherits "^2.0.3" readable-stream "^3.1.1" +tar-stream@^3.1.5: + version "3.1.6" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-3.1.6.tgz#6520607b55a06f4a2e2e04db360ba7d338cc5bab" + integrity sha512-B/UyjYwPpMBv+PaFSWAmtYjwdrlEaZQEhMIBFNC5oEG8lpiW8XjcSdmEaClj28ArfKScKHs2nshz3k2le6crsg== + dependencies: + b4a "^1.6.4" + fast-fifo "^1.2.0" + streamx "^2.15.0" + tar@6.1.11: version "6.1.11" resolved "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621" @@ -27197,6 +29346,11 @@ tree-sync@^2.0.0, tree-sync@^2.1.0: quick-temp "^0.1.5" walk-sync "^0.3.3" +trim-lines@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-3.0.1.tgz#d802e332a07df861c48802c04321017b1bd87338" + integrity sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg== + trim-newlines@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" @@ -27217,6 +29371,16 @@ triple-beam@^1.3.0: resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9" integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw== +trough@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/trough/-/trough-2.1.0.tgz#0f7b511a4fde65a46f18477ab38849b22c554876" + integrity sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g== + +ts-api-utils@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.0.3.tgz#f12c1c781d04427313dbac808f453f050e54a331" + integrity sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg== + ts-interface-checker@^0.1.9: version "0.1.13" resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699" @@ -27279,6 +29443,18 @@ tsconfig-paths@^4.1.2: minimist "^1.2.6" strip-bom "^3.0.0" +tsconfig-resolver@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/tsconfig-resolver/-/tsconfig-resolver-3.0.1.tgz#c9e62e328ecfbeaae4a4f1131a92cdbed12350c4" + integrity sha512-ZHqlstlQF449v8glscGRXzL6l2dZvASPCdXJRWG4gHEZlUVx2Jtmr+a2zeVG4LCsKhDXKRj5R3h0C/98UcVAQg== + dependencies: + "@types/json5" "^0.0.30" + "@types/resolve" "^1.17.0" + json5 "^2.1.3" + resolve "^1.17.0" + strip-bom "^4.0.0" + type-fest "^0.13.1" + tslib@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.1.tgz#410eb0d113e5b6356490eec749603725b021b43e" @@ -27304,7 +29480,7 @@ tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.2.0, tslib@^2.3 resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.2.tgz#1b6f07185c881557b0ffa84b111a0106989e8338" integrity sha512-5svOrSA2w3iGFDs1HibEVBGbDrAY82bFQ3HZ3ixB+88nsbsWQoKqDRb5UBYAUPEzbBn6dAp5gRNXglySbx1MlA== -tsutils@^3.0.0, tsutils@^3.17.1, tsutils@^3.21.0: +tsutils@^3.17.1, tsutils@^3.21.0: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== @@ -27366,6 +29542,11 @@ type-fest@^0.11.0: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1" integrity sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ== +type-fest@^0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.13.1.tgz#0172cb5bce80b0bd542ea348db50c7e21834d934" + integrity sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg== + type-fest@^0.18.0: version "0.18.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.18.1.tgz#db4bc151a4a2cf4eebf9add5db75508db6cc841f" @@ -27401,7 +29582,7 @@ type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== -type-fest@^2.3.3: +type-fest@^2.13.0, type-fest@^2.3.3: version "2.19.0" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== @@ -27570,6 +29751,11 @@ undici@^5.21.0: dependencies: busboy "^1.6.0" +unherit@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/unherit/-/unherit-3.0.1.tgz#65b98bb7cb58cee755d7ec699a49e9e8ff172e23" + integrity sha512-akOOQ/Yln8a2sgcLj4U0Jmx0R5jpIg2IUyRrWOzmEbjBtGzBdHtSeFKgoEcoH4KYIG/Pb8GQ/BwtYm0GCq1Sqg== + unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" @@ -27598,6 +29784,19 @@ unicode-property-aliases-ecmascript@^2.0.0: resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.0.0.tgz#0a36cb9a585c4f6abd51ad1deddb285c165297c8" integrity sha512-5Zfuy9q/DFr4tfO7ZPeVXb1aPoeQSdeFMLpYuFebehDAhbuevLs5yxSZmIFN1tP5F9Wl4IpJrYojg85/zgyZHQ== +unified@^10.0.0, unified@^10.1.2: + version "10.1.2" + resolved "https://registry.yarnpkg.com/unified/-/unified-10.1.2.tgz#b1d64e55dafe1f0b98bb6c719881103ecf6c86df" + integrity sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q== + dependencies: + "@types/unist" "^2.0.0" + bail "^2.0.0" + extend "^3.0.0" + is-buffer "^2.0.0" + is-plain-obj "^4.0.0" + trough "^2.0.0" + vfile "^5.0.0" + union-value@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" @@ -27667,6 +29866,88 @@ unique-string@^2.0.0: dependencies: crypto-random-string "^2.0.0" +unist-util-generated@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/unist-util-generated/-/unist-util-generated-2.0.1.tgz#e37c50af35d3ed185ac6ceacb6ca0afb28a85cae" + integrity sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A== + +unist-util-is@^5.0.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-5.2.1.tgz#b74960e145c18dcb6226bc57933597f5486deae9" + integrity sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw== + dependencies: + "@types/unist" "^2.0.0" + +unist-util-is@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-6.0.0.tgz#b775956486aff107a9ded971d996c173374be424" + integrity sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw== + dependencies: + "@types/unist" "^3.0.0" + +unist-util-modify-children@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/unist-util-modify-children/-/unist-util-modify-children-3.1.1.tgz#c4018b86441aa3b54b3edff1151d0aa062384c82" + integrity sha512-yXi4Lm+TG5VG+qvokP6tpnk+r1EPwyYL04JWDxLvgvPV40jANh7nm3udk65OOWquvbMDe+PL9+LmkxDpTv/7BA== + dependencies: + "@types/unist" "^2.0.0" + array-iterate "^2.0.0" + +unist-util-position@^4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-4.0.4.tgz#93f6d8c7d6b373d9b825844645877c127455f037" + integrity sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg== + dependencies: + "@types/unist" "^2.0.0" + +unist-util-stringify-position@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz#03ad3348210c2d930772d64b489580c13a7db39d" + integrity sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg== + dependencies: + "@types/unist" "^2.0.0" + +unist-util-visit-children@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/unist-util-visit-children/-/unist-util-visit-children-2.0.2.tgz#0f00a5caff567074568da2d89c54b5ee4a8c5440" + integrity sha512-+LWpMFqyUwLGpsQxpumsQ9o9DG2VGLFrpz+rpVXYIEdPy57GSy5HioC0g3bg/8WP9oCLlapQtklOzQ8uLS496Q== + dependencies: + "@types/unist" "^2.0.0" + +unist-util-visit-parents@^5.0.0, unist-util-visit-parents@^5.1.1: + version "5.1.3" + resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz#b4520811b0ca34285633785045df7a8d6776cfeb" + integrity sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg== + dependencies: + "@types/unist" "^2.0.0" + unist-util-is "^5.0.0" + +unist-util-visit-parents@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz#4d5f85755c3b8f0dc69e21eca5d6d82d22162815" + integrity sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw== + dependencies: + "@types/unist" "^3.0.0" + unist-util-is "^6.0.0" + +unist-util-visit@^4.0.0, unist-util-visit@^4.1.0, unist-util-visit@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-4.1.2.tgz#125a42d1eb876283715a3cb5cceaa531828c72e2" + integrity sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg== + dependencies: + "@types/unist" "^2.0.0" + unist-util-is "^5.0.0" + unist-util-visit-parents "^5.1.1" + +unist-util-visit@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-5.0.0.tgz#a7de1f31f72ffd3519ea71814cccf5fd6a9217d6" + integrity sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg== + dependencies: + "@types/unist" "^3.0.0" + unist-util-is "^6.0.0" + unist-util-visit-parents "^6.0.0" + universal-analytics@0.4.23: version "0.4.23" resolved "https://registry.yarnpkg.com/universal-analytics/-/universal-analytics-0.4.23.tgz#d915e676850c25c4156762471bdd7cf2eaaca8ac" @@ -27744,6 +30025,14 @@ update-browserslist-db@^1.0.10, update-browserslist-db@^1.0.9: escalade "^3.1.1" picocolors "^1.0.0" +update-browserslist-db@^1.0.13: + version "1.0.13" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz#3c5e4f5c083661bd38ef64b6328c26ed6c8248c4" + integrity sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg== + dependencies: + escalade "^3.1.1" + picocolors "^1.0.0" + update-notifier@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-5.1.0.tgz#4ab0d7c7f36a231dd7316cf7729313f0214d9ad9" @@ -27945,6 +30234,16 @@ uuid@^9.0.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5" integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg== +uvu@^0.5.0: + version "0.5.6" + resolved "https://registry.yarnpkg.com/uvu/-/uvu-0.5.6.tgz#2754ca20bcb0bb59b64e9985e84d2e81058502df" + integrity sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA== + dependencies: + dequal "^2.0.0" + diff "^5.0.0" + kleur "^4.0.3" + sade "^1.7.3" + v8-compile-cache-lib@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" @@ -28057,6 +30356,32 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" +vfile-location@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-4.1.0.tgz#69df82fb9ef0a38d0d02b90dd84620e120050dd0" + integrity sha512-YF23YMyASIIJXpktBa4vIGLJ5Gs88UB/XePgqPmTa7cDA+JeO3yclbpheQYCHjVHBn/yePzrXuygIL+xbvRYHw== + dependencies: + "@types/unist" "^2.0.0" + vfile "^5.0.0" + +vfile-message@^3.0.0: + version "3.1.4" + resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-3.1.4.tgz#15a50816ae7d7c2d1fa87090a7f9f96612b59dea" + integrity sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw== + dependencies: + "@types/unist" "^2.0.0" + unist-util-stringify-position "^3.0.0" + +vfile@^5.0.0, vfile@^5.3.7: + version "5.3.7" + resolved "https://registry.yarnpkg.com/vfile/-/vfile-5.3.7.tgz#de0677e6683e3380fafc46544cfe603118826ab7" + integrity sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g== + dependencies: + "@types/unist" "^2.0.0" + is-buffer "^2.0.0" + unist-util-stringify-position "^3.0.0" + vfile-message "^3.0.0" + vite-node@0.29.2: version "0.29.2" resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-0.29.2.tgz#463626197e248971774075faf3d6896c29cf8062" @@ -28093,6 +30418,17 @@ vite@4.0.5: optionalDependencies: fsevents "~2.3.2" +vite@^4.4.9: + version "4.4.11" + resolved "https://registry.yarnpkg.com/vite/-/vite-4.4.11.tgz#babdb055b08c69cfc4c468072a2e6c9ca62102b0" + integrity sha512-ksNZJlkcU9b0lBwAGZGGaZHCMqHsc8OpgtoYhsQ4/I2v5cnpmmmqe5pM4nv/4Hn6G/2GhTdj0DhZh2e+Er1q5A== + dependencies: + esbuild "^0.18.10" + postcss "^8.4.27" + rollup "^3.27.1" + optionalDependencies: + fsevents "~2.3.2" + vitefu@^0.2.4: version "0.2.4" resolved "https://registry.yarnpkg.com/vitefu/-/vitefu-0.2.4.tgz#212dc1a9d0254afe65e579351bed4e25d81e0b35" @@ -28143,6 +30479,16 @@ void-elements@^2.0.0: resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" integrity sha1-wGavtYK7HLQSjWDqkjkulNXp2+w= +vscode-oniguruma@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz#439bfad8fe71abd7798338d1cd3dc53a8beea94b" + integrity sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA== + +vscode-textmate@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-8.0.0.tgz#2c7a3b1163ef0441097e0b5d6389cd5504b59e5d" + integrity sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg== + vue@~3.2.41: version "3.2.45" resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.45.tgz#94a116784447eb7dbd892167784619fef379b3c8" @@ -28305,6 +30651,11 @@ web-encoding@1.1.5: optionalDependencies: "@zxing/text-encoding" "0.9.0" +web-namespaces@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-2.0.1.tgz#1010ff7c650eccb2592cebeeaf9a1b253fd40692" + integrity sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ== + web-streams-polyfill@^3.1.1: version "3.2.1" resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6" @@ -28576,7 +30927,7 @@ webpack@5.50.0: watchpack "^2.2.0" webpack-sources "^3.2.0" -webpack@^4.30.0, webpack@^4.44.1: +webpack@^4.44.1: version "4.46.0" resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.46.0.tgz#bf9b4404ea20a073605e0a011d188d77cb6ad542" integrity sha512-6jJuJjg8znb/xRItk7bkT0+Q7AHCYjjFnvKIWQPkNIOyRqoCGvkOs0ipeQzrqz4l5FtN5ZI/ukEHroeX/o1/5Q== @@ -28605,6 +30956,35 @@ webpack@^4.30.0, webpack@^4.44.1: watchpack "^1.7.4" webpack-sources "^1.4.1" +webpack@^4.47.0: + version "4.47.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.47.0.tgz#8b8a02152d7076aeb03b61b47dad2eeed9810ebc" + integrity sha512-td7fYwgLSrky3fI1EuU5cneU4+pbH6GgOfuKNS1tNPcfdGinGELAqsb/BP4nnvZyKSG2i/xFGU7+n2PvZA8HJQ== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/helper-module-context" "1.9.0" + "@webassemblyjs/wasm-edit" "1.9.0" + "@webassemblyjs/wasm-parser" "1.9.0" + acorn "^6.4.1" + ajv "^6.10.2" + ajv-keywords "^3.4.1" + chrome-trace-event "^1.0.2" + enhanced-resolve "^4.5.0" + eslint-scope "^4.0.3" + json-parse-better-errors "^1.0.2" + loader-runner "^2.4.0" + loader-utils "^1.2.3" + memory-fs "^0.4.1" + micromatch "^3.1.10" + mkdirp "^0.5.3" + neo-async "^2.6.1" + node-libs-browser "^2.2.1" + schema-utils "^1.0.0" + tapable "^1.1.3" + terser-webpack-plugin "^1.4.3" + watchpack "^1.7.4" + webpack-sources "^1.4.1" + webpack@^5.52.0, webpack@~5.74.0: version "5.74.0" resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.74.0.tgz#02a5dac19a17e0bb47093f2be67c695102a55980" @@ -28749,6 +31129,27 @@ which-module@^2.0.0: resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= +which-pm-runs@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.1.0.tgz#35ccf7b1a0fce87bd8b92a478c9d045785d3bf35" + integrity sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA== + +which-pm@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/which-pm/-/which-pm-2.0.0.tgz#8245609ecfe64bf751d0eef2f376d83bf1ddb7ae" + integrity sha512-Lhs9Pmyph0p5n5Z3mVnN0yWcbQYUAD7rbQUiMsQxOJ3T57k7RFe35SUwWMf7dsbDZks1uOmw4AecB/JMDj3v/w== + dependencies: + load-yaml-file "^0.2.0" + path-exists "^4.0.0" + +which-pm@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/which-pm/-/which-pm-2.1.1.tgz#0be2b70c67e94a32e87b9768a94a7f0954f2dcfa" + integrity sha512-xzzxNw2wMaoCWXiGE8IJ9wuPMU+EYhFksjHxrRT8kMT5SnocBPRg69YAMtyV4D12fP582RA+k3P8H9J5EMdIxQ== + dependencies: + load-yaml-file "^0.2.0" + path-exists "^4.0.0" + which-typed-array@^1.1.2: version "1.1.4" resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.4.tgz#8fcb7d3ee5adf2d771066fba7cf37e32fe8711ff" @@ -28812,6 +31213,13 @@ widest-line@^3.1.0: dependencies: string-width "^4.0.0" +widest-line@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-4.0.1.tgz#a0fc673aaba1ea6f0a0d35b3c2795c9a9cc2ebf2" + integrity sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig== + dependencies: + string-width "^5.0.1" + wildcard@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec" @@ -29234,9 +31642,19 @@ yocto-queue@^1.0.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251" integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g== +zod@3.21.1: + version "3.21.1" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.21.1.tgz#ac5bb7cf68876281ebd02f95ac4bb9a080370282" + integrity sha512-+dTu2m6gmCbO9Ahm4ZBDapx2O6ZY9QSPXst2WXjcznPMwf2YNpn3RevLx4KkZp1OPW/ouFcoBtBzFz/LeY69oA== + zone.js@^0.11.8, zone.js@~0.11.4: version "0.11.8" resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.11.8.tgz#40dea9adc1ad007b5effb2bfed17f350f1f46a21" integrity sha512-82bctBg2hKcEJ21humWIkXRlLBBmrc3nN7DFh5LGGhcyycO2S7FN8NmdvlcKaGFDNVL4/9kFLmwmInTavdJERA== dependencies: tslib "^2.3.0" + +zwitch@^2.0.0, zwitch@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7" + integrity sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==