From 81df97180ee19a958aa5a57c56d3b69c4fbf8fc5 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 2 Oct 2023 15:41:58 +0200 Subject: [PATCH 01/42] fix(types): Update signature of `processEvent` integration hook (#9151) --- packages/types/src/integration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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; } From bab06c2e17ac053a754039728bb73631e35050c3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Oct 2023 15:44:36 +0200 Subject: [PATCH 02/42] ci(deps): bump actions/upload-artifact from 3.1.2 to 3.1.3 (#9147) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 3.1.2 to 3.1.3.
Release notes

Sourced from actions/upload-artifact's releases.

v3.1.3

What's Changed

Full Changelog: https://github.com/actions/upload-artifact/compare/v3...v3.1.3

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/upload-artifact&package-manager=github_actions&previous-version=3.1.2&new-version=3.1.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 84c26af7e9b1..3d6f3d2e9c2f 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: | @@ -951,7 +951,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 }} From 7062449fdf5fb7f3fbf77dc56248a08adaeffbb9 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Mon, 2 Oct 2023 16:30:12 +0200 Subject: [PATCH 03/42] fix(node): Only require `inspector` when needed (#9149) --- packages/node/src/anr/index.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/node/src/anr/index.ts b/packages/node/src/anr/index.ts index 5c4edd808aa3..99bb5901b4ea 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 { spawn } from 'child_process'; -import * as inspector from 'inspector'; import { addGlobalEventProcessor, captureEvent, flush } from '..'; import { captureStackTrace } from './debugger'; @@ -98,12 +97,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; From 5d8b6748d6166b2c701c87514b7dc24d5adb88da Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 3 Oct 2023 09:22:58 +0200 Subject: [PATCH 04/42] feat(core): Extract `sampleTransaction` method out (#9136) This extracts the `sampleTransaction` method out and exports this from `core` so we can use it for POTEL (where we build transactions differently). --- packages/core/src/tracing/hubextensions.ts | 130 +-------------------- packages/core/src/tracing/index.ts | 1 + packages/core/src/tracing/sampling.ts | 122 +++++++++++++++++++ 3 files changed, 128 insertions(+), 125 deletions(-) create mode 100644 packages/core/src/tracing/sampling.ts 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..40d667c67ff0 100644 --- a/packages/core/src/tracing/index.ts +++ b/packages/core/src/tracing/index.ts @@ -11,3 +11,4 @@ export type { SpanStatusType } from './span'; export { trace, getActiveSpan, startSpan, startInactiveSpan, startActiveSpan, startSpanManual } from './trace'; export { getDynamicSamplingContextFromClient } from './dynamicSamplingContext'; export { setMeasurement } from './measurement'; +export { sampleTransaction } from './sampling'; 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; +} From 1140b2539d5a61f7a467bf9630cc11365a38d4bb Mon Sep 17 00:00:00 2001 From: rodolfoBee <64906376+rodolfoBee@users.noreply.github.com> Date: Tue, 3 Oct 2023 15:05:38 +0200 Subject: [PATCH 05/42] chore: Update vite plugin link (#9163) --- packages/sveltekit/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 25f1c49a46706c4ff558830f531a73b441c9a3e5 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 3 Oct 2023 15:36:23 +0200 Subject: [PATCH 06/42] fix: Ensure we never mutate options passed to `init` (#9162) In various places we mutate the `options` passed to `init()`, which is a) not a great pattern and b) may break if users pass in frozen or similar objects (for whatever reason). I also normalized this to ensure a passed in `_metadata.sdk` always takes precedent (we had this in most places, but not all). Closes https://github.com/getsentry/sentry-javascript/issues/9155 --- packages/angular-ivy/src/sdk.ts | 32 +++++++++++--------- packages/angular/src/sdk.ts | 32 +++++++++++--------- packages/nextjs/src/client/index.ts | 13 +++++--- packages/nextjs/src/edge/index.ts | 11 +++++-- packages/nextjs/src/server/index.ts | 20 ++++++------ packages/react/src/sdk.ts | 11 +++++-- packages/serverless/src/awslambda.ts | 15 ++++----- packages/serverless/src/gcpfunction/index.ts | 15 ++++----- packages/svelte/src/sdk.ts | 13 +++++--- 9 files changed, 93 insertions(+), 69 deletions(-) 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/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/react/src/sdk.ts b/packages/react/src/sdk.ts index e224d72945bb..aeede1fc7cd5 100644 --- a/packages/react/src/sdk.ts +++ b/packages/react/src/sdk.ts @@ -1,12 +1,17 @@ import type { BrowserOptions } from '@sentry/browser'; import { init as browserInit, SDK_VERSION } from '@sentry/browser'; +import type { SdkMetadata } from '@sentry/types'; /** * Inits the React 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.react', packages: [ { @@ -16,5 +21,5 @@ export function init(options: BrowserOptions): void { ], version: SDK_VERSION, }; - browserInit(options); + browserInit(opts); } diff --git a/packages/serverless/src/awslambda.ts b/packages/serverless/src/awslambda.ts index e8847fc7b212..443b0f1f8fa7 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 @@ -61,12 +61,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 +79,7 @@ export function init(options: AWSLambdaOptions = {}): void { version: Sentry.SDK_VERSION, }; - Sentry.init(options); + Sentry.init(opts); Sentry.addGlobalEventProcessor(serverlessEventProcessor); } 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/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(); } From dfc6d2e3c74bfd25781168c421c6c76f499e4e8c Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 3 Oct 2023 15:37:12 +0200 Subject: [PATCH 07/42] ref(serverless): Properly deprecate `rethrowAfterCapture` option (#9159) This is unused since v6 but we never got around to actually remove this, so let's properly deprecate this for v8. --- packages/serverless/src/awslambda.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/serverless/src/awslambda.ts b/packages/serverless/src/awslambda.ts index 443b0f1f8fa7..b26203bce4db 100644 --- a/packages/serverless/src/awslambda.ts +++ b/packages/serverless/src/awslambda.ts @@ -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; From e68f54f19c7526e637a7ffde235d39d1e9530680 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 3 Oct 2023 15:37:33 +0200 Subject: [PATCH 08/42] build(lint): Bump `eslint-plugin-deprecation` to 1.5.0 (#9160) There were some bugs in the previous version where deprecated messages would show up as `[object Object]` instead of showing the actual deprecation messages. bumping this to 1.5.0 fixes this. (esp. https://github.com/gund/eslint-plugin-deprecation/releases/tag/v1.3.1 is important for us) --- packages/eslint-config-sdk/package.json | 2 +- packages/eslint-config-sdk/src/index.js | 12 ++- yarn.lock | 122 +++++++++++++++--------- 3 files changed, 84 insertions(+), 52 deletions(-) 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/yarn.lock b/yarn.lock index bc849269e256..694880f6b6df 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5346,11 +5346,16 @@ "@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" @@ -5602,6 +5607,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" @@ -5758,17 +5768,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 +5794,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 +5812,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 +5827,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 +5858,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 +5912,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 +5949,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" @@ -13041,14 +13061,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 +13189,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 +13205,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 +13234,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" @@ -25020,7 +25045,7 @@ 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== @@ -27217,6 +27242,11 @@ triple-beam@^1.3.0: resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9" integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw== +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" @@ -27304,7 +27334,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== From af788e9b75970f88a3f331f23523c1228d2eee21 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 3 Oct 2023 15:40:58 +0200 Subject: [PATCH 09/42] ref(utils): Deprecate `walk` method (#9157) This was supposed to be removed in v7, but was forgotten. --- packages/utils/src/normalize.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 */ From abd8b4d6a346cab55110cec83c15c1b0df3768c5 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 3 Oct 2023 16:22:28 +0200 Subject: [PATCH 10/42] ref(integrations): Refactor pluggable integrations to use `processEvent` (#9021) This refactors pluggable integrations to use `processEvent`. --- packages/integrations/src/contextlines.ts | 23 +-- packages/integrations/src/dedupe.ts | 46 +++--- packages/integrations/src/extraerrordata.ts | 149 ++++++++++---------- packages/integrations/src/rewriteframes.ts | 21 +-- packages/integrations/src/sessiontiming.ts | 19 ++- packages/integrations/src/transaction.ts | 39 +++-- packages/integrations/test/dedupe.test.ts | 52 +++---- 7 files changed, 166 insertions(+), 183 deletions(-) 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(); }); }); }); From dee4f69a04a7def432202538bb7d2a77fabeea18 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 4 Oct 2023 12:14:37 +0200 Subject: [PATCH 11/42] feat(core): Add `continueTrace` method (#9164) This adds a new `continueTrace` method which can be used to continue a trace from headers. This method has the following signature: ```ts function continueTrace( { sentryTrace, baggage, }: { sentryTrace: Parameters[0]; baggage: Parameters[1]; }, callback: (transactionContext: Partial) => V, ): V ``` --- packages/browser/src/exports.ts | 1 + packages/bun/src/index.ts | 1 + packages/core/src/tracing/index.ts | 12 +- packages/core/src/tracing/trace.ts | 44 +++++- packages/core/test/lib/tracing/trace.test.ts | 153 ++++++++++++++++++- packages/node/src/index.ts | 1 + packages/serverless/src/index.ts | 1 + packages/sveltekit/src/server/index.ts | 1 + packages/vercel-edge/src/index.ts | 1 + 9 files changed, 211 insertions(+), 4 deletions(-) diff --git a/packages/browser/src/exports.ts b/packages/browser/src/exports.ts index f46b55f45214..c9e7e6e34c73 100644 --- a/packages/browser/src/exports.ts +++ b/packages/browser/src/exports.ts @@ -41,6 +41,7 @@ export { startSpan, startInactiveSpan, startSpanManual, + continueTrace, SDK_VERSION, setContext, setExtra, diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index d1c4a69f0ae5..b1bd9dac5553 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -59,6 +59,7 @@ export { startSpan, startInactiveSpan, startSpanManual, + continueTrace, } from '@sentry/core'; export type { SpanStatusType } from '@sentry/core'; export { autoDiscoverNodePerformanceMonitoringIntegrations } from '@sentry/node'; diff --git a/packages/core/src/tracing/index.ts b/packages/core/src/tracing/index.ts index 40d667c67ff0..2ace95aef323 100644 --- a/packages/core/src/tracing/index.ts +++ b/packages/core/src/tracing/index.ts @@ -7,8 +7,16 @@ 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'; export { sampleTransaction } from './sampling'; 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/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/node/src/index.ts b/packages/node/src/index.ts index 503f2749ea29..5fede4a51074 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -61,6 +61,7 @@ export { startActiveSpan, startInactiveSpan, startSpanManual, + continueTrace, } from '@sentry/core'; export type { SpanStatusType } from '@sentry/core'; export { autoDiscoverNodePerformanceMonitoringIntegrations } from './tracing'; diff --git a/packages/serverless/src/index.ts b/packages/serverless/src/index.ts index a17d0463202d..e0490df7e0d2 100644 --- a/packages/serverless/src/index.ts +++ b/packages/serverless/src/index.ts @@ -56,4 +56,5 @@ export { startActiveSpan, startInactiveSpan, startSpanManual, + continueTrace, } from '@sentry/node'; diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index 90c651a41175..6f02af4669fa 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -51,6 +51,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/vercel-edge/src/index.ts b/packages/vercel-edge/src/index.ts index cd596269a36f..43aa34b56557 100644 --- a/packages/vercel-edge/src/index.ts +++ b/packages/vercel-edge/src/index.ts @@ -58,6 +58,7 @@ export { startSpan, startInactiveSpan, startSpanManual, + continueTrace, } from '@sentry/core'; export type { SpanStatusType } from '@sentry/core'; From d488f493880efc46ae4707095d32526fde6a3dae Mon Sep 17 00:00:00 2001 From: Stephanie Anderson Date: Wed, 4 Oct 2023 12:43:39 +0200 Subject: [PATCH 12/42] chore: Add craft config for adding ember to the release registry (#9167) --- .craft.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.craft.yml b/.craft.yml index 5efdae48aa01..270dafe59d8e 100644 --- a/.craft.yml +++ b/.craft.yml @@ -180,3 +180,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$/ From b3323279d501fd2f735a163a956b551b424194cf Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 4 Oct 2023 15:03:30 +0200 Subject: [PATCH 13/42] fix(ember): Drop undefined config values (#9175) It seems to trip something up somewhere when config is undefined, so we just remove these here to avoid this. These options are serialized etc. so there may be something weird going on there... Fixes https://github.com/getsentry/sentry-javascript/issues/9168 --- packages/ember/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/ember/index.js b/packages/ember/index.js index de05e5d6089f..d2db76846bfa 100644 --- a/packages/ember/index.js +++ b/packages/ember/index.js @@ -2,6 +2,8 @@ const fs = require('fs'); const crypto = require('crypto'); +const { dropUndefinedKeys } = require('@sentry/utils'); + function readSnippet(fileName) { return fs.readFileSync(`${__dirname}/vendor/${fileName}`, 'utf8'); } @@ -30,7 +32,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 From 945d873ec9804fb3f0ba994ab17fcec0b01fa823 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 4 Oct 2023 15:21:31 +0200 Subject: [PATCH 14/42] fix(sveltekit): Flush in server wrappers before exiting (#9153) When deploying to Vercel (non-edge), server code is converted into Lambda functions. These functions have a tendency to freeze/shut down before the SDK is able to flush out events, causing events to be dropped. We already handle this in NextJS by manually calling `flush` if we detect we're in a serverless/Lambda environment. This patch applies the (almost) same logic to SvelteKit's server side instrumentation. --- packages/sveltekit/src/server/handle.ts | 56 ++++++++++-------- packages/sveltekit/src/server/handleError.ts | 6 +- packages/sveltekit/src/server/load.ts | 28 +++++++-- packages/sveltekit/src/server/utils.ts | 18 +++++- packages/sveltekit/test/server/load.test.ts | 61 +++++++++----------- 5 files changed, 105 insertions(+), 64 deletions(-) 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/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(); From 42361811d8e49c792dded68ad5eca55ea10f4c68 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 6 Oct 2023 17:34:34 +0200 Subject: [PATCH 15/42] chore(astro): Add `@sentry/astro` package (#9187) Add the boilerplate files for the new `@sentry/astro` package. Amongst the usual files, some noteworthy things include: * Bumped the Volta Node version from 16 to 18 for the entire monorepo. * Using Vitest instead of Jest, like we do in SvelteKit. ref #9182 --- package.json | 6 +- packages/astro/.eslintrc.js | 15 + packages/astro/LICENSE | 14 + packages/astro/README.md | 31 + packages/astro/package.json | 53 + packages/astro/rollup.npm.config.js | 12 + packages/astro/src/index.client.ts | 1 + packages/astro/src/index.server.ts | 1 + packages/astro/src/index.types.ts | 1 + packages/astro/test/index.client.test.ts | 5 + packages/astro/tsconfig.json | 9 + packages/astro/tsconfig.test.json | 10 + packages/astro/tsconfig.types.json | 10 + packages/astro/vite.config.ts | 13 + packages/browser/package.json | 2 +- .../e2e-tests/verdaccio-config/config.yaml | 6 + scripts/node-unit-tests.ts | 14 +- yarn.lock | 2330 ++++++++++++++++- 18 files changed, 2510 insertions(+), 23 deletions(-) create mode 100644 packages/astro/.eslintrc.js create mode 100644 packages/astro/LICENSE create mode 100644 packages/astro/README.md create mode 100644 packages/astro/package.json create mode 100644 packages/astro/rollup.npm.config.js create mode 100644 packages/astro/src/index.client.ts create mode 100644 packages/astro/src/index.server.ts create mode 100644 packages/astro/src/index.types.ts create mode 100644 packages/astro/test/index.client.test.ts create mode 100644 packages/astro/tsconfig.json create mode 100644 packages/astro/tsconfig.test.json create mode 100644 packages/astro/tsconfig.types.json create mode 100644 packages/astro/vite.config.ts diff --git a/package.json b/package.json index 6ae7e4f1d2cf..9546ebf2f9df 100644 --- a/package.json +++ b/package.json @@ -34,12 +34,13 @@ "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", @@ -126,7 +127,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/astro/.eslintrc.js b/packages/astro/.eslintrc.js new file mode 100644 index 000000000000..29b78099e7c6 --- /dev/null +++ b/packages/astro/.eslintrc.js @@ -0,0 +1,15 @@ +module.exports = { + env: { + browser: true, + node: true, + }, + extends: ['../../.eslintrc.js'], + overrides: [ + { + files: ['vite.config.ts'], + parserOptions: { + project: ['tsconfig.test.json'], + }, + }, + ], +}; 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..1ef88df544c3 --- /dev/null +++ b/packages/astro/README.md @@ -0,0 +1,31 @@ +

+ + 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 + +TODO diff --git a/packages/astro/package.json b/packages/astro/package.json new file mode 100644 index 000000000000..5a065208fe50 --- /dev/null +++ b/packages/astro/package.json @@ -0,0 +1,53 @@ +{ + "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", + "author": "Sentry", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "main": "build/cjs/index.server.js", + "module": "build/esm/index.server.js", + "browser": "build/esm/index.client.js", + "types": "build/types/index.types.d.ts", + "publishConfig": { + "access": "public" + }, + "peerDependencies": { + "astro": "1.x" + }, + "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..23b23ff7a8d7 --- /dev/null +++ b/packages/astro/rollup.npm.config.js @@ -0,0 +1,12 @@ +import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js'; + +export default makeNPMConfigVariants( + makeBaseNPMConfig({ + entrypoints: ['src/index.server.ts', 'src/index.client.ts'], + packageSpecificConfig: { + output: { + dynamicImportInCjs: true, + }, + }, + }), +); diff --git a/packages/astro/src/index.client.ts b/packages/astro/src/index.client.ts new file mode 100644 index 000000000000..dea210bd3fb8 --- /dev/null +++ b/packages/astro/src/index.client.ts @@ -0,0 +1 @@ +export const client = true; diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts new file mode 100644 index 000000000000..126a907bffce --- /dev/null +++ b/packages/astro/src/index.server.ts @@ -0,0 +1 @@ +export const server = true; diff --git a/packages/astro/src/index.types.ts b/packages/astro/src/index.types.ts new file mode 100644 index 000000000000..6c4477dd4f04 --- /dev/null +++ b/packages/astro/src/index.types.ts @@ -0,0 +1 @@ +export type Placeholder = true; diff --git a/packages/astro/test/index.client.test.ts b/packages/astro/test/index.client.test.ts new file mode 100644 index 000000000000..f4bf62c84f1d --- /dev/null +++ b/packages/astro/test/index.client.test.ts @@ -0,0 +1,5 @@ +describe('placeholder', () => { + it('holds a place', () => { + expect(true).toBe(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/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/e2e-tests/verdaccio-config/config.yaml b/packages/e2e-tests/verdaccio-config/config.yaml index 80a5afc70008..da3717dd283a 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 diff --git a/scripts/node-unit-tests.ts b/scripts/node-unit-tests.ts index 28167e15d557..dc923c8bf9a5 100644 --- a/scripts/node-unit-tests.ts +++ b/scripts/node-unit-tests.ts @@ -35,6 +35,7 @@ const SKIP_TEST_PACKAGES: Record = { '@sentry-internal/replay-worker', '@sentry/node-experimental', '@sentry/vercel-edge', + '@sentry/astro', ], legacyDeps: [ 'jsdom@15.x', @@ -53,22 +54,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/yarn.lock b/yarn.lock index 694880f6b6df..aa0312659573 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" @@ -4864,6 +5364,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 +5468,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 +5792,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" @@ -5361,6 +5886,11 @@ 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" @@ -5378,6 +5908,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" @@ -5406,6 +5950,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" @@ -5413,6 +5962,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" @@ -5461,6 +6017,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" @@ -5576,6 +6137,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" @@ -5697,6 +6263,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" @@ -6504,6 +7080,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" @@ -6648,7 +7229,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== @@ -6724,6 +7305,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" @@ -7044,6 +7630,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" @@ -7213,6 +7804,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" @@ -7392,6 +8046,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" @@ -8198,6 +8857,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" @@ -8319,6 +8983,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" @@ -8420,6 +9093,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" @@ -9040,6 +9727,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" @@ -9143,6 +9840,14 @@ buffer@^5.2.1, buffer@^5.5.0, buffer@^5.6.0: base64-js "^1.3.1" ieee754 "^1.1.13" +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" @@ -9483,6 +10188,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" @@ -9510,6 +10220,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" @@ -9535,6 +10250,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" @@ -9612,11 +10332,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" @@ -9715,6 +10455,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" @@ -9807,6 +10552,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" @@ -9821,11 +10571,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" @@ -9922,6 +10684,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" @@ -10002,7 +10769,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== @@ -10031,6 +10798,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" @@ -10084,6 +10859,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" @@ -10131,6 +10911,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" @@ -10373,6 +11158,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" @@ -11206,6 +11996,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" @@ -11225,6 +12022,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" @@ -11400,6 +12204,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" @@ -11435,6 +12244,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" @@ -11537,6 +12351,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" @@ -11570,7 +12389,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== @@ -11591,6 +12410,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" @@ -11771,6 +12595,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" @@ -11856,6 +12685,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" @@ -12556,6 +13390,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" @@ -12791,6 +13630,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" @@ -12984,6 +13828,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" @@ -13014,6 +13914,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" @@ -13363,6 +14268,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" @@ -13514,6 +14426,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" @@ -13537,6 +14464,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" @@ -13679,6 +14611,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" @@ -13701,6 +14638,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" @@ -14029,6 +14977,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" @@ -14644,6 +15600,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" @@ -14721,6 +15682,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" @@ -15094,7 +16065,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== @@ -15128,6 +16099,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" @@ -15336,6 +16317,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" @@ -15571,6 +16634,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" @@ -15584,6 +16652,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" @@ -15770,6 +16843,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" @@ -15808,7 +16886,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== @@ -15942,6 +17020,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" @@ -16258,7 +17341,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== @@ -16308,6 +17391,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" @@ -16362,6 +17452,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" @@ -16435,6 +17530,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" @@ -16448,6 +17550,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" @@ -16549,6 +17656,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" @@ -16635,6 +17747,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" @@ -16691,6 +17808,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" @@ -16730,6 +17852,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" @@ -17464,7 +18593,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== @@ -17669,7 +18798,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== @@ -17990,7 +19119,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== @@ -18354,6 +19483,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" @@ -18683,6 +19822,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" @@ -18739,6 +19886,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" @@ -18890,6 +20042,13 @@ magic-string@^0.30.0: dependencies: "@jridgewell/sourcemap-codec" "^1.4.13" +magic-string@^0.30.3: + 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" @@ -19075,6 +20234,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" @@ -19109,6 +20273,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" @@ -19250,6 +20561,279 @@ methods@~1.1.2: resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= +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: + 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" + +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: + 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" + +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: + 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" + +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" + +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: + 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" + +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" + +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" + +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" @@ -19332,11 +20916,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" @@ -19456,6 +21050,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" @@ -19614,6 +21213,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" @@ -19909,6 +21513,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" @@ -20128,6 +21737,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" @@ -20146,6 +21762,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" @@ -20156,6 +21779,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" @@ -20303,6 +21931,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" @@ -20737,6 +22370,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" @@ -21014,6 +22654,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" @@ -21132,6 +22779,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" @@ -21534,6 +23196,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" @@ -21590,7 +23261,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== @@ -21670,6 +23341,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" @@ -21728,6 +23404,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" @@ -23026,6 +24707,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" @@ -23075,6 +24765,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" @@ -23094,6 +24802,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" @@ -23176,11 +24894,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" @@ -23266,6 +24998,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" @@ -23305,6 +25045,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" @@ -23494,6 +25239,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" @@ -24148,6 +25898,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" @@ -24164,6 +25952,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" @@ -24473,6 +26299,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" @@ -24504,11 +26339,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" @@ -24695,6 +26577,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" @@ -24764,7 +26653,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== @@ -24983,6 +26872,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" @@ -25052,7 +26949,7 @@ semver@7.x, semver@^7.0.0, semver@^7.1.1, semver@^7.1.3, semver@^7.2.1, semver@^ 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== @@ -25113,6 +27010,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" @@ -25168,6 +27070,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" @@ -25211,6 +27127,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" @@ -25240,6 +27166,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" @@ -25257,6 +27188,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" @@ -25673,11 +27618,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" @@ -25719,6 +27669,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" @@ -25972,6 +27927,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" @@ -26031,7 +27993,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= @@ -26073,6 +28035,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" @@ -26145,6 +28115,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" @@ -26205,6 +28184,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" @@ -26256,6 +28243,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" @@ -26283,6 +28282,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" @@ -26639,6 +28643,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" @@ -26650,6 +28673,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" @@ -27222,6 +29254,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" @@ -27242,6 +29279,11 @@ 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" @@ -27309,6 +29351,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" @@ -27396,6 +29450,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" @@ -27431,7 +29490,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== @@ -27600,6 +29659,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" @@ -27628,6 +29692,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" @@ -27697,6 +29774,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" @@ -27774,6 +29933,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" @@ -27975,6 +30142,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" @@ -28087,6 +30264,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" @@ -28123,6 +30326,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" @@ -28173,6 +30387,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" @@ -28335,6 +30559,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" @@ -28606,7 +30835,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== @@ -28635,6 +30864,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" @@ -28779,6 +31037,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" @@ -28842,6 +31121,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" @@ -29264,9 +31550,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== From 81b4b5eeadbb0f90f6f2a67149ab196b812bf345 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 9 Oct 2023 12:17:30 +0200 Subject: [PATCH 16/42] feat(vue): Expose `VueIntegration` to initialize vue app later (#9180) This exposes a new `VueIntegration` from `@sentry/vue` which can be used to initialize vue error tracking for a late-defined vue app. --- packages/vue/src/errorhandler.ts | 4 +- packages/vue/src/index.ts | 1 + packages/vue/src/integration.ts | 99 +++++++++++++++++++ packages/vue/src/sdk.ts | 94 +++--------------- packages/vue/src/types.ts | 8 +- .../test/integration/VueIntegration.test.ts | 68 +++++++++++++ packages/vue/test/integration/init.test.ts | 65 ++++++++---- 7 files changed, 240 insertions(+), 99 deletions(-) create mode 100644 packages/vue/src/integration.ts create mode 100644 packages/vue/test/integration/VueIntegration.test.ts 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()); + } +} From aeb4462b75091014254a4885cdf660766e2a4efe Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 9 Oct 2023 12:19:12 +0200 Subject: [PATCH 17/42] feat(node-experimental): Use native OTEL Spans (#9161) This PR changes the performance handling of the node-experimental package fundamentally, aligning it even more with the OpenTelemetry model. --- .github/workflows/build.yml | 1 + .../node-experimental-fastify-app/.gitignore | 1 + .../node-experimental-fastify-app/.npmrc | 2 + .../event-proxy-server.ts | 253 ++++++++ .../package.json | 32 + .../playwright.config.ts | 62 ++ .../node-experimental-fastify-app/src/app.js | 62 ++ .../src/tracing.js | 10 + .../start-event-proxy.ts | 6 + .../tests/server.test.ts | 77 +++ .../tests/transactions.test.ts | 124 ++++ .../tsconfig.json | 10 + packages/e2e-tests/test-registry.npmrc | 3 + packages/node-experimental/package.json | 1 + packages/node-experimental/src/constants.ts | 12 + packages/node-experimental/src/index.ts | 5 +- .../src/integrations/express.ts | 4 +- .../src/integrations/fastify.ts | 4 +- .../src/integrations/graphql.ts | 4 +- .../src/integrations/http.ts | 152 ++--- .../src/integrations/mongo.ts | 4 +- .../src/integrations/mongoose.ts | 4 +- .../src/integrations/mysql2.ts | 4 +- .../src/integrations/postgres.ts | 4 +- .../src/opentelemetry/spanData.ts | 52 ++ .../src/opentelemetry/spanExporter.ts | 316 +++++++++ .../src/opentelemetry/spanProcessor.ts | 111 ++++ packages/node-experimental/src/sdk/client.ts | 22 +- packages/node-experimental/src/sdk/hub.ts | 29 +- .../src/sdk/hubextensions.ts | 74 +-- packages/node-experimental/src/sdk/init.ts | 5 + .../node-experimental/src/sdk/initOtel.ts | 28 +- packages/node-experimental/src/sdk/scope.ts | 154 ++++- packages/node-experimental/src/sdk/trace.ts | 104 ++- .../node-experimental/src/sdk/transaction.ts | 62 ++ packages/node-experimental/src/types.ts | 37 +- .../src/utils/addOriginToSpan.ts | 15 +- .../src/utils/convertOtelTimeToSeconds.ts | 4 + .../src/utils/getActiveSpan.ts | 25 + .../src/utils/getRequestSpanData.ts | 22 +- .../src/utils/getSpanKind.ts | 18 + .../src/utils/groupSpansWithParents.ts | 80 +++ .../src/utils/setupEventContextTrace.ts | 32 + .../node-experimental/src/utils/spanTypes.ts | 58 ++ .../test/helpers/createSpan.ts | 30 + .../test/helpers/mockSdkInit.ts | 44 +- .../test/integration/breadcrumbs.test.ts | 362 +++++++++++ .../test/integration/otelTimedEvents.test.ts | 57 ++ .../test/integration/scope.test.ts | 235 +++++++ .../test/integration/transactions.test.ts | 604 ++++++++++++++++++ .../node-experimental/test/sdk/client.test.ts | 47 ++ .../node-experimental/test/sdk/hub.test.ts | 43 ++ .../test/sdk/hubextensions.test.ts | 26 + .../node-experimental/test/sdk/init.test.ts | 3 + .../test/sdk/otelAsyncContextStrategy.test.ts | 140 ++++ .../node-experimental/test/sdk/scope.test.ts | 438 +++++++++++++ .../node-experimental/test/sdk/trace.test.ts | 202 ++++-- .../test/sdk/transaction.test.ts | 245 +++++++ .../utils/convertOtelTimeToSeconds.test.ts | 9 + .../test/utils/getActiveSpan.test.ts | 151 +++++ .../test/utils/getRequestSpanData.test.ts | 59 ++ .../test/utils/groupSpansWithParents.test.ts | 123 ++++ .../test/utils/setupEventContextTrace.test.ts | 111 ++++ .../test/utils/spanTypes.test.ts | 98 +++ packages/opentelemetry-node/src/index.ts | 7 +- .../opentelemetry-node/src/spanprocessor.ts | 6 +- .../src/utils/mapOtelStatus.ts | 3 +- .../src/utils/parseOtelSpanDescription.ts | 2 +- yarn.lock | 2 +- 69 files changed, 4766 insertions(+), 370 deletions(-) create mode 100644 packages/e2e-tests/test-applications/node-experimental-fastify-app/.gitignore create mode 100644 packages/e2e-tests/test-applications/node-experimental-fastify-app/.npmrc create mode 100644 packages/e2e-tests/test-applications/node-experimental-fastify-app/event-proxy-server.ts create mode 100644 packages/e2e-tests/test-applications/node-experimental-fastify-app/package.json create mode 100644 packages/e2e-tests/test-applications/node-experimental-fastify-app/playwright.config.ts create mode 100644 packages/e2e-tests/test-applications/node-experimental-fastify-app/src/app.js create mode 100644 packages/e2e-tests/test-applications/node-experimental-fastify-app/src/tracing.js create mode 100644 packages/e2e-tests/test-applications/node-experimental-fastify-app/start-event-proxy.ts create mode 100644 packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/server.test.ts create mode 100644 packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/transactions.test.ts create mode 100644 packages/e2e-tests/test-applications/node-experimental-fastify-app/tsconfig.json create mode 100644 packages/node-experimental/src/opentelemetry/spanData.ts create mode 100644 packages/node-experimental/src/opentelemetry/spanExporter.ts create mode 100644 packages/node-experimental/src/opentelemetry/spanProcessor.ts create mode 100644 packages/node-experimental/src/sdk/transaction.ts create mode 100644 packages/node-experimental/src/utils/convertOtelTimeToSeconds.ts create mode 100644 packages/node-experimental/src/utils/getActiveSpan.ts create mode 100644 packages/node-experimental/src/utils/getSpanKind.ts create mode 100644 packages/node-experimental/src/utils/groupSpansWithParents.ts create mode 100644 packages/node-experimental/src/utils/setupEventContextTrace.ts create mode 100644 packages/node-experimental/src/utils/spanTypes.ts create mode 100644 packages/node-experimental/test/helpers/createSpan.ts create mode 100644 packages/node-experimental/test/integration/breadcrumbs.test.ts create mode 100644 packages/node-experimental/test/integration/otelTimedEvents.test.ts create mode 100644 packages/node-experimental/test/integration/scope.test.ts create mode 100644 packages/node-experimental/test/integration/transactions.test.ts create mode 100644 packages/node-experimental/test/sdk/client.test.ts create mode 100644 packages/node-experimental/test/sdk/hub.test.ts create mode 100644 packages/node-experimental/test/sdk/hubextensions.test.ts create mode 100644 packages/node-experimental/test/sdk/otelAsyncContextStrategy.test.ts create mode 100644 packages/node-experimental/test/sdk/scope.test.ts create mode 100644 packages/node-experimental/test/sdk/transaction.test.ts create mode 100644 packages/node-experimental/test/utils/convertOtelTimeToSeconds.test.ts create mode 100644 packages/node-experimental/test/utils/getActiveSpan.test.ts create mode 100644 packages/node-experimental/test/utils/getRequestSpanData.test.ts create mode 100644 packages/node-experimental/test/utils/groupSpansWithParents.test.ts create mode 100644 packages/node-experimental/test/utils/setupEventContextTrace.test.ts create mode 100644 packages/node-experimental/test/utils/spanTypes.test.ts diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3d6f3d2e9c2f..9f1440515c4f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -823,6 +823,7 @@ jobs: 'standard-frontend-react-tracing-import', 'sveltekit', 'generic-ts3.8', + 'node-experimental-fastify-app', ] build-command: - false 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..62e194170fa8 --- /dev/null +++ b/packages/e2e-tests/test-applications/node-experimental-fastify-app/src/app.js @@ -0,0 +1,62 @@ +require('./tracing'); + +const Sentry = require('@sentry/node-experimental'); +const { fastify } = require('fastify'); +const fastifyPlugin = require('fastify-plugin'); + +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-transaction', async function (req, res) { + Sentry.startSpan({ name: 'test-span' }, () => { + Sentry.startSpan({ name: 'child-span' }, () => {}); + }); + + await Sentry.flush(); + + res.send({ + transactionIds: global.transactionIds || [], + }); +}); + +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 }); + +Sentry.addGlobalEventProcessor(event => { + global.transactionIds = global.transactionIds || []; + + if (event.type === 'transaction') { + const eventId = event.event_id; + + if (eventId) { + global.transactionIds.push(eventId); + } + } + + return event; +}); 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/server.test.ts b/packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/server.test.ts new file mode 100644 index 000000000000..9a9848eefa1a --- /dev/null +++ b/packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/server.test.ts @@ -0,0 +1,77 @@ +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); +}); + +test('Sends transactions to Sentry', async ({ baseURL }) => { + const { data } = await axios.get(`${baseURL}/test-transaction`); + const { transactionIds } = data; + + console.log(`Polling for transaction eventIds: ${JSON.stringify(transactionIds)}`); + + expect(transactionIds.length).toBe(1); + + await Promise.all( + transactionIds.map(async (transactionId: string) => { + const url = `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${transactionId}/`; + + 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/transactions.test.ts b/packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/transactions.test.ts new file mode 100644 index 000000000000..00cc2b149e13 --- /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 - anonymous', + 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/node-experimental/package.json b/packages/node-experimental/package.json index 75b177ade68d..e1e180b7faa2 100644 --- a/packages/node-experimental/package.json +++ b/packages/node-experimental/package.json @@ -24,6 +24,7 @@ }, "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", diff --git a/packages/node-experimental/src/constants.ts b/packages/node-experimental/src/constants.ts index dc714590556a..930574157d73 100644 --- a/packages/node-experimental/src/constants.ts +++ b/packages/node-experimental/src/constants.ts @@ -1,3 +1,15 @@ 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'; 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/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/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/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..90af46e8672d --- /dev/null +++ b/packages/node-experimental/src/opentelemetry/spanExporter.ts @@ -0,0 +1,316 @@ +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_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, + ...metadata, + }, + data: removeSentryAttributes(data), + origin, + tags, + }) 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]; + /* 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..2a202c0f7df9 --- /dev/null +++ b/packages/node-experimental/src/opentelemetry/spanProcessor.ts @@ -0,0 +1,111 @@ +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 { + _INTERNAL_SENTRY_TRACE_PARENT_CONTEXT_KEY, + maybeCaptureExceptionForTimedEvent, +} from '@sentry/opentelemetry-node'; +import type { Hub, TraceparentData } from '@sentry/types'; + +import { OTEL_ATTR_PARENT_SAMPLED, OTEL_CONTEXT_HUB_KEY } from '../constants'; +import { Http } from '../integrations'; +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); + } + + // We need to set this here based on the parent context + const parentSampled = getParentSampled(span, parentContext); + if (typeof parentSampled === 'boolean') { + span.setAttribute(OTEL_ATTR_PARENT_SAMPLED, parentSampled); + } + + return super.onStart(span, parentContext); + } + + /** @inheritDoc */ + public onEnd(span: Span): void { + 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 getTraceParentData(parentContext: Context): TraceparentData | undefined { + return parentContext.getValue(_INTERNAL_SENTRY_TRACE_PARENT_CONTEXT_KEY) as TraceparentData | undefined; +} + +function getParentSampled(span: Span, parentContext: Context): boolean | undefined { + const spanContext = span.spanContext(); + const traceId = spanContext.traceId; + const traceparentData = getTraceParentData(parentContext); + + // Only inherit sample rate if `traceId` is the same + return traceparentData && traceId === traceparentData.traceId ? traceparentData.parentSampled : undefined; +} + +function shouldCaptureSentrySpan(span: Span): boolean { + const client = getCurrentHub().getClient(); + const httpIntegration = client ? client.getIntegration(Http) : 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 ( + httpIntegration && + (span.kind === SpanKind.CLIENT || span.kind === SpanKind.SERVER) && + span.attributes[SemanticAttributes.HTTP_URL] && + span.attributes[SemanticAttributes.HTTP_METHOD] && + !httpIntegration.shouldCreateSpansForRequests + ) { + return 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..588b98cd1b43 100644 --- a/packages/node-experimental/src/sdk/init.ts +++ b/packages/node-experimental/src/sdk/init.ts @@ -5,6 +5,7 @@ import { getAutoPerformanceIntegrations } from '../integrations/getAutoPerforman import { Http } from '../integrations/http'; import type { NodeExperimentalOptions } from '../types'; import { NodeExperimentalClient } from './client'; +import { getCurrentHub } from './hub'; import { initOtel } from './initOtel'; import { setOtelContextAsyncContextStrategy } from './otelAsyncContextStrategy'; @@ -19,6 +20,10 @@ export const defaultIntegrations = [ * 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..855a443889bb 100644 --- a/packages/node-experimental/src/sdk/initOtel.ts +++ b/packages/node-experimental/src/sdk/initOtel.ts @@ -2,18 +2,21 @@ import { diag, DiagLogLevel } from '@opentelemetry/api'; import { Resource } from '@opentelemetry/resources'; import { AlwaysOnSampler, 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 { SentryPropagator } from '@sentry/opentelemetry-node'; import { logger } from '@sentry/utils'; +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) { @@ -27,6 +30,18 @@ export function initOtel(): () => void { diag.setLogger(otelLogger, DiagLogLevel.DEBUG); } + if (client) { + setupEventContextTrace(client); + } + + const provider = setupOtel(); + if (client) { + client.traceProvider = provider; + } +} + +/** Just exported for tests. */ +export function setupOtel(): BasicTracerProvider { // Create and configure NodeTracerProvider const provider = new BasicTracerProvider({ sampler: new AlwaysOnSampler(), @@ -35,6 +50,7 @@ export function initOtel(): () => void { [SemanticResourceAttributes.SERVICE_NAMESPACE]: 'sentry', [SemanticResourceAttributes.SERVICE_VERSION]: SDK_VERSION, }), + forceFlushTimeoutMillis: 500, }); provider.addSpanProcessor(new SentrySpanProcessor()); @@ -47,9 +63,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..c301dd6e9521 --- /dev/null +++ b/packages/node-experimental/src/sdk/transaction.ts @@ -0,0 +1,62 @@ +import type { Hub } from '@sentry/core'; +import { sampleTransaction, Transaction } from '@sentry/core'; +import type { + ClientOptions, + CustomSamplingContext, + 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, + customSamplingContext?: CustomSamplingContext, +): Transaction { + const client = hub.getClient(); + const options: Partial = (client && client.getOptions()) || {}; + + let transaction = new NodeExperimentalTransaction(transactionContext, hub as Hub); + transaction = sampleTransaction(transaction, options, { + parentSampled: transactionContext.parentSampled, + transactionContext, + ...customSamplingContext, + }); + if (transaction.sampled) { + 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/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..00ec85700316 --- /dev/null +++ b/packages/node-experimental/test/integration/transactions.test.ts @@ -0,0 +1,604 @@ +import { context, SpanKind, trace, TraceFlags } from '@opentelemetry/api'; +import type { SpanProcessor } from '@opentelemetry/sdk-trace-base'; +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; +import { _INTERNAL_SENTRY_TRACE_PARENT_CONTEXT_KEY } from '@sentry/opentelemetry-node'; +import type { TransactionEvent } from '@sentry/types'; +import { logger } from '@sentry/utils'; + +import * as Sentry from '../../src'; +import { startSpan } from '../../src'; +import type { Http } 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 traceParentData = { + traceId, + parentSpanId, + parentSampled: 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(_INTERNAL_SENTRY_TRACE_PARENT_CONTEXT_KEY, traceParentData), + 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 creates spans for http requests if disabled in http integration xxx', 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(() => { + return { + shouldCreateSpansForRequests: false, + } as Http; + }); + + 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); + }); +}); 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..03ee60ecbf0b --- /dev/null +++ b/packages/node-experimental/test/sdk/client.test.ts @@ -0,0 +1,47 @@ +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), + }); + }); + + 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..346683bf45f3 --- /dev/null +++ b/packages/node-experimental/test/sdk/otelAsyncContextStrategy.test.ts @@ -0,0 +1,140 @@ +import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; +import type { Hub } from '@sentry/core'; +import { runWithAsyncContext, setAsyncContextStrategy } from '@sentry/core'; + +import { getCurrentHub } from '../../src/sdk/hub'; +import { setupOtel } from '../../src/sdk/initOtel'; +import { setOtelContextAsyncContextStrategy } from '../../src/sdk/otelAsyncContextStrategy'; +import { cleanupOtel } from '../helpers/mockSdkInit'; + +describe('otelAsyncContextStrategy', () => { + let provider: BasicTracerProvider | undefined; + + beforeEach(() => { + provider = setupOtel(); + 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..76ecb28a5f9d 100644 --- a/packages/node-experimental/test/sdk/trace.test.ts +++ b/packages/node-experimental/test/sdk/trace.test.ts @@ -1,63 +1,70 @@ -import { Span, Transaction } from '@sentry/core'; +import type { Span } from '@opentelemetry/sdk-trace-base'; import * as Sentry from '../../src'; -import { mockSdkInit } from '../helpers/mockSdkInit'; +import { OTEL_ATTR_OP, OTEL_ATTR_ORIGIN, OTEL_ATTR_SOURCE } 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 +72,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 +119,55 @@ 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({}); + + 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', + }); + + expect(getSpanMetadata(span!)).toEqual({ requestPath: 'test-path' }); + }, + ); + }); }); describe('startInactiveSpan', () => { @@ -138,36 +175,87 @@ 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({}); - innerSpan?.finish(); + expect(getSpanMetadata(span!)).toEqual(undefined); - expect(innerSpan?.endTimestamp).toEqual(expect.any(Number)); - expect(Sentry.getActiveSpan()).toEqual(outerSpan); + 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_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(); + }); +}); 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..bf27549c8017 --- /dev/null +++ b/packages/node-experimental/test/sdk/transaction.test.ts @@ -0,0 +1,245 @@ +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 an unsampled NodeExperimentalTransaction by default', () => { + const client = new NodeExperimentalClient(getDefaultNodeExperimentalClientOptions()); + const mockEmit = jest.spyOn(client, 'emit').mockImplementation(() => {}); + const hub = getCurrentHub(); + hub.bindClient(client); + + const transaction = startTransaction(hub, { name: 'test' }); + + expect(transaction).toBeInstanceOf(NodeExperimentalTransaction); + expect(mockEmit).toBeCalledTimes(1); + expect(mockEmit).toBeCalledWith('startTransaction', transaction); + + expect(transaction.sampled).toBe(false); + expect(transaction.spanRecorder).toBeUndefined(); + 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('creates a sampled NodeExperimentalTransaction based on the tracesSampleRate', () => { + const client = new NodeExperimentalClient(getDefaultNodeExperimentalClientOptions({ tracesSampleRate: 1 })); + const hub = getCurrentHub(); + hub.bindClient(client); + + const transaction = startTransaction(hub, { name: 'test' }); + + expect(transaction).toBeInstanceOf(NodeExperimentalTransaction); + + expect(transaction.sampled).toBe(true); + expect(transaction.spanRecorder).toBeDefined(); + expect(transaction.spanRecorder?.spans).toHaveLength(1); + expect(transaction.metadata).toEqual({ + source: 'custom', + spanMetadata: {}, + sampleRate: 1, + }); + + 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.sampled).toBe(false); + expect(transaction.spanRecorder).toBeUndefined(); + expect(transaction.metadata).toEqual({ + source: 'custom', + spanMetadata: {}, + }); + + expect(transaction.toJSON()).toEqual( + expect.objectContaining({ + origin: 'manual', + span_id: 'span1', + start_timestamp: 1234, + trace_id: 'trace1', + }), + ); + }); + + it('inherits sampled based on parentSampled', () => { + const client = new NodeExperimentalClient(getDefaultNodeExperimentalClientOptions({ tracesSampleRate: 0 })); + const hub = getCurrentHub(); + hub.bindClient(client); + + const transaction = startTransaction(hub, { + name: 'test', + startTimestamp: 1234, + spanId: 'span1', + traceId: 'trace1', + parentSampled: true, + }); + + expect(transaction.sampled).toBe(true); + }); +}); 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..61b7d4f5d6c5 --- /dev/null +++ b/packages/node-experimental/test/utils/getActiveSpan.test.ts @@ -0,0 +1,151 @@ +import { trace } from '@opentelemetry/api'; +import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; + +import { setupOtel } from '../../src/sdk/initOtel'; +import { getActiveSpan, getRootSpan } from '../../src/utils/getActiveSpan'; +import { cleanupOtel } from '../helpers/mockSdkInit'; + +describe('getActiveSpan', () => { + let provider: BasicTracerProvider | undefined; + + beforeEach(() => { + provider = setupOtel(); + }); + + 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(() => { + provider = setupOtel(); + }); + + 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..390fa255a146 --- /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(); + }); + + 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..e4c2ca907ce9 --- /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: 'xxx' }, 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: 'xxx' }, 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/opentelemetry-node/src/index.ts b/packages/opentelemetry-node/src/index.ts index 630acd960059..0d3c905eaf2c 100644 --- a/packages/opentelemetry-node/src/index.ts +++ b/packages/opentelemetry-node/src/index.ts @@ -1,7 +1,10 @@ -import { getSentrySpan } from './utils/spanMap'; +import { SENTRY_TRACE_PARENT_CONTEXT_KEY } from './constants'; 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'; @@ -16,4 +19,4 @@ export type { AdditionalOtelSpanData } from './utils/spanData'; * * @private */ -export { getSentrySpan as _INTERNAL_getSentrySpan }; +export { SENTRY_TRACE_PARENT_CONTEXT_KEY as _INTERNAL_SENTRY_TRACE_PARENT_CONTEXT_KEY }; 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 Date: Mon, 9 Oct 2023 15:33:58 +0200 Subject: [PATCH 18/42] feat(core): Add `addIntegration` utility (#9186) To make it easier to async add integrations later, which is useful e.g. for replay but also for other cases. Now you can just do: ```js Sentry.addIntegration(new Sentry.Replay()); ``` --- packages/browser/src/exports.ts | 1 + packages/bun/src/index.ts | 1 + packages/core/src/index.ts | 2 +- packages/core/src/integration.ts | 12 +++++ packages/core/test/lib/base.test.ts | 3 ++ packages/core/test/lib/integration.test.ts | 52 ++++++++++++++++++- .../test/lib/transports/multiplexed.test.ts | 5 ++ packages/node/src/index.ts | 1 + packages/serverless/src/index.ts | 1 + packages/sveltekit/src/server/index.ts | 1 + packages/vercel-edge/src/index.ts | 1 + 11 files changed, 78 insertions(+), 2 deletions(-) diff --git a/packages/browser/src/exports.ts b/packages/browser/src/exports.ts index c9e7e6e34c73..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, diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index b1bd9dac5553..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, 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/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/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/node/src/index.ts b/packages/node/src/index.ts index 5fede4a51074..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, diff --git a/packages/serverless/src/index.ts b/packages/serverless/src/index.ts index e0490df7e0d2..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, diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index 6f02af4669fa..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, diff --git a/packages/vercel-edge/src/index.ts b/packages/vercel-edge/src/index.ts index 43aa34b56557..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, From 960484fa8e72365d4f1cc3cd0040a3d3f66e4da3 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 10 Oct 2023 15:22:49 +0200 Subject: [PATCH 19/42] feat(astro): Add server and client SDK `init` functions (#9189) Adds `init` functions for the server and client side of the Astro SDK with tests. For now, most of what these functions do is set the SDK metadata. On the client side, we add BrowserTracing, similarly to Next/SvelteKit. ref #9182 --- packages/astro/package.json | 7 ++ packages/astro/src/client/sdk.ts | 42 ++++++++ packages/astro/src/common/metadata.ts | 31 ++++++ packages/astro/src/index.client.ts | 4 +- packages/astro/src/index.server.ts | 61 ++++++++++- packages/astro/src/index.types.ts | 26 ++++- packages/astro/src/server/sdk.ts | 19 ++++ packages/astro/test/client/sdk.test.ts | 124 ++++++++++++++++++++++ packages/astro/test/server/sdk.test.ts | 52 +++++++++ packages/sveltekit/src/common/metadata.ts | 2 +- 10 files changed, 364 insertions(+), 4 deletions(-) create mode 100644 packages/astro/src/client/sdk.ts create mode 100644 packages/astro/src/common/metadata.ts create mode 100644 packages/astro/src/server/sdk.ts create mode 100644 packages/astro/test/client/sdk.test.ts create mode 100644 packages/astro/test/server/sdk.test.ts diff --git a/packages/astro/package.json b/packages/astro/package.json index 5a065208fe50..96e8a48c0a8b 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -19,6 +19,13 @@ "peerDependencies": { "astro": "1.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" + }, "devDependencies": { "astro": "^3.2.3", "rollup": "^3.20.2", 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 index dea210bd3fb8..2b85c05c3af1 100644 --- a/packages/astro/src/index.client.ts +++ b/packages/astro/src/index.client.ts @@ -1 +1,3 @@ -export const client = true; +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 index 126a907bffce..2e008583cbe8 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -1 +1,60 @@ -export const server = true; +// 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. +// 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'; diff --git a/packages/astro/src/index.types.ts b/packages/astro/src/index.types.ts index 6c4477dd4f04..e8ff7457f597 100644 --- a/packages/astro/src/index.types.ts +++ b/packages/astro/src/index.types.ts @@ -1 +1,25 @@ -export type Placeholder = true; +/* 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/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/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/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 From a173fa22ae89380114ac4cc8766d9834704aab86 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 10 Oct 2023 16:02:13 +0200 Subject: [PATCH 20/42] build: Small fixes for e2e tests & build (#9213) --- packages/bun/package.json | 2 +- packages/e2e-tests/package.json | 2 +- .../node-experimental-fastify-app/src/app.js | 2 - .../tests/{server.test.ts => errors.test.ts} | 38 ------------------- packages/tracing-internal/package.json | 2 +- packages/vercel-edge/package.json | 2 +- 6 files changed, 4 insertions(+), 44 deletions(-) rename packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/{server.test.ts => errors.test.ts} (51%) 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/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/src/app.js b/packages/e2e-tests/test-applications/node-experimental-fastify-app/src/app.js index 62e194170fa8..2c123cbb12cc 100644 --- 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 @@ -30,8 +30,6 @@ app.get('/test-transaction', async function (req, res) { Sentry.startSpan({ name: 'child-span' }, () => {}); }); - await Sentry.flush(); - res.send({ transactionIds: global.transactionIds || [], }); diff --git a/packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/server.test.ts b/packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/errors.test.ts similarity index 51% rename from packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/server.test.ts rename to packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/errors.test.ts index 9a9848eefa1a..4656ba23e7de 100644 --- a/packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/server.test.ts +++ b/packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/errors.test.ts @@ -37,41 +37,3 @@ test('Sends exception to Sentry', async ({ baseURL }) => { ) .toBe(200); }); - -test('Sends transactions to Sentry', async ({ baseURL }) => { - const { data } = await axios.get(`${baseURL}/test-transaction`); - const { transactionIds } = data; - - console.log(`Polling for transaction eventIds: ${JSON.stringify(transactionIds)}`); - - expect(transactionIds.length).toBe(1); - - await Promise.all( - transactionIds.map(async (transactionId: string) => { - const url = `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${transactionId}/`; - - 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/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/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\"", From 5fd50333baf92cb5c48d391167d0564243df6895 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 11 Oct 2023 10:53:07 +0200 Subject: [PATCH 21/42] fix(node-experimental): Sample in OTEL Sampler (#9203) In order for sampling propagation to work, we need to sample in OTEL, not in Sentry. As the sentry spans are only created later. Note that this _does not_ fix propagation yet in node-experimental, once this is merged I still need to adjust the propagator (or rather, fork it) from opentelemetry-node to work without sentry spans. --- packages/node-experimental/src/constants.ts | 1 + .../src/opentelemetry/sampler.ts | 190 +++++++++++ .../src/opentelemetry/spanExporter.ts | 11 +- .../src/opentelemetry/spanProcessor.ts | 31 +- .../node-experimental/src/sdk/initOtel.ts | 27 +- .../node-experimental/src/sdk/transaction.ts | 30 +- .../getDefaultNodePreviewClientOptions.ts | 1 + .../test/integration/transactions.test.ts | 2 +- .../node-experimental/test/sdk/client.test.ts | 1 + .../test/sdk/otelAsyncContextStrategy.test.ts | 6 +- .../node-experimental/test/sdk/trace.test.ts | 310 +++++++++++++++++- .../test/sdk/transaction.test.ts | 54 +-- .../test/utils/getActiveSpan.test.ts | 10 +- .../test/utils/setupEventContextTrace.test.ts | 2 +- .../test/utils/spanTypes.test.ts | 4 +- 15 files changed, 561 insertions(+), 119 deletions(-) create mode 100644 packages/node-experimental/src/opentelemetry/sampler.ts diff --git a/packages/node-experimental/src/constants.ts b/packages/node-experimental/src/constants.ts index 930574157d73..c41660be0fa2 100644 --- a/packages/node-experimental/src/constants.ts +++ b/packages/node-experimental/src/constants.ts @@ -13,3 +13,4 @@ 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'; diff --git a/packages/node-experimental/src/opentelemetry/sampler.ts b/packages/node-experimental/src/opentelemetry/sampler.ts new file mode 100644 index 000000000000..327294fbf272 --- /dev/null +++ b/packages/node-experimental/src/opentelemetry/sampler.ts @@ -0,0 +1,190 @@ +/* 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 { _INTERNAL_SENTRY_TRACE_PARENT_CONTEXT_KEY } from '@sentry/opentelemetry-node'; +import type { Client, ClientOptions, SamplingContext, TraceparentData } from '@sentry/types'; +import { isNaN, logger } from '@sentry/utils'; + +import { OTEL_ATTR_PARENT_SAMPLED, OTEL_ATTR_SENTRY_SAMPLE_RATE } 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 getTraceParentData(parentContext: Context): TraceparentData | undefined { + return parentContext.getValue(_INTERNAL_SENTRY_TRACE_PARENT_CONTEXT_KEY) as TraceparentData | undefined; +} + +function getParentRemoteSampled(spanContext: SpanContext, context: Context): boolean | undefined { + const traceId = spanContext.traceId; + const traceparentData = getTraceParentData(context); + + // Only inherit sample rate if `traceId` is the same + return traceparentData && traceId === traceparentData.traceId ? traceparentData.parentSampled : undefined; +} diff --git a/packages/node-experimental/src/opentelemetry/spanExporter.ts b/packages/node-experimental/src/opentelemetry/spanExporter.ts index 90af46e8672d..e242f74d6104 100644 --- a/packages/node-experimental/src/opentelemetry/spanExporter.ts +++ b/packages/node-experimental/src/opentelemetry/spanExporter.ts @@ -9,7 +9,13 @@ import { mapOtelStatus, parseOtelSpanDescription } from '@sentry/opentelemetry-n 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_SOURCE } from '../constants'; +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'; @@ -172,11 +178,13 @@ function createTransactionForOtelSpan(span: ReadableSpan): NodeExperimentalTrans 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', { @@ -270,6 +278,7 @@ function removeSentryAttributes(data: Record): Record(); const httpIntegration = client ? client.getIntegration(Http) : undefined; diff --git a/packages/node-experimental/src/sdk/initOtel.ts b/packages/node-experimental/src/sdk/initOtel.ts index 855a443889bb..134926f19c35 100644 --- a/packages/node-experimental/src/sdk/initOtel.ts +++ b/packages/node-experimental/src/sdk/initOtel.ts @@ -1,11 +1,12 @@ 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 { SDK_VERSION } from '@sentry/core'; import { SentryPropagator } from '@sentry/opentelemetry-node'; import { logger } from '@sentry/utils'; +import { SentrySampler } from '../opentelemetry/sampler'; import { SentrySpanProcessor } from '../opentelemetry/spanProcessor'; import type { NodeExperimentalClient } from '../types'; import { setupEventContextTrace } from '../utils/setupEventContextTrace'; @@ -19,7 +20,15 @@ import { getCurrentHub } from './hub'; 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; @@ -30,21 +39,17 @@ export function initOtel(): void { diag.setLogger(otelLogger, DiagLogLevel.DEBUG); } - if (client) { - setupEventContextTrace(client); - } + setupEventContextTrace(client); - const provider = setupOtel(); - if (client) { - client.traceProvider = provider; - } + const provider = setupOtel(client); + client.traceProvider = provider; } /** Just exported for tests. */ -export function setupOtel(): BasicTracerProvider { +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', diff --git a/packages/node-experimental/src/sdk/transaction.ts b/packages/node-experimental/src/sdk/transaction.ts index c301dd6e9521..382d9a926f5d 100644 --- a/packages/node-experimental/src/sdk/transaction.ts +++ b/packages/node-experimental/src/sdk/transaction.ts @@ -1,35 +1,21 @@ import type { Hub } from '@sentry/core'; -import { sampleTransaction, Transaction } from '@sentry/core'; -import type { - ClientOptions, - CustomSamplingContext, - Hub as HubInterface, - Scope, - TransactionContext, -} from '@sentry/types'; +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, - customSamplingContext?: CustomSamplingContext, -): Transaction { +export function startTransaction(hub: HubInterface, transactionContext: TransactionContext): Transaction { const client = hub.getClient(); const options: Partial = (client && client.getOptions()) || {}; - let transaction = new NodeExperimentalTransaction(transactionContext, hub as Hub); - transaction = sampleTransaction(transaction, options, { - parentSampled: transactionContext.parentSampled, - transactionContext, - ...customSamplingContext, - }); - if (transaction.sampled) { - transaction.initSpanRecorder(options._experiments && (options._experiments.maxSpans as number)); - } + 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); } 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/integration/transactions.test.ts b/packages/node-experimental/test/integration/transactions.test.ts index 00ec85700316..7979b771c440 100644 --- a/packages/node-experimental/test/integration/transactions.test.ts +++ b/packages/node-experimental/test/integration/transactions.test.ts @@ -541,7 +541,7 @@ describe('Integration | Transactions', () => { ); }); - it('does not creates spans for http requests if disabled in http integration xxx', async () => { + it('does not creates spans for http requests if disabled in http integration', async () => { const beforeSendTransaction = jest.fn(() => null); mockSdkInit({ enableTracing: true, beforeSendTransaction }); diff --git a/packages/node-experimental/test/sdk/client.test.ts b/packages/node-experimental/test/sdk/client.test.ts index 03ee60ecbf0b..b7db215a4cd8 100644 --- a/packages/node-experimental/test/sdk/client.test.ts +++ b/packages/node-experimental/test/sdk/client.test.ts @@ -29,6 +29,7 @@ describe('NodeExperimentalClient', () => { platform: 'node', runtime: { name: 'node', version: expect.any(String) }, serverName: expect.any(String), + tracesSampleRate: 1, }); }); diff --git a/packages/node-experimental/test/sdk/otelAsyncContextStrategy.test.ts b/packages/node-experimental/test/sdk/otelAsyncContextStrategy.test.ts index 346683bf45f3..518d61000fee 100644 --- a/packages/node-experimental/test/sdk/otelAsyncContextStrategy.test.ts +++ b/packages/node-experimental/test/sdk/otelAsyncContextStrategy.test.ts @@ -2,16 +2,20 @@ 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(() => { - provider = setupOtel(); + const options = getDefaultNodeExperimentalClientOptions(); + const client = new NodeExperimentalClient(options); + provider = setupOtel(client); setOtelContextAsyncContextStrategy(); }); diff --git a/packages/node-experimental/test/sdk/trace.test.ts b/packages/node-experimental/test/sdk/trace.test.ts index 76ecb28a5f9d..e5f5a1aa7ac7 100644 --- a/packages/node-experimental/test/sdk/trace.test.ts +++ b/packages/node-experimental/test/sdk/trace.test.ts @@ -1,7 +1,9 @@ +import { context, trace, TraceFlags } from '@opentelemetry/api'; import type { Span } from '@opentelemetry/sdk-trace-base'; +import { _INTERNAL_SENTRY_TRACE_PARENT_CONTEXT_KEY } from '@sentry/opentelemetry-node'; import * as Sentry from '../../src'; -import { OTEL_ATTR_OP, OTEL_ATTR_ORIGIN, OTEL_ATTR_SOURCE } from '../../src/constants'; +import { OTEL_ATTR_OP, OTEL_ATTR_ORIGIN, OTEL_ATTR_SENTRY_SAMPLE_RATE, OTEL_ATTR_SOURCE } from '../../src/constants'; import { getSpanMetadata } from '../../src/opentelemetry/spanData'; import { getActiveSpan } from '../../src/utils/getActiveSpan'; import { cleanupOtel, mockSdkInit } from '../helpers/mockSdkInit'; @@ -142,7 +144,9 @@ describe('trace', () => { }, span => { expect(span).toBeDefined(); - expect(span?.attributes).toEqual({}); + expect(span?.attributes).toEqual({ + [OTEL_ATTR_SENTRY_SAMPLE_RATE]: 1, + }); expect(getSpanMetadata(span!)).toEqual(undefined); }, @@ -162,6 +166,7 @@ describe('trace', () => { [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' }); @@ -210,7 +215,9 @@ describe('trace', () => { }); expect(span).toBeDefined(); - expect(span?.attributes).toEqual({}); + expect(span?.attributes).toEqual({ + [OTEL_ATTR_SENTRY_SAMPLE_RATE]: 1, + }); expect(getSpanMetadata(span!)).toEqual(undefined); @@ -224,6 +231,7 @@ describe('trace', () => { 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', @@ -259,3 +267,299 @@ describe('trace (tracing disabled)', () => { 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 traceParentData = { + traceId, + parentSpanId, + parentSampled: true, + }; + + // We simulate the correct context we'd normally get from the SentryPropagator + context.with( + trace.setSpanContext( + context.active().setValue(_INTERNAL_SENTRY_TRACE_PARENT_CONTEXT_KEY, traceParentData), + 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 traceParentData = { + traceId, + parentSpanId, + parentSampled: false, + }; + + // We simulate the correct context we'd normally get from the SentryPropagator + context.with( + trace.setSpanContext( + context.active().setValue(_INTERNAL_SENTRY_TRACE_PARENT_CONTEXT_KEY, traceParentData), + 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; + + 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 }, + }); + }); + + 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 traceParentData = { + traceId, + parentSpanId, + parentSampled: true, + }; + + // We simulate the correct context we'd normally get from the SentryPropagator + context.with( + trace.setSpanContext( + context.active().setValue(_INTERNAL_SENTRY_TRACE_PARENT_CONTEXT_KEY, traceParentData), + 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 index bf27549c8017..132696655b09 100644 --- a/packages/node-experimental/test/sdk/transaction.test.ts +++ b/packages/node-experimental/test/sdk/transaction.test.ts @@ -139,37 +139,8 @@ describe('startTranscation', () => { jest.resetAllMocks(); }); - it('creates an unsampled NodeExperimentalTransaction by default', () => { - const client = new NodeExperimentalClient(getDefaultNodeExperimentalClientOptions()); - const mockEmit = jest.spyOn(client, 'emit').mockImplementation(() => {}); - const hub = getCurrentHub(); - hub.bindClient(client); - - const transaction = startTransaction(hub, { name: 'test' }); - - expect(transaction).toBeInstanceOf(NodeExperimentalTransaction); - expect(mockEmit).toBeCalledTimes(1); - expect(mockEmit).toBeCalledWith('startTransaction', transaction); - - expect(transaction.sampled).toBe(false); - expect(transaction.spanRecorder).toBeUndefined(); - 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('creates a sampled NodeExperimentalTransaction based on the tracesSampleRate', () => { - const client = new NodeExperimentalClient(getDefaultNodeExperimentalClientOptions({ tracesSampleRate: 1 })); + it('creates a NodeExperimentalTransaction', () => { + const client = new NodeExperimentalClient(getDefaultNodeExperimentalClientOptions({ tracesSampleRate: 0 })); const hub = getCurrentHub(); hub.bindClient(client); @@ -177,13 +148,12 @@ describe('startTranscation', () => { expect(transaction).toBeInstanceOf(NodeExperimentalTransaction); - expect(transaction.sampled).toBe(true); + expect(transaction.sampled).toBe(undefined); expect(transaction.spanRecorder).toBeDefined(); expect(transaction.spanRecorder?.spans).toHaveLength(1); expect(transaction.metadata).toEqual({ source: 'custom', spanMetadata: {}, - sampleRate: 1, }); expect(transaction.toJSON()).toEqual( @@ -210,8 +180,6 @@ describe('startTranscation', () => { expect(transaction).toBeInstanceOf(NodeExperimentalTransaction); - expect(transaction.sampled).toBe(false); - expect(transaction.spanRecorder).toBeUndefined(); expect(transaction.metadata).toEqual({ source: 'custom', spanMetadata: {}, @@ -226,20 +194,4 @@ describe('startTranscation', () => { }), ); }); - - it('inherits sampled based on parentSampled', () => { - const client = new NodeExperimentalClient(getDefaultNodeExperimentalClientOptions({ tracesSampleRate: 0 })); - const hub = getCurrentHub(); - hub.bindClient(client); - - const transaction = startTransaction(hub, { - name: 'test', - startTimestamp: 1234, - spanId: 'span1', - traceId: 'trace1', - parentSampled: true, - }); - - expect(transaction.sampled).toBe(true); - }); }); diff --git a/packages/node-experimental/test/utils/getActiveSpan.test.ts b/packages/node-experimental/test/utils/getActiveSpan.test.ts index 61b7d4f5d6c5..b97ced5bdbf8 100644 --- a/packages/node-experimental/test/utils/getActiveSpan.test.ts +++ b/packages/node-experimental/test/utils/getActiveSpan.test.ts @@ -1,15 +1,19 @@ 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(() => { - provider = setupOtel(); + const options = getDefaultNodeExperimentalClientOptions(); + const client = new NodeExperimentalClient(options); + provider = setupOtel(client); }); afterEach(() => { @@ -93,7 +97,9 @@ describe('getRootSpan', () => { let provider: BasicTracerProvider | undefined; beforeEach(() => { - provider = setupOtel(); + const options = getDefaultNodeExperimentalClientOptions(); + const client = new NodeExperimentalClient(options); + provider = setupOtel(client); }); afterEach(async () => { diff --git a/packages/node-experimental/test/utils/setupEventContextTrace.test.ts b/packages/node-experimental/test/utils/setupEventContextTrace.test.ts index 390fa255a146..15d7f0976b9e 100644 --- a/packages/node-experimental/test/utils/setupEventContextTrace.test.ts +++ b/packages/node-experimental/test/utils/setupEventContextTrace.test.ts @@ -32,7 +32,7 @@ describe('setupEventContextTrace', () => { makeMain(hub); setupEventContextTrace(client); - provider = setupOtel(); + provider = setupOtel(client); }); afterEach(() => { diff --git a/packages/node-experimental/test/utils/spanTypes.test.ts b/packages/node-experimental/test/utils/spanTypes.test.ts index e4c2ca907ce9..fcd4703db9ce 100644 --- a/packages/node-experimental/test/utils/spanTypes.test.ts +++ b/packages/node-experimental/test/utils/spanTypes.test.ts @@ -31,7 +31,7 @@ describe('spanTypes', () => { it.each([ [{}, false], [{ kind: null }, false], - [{ kind: 'xxx' }, true], + [{ kind: 'TEST_KIND' }, true], ])('works with %p', (span, expected) => { const castSpan = span as unknown as Span; const actual = spanHasKind(castSpan); @@ -48,7 +48,7 @@ describe('spanTypes', () => { it.each([ [{}, false], [{ parentSpanId: null }, false], - [{ parentSpanId: 'xxx' }, true], + [{ parentSpanId: 'TEST_PARENT_ID' }, true], ])('works with %p', (span, expected) => { const castSpan = span as unknown as Span; const actual = spanHasParentId(castSpan); From 3c98e459b16239213e7635b9b7ac3cd55c7e5cc3 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 11 Oct 2023 12:50:55 +0200 Subject: [PATCH 22/42] build(node-experimental): Update fastify instrumentation & E2E test (#9223) E2E tests started failing for fastify because of 0.32.3 (https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/plugins/node/opentelemetry-instrumentation-fastify/CHANGELOG.md) being released. This includes this change https://github.com/open-telemetry/opentelemetry-js-contrib/pull/1680 (which actually we wrote xD) that changes some span names, which lead to E2E test failing. --- .../tests/transactions.test.ts | 2 +- packages/node-experimental/package.json | 2 +- yarn.lock | 21 ++++++++++++++----- 3 files changed, 18 insertions(+), 7 deletions(-) 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 index 00cc2b149e13..f34beaa63926 100644 --- 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 @@ -47,7 +47,7 @@ test('Sends an API route transaction', async ({ baseURL }) => { 'http.route': '/test-transaction', 'otel.kind': 'INTERNAL', }, - description: 'request handler - anonymous', + description: 'request handler - fastify -> app-auto-0', parent_span_id: expect.any(String), span_id: expect.any(String), start_timestamp: expect.any(Number), diff --git a/packages/node-experimental/package.json b/packages/node-experimental/package.json index e1e180b7faa2..4a0f64d033c3 100644 --- a/packages/node-experimental/package.json +++ b/packages/node-experimental/package.json @@ -28,7 +28,7 @@ "@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", diff --git a/yarn.lock b/yarn.lock index 334bf92dfa7b..d52033c73e40 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4367,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": @@ -4471,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" From a7be59ee29a0468f31db5f4632232c5ff6ef3656 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 11 Oct 2023 13:52:21 +0200 Subject: [PATCH 23/42] fix(ember): Avoid pulling in utils at build time (#9221) It seems some have an issue because of this, and maybe this is overkill anyhow. So I just inline a minimal version of this into the build file here instead, let's see if that fixes it. Fixes https://github.com/getsentry/sentry-javascript/issues/9215 --- packages/ember/index.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/ember/index.js b/packages/ember/index.js index d2db76846bfa..96e79bccf704 100644 --- a/packages/ember/index.js +++ b/packages/ember/index.js @@ -2,8 +2,6 @@ const fs = require('fs'); const crypto = require('crypto'); -const { dropUndefinedKeys } = require('@sentry/utils'); - function readSnippet(fileName) { return fs.readFileSync(`${__dirname}/vendor/${fileName}`, 'utf8'); } @@ -103,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; +} From 2875cd91fb24645b67660d1aac7302dcdd61c090 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 11 Oct 2023 13:54:49 +0200 Subject: [PATCH 24/42] feat(node-experimental): Use new Propagator for OTEL Spans (#9214) This fixes the propagator for node-experimental. The "old" propagator from opentelemetry-node relies on having a transaction for the active span etc, which we don't have anymore. So the propagator never attached the correct stuff etc. So now there is a new propagator for node-experimental: * Instead of keeping DSC & TraceparentData on the OTEL Context, we just keep the PropagationContext (which includes the DSC...) * Add an E2E test to make sure we attach the outgoing header correctly --- .../node-experimental-fastify-app/src/app.js | 47 ++- .../tests/propagation.test.ts | 100 +++++ packages/node-experimental/src/constants.ts | 5 + .../src/opentelemetry/propagator.ts | 131 ++++++ .../src/opentelemetry/sampler.ts | 17 +- .../node-experimental/src/sdk/initOtel.ts | 3 +- .../test/integration/transactions.test.ts | 11 +- .../test/opentelemetry/propagator.test.ts | 375 ++++++++++++++++++ .../node-experimental/test/sdk/trace.test.ts | 31 +- packages/opentelemetry-node/src/index.ts | 12 - 10 files changed, 679 insertions(+), 53 deletions(-) create mode 100644 packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/propagation.test.ts create mode 100644 packages/node-experimental/src/opentelemetry/propagator.ts create mode 100644 packages/node-experimental/test/opentelemetry/propagator.test.ts 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 index 2c123cbb12cc..d4a232fdf46b 100644 --- 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 @@ -3,6 +3,7 @@ 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); @@ -25,14 +26,24 @@ 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-transaction', async function (req, res) { Sentry.startSpan({ name: 'test-span' }, () => { Sentry.startSpan({ name: 'child-span' }, () => {}); }); - res.send({ - transactionIds: global.transactionIds || [], - }); + res.send({}); }); app.get('/test-error', async function (req, res) { @@ -45,16 +56,20 @@ app.get('/test-error', async function (req, res) { app.listen({ port: port }); -Sentry.addGlobalEventProcessor(event => { - global.transactionIds = global.transactionIds || []; - - if (event.type === 'transaction') { - const eventId = event.event_id; - - if (eventId) { - global.transactionIds.push(eventId); - } - } - - return event; -}); +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/tests/propagation.test.ts b/packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/propagation.test.ts new file mode 100644 index 000000000000..6b5ffa56fdba --- /dev/null +++ b/packages/e2e-tests/test-applications/node-experimental-fastify-app/tests/propagation.test.ts @@ -0,0 +1,100 @@ +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, + }, + }), + }), + ); +}); diff --git a/packages/node-experimental/src/constants.ts b/packages/node-experimental/src/constants.ts index c41660be0fa2..8d06aa411c1c 100644 --- a/packages/node-experimental/src/constants.ts +++ b/packages/node-experimental/src/constants.ts @@ -14,3 +14,8 @@ 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/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 index 327294fbf272..373c3b314b70 100644 --- a/packages/node-experimental/src/opentelemetry/sampler.ts +++ b/packages/node-experimental/src/opentelemetry/sampler.ts @@ -4,11 +4,14 @@ 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 { _INTERNAL_SENTRY_TRACE_PARENT_CONTEXT_KEY } from '@sentry/opentelemetry-node'; -import type { Client, ClientOptions, SamplingContext, TraceparentData } from '@sentry/types'; +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 } from '../constants'; +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 @@ -177,14 +180,14 @@ function isValidSampleRate(rate: unknown): boolean { return true; } -function getTraceParentData(parentContext: Context): TraceparentData | undefined { - return parentContext.getValue(_INTERNAL_SENTRY_TRACE_PARENT_CONTEXT_KEY) as TraceparentData | undefined; +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 = getTraceParentData(context); + const traceparentData = getPropagationContext(context); // Only inherit sample rate if `traceId` is the same - return traceparentData && traceId === traceparentData.traceId ? traceparentData.parentSampled : undefined; + return traceparentData && traceId === traceparentData.traceId ? traceparentData.sampled : undefined; } diff --git a/packages/node-experimental/src/sdk/initOtel.ts b/packages/node-experimental/src/sdk/initOtel.ts index 134926f19c35..b60dac87aeda 100644 --- a/packages/node-experimental/src/sdk/initOtel.ts +++ b/packages/node-experimental/src/sdk/initOtel.ts @@ -3,9 +3,9 @@ import { Resource } from '@opentelemetry/resources'; import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; import { SDK_VERSION } from '@sentry/core'; -import { SentryPropagator } from '@sentry/opentelemetry-node'; 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'; @@ -15,7 +15,6 @@ import { getCurrentHub } from './hub'; /** * Initialize OpenTelemetry for Node. - * We use the @sentry/opentelemetry-node package to communicate with OpenTelemetry. */ export function initOtel(): void { const client = getCurrentHub().getClient(); diff --git a/packages/node-experimental/test/integration/transactions.test.ts b/packages/node-experimental/test/integration/transactions.test.ts index 7979b771c440..02b84ec8cef2 100644 --- a/packages/node-experimental/test/integration/transactions.test.ts +++ b/packages/node-experimental/test/integration/transactions.test.ts @@ -1,12 +1,12 @@ import { context, SpanKind, trace, TraceFlags } from '@opentelemetry/api'; import type { SpanProcessor } from '@opentelemetry/sdk-trace-base'; import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; -import { _INTERNAL_SENTRY_TRACE_PARENT_CONTEXT_KEY } from '@sentry/opentelemetry-node'; -import type { TransactionEvent } from '@sentry/types'; +import type { 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 } from '../../src/integrations'; import { SentrySpanProcessor } from '../../src/opentelemetry/spanProcessor'; import type { NodeExperimentalClient } from '../../src/sdk/client'; @@ -348,10 +348,11 @@ describe('Integration | Transactions', () => { traceFlags: TraceFlags.SAMPLED, }; - const traceParentData = { + const propagationContext: PropagationContext = { traceId, parentSpanId, - parentSampled: true, + spanId: '6e0c63257de34c93', + sampled: true, }; mockSdkInit({ enableTracing: true, beforeSendTransaction }); @@ -362,7 +363,7 @@ describe('Integration | Transactions', () => { // We simulate the correct context we'd normally get from the SentryPropagator context.with( trace.setSpanContext( - context.active().setValue(_INTERNAL_SENTRY_TRACE_PARENT_CONTEXT_KEY, traceParentData), + context.active().setValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY, propagationContext), spanContext, ), () => { 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/trace.test.ts b/packages/node-experimental/test/sdk/trace.test.ts index e5f5a1aa7ac7..e141372552a6 100644 --- a/packages/node-experimental/test/sdk/trace.test.ts +++ b/packages/node-experimental/test/sdk/trace.test.ts @@ -1,9 +1,15 @@ import { context, trace, TraceFlags } from '@opentelemetry/api'; import type { Span } from '@opentelemetry/sdk-trace-base'; -import { _INTERNAL_SENTRY_TRACE_PARENT_CONTEXT_KEY } from '@sentry/opentelemetry-node'; +import type { PropagationContext } from '@sentry/types'; import * as Sentry from '../../src'; -import { OTEL_ATTR_OP, OTEL_ATTR_ORIGIN, OTEL_ATTR_SENTRY_SAMPLE_RATE, OTEL_ATTR_SOURCE } from '../../src/constants'; +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'; @@ -367,16 +373,17 @@ describe('trace (sampling)', () => { traceFlags: TraceFlags.SAMPLED, }; - const traceParentData = { + const propagationContext: PropagationContext = { traceId, + sampled: true, parentSpanId, - parentSampled: true, + spanId: '6e0c63257de34c93', }; // We simulate the correct context we'd normally get from the SentryPropagator context.with( trace.setSpanContext( - context.active().setValue(_INTERNAL_SENTRY_TRACE_PARENT_CONTEXT_KEY, traceParentData), + context.active().setValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY, propagationContext), spanContext, ), () => { @@ -406,16 +413,17 @@ describe('trace (sampling)', () => { traceFlags: TraceFlags.NONE, }; - const traceParentData = { + const propagationContext: PropagationContext = { traceId, + sampled: false, parentSpanId, - parentSampled: false, + spanId: '6e0c63257de34c93', }; // We simulate the correct context we'd normally get from the SentryPropagator context.with( trace.setSpanContext( - context.active().setValue(_INTERNAL_SENTRY_TRACE_PARENT_CONTEXT_KEY, traceParentData), + context.active().setValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY, propagationContext), spanContext, ), () => { @@ -533,16 +541,17 @@ describe('trace (sampling)', () => { traceFlags: TraceFlags.SAMPLED, }; - const traceParentData = { + const propagationContext: PropagationContext = { traceId, + sampled: true, parentSpanId, - parentSampled: true, + spanId: '6e0c63257de34c93', }; // We simulate the correct context we'd normally get from the SentryPropagator context.with( trace.setSpanContext( - context.active().setValue(_INTERNAL_SENTRY_TRACE_PARENT_CONTEXT_KEY, traceParentData), + context.active().setValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY, propagationContext), spanContext, ), () => { diff --git a/packages/opentelemetry-node/src/index.ts b/packages/opentelemetry-node/src/index.ts index 0d3c905eaf2c..7ed84c517b45 100644 --- a/packages/opentelemetry-node/src/index.ts +++ b/packages/opentelemetry-node/src/index.ts @@ -1,5 +1,3 @@ -import { SENTRY_TRACE_PARENT_CONTEXT_KEY } from './constants'; - export { SentrySpanProcessor } from './spanprocessor'; export { SentryPropagator } from './propagator'; export { maybeCaptureExceptionForTimedEvent } from './utils/captureExceptionForTimedEvent'; @@ -10,13 +8,3 @@ export { mapOtelStatus } from './utils/mapOtelStatus'; 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 { SENTRY_TRACE_PARENT_CONTEXT_KEY as _INTERNAL_SENTRY_TRACE_PARENT_CONTEXT_KEY }; From f8526df98cd56aa2e0509f82b6cbdb867b02e9a1 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 11 Oct 2023 13:59:50 +0200 Subject: [PATCH 25/42] fix(node): Ensure mysql integration works without callback (#9222) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Turns out my "improvement" here actually broke streaming the query response 😬 as in that case you may not provide a callback. This PR fixes this by using `query.on('end')` instead in that case to finish the span. Fixes https://github.com/getsentry/sentry-javascript/issues/9207 --- .../mysql/withoutCallback/scenario.ts | 26 ++++++++++++++----- .../mysql/withoutCallback/test.ts | 4 +++ .../src/node/integrations/mysql.ts | 9 +++++-- 3 files changed, 31 insertions(+), 8 deletions(-) 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/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; }; }); } From bde088ea1b5883709d487299d566a6eafe90bc95 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 11 Oct 2023 14:00:01 +0200 Subject: [PATCH 26/42] revert: Expose `sampleTransaction` export from core (#9208) This was previously exported so we can use it in node-experimental, but turns out we can't do that anyhow because we need to sample in OTEL. This has not yet been released, so in order to avoid exporting stuff we don't need public let's revert this export. (I'll leave the actual restructuring of moving this into a dedicated file in, though, because I think it still makes sense that way). --- packages/core/src/tracing/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/src/tracing/index.ts b/packages/core/src/tracing/index.ts index 2ace95aef323..f0356a528c2d 100644 --- a/packages/core/src/tracing/index.ts +++ b/packages/core/src/tracing/index.ts @@ -19,4 +19,3 @@ export { } from './trace'; export { getDynamicSamplingContextFromClient } from './dynamicSamplingContext'; export { setMeasurement } from './measurement'; -export { sampleTransaction } from './sampling'; From bb67a1100d55d8ef73d5f34f57b5aebe1927045b Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Wed, 11 Oct 2023 14:09:26 +0200 Subject: [PATCH 27/42] fix(utils): Dereference DOM events after they have servered their purpose (#9224) --- packages/utils/src/instrument.ts | 50 ++++++++++++-------------------- packages/utils/src/object.ts | 2 +- 2 files changed, 20 insertions(+), 32 deletions(-) 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/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 From 729e432e493becb0450f83159c33c86e09b54676 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Wed, 11 Oct 2023 16:47:34 +0200 Subject: [PATCH 28/42] feat(utils): Move common node ANR code to utils (#9191) This PR moves common ANR code to utils so it can be used for Electron renderer ANR detection. ## `watchdogTimer` Moved to utils with the addition of an enabled flag which isn't used in node ## `createDebugPauseMessageHandler` Handles messages from the debugger protocol. - Collects script ids for later filename lookups - Collects, converts and passes stack frames to a callback when the debugger pauses - Now uses `stripSentryFramesAndReverse` to remove Sentry frames! --- packages/node/src/anr/debugger.ts | 56 ++----------- packages/node/src/anr/index.ts | 36 +------- packages/utils/src/anr.ts | 133 ++++++++++++++++++++++++++++++ packages/utils/src/index.ts | 1 + 4 files changed, 145 insertions(+), 81 deletions(-) create mode 100644 packages/utils/src/anr.ts 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 99bb5901b4ea..38aa697457dd 100644 --- a/packages/node/src/anr/index.ts +++ b/packages/node/src/anr/index.ts @@ -1,5 +1,5 @@ import type { Event, StackFrame } from '@sentry/types'; -import { logger } from '@sentry/utils'; +import { logger, watchdogTimer } from '@sentry/utils'; import { spawn } from 'child_process'; import { addGlobalEventProcessor, captureEvent, flush } from '..'; @@ -8,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. @@ -216,10 +186,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(); }); } 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'; From 49572bd756dc286c411142307019b6cded005c0a Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Wed, 11 Oct 2023 16:57:21 +0200 Subject: [PATCH 29/42] fix: Don't set `referrerPolicy` on serverside fetch transports (#9200) --- packages/bun/src/transports/index.ts | 1 - packages/vercel-edge/src/transports/index.ts | 1 - packages/vercel-edge/test/transports/index.test.ts | 1 - 3 files changed, 3 deletions(-) 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/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', }); }); From 5140e7324b978061f8cb9da5b3a00ec9d4732468 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Thu, 12 Oct 2023 12:14:48 +0100 Subject: [PATCH 30/42] feat: Deno SDK (#9206) Co-authored-by: Luca Forstner --- .github/workflows/build.yml | 37 ++- .gitignore | 2 + .vscode/extensions.json | 5 +- .vscode/settings.json | 3 +- package.json | 3 +- packages/deno/.eslintrc.js | 18 ++ packages/deno/LICENSE | 14 ++ packages/deno/README.md | 64 +++++ packages/deno/jest.config.js | 1 + packages/deno/package.json | 61 +++++ packages/deno/rollup.config.js | 24 ++ packages/deno/rollup.types.config.js | 17 ++ packages/deno/scripts/download-deno-types.mjs | 7 + packages/deno/scripts/download.mjs | 10 + packages/deno/scripts/install-deno.mjs | 26 ++ packages/deno/src/client.ts | 44 ++++ packages/deno/src/index.ts | 77 ++++++ packages/deno/src/integrations/context.ts | 64 +++++ .../deno/src/integrations/contextlines.ts | 116 +++++++++ .../deno/src/integrations/globalhandlers.ts | 165 +++++++++++++ packages/deno/src/integrations/index.ts | 4 + .../deno/src/integrations/normalizepaths.ts | 100 ++++++++ packages/deno/src/sdk.ts | 102 ++++++++ packages/deno/src/transports/index.ts | 46 ++++ packages/deno/src/types.ts | 59 +++++ .../deno/test/__snapshots__/mod.test.ts.snap | 222 ++++++++++++++++++ packages/deno/test/example.ts | 14 ++ packages/deno/test/mod.test.ts | 76 ++++++ packages/deno/test/normalize.ts | 207 ++++++++++++++++ packages/deno/test/transport.ts | 30 +++ packages/deno/tsconfig.build.json | 12 + packages/deno/tsconfig.json | 9 + packages/deno/tsconfig.test.json | 9 + packages/deno/tsconfig.types.json | 9 + scripts/node-unit-tests.ts | 1 + yarn.lock | 36 ++- 36 files changed, 1687 insertions(+), 7 deletions(-) create mode 100644 packages/deno/.eslintrc.js create mode 100644 packages/deno/LICENSE create mode 100644 packages/deno/README.md create mode 100644 packages/deno/jest.config.js create mode 100644 packages/deno/package.json create mode 100644 packages/deno/rollup.config.js create mode 100644 packages/deno/rollup.types.config.js create mode 100644 packages/deno/scripts/download-deno-types.mjs create mode 100644 packages/deno/scripts/download.mjs create mode 100644 packages/deno/scripts/install-deno.mjs create mode 100644 packages/deno/src/client.ts create mode 100644 packages/deno/src/index.ts create mode 100644 packages/deno/src/integrations/context.ts create mode 100644 packages/deno/src/integrations/contextlines.ts create mode 100644 packages/deno/src/integrations/globalhandlers.ts create mode 100644 packages/deno/src/integrations/index.ts create mode 100644 packages/deno/src/integrations/normalizepaths.ts create mode 100644 packages/deno/src/sdk.ts create mode 100644 packages/deno/src/transports/index.ts create mode 100644 packages/deno/src/types.ts create mode 100644 packages/deno/test/__snapshots__/mod.test.ts.snap create mode 100644 packages/deno/test/example.ts create mode 100644 packages/deno/test/mod.test.ts create mode 100644 packages/deno/test/normalize.ts create mode 100644 packages/deno/test/transport.ts create mode 100644 packages/deno/tsconfig.build.json create mode 100644 packages/deno/tsconfig.json create mode 100644 packages/deno/tsconfig.test.json create mode 100644 packages/deno/tsconfig.types.json diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9f1440515c4f..3191f92adefb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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] @@ -895,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, 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/package.json b/package.json index 9546ebf2f9df..b5cbf9ddc38e 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "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", @@ -45,6 +45,7 @@ "packages/browser-integration-tests", "packages/bun", "packages/core", + "packages/deno", "packages/e2e-tests", "packages/ember", "packages/eslint-config-sdk", 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..7ee06d1874dc --- /dev/null +++ b/packages/deno/package.json @@ -0,0 +1,61 @@ +{ + "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, + "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/scripts/node-unit-tests.ts b/scripts/node-unit-tests.ts index dc923c8bf9a5..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 = { diff --git a/yarn.lock b/yarn.lock index d52033c73e40..b8f2e0477bbd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4832,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" @@ -4891,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" @@ -5993,6 +6013,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" @@ -20053,7 +20078,7 @@ magic-string@^0.30.0: dependencies: "@jridgewell/sourcemap-codec" "^1.4.13" -magic-string@^0.30.3: +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== @@ -26520,6 +26545,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" From ddcf5e67f91c304b87c589ee430109e31105b557 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 12 Oct 2023 13:43:28 +0200 Subject: [PATCH 31/42] feat(node-experimental): Add NodeFetch integration (#9226) This adds a new `NodeFetch` integration that is enabled by default for Node 16+. We use [opentelemetry-instrumentation-node-fetch](https://github.com/gas-buddy/opentelemetry-instrumentation-fetch-node) under the hood, which seems to have the best support/functionality. --- .../node-experimental-fastify-app/src/app.js | 7 + .../tests/propagation.test.ts | 91 +++++++++++++ packages/node-experimental/package.json | 3 +- .../src/integrations/index.ts | 1 + .../src/integrations/node-fetch.ts | 123 ++++++++++++++++++ .../src/opentelemetry/spanProcessor.ts | 13 +- packages/node-experimental/src/sdk/init.ts | 11 +- .../test/integration/transactions.test.ts | 84 +++++++++++- .../test/browser/backgroundtab.test.ts | 2 +- yarn.lock | 11 +- 10 files changed, 331 insertions(+), 15 deletions(-) create mode 100644 packages/node-experimental/src/integrations/node-fetch.ts 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 index d4a232fdf46b..50fe45767504 100644 --- 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 @@ -38,6 +38,13 @@ app.get('/test-outgoing-http', async function (req, res) { 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' }, () => {}); 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 index 6b5ffa56fdba..8dbcb590b331 100644 --- 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 @@ -98,3 +98,94 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => { }), ); }); + +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/node-experimental/package.json b/packages/node-experimental/package.json index 4a0f64d033c3..b13ce269e821 100644 --- a/packages/node-experimental/package.json +++ b/packages/node-experimental/package.json @@ -45,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/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/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/opentelemetry/spanProcessor.ts b/packages/node-experimental/src/opentelemetry/spanProcessor.ts index 25a04bf8577c..c7e07d11aa8e 100644 --- a/packages/node-experimental/src/opentelemetry/spanProcessor.ts +++ b/packages/node-experimental/src/opentelemetry/spanProcessor.ts @@ -9,6 +9,7 @@ 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'; @@ -76,18 +77,22 @@ export class SentrySpanProcessor extends BatchSpanProcessor implements SpanProce 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 ( - httpIntegration && (span.kind === SpanKind.CLIENT || span.kind === SpanKind.SERVER) && span.attributes[SemanticAttributes.HTTP_URL] && - span.attributes[SemanticAttributes.HTTP_METHOD] && - !httpIntegration.shouldCreateSpansForRequests + span.attributes[SemanticAttributes.HTTP_METHOD] ) { - return false; + const shouldCreateSpansForRequests = + span.attributes['http.client'] === 'fetch' + ? fetchIntegration?.shouldCreateSpansForRequests + : httpIntegration?.shouldCreateSpansForRequests; + + return shouldCreateSpansForRequests !== false; } return true; diff --git a/packages/node-experimental/src/sdk/init.ts b/packages/node-experimental/src/sdk/init.ts index 588b98cd1b43..c33a90f037d7 100644 --- a/packages/node-experimental/src/sdk/init.ts +++ b/packages/node-experimental/src/sdk/init.ts @@ -1,21 +1,30 @@ 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. */ diff --git a/packages/node-experimental/test/integration/transactions.test.ts b/packages/node-experimental/test/integration/transactions.test.ts index 02b84ec8cef2..4d657fc4cbf5 100644 --- a/packages/node-experimental/test/integration/transactions.test.ts +++ b/packages/node-experimental/test/integration/transactions.test.ts @@ -1,13 +1,13 @@ import { context, SpanKind, trace, TraceFlags } from '@opentelemetry/api'; import type { SpanProcessor } from '@opentelemetry/sdk-trace-base'; import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; -import type { PropagationContext, TransactionEvent } from '@sentry/types'; +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 } from '../../src/integrations'; +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'; @@ -542,7 +542,7 @@ describe('Integration | Transactions', () => { ); }); - it('does not creates spans for http requests if disabled in http integration', async () => { + it('does not create spans for http requests if disabled in http integration', async () => { const beforeSendTransaction = jest.fn(() => null); mockSdkInit({ enableTracing: true, beforeSendTransaction }); @@ -552,10 +552,79 @@ describe('Integration | Transactions', () => { const hub = getCurrentHub(); const client = hub.getClient() as NodeExperimentalClient; - jest.spyOn(client, 'getIntegration').mockImplementation(() => { - return { - shouldCreateSpansForRequests: false, - } as Http; + 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( @@ -565,6 +634,7 @@ describe('Integration | Transactions', () => { attributes: { [SemanticAttributes.HTTP_METHOD]: 'GET', [SemanticAttributes.HTTP_URL]: 'https://example.com', + 'http.client': 'fetch', }, }, span => { 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/yarn.lock b/yarn.lock index b8f2e0477bbd..7dec17d9f84c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4460,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== @@ -22728,6 +22728,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" From aadfd2b84492507505c474d7c2096b03ca95fd50 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Thu, 12 Oct 2023 13:49:36 +0100 Subject: [PATCH 32/42] fix(node): Strip .mjs and .cjs extensions from module name (#9231) --- packages/node/src/module.ts | 4 ++-- packages/node/test/module.test.ts | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) 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'); + }); }); From d1d7e797201420ab4718a978f24dbb21c29d418f Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Thu, 12 Oct 2023 14:56:21 +0200 Subject: [PATCH 33/42] ci: Add configs for deno (#9232) --- packages/deno/package.json | 3 +++ packages/e2e-tests/verdaccio-config/config.yaml | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/packages/deno/package.json b/packages/deno/package.json index 7ee06d1874dc..be3a7aa7ca9a 100644 --- a/packages/deno/package.json +++ b/packages/deno/package.json @@ -10,6 +10,9 @@ "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", diff --git a/packages/e2e-tests/verdaccio-config/config.yaml b/packages/e2e-tests/verdaccio-config/config.yaml index da3717dd283a..938b877a50e5 100644 --- a/packages/e2e-tests/verdaccio-config/config.yaml +++ b/packages/e2e-tests/verdaccio-config/config.yaml @@ -68,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 From f60f7631099a3b0139925884e57e43999ac15792 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Thu, 12 Oct 2023 14:29:48 +0100 Subject: [PATCH 34/42] fix(node): Remove ANR `debug` option and instead add logger.isEnabled() (#9230) A separate `debug` option for `enableAnrDetection` is confusing and is not required if we add an `isEnabled()` function to the logger. --- .../suites/anr/basic.js | 3 ++- .../suites/anr/basic.mjs | 3 ++- .../suites/anr/forked.js | 3 ++- .../node-integration-tests/suites/anr/test.ts | 22 ++++++++++++++++--- packages/node/src/anr/index.ts | 13 +++++------ packages/utils/src/logger.ts | 2 ++ 6 files changed, 32 insertions(+), 14 deletions(-) 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/src/anr/index.ts b/packages/node/src/anr/index.ts index 38aa697457dd..1d4d61b78b55 100644 --- a/packages/node/src/anr/index.ts +++ b/packages/node/src/anr/index.ts @@ -36,7 +36,7 @@ interface Options { */ captureStackTrace: boolean; /** - * Log debug information. + * @deprecated Use 'init' debug option instead */ debug: boolean; } @@ -94,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 { @@ -111,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(); @@ -142,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'; @@ -233,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/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__) { From e507110d89fd8117bda0a83ad7f7d2ee88681615 Mon Sep 17 00:00:00 2001 From: Alden Quimby Date: Thu, 12 Oct 2023 09:43:18 -0400 Subject: [PATCH 35/42] feat(tracing): allow direct pg module to enable esbuild support (#9227) --- .../src/node/integrations/postgres.ts | 33 ++++++++++--- .../test/integrations/node/postgres.test.ts | 47 ++++++++++++++----- 2 files changed, 62 insertions(+), 18 deletions(-) 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/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(); + }); }); From 28b6d751e149ef2410be559916cf9260242630f5 Mon Sep 17 00:00:00 2001 From: Alden Quimby Date: Thu, 12 Oct 2023 15:34:33 -0400 Subject: [PATCH 36/42] feat(serverlesss): allow disabling transaction traces (#9154) ## Background - My team runs Express inside Lambda - we have a monolithic Lambda function with express handling routing, middleware, etc - We are using `@sentry/node` today, and tried adding `@sentry/serverless` for it's timeout warning, auto-handling of `flush`, and links to cloudwatch logs - But this caused all of our performance transaction traces to have an incorrect name (and various other tag + context issues) ## Proposal - A new `startTrace` boolean flag that turns on/off the transaction tracing feature of `@sentry/serverless` - This will allow our team to use `@sentry/serverless`, but still rely on express `tracingHandler` for performance traces - This also follows the general approach of the serverless package, where each feature is gated by a wrapOptions flag --- packages/serverless/src/awslambda.ts | 80 ++++++++++++------- .../serverless/test/__mocks__/@sentry/node.ts | 1 + packages/serverless/test/awslambda.test.ts | 32 ++++++-- 3 files changed, 76 insertions(+), 37 deletions(-) diff --git a/packages/serverless/src/awslambda.ts b/packages/serverless/src/awslambda.ts index b26203bce4db..e29f77aaf879 100644 --- a/packages/serverless/src/awslambda.ts +++ b/packages/serverless/src/awslambda.ts @@ -47,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 })]; @@ -178,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, @@ -204,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. * @@ -222,6 +235,7 @@ export function wrapHandler( captureTimeoutWarning: true, timeoutWarningLimit: 500, captureAllSettledReasons: false, + startTrace: true, ...wrapOptions, }; let timeoutWarningTimer: NodeJS.Timeout; @@ -279,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/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) => { From 8bc4d734ea43d0d272ebb7b3c4ccdfbeb6eb58f8 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 13 Oct 2023 10:40:03 +0200 Subject: [PATCH 37/42] chore(astro): Add Astro package to Craft NPM targets (#9233) This PR adds the `@sentry/astro` NPM publishing target so that we can publish the SDK with the next release. ref #9182 --- .craft.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.craft.yml b/.craft.yml index 270dafe59d8e..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 From 1ba8d9f8129e25c6c21c277af38c1bbe6865c691 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 13 Oct 2023 10:56:17 +0200 Subject: [PATCH 38/42] feat(astro): Add `sentryAstro` integration (#9218) Add the `sentryAstro` Astro integration which will be the core component of the Astro SDK. The integration takes care of: * injecting SDK init code into the client and server entry points. * emitting source maps and adding the Sentry Vite plugin for source maps upload The idea is: Basic SDK setup will only require `sentryAstro` to be added by users. Everything else happens automatically. We also support a more custom setup but it will require additional config files. The reason is that we can only inject serialized code. We cannot take e.g. function callbacks (e.g. `beforeSend`) in the options of `sentryAstro`. --------- Co-authored-by: Francesco Novy --- .../astro/{.eslintrc.js => .eslintrc.cjs} | 7 ++ packages/astro/.npmignore | 10 ++ packages/astro/README.md | 96 +++++++++++++++- packages/astro/package.json | 17 ++- packages/astro/rollup.npm.config.js | 7 +- packages/astro/scripts/syncIntegration.ts | 20 ++++ packages/astro/src/index.server.ts | 5 + packages/astro/src/integration/index.ts | 81 ++++++++++++++ packages/astro/src/integration/snippets.ts | 45 ++++++++ packages/astro/src/integration/types.ts | 86 +++++++++++++++ packages/astro/test/index.client.test.ts | 5 - packages/astro/test/integration/index.test.ts | 103 ++++++++++++++++++ .../astro/test/integration/snippets.test.ts | 93 ++++++++++++++++ .../astro/test/server/index.server.test.ts | 7 ++ packages/astro/tsconfig.dev.json | 14 +++ rollup/npmHelpers.js | 16 ++- rollup/plugins/npmPlugins.js | 3 +- scripts/prepack.ts | 7 +- yarn.lock | 38 +++++++ 19 files changed, 638 insertions(+), 22 deletions(-) rename packages/astro/{.eslintrc.js => .eslintrc.cjs} (53%) create mode 100644 packages/astro/.npmignore create mode 100644 packages/astro/scripts/syncIntegration.ts create mode 100644 packages/astro/src/integration/index.ts create mode 100644 packages/astro/src/integration/snippets.ts create mode 100644 packages/astro/src/integration/types.ts delete mode 100644 packages/astro/test/index.client.test.ts create mode 100644 packages/astro/test/integration/index.test.ts create mode 100644 packages/astro/test/integration/snippets.test.ts create mode 100644 packages/astro/test/server/index.server.test.ts create mode 100644 packages/astro/tsconfig.dev.json diff --git a/packages/astro/.eslintrc.js b/packages/astro/.eslintrc.cjs similarity index 53% rename from packages/astro/.eslintrc.js rename to packages/astro/.eslintrc.cjs index 29b78099e7c6..c706032aaf35 100644 --- a/packages/astro/.eslintrc.js +++ b/packages/astro/.eslintrc.cjs @@ -11,5 +11,12 @@ module.exports = { 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/README.md b/packages/astro/README.md index 1ef88df544c3..a2738298a67c 100644 --- a/packages/astro/README.md +++ b/packages/astro/README.md @@ -28,4 +28,98 @@ This package is a wrapper around `@sentry/node` for the server and `@sentry/brow ## Installation and Setup -TODO +### 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 index 96e8a48c0a8b..60a2007a1553 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -9,22 +9,33 @@ "engines": { "node": ">=18.14.1" }, - "main": "build/cjs/index.server.js", + "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": "1.x" + "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/types": "7.73.0", + "@sentry/vite-plugin": "^2.8.0" }, "devDependencies": { "astro": "^3.2.3", diff --git a/packages/astro/rollup.npm.config.js b/packages/astro/rollup.npm.config.js index 23b23ff7a8d7..06dd0b3e4ec1 100644 --- a/packages/astro/rollup.npm.config.js +++ b/packages/astro/rollup.npm.config.js @@ -1,12 +1,17 @@ import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js'; -export default makeNPMConfigVariants( +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/index.server.ts b/packages/astro/src/index.server.ts index 2e008583cbe8..7a28bf907d48 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -2,6 +2,9 @@ // 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, @@ -58,3 +61,5 @@ export { export * from '@sentry/node'; export { init } from './server/sdk'; + +export default sentryAstro; 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/test/index.client.test.ts b/packages/astro/test/index.client.test.ts deleted file mode 100644 index f4bf62c84f1d..000000000000 --- a/packages/astro/test/index.client.test.ts +++ /dev/null @@ -1,5 +0,0 @@ -describe('placeholder', () => { - it('holds a place', () => { - expect(true).toBe(true); - }); -}); 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/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/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/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 7dec17d9f84c..ceb462aa1dad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5024,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" @@ -5059,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" @@ -5066,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" @@ -12617,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" From a8c85643b428134fe629f1a02e3a6099be4ceca3 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 13 Oct 2023 10:56:47 +0200 Subject: [PATCH 39/42] chore(astro): Add Astro integration keywords (#9193) Adding [specific keywords](https://docs.astro.build/en/reference/publish-to-npm/#integrations-library) to an NPM package makes them show up in [Astro's integration library page](https://astro.build/integrations/). This PR adds them to the Astro package.json. ref #9182 --- packages/astro/package.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/astro/package.json b/packages/astro/package.json index 60a2007a1553..c3c193a3a6d1 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -4,6 +4,12 @@ "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": { From 73a808acda6d6bafdb9edba8b3b70406493cfe16 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Fri, 13 Oct 2023 05:14:05 -0400 Subject: [PATCH 40/42] fix(replay): Fix potential broken CSS in styled-components (#9234) Fixes an issue where the Replay integration can potentially break applications that use `styled-components`. `styled-components` [relies on an exception being throw](https://github.com/styled-components/styled-components/blob/b7b374bb1ceff1699f7035b15881bc807110199a/packages/styled-components/src/sheet/Tag.ts#L32-L40) for CSS rules that are not supported by the browser engine. However, our SDK suppressed any exceptions thrown from within rrweb, so `styled-components` assumes that an unsupported rule was inserted successfully and increases a rule index, which causes following inserted rules to fail due to an out-of-bounds error. This was a regression from v1 as [we were always re-throwing the exception](https://github.com/getsentry/rrweb/blob/sentry-v1/packages/rrweb/src/sentry/callbackWrapper.ts#L17) Fixes https://github.com/getsentry/sentry-javascript/issues/9170 --------- Co-authored-by: Francesco Novy --- .../suites/replay/exceptions/template.html | 8 +++++ .../suites/replay/exceptions/test.ts | 35 +++++++++++++++++++ packages/replay/src/integration.ts | 10 +++--- 3 files changed, 47 insertions(+), 6 deletions(-) create mode 100644 packages/browser-integration-tests/suites/replay/exceptions/template.html create mode 100644 packages/browser-integration-tests/suites/replay/exceptions/test.ts 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/replay/src/integration.ts b/packages/replay/src/integration.ts index 753fd62d5660..725a61681d1c 100644 --- a/packages/replay/src/integration.ts +++ b/packages/replay/src/integration.ts @@ -145,15 +145,13 @@ export class Replay implements Integration { // collect fonts, but be aware that `sentry.io` needs to be an allowed // origin for playback collectFonts: true, - errorHandler: (err: Error) => { + 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; }, }; From 6062d0c3adff1e27e6840034e7d66b51bc4c4a6c Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 13 Oct 2023 11:59:54 +0200 Subject: [PATCH 41/42] fix(replay): bump rrweb to 2.0.1 (#9240) * Fix checking for patchTarget in initAdoptedStyleSheetObserver [#110](https://github.com/getsentry/rrweb/pull/110) https://github.com/getsentry/rrweb/blob/sentry-v2/CHANGELOG.md#201 --- packages/replay/package.json | 4 ++-- yarn.lock | 42 ++++++++++++++++++------------------ 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/packages/replay/package.json b/packages/replay/package.json index 744983ab066e..3fc5266a2846 100644 --- a/packages/replay/package.json +++ b/packages/replay/package.json @@ -52,8 +52,8 @@ "devDependencies": { "@babel/core": "^7.17.5", "@sentry-internal/replay-worker": "7.73.0", - "@sentry-internal/rrweb": "2.0.0", - "@sentry-internal/rrweb-snapshot": "2.0.0", + "@sentry-internal/rrweb": "2.0.1", + "@sentry-internal/rrweb-snapshot": "2.0.1", "jsdom-worker": "^0.2.1", "tslib": "^2.4.1 || ^1.9.3" }, diff --git a/yarn.lock b/yarn.lock index ceb462aa1dad..f64756e09026 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4977,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" From 85ac5e37e113fe744d02704e6a9b11f90d83f3c8 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 13 Oct 2023 11:18:42 +0200 Subject: [PATCH 42/42] meta: Update CHANGELOG for 7.74.0 --- CHANGELOG.md | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) 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