From cd0bc3b58d4f4144783997d446eeff960c29b8d2 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Tue, 19 Sep 2023 12:04:14 -0400 Subject: [PATCH] feat(vercel-edge): add Vercel Edge Runtime package (#9041) This PR adds a `@sentry/vercel-edge` SDK that can be used by our Next.js or Sveltekit SDKs for edge runtime support. --- package.json | 3 +- .../e2e-tests/verdaccio-config/config.yaml | 6 + packages/vercel-edge/.eslintrc.js | 12 + packages/vercel-edge/LICENSE | 14 + packages/vercel-edge/README.md | 64 ++++ packages/vercel-edge/jest.config.js | 7 + packages/vercel-edge/package.json | 71 ++++ packages/vercel-edge/rollup.npm.config.js | 3 + packages/vercel-edge/src/async.ts | 59 ++++ packages/vercel-edge/src/client.ts | 44 +++ packages/vercel-edge/src/index.ts | 73 ++++ packages/vercel-edge/src/sdk.ts | 96 +++++ packages/vercel-edge/src/transports/index.ts | 103 ++++++ packages/vercel-edge/src/transports/types.ts | 8 + packages/vercel-edge/src/types.ts | 68 ++++ packages/vercel-edge/src/utils/vercel.ts | 13 + .../vercel-edge/test/transports/index.test.ts | 163 +++++++++ packages/vercel-edge/tsconfig.json | 10 + packages/vercel-edge/tsconfig.test.json | 12 + packages/vercel-edge/tsconfig.types.json | 10 + scripts/node-unit-tests.ts | 333 +++++++++--------- yarn.lock | 166 +++++++++ 22 files changed, 1179 insertions(+), 159 deletions(-) create mode 100644 packages/vercel-edge/.eslintrc.js create mode 100644 packages/vercel-edge/LICENSE create mode 100644 packages/vercel-edge/README.md create mode 100644 packages/vercel-edge/jest.config.js create mode 100644 packages/vercel-edge/package.json create mode 100644 packages/vercel-edge/rollup.npm.config.js create mode 100644 packages/vercel-edge/src/async.ts create mode 100644 packages/vercel-edge/src/client.ts create mode 100644 packages/vercel-edge/src/index.ts create mode 100644 packages/vercel-edge/src/sdk.ts create mode 100644 packages/vercel-edge/src/transports/index.ts create mode 100644 packages/vercel-edge/src/transports/types.ts create mode 100644 packages/vercel-edge/src/types.ts create mode 100644 packages/vercel-edge/src/utils/vercel.ts create mode 100644 packages/vercel-edge/test/transports/index.test.ts create mode 100644 packages/vercel-edge/tsconfig.json create mode 100644 packages/vercel-edge/tsconfig.test.json create mode 100644 packages/vercel-edge/tsconfig.types.json diff --git a/package.json b/package.json index 8c50e83c69b0..6ae7e4f1d2cf 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}\" --ignore \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests,overhead-metrics}\"", + "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-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", @@ -70,6 +70,7 @@ "packages/types", "packages/typescript", "packages/utils", + "packages/vercel-edge", "packages/vue", "packages/wasm" ], diff --git a/packages/e2e-tests/verdaccio-config/config.yaml b/packages/e2e-tests/verdaccio-config/config.yaml index 05895a1adbed..80a5afc70008 100644 --- a/packages/e2e-tests/verdaccio-config/config.yaml +++ b/packages/e2e-tests/verdaccio-config/config.yaml @@ -164,6 +164,12 @@ packages: unpublish: $all # proxy: npmjs # Don't proxy for E2E tests! + '@sentry/vercel-edge': + access: $all + publish: $all + unpublish: $all + # proxy: npmjs # Don't proxy for E2E tests! + '@sentry/vue': access: $all publish: $all diff --git a/packages/vercel-edge/.eslintrc.js b/packages/vercel-edge/.eslintrc.js new file mode 100644 index 000000000000..bec6469d0e28 --- /dev/null +++ b/packages/vercel-edge/.eslintrc.js @@ -0,0 +1,12 @@ +module.exports = { + env: { + node: true, + }, + extends: ['../../.eslintrc.js'], + 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', + }, +}; diff --git a/packages/vercel-edge/LICENSE b/packages/vercel-edge/LICENSE new file mode 100644 index 000000000000..d11896ba1181 --- /dev/null +++ b/packages/vercel-edge/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/vercel-edge/README.md b/packages/vercel-edge/README.md new file mode 100644 index 000000000000..d3e7849ffdab --- /dev/null +++ b/packages/vercel-edge/README.md @@ -0,0 +1,64 @@ +

+ + Sentry + +

+ +# Official Sentry SDK for Vercel Edge Runtime [ALPHA] + +[![npm version](https://img.shields.io/npm/v/@sentry/vercel-edge.svg)](https://www.npmjs.com/package/@sentry/vercel-edge) +[![npm dm](https://img.shields.io/npm/dm/@sentry/vercel-edge.svg)](https://www.npmjs.com/package/@sentry/vercel-edge) +[![npm dt](https://img.shields.io/npm/dt/@sentry/vercel-edge.svg)](https://www.npmjs.com/package/@sentry/vercel-edge) + +## Links + +- [Official SDK Docs](https://docs.sentry.io/quickstart/) +- [TypeDoc](http://getsentry.github.io/sentry-javascript/) + +**Note: This SDK is still in an alpha state. Breaking changes can occur at any time.** + +## Usage + +To use this SDK, call `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 +// ES5 Syntax +const Sentry = require('@sentry/vercel-edge'); +// ES6 Syntax +import * as Sentry from '@sentry/vercel-edge'; + +Sentry.init({ + dsn: '__DSN__', + // ... +}); +``` + +To set context information or send manual events, use the exported functions of `@sentry/vercel-edge`. 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/vercel-edge/jest.config.js b/packages/vercel-edge/jest.config.js new file mode 100644 index 000000000000..dfc8f746b929 --- /dev/null +++ b/packages/vercel-edge/jest.config.js @@ -0,0 +1,7 @@ +const baseConfig = require('../../jest/jest.config.js'); + +module.exports = { + ...baseConfig, + // TODO: Fix tests to work with the Edge environment + // testEnvironment: '@edge-runtime/jest-environment', +}; diff --git a/packages/vercel-edge/package.json b/packages/vercel-edge/package.json new file mode 100644 index 000000000000..73559218598a --- /dev/null +++ b/packages/vercel-edge/package.json @@ -0,0 +1,71 @@ +{ + "name": "@sentry/vercel-edge", + "version": "7.69.0", + "description": "Offical Sentry SDK for the Vercel Edge Runtime", + "repository": "git://github.com/getsentry/sentry-javascript.git", + "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/vercel-edge", + "author": "Sentry", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "main": "build/cjs/index.js", + "module": "build/esm/index.js", + "types": "build/types/index.d.ts", + "typesVersions": { + "<4.9": { + "build/types/index.d.ts": [ + "build/types-ts3.8/index.d.ts" + ] + } + }, + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@sentry/core": "7.69.0", + "@sentry/types": "7.69.0", + "@sentry/utils": "7.69.0", + "tslib": "^2.4.1 || ^1.9.3" + }, + "devDependencies": { + "@edge-runtime/jest-environment": "2.2.3", + "@edge-runtime/types": "2.2.3" + }, + "scripts": { + "build": "run-p build:transpile build:types", + "build:dev": "yarn build", + "build:transpile": "rollup -c rollup.npm.config.js", + "build:types": "run-s build:types:core build:types:downlevel", + "build:types:core": "tsc -p tsconfig.types.json", + "build:types:downlevel": "yarn downlevel-dts build/types build/types-ts3.8 --to ts3.8", + "build:watch": "run-p build:transpile:watch build:types:watch", + "build:dev:watch": "yarn build:watch", + "build:transpile:watch": "rollup -c rollup.npm.config.js --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.ts", + "clean": "rimraf build coverage sentry-core-*.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": "jest", + "test:watch": "jest --watch", + "version": "node ../../scripts/versionbump.js src/version.ts", + "yalc:publish": "ts-node ../../scripts/prepack.ts && yalc publish build --push" + }, + "volta": { + "extends": "../../package.json" + }, + "sideEffects": false, + "madge":{ + "detectiveOptions": { + "ts": { + "skipTypeImports": true + } + } + } +} diff --git a/packages/vercel-edge/rollup.npm.config.js b/packages/vercel-edge/rollup.npm.config.js new file mode 100644 index 000000000000..5a62b528ef44 --- /dev/null +++ b/packages/vercel-edge/rollup.npm.config.js @@ -0,0 +1,3 @@ +import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js'; + +export default makeNPMConfigVariants(makeBaseNPMConfig()); diff --git a/packages/vercel-edge/src/async.ts b/packages/vercel-edge/src/async.ts new file mode 100644 index 000000000000..36c6317248b4 --- /dev/null +++ b/packages/vercel-edge/src/async.ts @@ -0,0 +1,59 @@ +import type { Carrier, Hub, RunWithAsyncContextOptions } from '@sentry/core'; +import { ensureHubOnCarrier, getHubFromCarrier, setAsyncContextStrategy } from '@sentry/core'; +import { GLOBAL_OBJ, logger } from '@sentry/utils'; + +interface AsyncLocalStorage { + getStore(): T | undefined; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + run(store: T, callback: (...args: TArgs) => R, ...args: TArgs): R; +} + +// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any +const MaybeGlobalAsyncLocalStorage = (GLOBAL_OBJ as any).AsyncLocalStorage; + +let asyncStorage: AsyncLocalStorage; + +/** + * Sets the async context strategy to use AsyncLocalStorage which should be available in the edge runtime. + */ +export function setAsyncLocalStorageAsyncContextStrategy(): void { + if (!MaybeGlobalAsyncLocalStorage) { + __DEBUG_BUILD__ && + logger.warn( + "Tried to register AsyncLocalStorage async context strategy in a runtime that doesn't support AsyncLocalStorage.", + ); + return; + } + + if (!asyncStorage) { + asyncStorage = new MaybeGlobalAsyncLocalStorage(); + } + + function getCurrentHub(): Hub | undefined { + return asyncStorage.getStore(); + } + + function createNewHub(parent: Hub | undefined): Hub { + const carrier: Carrier = {}; + ensureHubOnCarrier(carrier, parent); + return getHubFromCarrier(carrier); + } + + function runWithAsyncContext(callback: () => T, options: RunWithAsyncContextOptions): T { + const existingHub = getCurrentHub(); + + if (existingHub && options?.reuseExisting) { + // We're already in an async context, so we don't need to create a new one + // just call the callback with the current hub + return callback(); + } + + const newHub = createNewHub(existingHub); + + return asyncStorage.run(newHub, () => { + return callback(); + }); + } + + setAsyncContextStrategy({ getCurrentHub, runWithAsyncContext }); +} diff --git a/packages/vercel-edge/src/client.ts b/packages/vercel-edge/src/client.ts new file mode 100644 index 000000000000..448ecb199dce --- /dev/null +++ b/packages/vercel-edge/src/client.ts @@ -0,0 +1,44 @@ +import type { ServerRuntimeClientOptions } from '@sentry/core'; +import { SDK_VERSION, ServerRuntimeClient } from '@sentry/core'; + +import type { VercelEdgeClientOptions } from './types'; + +declare const process: { + env: Record; +}; + +/** + * The Sentry Vercel Edge Runtime SDK Client. + * + * @see VercelEdgeClientOptions for documentation on configuration options. + * @see ServerRuntimeClient for usage documentation. + */ +export class VercelEdgeClient extends ServerRuntimeClient { + /** + * Creates a new Vercel Edge Runtime SDK instance. + * @param options Configuration options for this SDK. + */ + public constructor(options: VercelEdgeClientOptions) { + options._metadata = options._metadata || {}; + options._metadata.sdk = options._metadata.sdk || { + name: 'sentry.javascript.vercel-edge', + packages: [ + { + name: 'npm:@sentry/vercel-edge', + version: SDK_VERSION, + }, + ], + version: SDK_VERSION, + }; + + const clientOptions: ServerRuntimeClientOptions = { + ...options, + platform: 'vercel-edge', + // TODO: Grab version information + runtime: { name: 'vercel-edge' }, + serverName: options.serverName || process.env.SENTRY_NAME, + }; + + super(clientOptions); + } +} diff --git a/packages/vercel-edge/src/index.ts b/packages/vercel-edge/src/index.ts new file mode 100644 index 000000000000..cd596269a36f --- /dev/null +++ b/packages/vercel-edge/src/index.ts @@ -0,0 +1,73 @@ +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 { VercelEdgeOptions } 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 { VercelEdgeClient } from './client'; +export { defaultIntegrations, init } from './sdk'; + +import { Integrations as CoreIntegrations } from '@sentry/core'; + +const INTEGRATIONS = { + ...CoreIntegrations, +}; + +export { INTEGRATIONS as Integrations }; diff --git a/packages/vercel-edge/src/sdk.ts b/packages/vercel-edge/src/sdk.ts new file mode 100644 index 000000000000..f2c57c7f9546 --- /dev/null +++ b/packages/vercel-edge/src/sdk.ts @@ -0,0 +1,96 @@ +import { getIntegrationsToSetup, initAndBind, Integrations as CoreIntegrations } from '@sentry/core'; +import { createStackParser, GLOBAL_OBJ, nodeStackLineParser, stackParserFromStackParserOptions } from '@sentry/utils'; + +import { setAsyncLocalStorageAsyncContextStrategy } from './async'; +import { VercelEdgeClient } from './client'; +import { makeEdgeTransport } from './transports'; +import type { VercelEdgeClientOptions, VercelEdgeOptions } from './types'; +import { getVercelEnv } from './utils/vercel'; + +declare const process: { + env: Record; +}; + +const nodeStackParser = createStackParser(nodeStackLineParser()); + +export const defaultIntegrations = [new CoreIntegrations.InboundFilters(), new CoreIntegrations.FunctionToString()]; + +/** Inits the Sentry NextJS SDK on the Edge Runtime. */ +export function init(options: VercelEdgeOptions = {}): void { + setAsyncLocalStorageAsyncContextStrategy(); + + if (options.defaultIntegrations === undefined) { + options.defaultIntegrations = defaultIntegrations; + } + + if (options.dsn === undefined && process.env.SENTRY_DSN) { + options.dsn = process.env.SENTRY_DSN; + } + + if (options.tracesSampleRate === undefined && process.env.SENTRY_TRACES_SAMPLE_RATE) { + const tracesSampleRate = parseFloat(process.env.SENTRY_TRACES_SAMPLE_RATE); + if (isFinite(tracesSampleRate)) { + options.tracesSampleRate = tracesSampleRate; + } + } + + if (options.release === undefined) { + const detectedRelease = getSentryRelease(); + if (detectedRelease !== undefined) { + options.release = detectedRelease; + } else { + // If release is not provided, then we should disable autoSessionTracking + options.autoSessionTracking = false; + } + } + + options.environment = + options.environment || process.env.SENTRY_ENVIRONMENT || getVercelEnv(false) || process.env.NODE_ENV; + + if (options.autoSessionTracking === undefined && options.dsn !== undefined) { + options.autoSessionTracking = true; + } + + if (options.instrumenter === undefined) { + options.instrumenter = 'sentry'; + } + + const clientOptions: VercelEdgeClientOptions = { + ...options, + stackParser: stackParserFromStackParserOptions(options.stackParser || nodeStackParser), + integrations: getIntegrationsToSetup(options), + transport: options.transport || makeEdgeTransport, + }; + + initAndBind(VercelEdgeClient, clientOptions); +} + +/** + * Returns a release dynamically from environment variables. + */ +export function getSentryRelease(fallback?: string): string | undefined { + // Always read first as Sentry takes this as precedence + if (process.env.SENTRY_RELEASE) { + return process.env.SENTRY_RELEASE; + } + + // This supports the variable that sentry-webpack-plugin injects + if (GLOBAL_OBJ.SENTRY_RELEASE && GLOBAL_OBJ.SENTRY_RELEASE.id) { + return GLOBAL_OBJ.SENTRY_RELEASE.id; + } + + return ( + // GitHub Actions - https://help.github.com/en/actions/configuring-and-managing-workflows/using-environment-variables#default-environment-variables + process.env.GITHUB_SHA || + // Vercel - https://vercel.com/docs/v2/build-step#system-environment-variables + process.env.VERCEL_GIT_COMMIT_SHA || + process.env.VERCEL_GITHUB_COMMIT_SHA || + process.env.VERCEL_GITLAB_COMMIT_SHA || + process.env.VERCEL_BITBUCKET_COMMIT_SHA || + // Zeit (now known as Vercel) + process.env.ZEIT_GITHUB_COMMIT_SHA || + process.env.ZEIT_GITLAB_COMMIT_SHA || + process.env.ZEIT_BITBUCKET_COMMIT_SHA || + fallback + ); +} diff --git a/packages/vercel-edge/src/transports/index.ts b/packages/vercel-edge/src/transports/index.ts new file mode 100644 index 000000000000..a479425f96e6 --- /dev/null +++ b/packages/vercel-edge/src/transports/index.ts @@ -0,0 +1,103 @@ +import { createTransport } from '@sentry/core'; +import type { BaseTransportOptions, Transport, TransportMakeRequestResponse, TransportRequest } from '@sentry/types'; +import { SentryError } from '@sentry/utils'; + +export interface VercelEdgeTransportOptions extends BaseTransportOptions { + /** Fetch API init parameters. */ + fetchOptions?: RequestInit; + /** Custom headers for the transport. */ + headers?: { [key: string]: string }; +} + +const DEFAULT_TRANSPORT_BUFFER_SIZE = 30; + +/** + * This is a modified promise buffer that collects tasks until drain is called. + * We need this in the edge runtime because edge function invocations may not share I/O objects, like fetch requests + * and responses, and the normal PromiseBuffer inherently buffers stuff inbetween incoming requests. + * + * A limitation we need to be aware of is that DEFAULT_TRANSPORT_BUFFER_SIZE is the maximum amount of payloads the + * SDK can send for a given edge function invocation. + */ +export class IsolatedPromiseBuffer { + // We just have this field because the promise buffer interface requires it. + // If we ever remove it from the interface we should also remove it here. + public $: Array>; + + private _taskProducers: (() => PromiseLike)[]; + + private readonly _bufferSize: number; + + public constructor(_bufferSize = DEFAULT_TRANSPORT_BUFFER_SIZE) { + this.$ = []; + this._taskProducers = []; + this._bufferSize = _bufferSize; + } + + /** + * @inheritdoc + */ + public add(taskProducer: () => PromiseLike): PromiseLike { + if (this._taskProducers.length >= this._bufferSize) { + return Promise.reject(new SentryError('Not adding Promise because buffer limit was reached.')); + } + + this._taskProducers.push(taskProducer); + return Promise.resolve(); + } + + /** + * @inheritdoc + */ + public drain(timeout?: number): PromiseLike { + const oldTaskProducers = [...this._taskProducers]; + this._taskProducers = []; + + return new Promise(resolve => { + const timer = setTimeout(() => { + if (timeout && timeout > 0) { + resolve(false); + } + }, timeout); + + void Promise.all( + oldTaskProducers.map(taskProducer => + taskProducer().then(null, () => { + // catch all failed requests + }), + ), + ).then(() => { + // resolve to true if all fetch requests settled + clearTimeout(timer); + resolve(true); + }); + }); + } +} + +/** + * Creates a Transport that uses the Edge Runtimes native fetch API to send events to Sentry. + */ +export function makeEdgeTransport(options: VercelEdgeTransportOptions): Transport { + function makeRequest(request: TransportRequest): PromiseLike { + const requestOptions: RequestInit = { + body: request.body, + method: 'POST', + referrerPolicy: 'origin', + headers: options.headers, + ...options.fetchOptions, + }; + + 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'), + }, + }; + }); + } + + return createTransport(options, makeRequest, new IsolatedPromiseBuffer(options.bufferSize)); +} diff --git a/packages/vercel-edge/src/transports/types.ts b/packages/vercel-edge/src/transports/types.ts new file mode 100644 index 000000000000..e6c0ab869662 --- /dev/null +++ b/packages/vercel-edge/src/transports/types.ts @@ -0,0 +1,8 @@ +import type { BaseTransportOptions } from '@sentry/types'; + +export interface VercelEdgeTransportOptions extends BaseTransportOptions { + /** Fetch API init parameters. */ + fetchOptions?: RequestInit; + /** Custom headers for the transport. */ + headers?: { [key: string]: string }; +} diff --git a/packages/vercel-edge/src/types.ts b/packages/vercel-edge/src/types.ts new file mode 100644 index 000000000000..ec91431d8e60 --- /dev/null +++ b/packages/vercel-edge/src/types.ts @@ -0,0 +1,68 @@ +import type { ClientOptions, Options, TracePropagationTargets } from '@sentry/types'; + +import type { VercelEdgeClient } from './client'; +import type { VercelEdgeTransportOptions } from './transports'; + +export interface BaseVercelEdgeOptions { + /** + * 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; + + /** + * Specify a custom VercelEdgeClient to be used. Must extend VercelEdgeClient! + * This is not a public, supported API, but used internally only. + * + * @hidden + * */ + clientClass?: typeof VercelEdgeClient; + + // 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 VercelEdge SDK + * @see @sentry/types Options for more information. + */ +export interface VercelEdgeOptions extends Options, BaseVercelEdgeOptions {} + +/** + * Configuration options for the Sentry VercelEdge SDK Client class + * @see VercelEdgeClient for more information. + */ +export interface VercelEdgeClientOptions extends ClientOptions, BaseVercelEdgeOptions {} diff --git a/packages/vercel-edge/src/utils/vercel.ts b/packages/vercel-edge/src/utils/vercel.ts new file mode 100644 index 000000000000..cb640aaff1c5 --- /dev/null +++ b/packages/vercel-edge/src/utils/vercel.ts @@ -0,0 +1,13 @@ +declare const process: { + env: Record; +}; + +/** + * Returns an environment setting value determined by Vercel's `VERCEL_ENV` environment variable. + * + * @param isClient Flag to indicate whether to use the `NEXT_PUBLIC_` prefixed version of the environment variable. + */ +export function getVercelEnv(isClient: boolean): string | undefined { + const vercelEnvVar = isClient ? process.env.NEXT_PUBLIC_VERCEL_ENV : process.env.VERCEL_ENV; + return vercelEnvVar ? `vercel-${vercelEnvVar}` : undefined; +} diff --git a/packages/vercel-edge/test/transports/index.test.ts b/packages/vercel-edge/test/transports/index.test.ts new file mode 100644 index 000000000000..cab31eca5bf2 --- /dev/null +++ b/packages/vercel-edge/test/transports/index.test.ts @@ -0,0 +1,163 @@ +import type { EventEnvelope, EventItem } from '@sentry/types'; +import { createEnvelope, serializeEnvelope } from '@sentry/utils'; +import { TextEncoder } from 'util'; + +import type { VercelEdgeTransportOptions } from '../../src/transports'; +import { IsolatedPromiseBuffer, makeEdgeTransport } from '../../src/transports'; + +const DEFAULT_EDGE_TRANSPORT_OPTIONS: VercelEdgeTransportOptions = { + url: 'https://sentry.io/api/42/store/?sentry_key=123&sentry_version=7', + recordDroppedEvent: () => undefined, + textEncoder: new TextEncoder(), +}; + +const ERROR_ENVELOPE = createEnvelope({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' }, [ + [{ type: 'event' }, { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' }] as EventItem, +]); + +class Headers { + headers: { [key: string]: string } = {}; + get(key: string) { + return this.headers[key] || null; + } + set(key: string, value: string) { + this.headers[key] = value; + } +} + +const mockFetch = jest.fn(); + +// @ts-expect-error fetch is not on global +const oldFetch = global.fetch; +// @ts-expect-error fetch is not on global +global.fetch = mockFetch; + +afterAll(() => { + // @ts-expect-error fetch is not on global + global.fetch = oldFetch; +}); + +describe('Edge Transport', () => { + it('calls fetch with the given URL', async () => { + mockFetch.mockImplementationOnce(() => + Promise.resolve({ + headers: new Headers(), + status: 200, + text: () => Promise.resolve({}), + }), + ); + + const transport = makeEdgeTransport(DEFAULT_EDGE_TRANSPORT_OPTIONS); + + expect(mockFetch).toHaveBeenCalledTimes(0); + await transport.send(ERROR_ENVELOPE); + await transport.flush(); + expect(mockFetch).toHaveBeenCalledTimes(1); + + expect(mockFetch).toHaveBeenLastCalledWith(DEFAULT_EDGE_TRANSPORT_OPTIONS.url, { + body: serializeEnvelope(ERROR_ENVELOPE, new TextEncoder()), + method: 'POST', + referrerPolicy: 'origin', + }); + }); + + it('sets rate limit headers', async () => { + const headers = { + get: jest.fn(), + }; + + mockFetch.mockImplementationOnce(() => + Promise.resolve({ + headers, + status: 200, + text: () => Promise.resolve({}), + }), + ); + + const transport = makeEdgeTransport(DEFAULT_EDGE_TRANSPORT_OPTIONS); + + expect(headers.get).toHaveBeenCalledTimes(0); + await transport.send(ERROR_ENVELOPE); + await transport.flush(); + + expect(headers.get).toHaveBeenCalledTimes(2); + expect(headers.get).toHaveBeenCalledWith('X-Sentry-Rate-Limits'); + expect(headers.get).toHaveBeenCalledWith('Retry-After'); + }); + + it('allows for custom options to be passed in', async () => { + mockFetch.mockImplementationOnce(() => + Promise.resolve({ + headers: new Headers(), + status: 200, + text: () => Promise.resolve({}), + }), + ); + + const REQUEST_OPTIONS: RequestInit = { + referrerPolicy: 'strict-origin', + keepalive: false, + referrer: 'http://example.org', + }; + + const transport = makeEdgeTransport({ ...DEFAULT_EDGE_TRANSPORT_OPTIONS, fetchOptions: REQUEST_OPTIONS }); + + await transport.send(ERROR_ENVELOPE); + await transport.flush(); + expect(mockFetch).toHaveBeenLastCalledWith(DEFAULT_EDGE_TRANSPORT_OPTIONS.url, { + body: serializeEnvelope(ERROR_ENVELOPE, new TextEncoder()), + method: 'POST', + ...REQUEST_OPTIONS, + }); + }); +}); + +describe('IsolatedPromiseBuffer', () => { + it('should not call tasks until drained', async () => { + const ipb = new IsolatedPromiseBuffer(); + + const task1 = jest.fn(() => Promise.resolve({})); + const task2 = jest.fn(() => Promise.resolve({})); + + await ipb.add(task1); + await ipb.add(task2); + + expect(task1).not.toHaveBeenCalled(); + expect(task2).not.toHaveBeenCalled(); + + await ipb.drain(); + + expect(task1).toHaveBeenCalled(); + expect(task2).toHaveBeenCalled(); + }); + + it('should not allow adding more items than the specified limit', async () => { + const ipb = new IsolatedPromiseBuffer(3); + + const task1 = jest.fn(() => Promise.resolve({})); + const task2 = jest.fn(() => Promise.resolve({})); + const task3 = jest.fn(() => Promise.resolve({})); + const task4 = jest.fn(() => Promise.resolve({})); + + await ipb.add(task1); + await ipb.add(task2); + await ipb.add(task3); + + await expect(ipb.add(task4)).rejects.toThrowError('Not adding Promise because buffer limit was reached.'); + }); + + it('should not throw when one of the tasks throws when drained', async () => { + const ipb = new IsolatedPromiseBuffer(); + + const task1 = jest.fn(() => Promise.resolve({})); + const task2 = jest.fn(() => Promise.reject(new Error())); + + await ipb.add(task1); + await ipb.add(task2); + + await expect(ipb.drain()).resolves.toEqual(true); + + expect(task1).toHaveBeenCalled(); + expect(task2).toHaveBeenCalled(); + }); +}); diff --git a/packages/vercel-edge/tsconfig.json b/packages/vercel-edge/tsconfig.json new file mode 100644 index 000000000000..f288bd1b84e2 --- /dev/null +++ b/packages/vercel-edge/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + + "include": ["src/**/*"], + + "compilerOptions": { + // package-specific options + "types": ["@edge-runtime/types"] + } +} diff --git a/packages/vercel-edge/tsconfig.test.json b/packages/vercel-edge/tsconfig.test.json new file mode 100644 index 000000000000..87f6afa06b86 --- /dev/null +++ b/packages/vercel-edge/tsconfig.test.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + + "include": ["test/**/*"], + + "compilerOptions": { + // should include all types from `./tsconfig.json` plus types for all test frameworks used + "types": ["node", "jest"] + + // other package-specific, test-specific options + } +} diff --git a/packages/vercel-edge/tsconfig.types.json b/packages/vercel-edge/tsconfig.types.json new file mode 100644 index 000000000000..65455f66bd75 --- /dev/null +++ b/packages/vercel-edge/tsconfig.types.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "outDir": "build/types" + } +} diff --git a/scripts/node-unit-tests.ts b/scripts/node-unit-tests.ts index 835e83c44896..28167e15d557 100644 --- a/scripts/node-unit-tests.ts +++ b/scripts/node-unit-tests.ts @@ -1,158 +1,175 @@ -import * as childProcess from 'child_process'; -import * as fs from 'fs'; - -const CURRENT_NODE_VERSION = process.version.replace('v', '').split('.')[0]; - -const DEFAULT_SKIP_TESTS_PACKAGES = [ - '@sentry-internal/eslint-plugin-sdk', - '@sentry/ember', - '@sentry/browser', - '@sentry/vue', - '@sentry/react', - '@sentry/angular', - '@sentry/svelte', - '@sentry/replay', - '@sentry/wasm', - '@sentry/bun', -]; - -// These packages don't support Node 8 for syntax or dependency reasons. -const NODE_8_SKIP_TESTS_PACKAGES = [ - '@sentry/gatsby', - '@sentry/serverless', - '@sentry/nextjs', - '@sentry/remix', - '@sentry/sveltekit', - '@sentry-internal/replay-worker', - '@sentry/node-experimental', -]; - -// We have to downgrade some of our dependencies in order to run tests in Node 8 and 10. -const NODE_8_LEGACY_DEPENDENCIES = [ - 'jsdom@15.x', - 'jest@25.x', - 'jest-environment-jsdom@25.x', - 'jest-environment-node@25.x', - 'ts-jest@25.x', - 'lerna@3.13.4', -]; - -const NODE_10_SKIP_TESTS_PACKAGES = [ - '@sentry/remix', - '@sentry/sveltekit', - '@sentry-internal/replay-worker', - '@sentry/node-experimental', -]; -const NODE_10_LEGACY_DEPENDENCIES = ['jsdom@16.x', 'lerna@3.13.4']; - -const NODE_12_SKIP_TESTS_PACKAGES = ['@sentry/remix', '@sentry/sveltekit', '@sentry/node-experimental']; -const NODE_12_LEGACY_DEPENDENCIES = ['lerna@3.13.4']; - -const NODE_14_SKIP_TESTS_PACKAGES = ['@sentry/sveltekit']; - -type JSONValue = string | number | boolean | null | JSONArray | JSONObject; - -type JSONObject = { - [key: string]: JSONValue; -}; -type JSONArray = Array; - -interface TSConfigJSON extends JSONObject { - compilerOptions: { lib: string[]; target: string }; -} - -/** - * Run the given shell command, piping the shell process's `stdin`, `stdout`, and `stderr` to that of the current - * process. Returns contents of `stdout`. - */ -function run(cmd: string, options?: childProcess.ExecSyncOptions): void { - childProcess.execSync(cmd, { stdio: 'inherit', ...options }); -} - -/** - * Install the given legacy dependencies, for compatibility with tests run in older versions of Node. - */ -function installLegacyDeps(legacyDeps: string[] = []): void { - // Ignoring engines and scripts lets us get away with having incompatible things installed for SDK packages we're not - // testing in the current node version, and ignoring the root check lets us install things at the repo root. - run(`yarn add --dev --ignore-engines --ignore-scripts --ignore-workspace-root-check ${legacyDeps.join(' ')}`); -} - -/** - * Modify a json file on disk. - * - * @param filepath The path to the file to be modified - * @param transformer A function which takes the JSON data as input and returns a mutated version. It may mutate the - * JSON data in place, but it isn't required to do so. - */ -export function modifyJSONFile(filepath: string, transformer: (json: JSONObject) => JSONObject): void { - const fileContents = fs - .readFileSync(filepath) - .toString() - // get rid of comments, which the `jsonc` format allows, but which will crash `JSON.parse` - .replace(/\/\/.*\n/g, ''); - const json = JSON.parse(fileContents); - const newJSON = transformer(json); - fs.writeFileSync(filepath, JSON.stringify(newJSON, null, 2)); -} - -const es6ifyTestTSConfig = (pkg: string): void => { - const filepath = `packages/${pkg}/tsconfig.test.json`; - const transformer = (json: JSONObject): JSONObject => { - const tsconfig = json as TSConfigJSON; - tsconfig.compilerOptions.target = 'es6'; - return json; - }; - modifyJSONFile(filepath, transformer); -}; - -/** - * Skip tests which don't run in Node 8. - * We're forced to skip these tests for compatibility reasons. - */ -function skipNodeV8Tests(): void { - run('rm -rf packages/tracing/test/browser'); -} - -/** - * Run tests, ignoring the given packages - */ -function runWithIgnores(skipPackages: string[] = []): void { - const ignoreFlags = skipPackages.map(dep => `--ignore="${dep}"`).join(' '); - run(`yarn test ${ignoreFlags}`); -} - -/** - * Run the tests, accounting for compatibility problems in older versions of Node. - */ -function runTests(): void { - const ignores = new Set(); - - DEFAULT_SKIP_TESTS_PACKAGES.forEach(dep => ignores.add(dep)); - - switch (CURRENT_NODE_VERSION) { - case '8': - NODE_8_SKIP_TESTS_PACKAGES.forEach(dep => ignores.add(dep)); - installLegacyDeps(NODE_8_LEGACY_DEPENDENCIES); - skipNodeV8Tests(); - es6ifyTestTSConfig('utils'); - break; - case '10': - NODE_10_SKIP_TESTS_PACKAGES.forEach(dep => ignores.add(dep)); - installLegacyDeps(NODE_10_LEGACY_DEPENDENCIES); - es6ifyTestTSConfig('utils'); - break; - case '12': - NODE_12_SKIP_TESTS_PACKAGES.forEach(dep => ignores.add(dep)); - installLegacyDeps(NODE_12_LEGACY_DEPENDENCIES); - es6ifyTestTSConfig('utils'); - break; - case '14': - NODE_14_SKIP_TESTS_PACKAGES.forEach(dep => ignores.add(dep)); - break; - } - - runWithIgnores(Array.from(ignores)); -} - -runTests(); +import * as childProcess from 'child_process'; +import * as fs from 'fs'; + +type NodeVersion = '8' | '10' | '12' | '14' | '16'; + +interface VersionConfig { + ignoredPackages: Array<`@${'sentry' | 'sentry-internal'}/${string}`>; + legacyDeps: Array<`${string}@${string}`>; + shouldES6Utils: boolean; +} + +const CURRENT_NODE_VERSION = process.version.replace('v', '').split('.')[0] as NodeVersion; + +const DEFAULT_SKIP_TESTS_PACKAGES = [ + '@sentry-internal/eslint-plugin-sdk', + '@sentry/ember', + '@sentry/browser', + '@sentry/vue', + '@sentry/react', + '@sentry/angular', + '@sentry/svelte', + '@sentry/replay', + '@sentry/wasm', + '@sentry/bun', +]; + +const SKIP_TEST_PACKAGES: Record = { + '8': { + ignoredPackages: [ + '@sentry/gatsby', + '@sentry/serverless', + '@sentry/nextjs', + '@sentry/remix', + '@sentry/sveltekit', + '@sentry-internal/replay-worker', + '@sentry/node-experimental', + '@sentry/vercel-edge', + ], + legacyDeps: [ + 'jsdom@15.x', + 'jest@25.x', + 'jest-environment-jsdom@25.x', + 'jest-environment-node@25.x', + 'ts-jest@25.x', + 'lerna@3.13.4', + ], + shouldES6Utils: true, + }, + '10': { + ignoredPackages: [ + '@sentry/remix', + '@sentry/sveltekit', + '@sentry-internal/replay-worker', + '@sentry/node-experimental', + '@sentry/vercel-edge', + ], + legacyDeps: ['jsdom@16.x', 'lerna@3.13.4'], + shouldES6Utils: true, + }, + '12': { + ignoredPackages: ['@sentry/remix', '@sentry/sveltekit', '@sentry/node-experimental', '@sentry/vercel-edge'], + legacyDeps: ['lerna@3.13.4'], + shouldES6Utils: true, + }, + '14': { + ignoredPackages: ['@sentry/sveltekit', '@sentry/vercel-edge'], + legacyDeps: [], + shouldES6Utils: false, + }, + '16': { + ignoredPackages: ['@sentry/vercel-edge'], + legacyDeps: [], + shouldES6Utils: false, + }, +}; + +type JSONValue = string | number | boolean | null | JSONArray | JSONObject; + +type JSONObject = { + [key: string]: JSONValue; +}; + +type JSONArray = Array; + +interface TSConfigJSON extends JSONObject { + compilerOptions: { lib: string[]; target: string }; +} + +/** + * Run the given shell command, piping the shell process's `stdin`, `stdout`, and `stderr` to that of the current + * process. Returns contents of `stdout`. + */ +function run(cmd: string, options?: childProcess.ExecSyncOptions): void { + childProcess.execSync(cmd, { stdio: 'inherit', ...options }); +} + +/** + * Install the given legacy dependencies, for compatibility with tests run in older versions of Node. + */ +function installLegacyDeps(legacyDeps: string[] = []): void { + // Ignoring engines and scripts lets us get away with having incompatible things installed for SDK packages we're not + // testing in the current node version, and ignoring the root check lets us install things at the repo root. + run(`yarn add --dev --ignore-engines --ignore-scripts --ignore-workspace-root-check ${legacyDeps.join(' ')}`); +} + +/** + * Modify a json file on disk. + * + * @param filepath The path to the file to be modified + * @param transformer A function which takes the JSON data as input and returns a mutated version. It may mutate the + * JSON data in place, but it isn't required to do so. + */ +export function modifyJSONFile(filepath: string, transformer: (json: T) => T): void { + const fileContents = fs + .readFileSync(filepath) + .toString() + // get rid of comments, which the `jsonc` format allows, but which will crash `JSON.parse` + .replace(/\/\/.*\n/g, ''); + const json = JSON.parse(fileContents); + const newJSON = transformer(json); + fs.writeFileSync(filepath, JSON.stringify(newJSON, null, 2)); +} + +const es6ifyTestTSConfig = (pkg: string): void => { + const filepath = `packages/${pkg}/tsconfig.test.json`; + const transformer = (tsconfig: TSConfigJSON): TSConfigJSON => { + tsconfig.compilerOptions.target = 'es6'; + return tsconfig; + }; + modifyJSONFile(filepath, transformer); +}; + +/** + * Skip tests which don't run in Node 8. + * We're forced to skip these tests for compatibility reasons. + */ +function skipNodeV8Tests(): void { + run('rm -rf packages/tracing/test/browser'); +} + +/** + * Run tests, ignoring the given packages + */ +function runWithIgnores(skipPackages: string[] = []): void { + const ignoreFlags = skipPackages.map(dep => `--ignore="${dep}"`).join(' '); + run(`yarn test ${ignoreFlags}`); +} + +/** + * Run the tests, accounting for compatibility problems in older versions of Node. + */ +function runTests(): void { + const ignores = new Set(); + + DEFAULT_SKIP_TESTS_PACKAGES.forEach(pkg => ignores.add(pkg)); + + if (CURRENT_NODE_VERSION === '8') { + skipNodeV8Tests(); + } + + const versionConfig = SKIP_TEST_PACKAGES[CURRENT_NODE_VERSION]; + if (versionConfig) { + versionConfig.ignoredPackages.forEach(dep => ignores.add(dep)); + if (versionConfig.legacyDeps.length > 0) { + installLegacyDeps(versionConfig.legacyDeps); + } + if (versionConfig.shouldES6Utils) { + es6ifyTestTSConfig('utils'); + } + } + + runWithIgnores(Array.from(ignores)); +} + +runTests(); diff --git a/yarn.lock b/yarn.lock index 8b412653caec..d283d72b7ad2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2281,6 +2281,41 @@ resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.3.tgz#90420f9f9c6d3987f176a19a7d8e764271a2f55d" integrity sha512-Fxt+AfXgjMoin2maPIYzFZnQjAXjAL0PHscM5pRTtatFqB+vZxAM9tLp2Optnuw3QOQC40jTNeGYFOMvyf7v9g== +"@edge-runtime/jest-environment@2.2.3": + version "2.2.3" + resolved "https://registry.yarnpkg.com/@edge-runtime/jest-environment/-/jest-environment-2.2.3.tgz#2fef094d769f45b5018b33bdc58e664b35bbc312" + integrity sha512-5DEv8nzuMFGoVbNYbOz7/mileYSbq1/oIvisyTeSfyjId7Pc5Qh2t3BH7ixLa62aVz7oCQlALM4cYGbZQZw1YQ== + dependencies: + "@edge-runtime/vm" "3.0.3" + "@jest/environment" "29.5.0" + "@jest/fake-timers" "29.5.0" + jest-mock "29.5.0" + jest-util "29.5.0" + +"@edge-runtime/primitives@3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@edge-runtime/primitives/-/primitives-3.0.3.tgz#adc6a4bd34c44faf81c954cf8e5816c7d408a1ea" + integrity sha512-YnfMWMRQABAH8IsnFMJWMW+SyB4ZeYBPnR7V0aqdnew7Pq60cbH5DyFjS/FhiLwvHQk9wBREmXD7PP0HooEQ1A== + +"@edge-runtime/primitives@4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@edge-runtime/primitives/-/primitives-4.0.1.tgz#12efffac1caa8a29ae8f86a3f87f20cc0ae07131" + integrity sha512-hxWUzx1SeyOed/Ea9Z6y6tyFKSj8gQWIdLilybTR2ene1IthLZE01A1SLGoch1szUdhFlUwpWDaYBYQw00lj2g== + +"@edge-runtime/types@2.2.3": + version "2.2.3" + resolved "https://registry.yarnpkg.com/@edge-runtime/types/-/types-2.2.3.tgz#cb57b7215bcf406324ec591346b7b51c75a54bdf" + integrity sha512-zL0ENQWwdocECEQXVopGTfnqI0tJ8wzDOCoQymoc8MLRz+Zw2V1W0ex9vczniTUzB+H/P7ubMgx3GFzLp3NPBg== + dependencies: + "@edge-runtime/primitives" "4.0.1" + +"@edge-runtime/vm@3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@edge-runtime/vm/-/vm-3.0.3.tgz#92f1930d1eb8d0ccf6a3c165561cc22b2d9ddff8" + integrity sha512-SPfI1JeIRNs/4EEE2Oc0X6gG3RqjD1TnKu2lwmwFXq0435xgZGKhc3UiKkYAdoMn2dNFD73nlabMKHBRoMRpxg== + dependencies: + "@edge-runtime/primitives" "3.0.3" + "@ember-data/rfc395-data@^0.0.4": version "0.0.4" resolved "https://registry.yarnpkg.com/@ember-data/rfc395-data/-/rfc395-data-0.0.4.tgz#ecb86efdf5d7733a76ff14ea651a1b0ed1f8a843" @@ -2999,6 +3034,16 @@ slash "^3.0.0" strip-ansi "^6.0.0" +"@jest/environment@29.5.0": + version "29.5.0" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-29.5.0.tgz#9152d56317c1fdb1af389c46640ba74ef0bb4c65" + integrity sha512-5FXw2+wD29YU1d4I2htpRX7jYnAyTRjP2CsXQdo9SAM8g3ifxWPSV0HnClSn71xwctr0U3oZIIH+dtbfmnbXVQ== + dependencies: + "@jest/fake-timers" "^29.5.0" + "@jest/types" "^29.5.0" + "@types/node" "*" + jest-mock "^29.5.0" + "@jest/environment@^27.5.1": version "27.5.1" resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-27.5.1.tgz#d7425820511fe7158abbecc010140c3fd3be9c74" @@ -3009,6 +3054,18 @@ "@types/node" "*" jest-mock "^27.5.1" +"@jest/fake-timers@29.5.0": + version "29.5.0" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-29.5.0.tgz#d4d09ec3286b3d90c60bdcd66ed28d35f1b4dc2c" + integrity sha512-9ARvuAAQcBwDAqOnglWq2zwNIRUDtk/SCkp/ToGEhFv5r86K21l+VEs0qNTaXtyiY0lEePl3kylijSYJQqdbDg== + dependencies: + "@jest/types" "^29.5.0" + "@sinonjs/fake-timers" "^10.0.2" + "@types/node" "*" + jest-message-util "^29.5.0" + jest-mock "^29.5.0" + jest-util "^29.5.0" + "@jest/fake-timers@^27.5.1": version "27.5.1" resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-27.5.1.tgz#76979745ce0579c8a94a4678af7a748eda8ada74" @@ -3021,6 +3078,18 @@ jest-mock "^27.5.1" jest-util "^27.5.1" +"@jest/fake-timers@^29.5.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-29.7.0.tgz#fd91bf1fffb16d7d0d24a426ab1a47a49881a565" + integrity sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ== + dependencies: + "@jest/types" "^29.6.3" + "@sinonjs/fake-timers" "^10.0.2" + "@types/node" "*" + jest-message-util "^29.7.0" + jest-mock "^29.7.0" + jest-util "^29.7.0" + "@jest/globals@^27.5.1": version "27.5.1" resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-27.5.1.tgz#7ac06ce57ab966566c7963431cef458434601b2b" @@ -3068,6 +3137,13 @@ dependencies: "@sinclair/typebox" "^0.25.16" +"@jest/schemas@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.3.tgz#430b5ce8a4e0044a7e3819663305a7b3091c8e03" + integrity sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA== + dependencies: + "@sinclair/typebox" "^0.27.8" + "@jest/source-map@^27.5.1": version "27.5.1" resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-27.5.1.tgz#6608391e465add4205eae073b55e7f279e04e8cf" @@ -3129,6 +3205,18 @@ "@types/yargs" "^16.0.0" chalk "^4.0.0" +"@jest/types@^29.5.0", "@jest/types@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.6.3.tgz#1131f8cf634e7e84c5e77bab12f052af585fba59" + integrity sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw== + dependencies: + "@jest/schemas" "^29.6.3" + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^17.0.8" + chalk "^4.0.0" + "@josephg/resolvable@^1.0.0": version "1.0.1" resolved "https://registry.yarnpkg.com/@josephg/resolvable/-/resolvable-1.0.1.tgz#69bc4db754d79e1a2f17a650d3466e038d94a5eb" @@ -4514,6 +4602,11 @@ resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.25.24.tgz#8c7688559979f7079aacaf31aa881c3aa410b718" integrity sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ== +"@sinclair/typebox@^0.27.8": + version "0.27.8" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" + integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== + "@sindresorhus/is@^0.14.0": version "0.14.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" @@ -5653,6 +5746,13 @@ dependencies: "@types/yargs-parser" "*" +"@types/yargs@^17.0.8": + version "17.0.24" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.24.tgz#b3ef8d50ad4aa6aecf6ddc97c580a00f5aa11902" + integrity sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw== + dependencies: + "@types/yargs-parser" "*" + "@types/yauzl@^2.9.1": version "2.10.0" resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.0.tgz#b3248295276cf8c6f153ebe6a9aba0c988cb2599" @@ -17055,6 +17155,30 @@ jest-message-util@^27.5.1: slash "^3.0.0" stack-utils "^2.0.3" +jest-message-util@^29.5.0, jest-message-util@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.7.0.tgz#8bc392e204e95dfe7564abbe72a404e28e51f7f3" + integrity sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w== + dependencies: + "@babel/code-frame" "^7.12.13" + "@jest/types" "^29.6.3" + "@types/stack-utils" "^2.0.0" + chalk "^4.0.0" + graceful-fs "^4.2.9" + micromatch "^4.0.4" + pretty-format "^29.7.0" + slash "^3.0.0" + stack-utils "^2.0.3" + +jest-mock@29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.5.0.tgz#26e2172bcc71d8b0195081ff1f146ac7e1518aed" + integrity sha512-GqOzvdWDE4fAV2bWQLQCkujxYWL7RxjCnj71b5VhDAGOevB3qj3Ovg26A5NI84ZpODxyzaozXLOh2NCgkbvyaw== + dependencies: + "@jest/types" "^29.5.0" + "@types/node" "*" + jest-util "^29.5.0" + jest-mock@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-27.5.1.tgz#19948336d49ef4d9c52021d34ac7b5f36ff967d6" @@ -17063,6 +17187,15 @@ jest-mock@^27.5.1: "@jest/types" "^27.5.1" "@types/node" "*" +jest-mock@^29.5.0, jest-mock@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.7.0.tgz#4e836cf60e99c6fcfabe9f99d017f3fdd50a6347" + integrity sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw== + dependencies: + "@jest/types" "^29.6.3" + "@types/node" "*" + jest-util "^29.7.0" + jest-pnp-resolver@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz#b704ac0ae028a89108a4d040b3f919dfddc8e33c" @@ -17189,6 +17322,18 @@ jest-snapshot@^27.5.1: pretty-format "^27.5.1" semver "^7.3.2" +jest-util@29.5.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.5.0.tgz#24a4d3d92fc39ce90425311b23c27a6e0ef16b8f" + integrity sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ== + dependencies: + "@jest/types" "^29.5.0" + "@types/node" "*" + chalk "^4.0.0" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" + jest-util@^27.0.0, jest-util@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-27.5.1.tgz#3ba9771e8e31a0b85da48fe0b0891fb86c01c2f9" @@ -17201,6 +17346,18 @@ jest-util@^27.0.0, jest-util@^27.5.1: graceful-fs "^4.2.9" picomatch "^2.2.3" +jest-util@^29.5.0, jest-util@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.7.0.tgz#23c2b62bfb22be82b44de98055802ff3710fc0bc" + integrity sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA== + dependencies: + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" + jest-validate@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-27.5.1.tgz#9197d54dc0bdb52260b8db40b46ae668e04df067" @@ -23002,6 +23159,15 @@ pretty-format@^29.5.0: ansi-styles "^5.0.0" react-is "^18.0.0" +pretty-format@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812" + integrity sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ== + dependencies: + "@jest/schemas" "^29.6.3" + ansi-styles "^5.0.0" + react-is "^18.0.0" + pretty-ms@^7.0.0: version "7.0.1" resolved "https://registry.yarnpkg.com/pretty-ms/-/pretty-ms-7.0.1.tgz#7d903eaab281f7d8e03c66f867e239dc32fb73e8"