From 6cfd7305500593c8bad24755ca4d2e568d4d6e89 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 5 Jan 2023 09:22:02 +0100 Subject: [PATCH 001/113] feat(core): Add `replay_event` type for events (#6481) --- packages/core/src/envelope.ts | 10 +++++++++- packages/core/src/hub.ts | 2 +- packages/hub/test/hub.test.ts | 7 ++++--- packages/replay/src/coreHandlers/handleGlobalEvent.ts | 7 ++----- packages/types/src/datacategory.ts | 2 ++ packages/types/src/event.ts | 2 +- packages/types/src/index.ts | 2 +- 7 files changed, 20 insertions(+), 12 deletions(-) diff --git a/packages/core/src/envelope.ts b/packages/core/src/envelope.ts index 1a2f44fd82c6..2ae474dd99b0 100644 --- a/packages/core/src/envelope.ts +++ b/packages/core/src/envelope.ts @@ -63,7 +63,15 @@ export function createEventEnvelope( tunnel?: string, ): EventEnvelope { const sdkInfo = getSdkMetadataForEnvelopeHeader(metadata); - const eventType = event.type || 'event'; + + /* + Note: Due to TS, event.type may be `replay_event`, theoretically. + In practice, we never call `createEventEnvelope` with `replay_event` type, + and we'd have to adjut a looot of types to make this work properly. + We want to avoid casting this around, as that could lead to bugs (e.g. when we add another type) + So the safe choice is to really guard against the replay_event type here. + */ + const eventType = event.type && event.type !== 'replay_event' ? event.type : 'event'; enhanceEventWithSdkInfo(event, metadata && metadata.sdk); diff --git a/packages/core/src/hub.ts b/packages/core/src/hub.ts index 5036158e08eb..e5f161559d39 100644 --- a/packages/core/src/hub.ts +++ b/packages/core/src/hub.ts @@ -233,7 +233,7 @@ export class Hub implements HubInterface { */ public captureEvent(event: Event, hint?: EventHint): string { const eventId = hint && hint.event_id ? hint.event_id : uuid4(); - if (event.type !== 'transaction') { + if (!event.type) { this._lastEventId = eventId; } diff --git a/packages/hub/test/hub.test.ts b/packages/hub/test/hub.test.ts index 5ce521ead4c4..c97670c0e7cb 100644 --- a/packages/hub/test/hub.test.ts +++ b/packages/hub/test/hub.test.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/unbound-method */ /* eslint-disable deprecation/deprecation */ -import { Client, Event } from '@sentry/types'; +import { Client, Event, EventType } from '@sentry/types'; import { getCurrentHub, Hub, Scope } from '../src'; @@ -358,10 +358,11 @@ describe('Hub', () => { expect(args[1].event_id).toEqual(hub.lastEventId()); }); - test('transactions do not set lastEventId', () => { + const eventTypesToIgnoreLastEventId: EventType[] = ['transaction', 'replay_event']; + it.each(eventTypesToIgnoreLastEventId)('eventType %s does not set lastEventId', eventType => { const event: Event = { extra: { b: 3 }, - type: 'transaction', + type: eventType, }; const testClient = makeClient(); const hub = new Hub(testClient); diff --git a/packages/replay/src/coreHandlers/handleGlobalEvent.ts b/packages/replay/src/coreHandlers/handleGlobalEvent.ts index 87dc6c790fad..59a286c3a770 100644 --- a/packages/replay/src/coreHandlers/handleGlobalEvent.ts +++ b/packages/replay/src/coreHandlers/handleGlobalEvent.ts @@ -12,10 +12,7 @@ import { isRrwebError } from '../util/isRrwebError'; export function handleGlobalEventListener(replay: ReplayContainer): (event: Event) => Event | null { return (event: Event) => { // Do not apply replayId to the root event - if ( - // @ts-ignore new event type - event.type === REPLAY_EVENT_NAME - ) { + if (event.type === REPLAY_EVENT_NAME) { // Replays have separate set of breadcrumbs, do not include breadcrumbs // from core SDK delete event.breadcrumbs; @@ -31,7 +28,7 @@ export function handleGlobalEventListener(replay: ReplayContainer): (event: Even // Only tag transactions with replayId if not waiting for an error // @ts-ignore private - if (event.type !== 'transaction' || replay.recordingMode === 'session') { + if (!event.type || replay.recordingMode === 'session') { event.tags = { ...event.tags, replayId: replay.session?.id }; } diff --git a/packages/types/src/datacategory.ts b/packages/types/src/datacategory.ts index a462957b3ccd..0968cefc752b 100644 --- a/packages/types/src/datacategory.ts +++ b/packages/types/src/datacategory.ts @@ -10,6 +10,8 @@ export type DataCategory = | 'error' // Transaction type event | 'transaction' + // Replay type event + | 'replay_event' // Events with `event_type` csp, hpkp, expectct, expectstaple | 'security' // Attachment bytes stored (unused for rate limiting diff --git a/packages/types/src/event.ts b/packages/types/src/event.ts index 3c8b27c49f10..e9df8d364c68 100644 --- a/packages/types/src/event.ts +++ b/packages/types/src/event.ts @@ -63,7 +63,7 @@ export interface Event { * Note that `ErrorEvent`s do not have a type (hence its undefined), * while all other events are required to have one. */ -export type EventType = 'transaction' | 'profile' | undefined; +export type EventType = 'transaction' | 'profile' | 'replay_event' | undefined; export interface ErrorEvent extends Event { type: undefined; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 56d5379f9e34..37857ce12155 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -25,7 +25,7 @@ export type { UserFeedbackItem, } from './envelope'; export type { ExtendedError } from './error'; -export type { Event, EventHint, ErrorEvent, TransactionEvent } from './event'; +export type { Event, EventHint, EventType, ErrorEvent, TransactionEvent } from './event'; export type { EventProcessor } from './eventprocessor'; export type { Exception } from './exception'; export type { Extra, Extras } from './extra'; From 55d708f986e7e27f4bfbfbd53583e1fa22c3b198 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 5 Jan 2023 10:01:00 +0100 Subject: [PATCH 002/113] feat(core): Add `addIntegration` method to client (#6651) --- packages/core/src/baseclient.ts | 9 ++++++++- packages/core/src/integration.ts | 19 ++++++++++++------- packages/types/src/client.ts | 10 ++++++++++ packages/types/src/options.ts | 4 ++-- 4 files changed, 32 insertions(+), 10 deletions(-) diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index c343c2249a6e..eb674a46ab99 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -37,7 +37,7 @@ import { import { getEnvelopeEndpointWithUrlEncodedAuth } from './api'; import { createEventEnvelope, createSessionEnvelope } from './envelope'; -import { IntegrationIndex, setupIntegrations } from './integration'; +import { IntegrationIndex, setupIntegration, setupIntegrations } from './integration'; import { Scope } from './scope'; import { updateSession } from './session'; import { prepareEvent } from './utils/prepareEvent'; @@ -291,6 +291,13 @@ export abstract class BaseClient implements Client { } } + /** + * @inheritDoc + */ + public addIntegration(integration: Integration): void { + setupIntegration(integration, this._integrations); + } + /** * @inheritDoc */ diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts index e80b2a0843fe..b8c9ef16da6d 100644 --- a/packages/core/src/integration.ts +++ b/packages/core/src/integration.ts @@ -88,14 +88,19 @@ export function setupIntegrations(integrations: Integration[]): IntegrationIndex const integrationIndex: IntegrationIndex = {}; integrations.forEach(integration => { - integrationIndex[integration.name] = integration; - - if (installedIntegrations.indexOf(integration.name) === -1) { - integration.setupOnce(addGlobalEventProcessor, getCurrentHub); - installedIntegrations.push(integration.name); - __DEBUG_BUILD__ && logger.log(`Integration installed: ${integration.name}`); - } + setupIntegration(integration, integrationIndex); }); return integrationIndex; } + +/** Setup a single integration. */ +export function setupIntegration(integration: Integration, integrationIndex: IntegrationIndex): void { + integrationIndex[integration.name] = integration; + + if (installedIntegrations.indexOf(integration.name) === -1) { + integration.setupOnce(addGlobalEventProcessor, getCurrentHub); + installedIntegrations.push(integration.name); + __DEBUG_BUILD__ && logger.log(`Integration installed: ${integration.name}`); + } +} diff --git a/packages/types/src/client.ts b/packages/types/src/client.ts index 6dee7e5c9ebb..00fe9f5191ed 100644 --- a/packages/types/src/client.ts +++ b/packages/types/src/client.ts @@ -108,6 +108,16 @@ export interface Client { /** Returns the client's instance of the given integration class, it any. */ getIntegration(integration: IntegrationClass): T | null; + /** + * Add an integration to the client. + * This can be used to e.g. lazy load integrations. + * In most cases, this should not be necessary, and you're better off just passing the integrations via `integrations: []` at initialization time. + * However, if you find the need to conditionally load & add an integration, you can use `addIntegration` to do so. + * + * TODO (v8): Make this a required method. + * */ + addIntegration?(integration: Integration): void; + /** This is an internal function to setup all integrations that should run on the client */ setupIntegrations(): void; diff --git a/packages/types/src/options.ts b/packages/types/src/options.ts index f8f8f461543b..8e6a8465d298 100644 --- a/packages/types/src/options.ts +++ b/packages/types/src/options.ts @@ -222,7 +222,7 @@ export interface ClientOptions number | boolean; - // TODO v8: Narrow the response type to `ErrorEvent` - this is technically a breaking change. + // TODO (v8): Narrow the response type to `ErrorEvent` - this is technically a breaking change. /** * An event-processing callback for error and message events, guaranteed to be invoked after all other event * processors, which allows an event to be modified or dropped. @@ -236,7 +236,7 @@ export interface ClientOptions PromiseLike | Event | null; - // TODO v8: Narrow the response type to `TransactionEvent` - this is technically a breaking change. + // TODO (v8): Narrow the response type to `TransactionEvent` - this is technically a breaking change. /** * An event-processing callback for transaction events, guaranteed to be invoked after all other event * processors. This allows an event to be modified or dropped before it's sent. From 0f9a3dfb864e8b0c57daac7dbda333abee20d04d Mon Sep 17 00:00:00 2001 From: Richard Date: Thu, 5 Jan 2023 03:04:57 -0700 Subject: [PATCH 003/113] feat(node): Check for invalid url in node transport (#6623) Co-authored-by: Luca Forstner --- packages/node/src/transports/http.ts | 13 ++++++++++++- packages/node/test/transports/http.test.ts | 16 ++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/packages/node/src/transports/http.ts b/packages/node/src/transports/http.ts index 687325b776f4..4bc5c696e05a 100644 --- a/packages/node/src/transports/http.ts +++ b/packages/node/src/transports/http.ts @@ -47,7 +47,18 @@ function streamFromBody(body: Uint8Array | string): Readable { * Creates a Transport that uses native the native 'http' and 'https' modules to send events to Sentry. */ export function makeNodeTransport(options: NodeTransportOptions): Transport { - const urlSegments = new URL(options.url); + let urlSegments: URL; + + try { + urlSegments = new URL(options.url); + } catch (e) { + // eslint-disable-next-line no-console + console.warn( + '[@sentry/node]: Invalid dsn or tunnel option, will not send any events. The tunnel option must be a full URL when used.', + ); + return createTransport(options, () => Promise.resolve({})); + } + const isHttps = urlSegments.protocol === 'https:'; // Proxy prioritization: http => `options.proxy` | `process.env.http_proxy` diff --git a/packages/node/test/transports/http.test.ts b/packages/node/test/transports/http.test.ts index 9594940c2014..0bf61118b9e9 100644 --- a/packages/node/test/transports/http.test.ts +++ b/packages/node/test/transports/http.test.ts @@ -399,4 +399,20 @@ describe('makeNewHttpTransport()', () => { ); }); }); + + it('should create a noop transport if an invalid url is passed', async () => { + const requestSpy = jest.spyOn(http, 'request'); + const transport = makeNodeTransport({ ...defaultOptions, url: 'foo' }); + await transport.send(EVENT_ENVELOPE); + expect(requestSpy).not.toHaveBeenCalled(); + }); + + it('should warn if an invalid url is passed', async () => { + const consoleWarnSpy = jest.spyOn(console, 'warn'); + const transport = makeNodeTransport({ ...defaultOptions, url: 'invalid url' }); + await transport.send(EVENT_ENVELOPE); + expect(consoleWarnSpy).toHaveBeenCalledWith( + '[@sentry/node]: Invalid dsn or tunnel option, will not send any events. The tunnel option must be a full URL when used.', + ); + }); }); From 92e951ff6d19d546f80317e4247c96b9692f513e Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 5 Jan 2023 11:33:59 +0100 Subject: [PATCH 004/113] build: Update @typescript-eslint packages & fix resulting linting issues (#6657) --- packages/browser/.eslintignore | 1 + packages/browser/src/helpers.ts | 2 +- packages/browser/src/sdk.ts | 1 - packages/browser/test/unit/sdk.test.ts | 5 +- packages/core/src/hub.ts | 1 - packages/eslint-config-sdk/package.json | 4 +- packages/nextjs/src/config/wrappers/types.ts | 4 +- packages/node/src/handlers.ts | 3 +- .../src/integrations/onuncaughtexception.ts | 4 +- packages/node/src/transports/http.ts | 3 +- packages/node/src/types.ts | 4 +- packages/react/src/redux.ts | 1 - packages/replay/src/eventBuffer.ts | 20 +-- packages/replay/src/integration.ts | 16 +-- packages/replay/src/util/debounce.ts | 2 +- .../tracing/src/browser/browsertracing.ts | 5 +- packages/tracing/src/browser/request.ts | 2 +- packages/types/src/envelope.ts | 2 +- packages/types/src/eventprocessor.ts | 2 +- packages/utils/src/object.ts | 4 +- yarn.lock | 131 ++++++++++++++---- 21 files changed, 141 insertions(+), 76 deletions(-) create mode 100644 packages/browser/.eslintignore diff --git a/packages/browser/.eslintignore b/packages/browser/.eslintignore new file mode 100644 index 000000000000..81c6b54e0be2 --- /dev/null +++ b/packages/browser/.eslintignore @@ -0,0 +1 @@ +tmp.js diff --git a/packages/browser/src/helpers.ts b/packages/browser/src/helpers.ts index 68a1d25f00ce..7b56e30fa829 100644 --- a/packages/browser/src/helpers.ts +++ b/packages/browser/src/helpers.ts @@ -179,5 +179,5 @@ export interface ReportDialogOptions { errorFormEntry?: string; successMessage?: string; /** Callback after reportDialog showed up */ - onLoad?(): void; + onLoad?(this: void): void; } diff --git a/packages/browser/src/sdk.ts b/packages/browser/src/sdk.ts index f4c05482b4a9..9b10074f8b51 100644 --- a/packages/browser/src/sdk.ts +++ b/packages/browser/src/sdk.ts @@ -164,7 +164,6 @@ export function showReportDialog(options: ReportDialogOptions = {}, hub: Hub = g script.src = getReportDialogEndpoint(dsn, options); if (options.onLoad) { - // eslint-disable-next-line @typescript-eslint/unbound-method script.onload = options.onLoad; } diff --git a/packages/browser/test/unit/sdk.test.ts b/packages/browser/test/unit/sdk.test.ts index 5d03d26d3312..facf8f9f4aae 100644 --- a/packages/browser/test/unit/sdk.test.ts +++ b/packages/browser/test/unit/sdk.test.ts @@ -1,14 +1,11 @@ /* eslint-disable @typescript-eslint/unbound-method */ -import { Scope } from '@sentry/core'; -import { createTransport } from '@sentry/core'; +import { createTransport, Scope } from '@sentry/core'; import { MockIntegration } from '@sentry/core/test/lib/sdk.test'; import { Client, Integration } from '@sentry/types'; import { resolvedSyncPromise } from '@sentry/utils'; import { BrowserOptions } from '../../src'; import { init } from '../../src/sdk'; -// eslint-disable-next-line no-var -declare var global: any; const PUBLIC_DSN = 'https://username@domain/123'; diff --git a/packages/core/src/hub.ts b/packages/core/src/hub.ts index e5f161559d39..aa3a8015c0a0 100644 --- a/packages/core/src/hub.ts +++ b/packages/core/src/hub.ts @@ -258,7 +258,6 @@ export class Hub implements HubInterface { if (!scope || !client) return; - // eslint-disable-next-line @typescript-eslint/unbound-method const { beforeBreadcrumb = null, maxBreadcrumbs = DEFAULT_BREADCRUMBS } = (client.getOptions && client.getOptions()) || {}; diff --git a/packages/eslint-config-sdk/package.json b/packages/eslint-config-sdk/package.json index 4b117f149d0e..a03cc033bb9d 100644 --- a/packages/eslint-config-sdk/package.json +++ b/packages/eslint-config-sdk/package.json @@ -21,8 +21,8 @@ "dependencies": { "@sentry-internal/eslint-plugin-sdk": "7.29.0", "@sentry-internal/typescript": "7.29.0", - "@typescript-eslint/eslint-plugin": "^3.9.0", - "@typescript-eslint/parser": "^3.9.0", + "@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-import": "^2.22.0", diff --git a/packages/nextjs/src/config/wrappers/types.ts b/packages/nextjs/src/config/wrappers/types.ts index 16197e4cedc7..7ad042cc2474 100644 --- a/packages/nextjs/src/config/wrappers/types.ts +++ b/packages/nextjs/src/config/wrappers/types.ts @@ -23,14 +23,14 @@ import type { NextApiRequest, NextApiResponse } from 'next'; // wrapped route from being wrapped again by the auto-wrapper. export type NextApiHandler = { - __sentry_route__?: string; (req: NextApiRequest, res: NextApiResponse): void | Promise | unknown | Promise; + __sentry_route__?: string; }; export type WrappedNextApiHandler = { + (req: NextApiRequest, res: NextApiResponse): Promise | Promise; __sentry_route__?: string; __sentry_wrapped__?: boolean; - (req: NextApiRequest, res: NextApiResponse): Promise | Promise; }; export type AugmentedNextApiRequest = NextApiRequest & { diff --git a/packages/node/src/handlers.ts b/packages/node/src/handlers.ts index 7433ad8720dd..a5e5ff3b6f11 100644 --- a/packages/node/src/handlers.ts +++ b/packages/node/src/handlers.ts @@ -256,7 +256,7 @@ export function errorHandler(options?: { * Callback method deciding whether error should be captured and sent to Sentry * @param error Captured middleware error */ - shouldHandleError?(error: MiddlewareError): boolean; + shouldHandleError?(this: void, error: MiddlewareError): boolean; }): ( error: MiddlewareError, req: http.IncomingMessage, @@ -269,7 +269,6 @@ export function errorHandler(options?: { res: http.ServerResponse, next: (error: MiddlewareError) => void, ): void { - // eslint-disable-next-line @typescript-eslint/unbound-method const shouldHandleError = (options && options.shouldHandleError) || defaultShouldHandleError; if (shouldHandleError(error)) { diff --git a/packages/node/src/integrations/onuncaughtexception.ts b/packages/node/src/integrations/onuncaughtexception.ts index 2108e2829f63..e97796a3a33c 100644 --- a/packages/node/src/integrations/onuncaughtexception.ts +++ b/packages/node/src/integrations/onuncaughtexception.ts @@ -29,7 +29,7 @@ interface OnUncaughtExceptionOptions { * `onFatalError` itself threw, or because an independent error happened somewhere else while `onFatalError` * was running. */ - onFatalError?(firstError: Error, secondError?: Error): void; + onFatalError?(this: void, firstError: Error, secondError?: Error): void; } /** Global Exception handler */ @@ -84,10 +84,8 @@ export class OnUncaughtException implements Integration { const client = getCurrentHub().getClient(); if (this._options.onFatalError) { - // eslint-disable-next-line @typescript-eslint/unbound-method onFatalError = this._options.onFatalError; } else if (client && client.getOptions().onFatalError) { - // eslint-disable-next-line @typescript-eslint/unbound-method onFatalError = client.getOptions().onFatalError as OnFatalErrorHandler; } diff --git a/packages/node/src/transports/http.ts b/packages/node/src/transports/http.ts index 4bc5c696e05a..8116f194cd6b 100644 --- a/packages/node/src/transports/http.ts +++ b/packages/node/src/transports/http.ts @@ -74,7 +74,8 @@ export function makeNodeTransport(options: NodeTransportOptions): Transport { // TODO(v7): Evaluate if we can set keepAlive to true. This would involve testing for memory leaks in older node // versions(>= 8) as they had memory leaks when using it: #2555 const agent = proxy - ? (new (require('https-proxy-agent'))(proxy) as http.Agent) + ? // eslint-disable-next-line @typescript-eslint/no-var-requires + (new (require('https-proxy-agent'))(proxy) as http.Agent) : new nativeHttpModule.Agent({ keepAlive, maxSockets: 30, timeout: 2000 }); const requestExecutor = createRequestExecutor(options, options.httpModule ?? nativeHttpModule, agent); diff --git a/packages/node/src/types.ts b/packages/node/src/types.ts index d4ca5dd4900d..7b71d7bab988 100644 --- a/packages/node/src/types.ts +++ b/packages/node/src/types.ts @@ -45,10 +45,10 @@ export interface BaseNodeOptions { * }); * ``` */ - shouldCreateSpanForRequest?(url: string): boolean; + shouldCreateSpanForRequest?(this: void, url: string): boolean; /** Callback that is executed when a fatal global error occurs. */ - onFatalError?(error: Error): void; + onFatalError?(this: void, error: Error): void; } /** diff --git a/packages/react/src/redux.ts b/packages/react/src/redux.ts index d0751b4f43c3..35f5348c5e08 100644 --- a/packages/react/src/redux.ts +++ b/packages/react/src/redux.ts @@ -111,7 +111,6 @@ function createReduxEnhancer(enhancerOptions?: Partial): } /* Allow user to configure scope with latest state */ - // eslint-disable-next-line @typescript-eslint/unbound-method const { configureScopeWithState } = options; if (typeof configureScopeWithState === 'function') { configureScopeWithState(scope, newState); diff --git a/packages/replay/src/eventBuffer.ts b/packages/replay/src/eventBuffer.ts index 1d670506de67..f5fbfb2497ff 100644 --- a/packages/replay/src/eventBuffer.ts +++ b/packages/replay/src/eventBuffer.ts @@ -43,14 +43,14 @@ class EventBufferArray implements EventBuffer { this._events = []; } - public destroy(): void { - this._events = []; - } - public get length(): number { return this._events.length; } + public destroy(): void { + this._events = []; + } + public addEvent(event: RecordingEvent, isCheckout?: boolean): void { if (isCheckout) { this._events = [event]; @@ -82,12 +82,6 @@ export class EventBufferCompressionWorker implements EventBuffer { this._worker = worker; } - public destroy(): void { - __DEBUG_BUILD__ && logger.log('[Replay] Destroying compression worker'); - this._worker?.terminate(); - this._worker = null; - } - /** * Note that this may not reflect what is actually in the event buffer. This * is only a local count of the buffer size since `addEvent` is async. @@ -96,6 +90,12 @@ export class EventBufferCompressionWorker implements EventBuffer { return this._eventBufferItemLength; } + public destroy(): void { + __DEBUG_BUILD__ && logger.log('[Replay] Destroying compression worker'); + this._worker?.terminate(); + this._worker = null; + } + public async addEvent(event: RecordingEvent, isCheckout?: boolean): Promise { if (isCheckout) { // This event is a checkout, make sure worker buffer is cleared before diff --git a/packages/replay/src/integration.ts b/packages/replay/src/integration.ts index 34862682b325..d5fe1275f7bd 100644 --- a/packages/replay/src/integration.ts +++ b/packages/replay/src/integration.ts @@ -36,14 +36,6 @@ export class Replay implements Integration { readonly options: ReplayPluginOptions; - protected get _isInitialized(): boolean { - return _initialized; - } - - protected set _isInitialized(value: boolean) { - _initialized = value; - } - private _replay?: ReplayContainer; constructor({ @@ -134,6 +126,14 @@ Sentry.init({ replaysOnErrorSampleRate: ${errorSampleRate} })`, this._isInitialized = true; } + protected get _isInitialized(): boolean { + return _initialized; + } + + protected set _isInitialized(value: boolean) { + _initialized = value; + } + /** * We previously used to create a transaction in `setupOnce` and it would * potentially create a transaction before some native SDK integrations have run diff --git a/packages/replay/src/util/debounce.ts b/packages/replay/src/util/debounce.ts index fafd541fa98b..88057c14a4c5 100644 --- a/packages/replay/src/util/debounce.ts +++ b/packages/replay/src/util/debounce.ts @@ -1,7 +1,7 @@ type DebouncedCallback = { + (): void | unknown; flush: () => void | unknown; cancel: () => void; - (): void | unknown; }; type CallbackFunction = () => unknown; type DebounceOptions = { maxWait?: number }; diff --git a/packages/tracing/src/browser/browsertracing.ts b/packages/tracing/src/browser/browsertracing.ts index 1207ab8be9de..c9ca077fb179 100644 --- a/packages/tracing/src/browser/browsertracing.ts +++ b/packages/tracing/src/browser/browsertracing.ts @@ -109,13 +109,14 @@ export interface BrowserTracingOptions extends RequestInstrumentationOptions { * * @returns A (potentially) modified context object, with `sampled = false` if the transaction should be dropped. */ - beforeNavigate?(context: TransactionContext): TransactionContext | undefined; + beforeNavigate?(this: void, context: TransactionContext): TransactionContext | undefined; /** * Instrumentation that creates routing change transactions. By default creates * pageload and navigation transactions. */ routingInstrumentation( + this: void, customStartTransaction: (context: TransactionContext) => T | undefined, startTransactionOnPageLoad?: boolean, startTransactionOnLocationChange?: boolean, @@ -187,7 +188,6 @@ export class BrowserTracing implements Integration { public setupOnce(_: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { this._getCurrentHub = getCurrentHub; - // eslint-disable-next-line @typescript-eslint/unbound-method const { routingInstrumentation: instrumentRouting, startTransactionOnLocationChange, @@ -230,7 +230,6 @@ export class BrowserTracing implements Integration { return undefined; } - // eslint-disable-next-line @typescript-eslint/unbound-method const { beforeNavigate, idleTimeout, finalTimeout, heartbeatInterval } = this.options; const isPageloadTransaction = context.op === 'pageload'; diff --git a/packages/tracing/src/browser/request.ts b/packages/tracing/src/browser/request.ts index dbd041619699..ca6c15b0e96d 100644 --- a/packages/tracing/src/browser/request.ts +++ b/packages/tracing/src/browser/request.ts @@ -49,7 +49,7 @@ export interface RequestInstrumentationOptions { * * Default: (url: string) => true */ - shouldCreateSpanForRequest?(url: string): boolean; + shouldCreateSpanForRequest?(this: void, url: string): boolean; } /** Data returned from fetch callback */ diff --git a/packages/types/src/envelope.ts b/packages/types/src/envelope.ts index 74cb2764b7e3..75c18498d5f1 100644 --- a/packages/types/src/envelope.ts +++ b/packages/types/src/envelope.ts @@ -44,7 +44,7 @@ export type BaseEnvelopeItemHeaders = { length?: number; }; -type BaseEnvelopeItem = [ItemHeader & BaseEnvelopeItemHeaders, P]; // P is for payload +type BaseEnvelopeItem = [ItemHeader & BaseEnvelopeItemHeaders, P]; // P is for payload type BaseEnvelope = [ EnvelopeHeader & BaseEnvelopeHeaders, diff --git a/packages/types/src/eventprocessor.ts b/packages/types/src/eventprocessor.ts index 5be90a755367..543c0163ea5f 100644 --- a/packages/types/src/eventprocessor.ts +++ b/packages/types/src/eventprocessor.ts @@ -7,6 +7,6 @@ import { Event, EventHint } from './event'; * Event processing will be deferred until your Promise is resolved. */ export interface EventProcessor { - id?: string; // This field can't be named "name" because functions already have this field natively (event: Event, hint: EventHint): PromiseLike | Event | null; + id?: string; // This field can't be named "name" because functions already have this field natively } diff --git a/packages/utils/src/object.ts b/packages/utils/src/object.ts index 5b789b29060d..1e02d1fd163e 100644 --- a/packages/utils/src/object.ts +++ b/packages/utils/src/object.ts @@ -99,9 +99,7 @@ export function urlEncode(object: { [key: string]: any }): string { * @returns An Event or Error turned into an object - or the value argurment itself, when value is neither an Event nor * an Error. */ -export function convertToPlainObject( - value: V, -): +export function convertToPlainObject(value: V): | { [ownProps: string]: unknown; type: string; diff --git a/yarn.lock b/yarn.lock index 94f6c6af9482..d1bfebbebeba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4055,11 +4055,6 @@ "@types/eslint" "*" "@types/estree" "*" -"@types/eslint-visitor-keys@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" - integrity sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag== - "@types/eslint@*": version "8.2.1" resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.2.1.tgz#13f3d69bac93c2ae008019c28783868d0a1d6605" @@ -4464,6 +4459,11 @@ resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.1.tgz#18845205e86ff0038517aab7a18a62a6b9f71275" integrity sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA== +"@types/semver@^7.3.12": + version "7.3.13" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.13.tgz#da4bfd73f49bd541d28920ab0e2bf0ee80f71c91" + integrity sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw== + "@types/semver@^7.3.9": version "7.3.9" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.9.tgz#152c6c20a7688c30b967ec1841d31ace569863fc" @@ -4592,19 +4592,22 @@ dependencies: "@types/node" "*" -"@typescript-eslint/eslint-plugin@^3.9.0": - version "3.10.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.10.1.tgz#7e061338a1383f59edc204c605899f93dc2e2c8f" - integrity sha512-PQg0emRtzZFWq6PxBcdxRH3QIQiyFO3WCVpRL3fgj5oQS3CDs3AeAKfv4DxNhzn8ITdNJGJ4D3Qw8eAJf3lXeQ== +"@typescript-eslint/eslint-plugin@^5.48.0": + version "5.48.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.48.0.tgz#54f8368d080eb384a455f60c2ee044e948a8ce67" + integrity sha512-SVLafp0NXpoJY7ut6VFVUU9I+YeFsDzeQwtK0WZ+xbRN3mtxJ08je+6Oi2N89qDn087COdO0u3blKZNv9VetRQ== dependencies: - "@typescript-eslint/experimental-utils" "3.10.1" - debug "^4.1.1" - functional-red-black-tree "^1.0.1" - regexpp "^3.0.0" - semver "^7.3.2" - tsutils "^3.17.1" + "@typescript-eslint/scope-manager" "5.48.0" + "@typescript-eslint/type-utils" "5.48.0" + "@typescript-eslint/utils" "5.48.0" + debug "^4.3.4" + ignore "^5.2.0" + natural-compare-lite "^1.4.0" + regexpp "^3.2.0" + semver "^7.3.7" + tsutils "^3.21.0" -"@typescript-eslint/experimental-utils@3.10.1", "@typescript-eslint/experimental-utils@^2.19.2 || ^3.0.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== @@ -4615,16 +4618,33 @@ eslint-scope "^5.0.0" eslint-utils "^2.0.0" -"@typescript-eslint/parser@^3.9.0": - version "3.10.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-3.10.1.tgz#1883858e83e8b442627e1ac6f408925211155467" - integrity sha512-Ug1RcWcrJP02hmtaXVS3axPPTTPnZjupqhgj+NnZ6BCkwSImWk/283347+x9wN+lqOdK9Eo3vsyiyDHgsmiEJw== +"@typescript-eslint/parser@^5.48.0": + version "5.48.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.48.0.tgz#02803355b23884a83e543755349809a50b7ed9ba" + integrity sha512-1mxNA8qfgxX8kBvRDIHEzrRGrKHQfQlbW6iHyfHYS0Q4X1af+S6mkLNtgCOsGVl8+/LUPrqdHMssAemkrQ01qg== dependencies: - "@types/eslint-visitor-keys" "^1.0.0" - "@typescript-eslint/experimental-utils" "3.10.1" - "@typescript-eslint/types" "3.10.1" - "@typescript-eslint/typescript-estree" "3.10.1" - eslint-visitor-keys "^1.1.0" + "@typescript-eslint/scope-manager" "5.48.0" + "@typescript-eslint/types" "5.48.0" + "@typescript-eslint/typescript-estree" "5.48.0" + debug "^4.3.4" + +"@typescript-eslint/scope-manager@5.48.0": + version "5.48.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.48.0.tgz#607731cb0957fbc52fd754fd79507d1b6659cecf" + integrity sha512-0AA4LviDtVtZqlyUQnZMVHydDATpD9SAX/RC5qh6cBd3xmyWvmXYF+WT1oOmxkeMnWDlUVTwdODeucUnjz3gow== + dependencies: + "@typescript-eslint/types" "5.48.0" + "@typescript-eslint/visitor-keys" "5.48.0" + +"@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" + integrity sha512-vbtPO5sJyFjtHkGlGK4Sthmta0Bbls4Onv0bEqOGm7hP9h8UpRsHJwsrCiWtCUndTRNQO/qe6Ijz9rnT/DB+7g== + dependencies: + "@typescript-eslint/typescript-estree" "5.48.0" + "@typescript-eslint/utils" "5.48.0" + debug "^4.3.4" + tsutils "^3.21.0" "@typescript-eslint/types@3.10.1": version "3.10.1" @@ -4636,6 +4656,11 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.23.0.tgz#da1654c8a5332f4d1645b2d9a1c64193cae3aa3b" integrity sha512-oqkNWyG2SLS7uTWLZf6Sr7Dm02gA5yxiz1RP87tvsmDsguVATdpVguHr4HoGOcFOpCvx9vtCSCyQUGfzq28YCw== +"@typescript-eslint/types@5.48.0": + version "5.48.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.48.0.tgz#d725da8dfcff320aab2ac6f65c97b0df30058449" + integrity sha512-UTe67B0Ypius0fnEE518NB2N8gGutIlTojeTg4nt0GQvikReVkurqxd2LvYa9q9M5MQ6rtpNyWTBxdscw40Xhw== + "@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" @@ -4650,6 +4675,19 @@ semver "^7.3.2" tsutils "^3.17.1" +"@typescript-eslint/typescript-estree@5.48.0": + version "5.48.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.48.0.tgz#a7f04bccb001003405bb5452d43953a382c2fac2" + integrity sha512-7pjd94vvIjI1zTz6aq/5wwE/YrfIyEPLtGJmRfyNR9NYIW+rOvzzUv3Cmq2hRKpvt6e9vpvPUQ7puzX7VSmsEw== + dependencies: + "@typescript-eslint/types" "5.48.0" + "@typescript-eslint/visitor-keys" "5.48.0" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + semver "^7.3.7" + tsutils "^3.21.0" + "@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" @@ -4663,6 +4701,20 @@ semver "^7.3.2" tsutils "^3.17.1" +"@typescript-eslint/utils@5.48.0": + version "5.48.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.48.0.tgz#eee926af2733f7156ad8d15e51791e42ce300273" + integrity sha512-x2jrMcPaMfsHRRIkL+x96++xdzvrdBCnYRd5QiW5Wgo1OB4kDYPbC1XjWP/TNqlfK93K/lUL92erq5zPLgFScQ== + dependencies: + "@types/json-schema" "^7.0.9" + "@types/semver" "^7.3.12" + "@typescript-eslint/scope-manager" "5.48.0" + "@typescript-eslint/types" "5.48.0" + "@typescript-eslint/typescript-estree" "5.48.0" + eslint-scope "^5.1.1" + eslint-utils "^3.0.0" + 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" @@ -4678,6 +4730,14 @@ "@typescript-eslint/types" "4.23.0" eslint-visitor-keys "^2.0.0" +"@typescript-eslint/visitor-keys@5.48.0": + version "5.48.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.48.0.tgz#4446d5e7f6cadde7140390c0e284c8702d944904" + integrity sha512-5motVPz5EgxQ0bHjut3chzBkJ3Z3sheYVcSwS5BpHZpLqSptSmELNtGixmgj65+rIfhvtQTz5i9OP2vtzdDH7Q== + dependencies: + "@typescript-eslint/types" "5.48.0" + eslint-visitor-keys "^3.3.0" + "@vue/compiler-core@3.2.45": version "3.2.45" resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.2.45.tgz#d9311207d96f6ebd5f4660be129fb99f01ddb41b" @@ -9405,7 +9465,7 @@ debug@3.2.6: dependencies: ms "^2.1.1" -debug@4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@~4.3.1, debug@~4.3.2: +debug@4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -11189,6 +11249,11 @@ eslint-visitor-keys@^2.0.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz#21fdc8fbcd9c795cc0321f0563702095751511a8" integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ== +eslint-visitor-keys@^3.3.0: + version "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@7.32.0: version "7.32.0" resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.32.0.tgz#c6d328a14be3fb08c8d1d21e12c02fdb7a2a812d" @@ -12792,7 +12857,7 @@ globby@10.0.0: merge2 "^1.2.3" slash "^3.0.0" -globby@^11.0.1, globby@^11.0.2, globby@^11.0.3: +globby@^11.0.1, globby@^11.0.2, globby@^11.0.3, globby@^11.1.0: version "11.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== @@ -17356,6 +17421,11 @@ native-url@0.3.4: dependencies: querystring "^0.2.0" +natural-compare-lite@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4" + integrity sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g== + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -20486,6 +20556,11 @@ regexpp@^3.0.0, regexpp@^3.1.0: resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2" integrity sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q== +regexpp@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" + integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== + regexpu-core@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-2.0.0.tgz#49d038837b8dcf8bfa5b9a42139938e6ea2ae240" @@ -23386,7 +23461,7 @@ tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.3 resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== -tsutils@^3.0.0, tsutils@^3.17.1: +tsutils@^3.0.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 0def7bccb6fb7741df70c5e53ed29a2e4032e3e6 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 5 Jan 2023 13:37:53 +0100 Subject: [PATCH 005/113] ref(vue): Use `string.repeat()` (#6663) --- packages/vue/src/components.ts | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/packages/vue/src/components.ts b/packages/vue/src/components.ts index 9aa78a5a23dc..86c7e8f2f65f 100644 --- a/packages/vue/src/components.ts +++ b/packages/vue/src/components.ts @@ -8,17 +8,8 @@ const ROOT_COMPONENT_NAME = ''; const ANONYMOUS_COMPONENT_NAME = ''; const repeat = (str: string, n: number): string => { - let res = ''; - while (n) { - if (n % 2 === 1) { - res += str; - } - if (n > 1) { - str += str; // eslint-disable-line no-param-reassign - } - n >>= 1; // eslint-disable-line no-bitwise, no-param-reassign - } - return res; + // string.repeat() is not supported by IE11, we fall back to just using the string in that case + return str.repeat ? str.repeat(n) : str; }; export const formatComponentName = (vm?: ViewModel, includeFile?: boolean): string => { From afb3700927b0ded138c5ec0e2ff234e8c43316be Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Thu, 5 Jan 2023 12:38:51 +0000 Subject: [PATCH 006/113] feat(integrations): Add HTTPClient integration (#6500) --- .../browser/src/integrations/httpcontext.ts | 2 +- .../integrations/httpclient/fetch/subject.js | 9 + .../integrations/httpclient/fetch/test.ts | 73 +++ .../suites/integrations/httpclient/init.js | 11 + .../integrations/httpclient/xhr/subject.js | 8 + .../integrations/httpclient/xhr/test.ts | 73 +++ packages/integrations/src/httpclient.ts | 439 ++++++++++++++++++ packages/integrations/src/index.ts | 1 + packages/types/src/context.ts | 9 + packages/types/src/hub.ts | 6 + 10 files changed, 630 insertions(+), 1 deletion(-) create mode 100644 packages/integration-tests/suites/integrations/httpclient/fetch/subject.js create mode 100644 packages/integration-tests/suites/integrations/httpclient/fetch/test.ts create mode 100644 packages/integration-tests/suites/integrations/httpclient/init.js create mode 100644 packages/integration-tests/suites/integrations/httpclient/xhr/subject.js create mode 100644 packages/integration-tests/suites/integrations/httpclient/xhr/test.ts create mode 100644 packages/integrations/src/httpclient.ts diff --git a/packages/browser/src/integrations/httpcontext.ts b/packages/browser/src/integrations/httpcontext.ts index b99eeeb7540b..88f2873882d4 100644 --- a/packages/browser/src/integrations/httpcontext.ts +++ b/packages/browser/src/integrations/httpcontext.ts @@ -36,7 +36,7 @@ export class HttpContext implements Integration { ...(referrer && { Referer: referrer }), ...(userAgent && { 'User-Agent': userAgent }), }; - const request = { ...(url && { url }), headers }; + const request = { ...event.request, ...(url && { url }), headers }; return { ...event, request }; } diff --git a/packages/integration-tests/suites/integrations/httpclient/fetch/subject.js b/packages/integration-tests/suites/integrations/httpclient/fetch/subject.js new file mode 100644 index 000000000000..94ab60d92ed9 --- /dev/null +++ b/packages/integration-tests/suites/integrations/httpclient/fetch/subject.js @@ -0,0 +1,9 @@ +fetch('http://localhost:7654/foo', { + method: 'GET', + credentials: 'include', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Cache: 'no-cache', + }, +}); diff --git a/packages/integration-tests/suites/integrations/httpclient/fetch/test.ts b/packages/integration-tests/suites/integrations/httpclient/fetch/test.ts new file mode 100644 index 000000000000..1d31c868d784 --- /dev/null +++ b/packages/integration-tests/suites/integrations/httpclient/fetch/test.ts @@ -0,0 +1,73 @@ +import { expect } from '@playwright/test'; +import { Event } from '@sentry/types'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; + +sentryTest( + 'should assign request and response context from a failed 500 fetch request', + async ({ getLocalTestPath, page }) => { + // Skipping this test when running in bundle mode, because `@sentry/integrations` bundle + // is not injected to the page with the current test setup. + if (process.env.PW_BUNDLE?.includes('bundle')) { + sentryTest.skip(); + } + + const url = await getLocalTestPath({ testDir: __dirname }); + + await page.route('**/foo', route => { + return route.fulfill({ + status: 500, + body: JSON.stringify({ + error: { + message: 'Internal Server Error', + }, + }), + headers: { + 'Content-Type': 'text/html', + }, + }); + }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + + expect(eventData.exception?.values).toHaveLength(1); + + // Not able to get the cookies from the request/response because of Playwright bug + // https://github.com/microsoft/playwright/issues/11035 + expect(eventData).toMatchObject({ + message: 'HTTP Client Error with status code: 500', + exception: { + values: [ + { + type: 'Error', + value: 'HTTP Client Error with status code: 500', + mechanism: { + type: 'http.client', + handled: true, + }, + }, + ], + }, + request: { + url: 'http://localhost:7654/foo', + method: 'GET', + headers: { + accept: 'application/json', + cache: 'no-cache', + 'content-type': 'application/json', + }, + }, + contexts: { + response: { + status_code: 500, + body_size: 45, + headers: { + 'content-type': 'text/html', + 'content-length': '45', + }, + }, + }, + }); + }, +); diff --git a/packages/integration-tests/suites/integrations/httpclient/init.js b/packages/integration-tests/suites/integrations/httpclient/init.js new file mode 100644 index 000000000000..5d43b49e75fb --- /dev/null +++ b/packages/integration-tests/suites/integrations/httpclient/init.js @@ -0,0 +1,11 @@ +import * as Sentry from '@sentry/browser'; +import { HttpClient } from '@sentry/integrations'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [new HttpClient()], + tracesSampleRate: 1, + sendDefaultPii: true, +}); diff --git a/packages/integration-tests/suites/integrations/httpclient/xhr/subject.js b/packages/integration-tests/suites/integrations/httpclient/xhr/subject.js new file mode 100644 index 000000000000..7a2e3cdd28c0 --- /dev/null +++ b/packages/integration-tests/suites/integrations/httpclient/xhr/subject.js @@ -0,0 +1,8 @@ +const xhr = new XMLHttpRequest(); + +xhr.open('GET', 'http://localhost:7654/foo', true); +xhr.withCredentials = true; +xhr.setRequestHeader('Accept', 'application/json'); +xhr.setRequestHeader('Content-Type', 'application/json'); +xhr.setRequestHeader('Cache', 'no-cache'); +xhr.send(); diff --git a/packages/integration-tests/suites/integrations/httpclient/xhr/test.ts b/packages/integration-tests/suites/integrations/httpclient/xhr/test.ts new file mode 100644 index 000000000000..7d19da9854f4 --- /dev/null +++ b/packages/integration-tests/suites/integrations/httpclient/xhr/test.ts @@ -0,0 +1,73 @@ +import { expect } from '@playwright/test'; +import { Event } from '@sentry/types'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; + +sentryTest( + 'should assign request and response context from a failed 500 XHR request', + async ({ getLocalTestPath, page }) => { + // Skipping this test when running in bundle mode, because `@sentry/integrations` bundle + // is not injected to the page with the current test setup. + if (process.env.PW_BUNDLE?.includes('bundle')) { + sentryTest.skip(); + } + + const url = await getLocalTestPath({ testDir: __dirname }); + + await page.route('**/foo', route => { + return route.fulfill({ + status: 500, + body: JSON.stringify({ + error: { + message: 'Internal Server Error', + }, + }), + headers: { + 'Content-Type': 'text/html', + }, + }); + }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + + expect(eventData.exception?.values).toHaveLength(1); + + // Not able to get the cookies from the request/response because of Playwright bug + // https://github.com/microsoft/playwright/issues/11035 + expect(eventData).toMatchObject({ + message: 'HTTP Client Error with status code: 500', + exception: { + values: [ + { + type: 'Error', + value: 'HTTP Client Error with status code: 500', + mechanism: { + type: 'http.client', + handled: true, + }, + }, + ], + }, + request: { + url: 'http://localhost:7654/foo', + method: 'GET', + headers: { + Accept: 'application/json', + Cache: 'no-cache', + 'Content-Type': 'application/json', + }, + }, + contexts: { + response: { + status_code: 500, + body_size: 45, + headers: { + 'content-type': 'text/html', + 'content-length': '45', + }, + }, + }, + }); + }, +); diff --git a/packages/integrations/src/httpclient.ts b/packages/integrations/src/httpclient.ts new file mode 100644 index 000000000000..8792ca5901fc --- /dev/null +++ b/packages/integrations/src/httpclient.ts @@ -0,0 +1,439 @@ +import { Event as SentryEvent, EventProcessor, Hub, Integration } from '@sentry/types'; +import { addExceptionMechanism, fill, GLOBAL_OBJ, logger, supportsNativeFetch } from '@sentry/utils'; + +export type HttpStatusCodeRange = [number, number] | number; +export type HttpRequestTarget = string | RegExp; +interface HttpClientOptions { + /** + * HTTP status codes that should be considered failed. + * This array can contain tuples of `[begin, end]` (both inclusive), + * single status codes, or a combinations of both + * + * Example: [[500, 505], 507] + * Default: [[500, 599]] + */ + failedRequestStatusCodes?: HttpStatusCodeRange[]; + + /** + * Targets to track for failed requests. + * This array can contain strings or regular expressions. + * + * Example: ['http://localhost', /api\/.*\/] + * Default: [/.*\/] + */ + failedRequestTargets?: HttpRequestTarget[]; +} + +/** HTTPClient integration creates events for failed client side HTTP requests. */ +export class HttpClient implements Integration { + /** + * @inheritDoc + */ + public static id: string = 'HttpClient'; + + /** + * @inheritDoc + */ + public name: string = HttpClient.id; + + private readonly _options: HttpClientOptions; + + /** + * Returns current hub. + */ + private _getCurrentHub?: () => Hub; + + /** + * @inheritDoc + * + * @param options + */ + public constructor(options?: Partial) { + this._options = { + failedRequestStatusCodes: [[500, 599]], + failedRequestTargets: [/.*/], + ...options, + }; + } + + /** + * @inheritDoc + * + * @param options + */ + public setupOnce(_: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { + this._getCurrentHub = getCurrentHub; + this._wrapFetch(); + this._wrapXHR(); + } + + /** + * Interceptor function for fetch requests + * + * @param requestInfo The Fetch API request info + * @param response The Fetch API response + * @param requestInit The request init object + */ + private _fetchResponseHandler(requestInfo: RequestInfo, response: Response, requestInit?: RequestInit): void { + if (this._getCurrentHub && this._shouldCaptureResponse(response.status, response.url)) { + const request = new Request(requestInfo, requestInit); + const hub = this._getCurrentHub(); + + let requestHeaders, responseHeaders, requestCookies, responseCookies; + + if (hub.shouldSendDefaultPii()) { + [{ headers: requestHeaders, cookies: requestCookies }, { headers: responseHeaders, cookies: responseCookies }] = + [ + { cookieHeader: 'Cookie', obj: request }, + { cookieHeader: 'Set-Cookie', obj: response }, + ].map(({ cookieHeader, obj }) => { + const headers = this._extractFetchHeaders(obj.headers); + let cookies; + + try { + const cookieString = headers[cookieHeader] || headers[cookieHeader.toLowerCase()] || undefined; + + if (cookieString) { + cookies = this._parseCookieString(cookieString); + } + } catch (e) { + __DEBUG_BUILD__ && logger.log(`Could not extract cookies from header ${cookieHeader}`); + } + + return { + headers, + cookies, + }; + }); + } + + const event = this._createEvent({ + url: request.url, + method: request.method, + status: response.status, + requestHeaders, + responseHeaders, + requestCookies, + responseCookies, + }); + + hub.captureEvent(event); + } + } + + /** + * Interceptor function for XHR requests + * + * @param xhr The XHR request + * @param method The HTTP method + * @param headers The HTTP headers + */ + private _xhrResponseHandler(xhr: XMLHttpRequest, method: string, headers: Record): void { + if (this._getCurrentHub && this._shouldCaptureResponse(xhr.status, xhr.responseURL)) { + let requestHeaders, responseCookies, responseHeaders; + const hub = this._getCurrentHub(); + + if (hub.shouldSendDefaultPii()) { + try { + const cookieString = xhr.getResponseHeader('Set-Cookie') || xhr.getResponseHeader('set-cookie') || undefined; + + if (cookieString) { + responseCookies = this._parseCookieString(cookieString); + } + } catch (e) { + __DEBUG_BUILD__ && logger.log('Could not extract cookies from response headers'); + } + + try { + responseHeaders = this._getXHRResponseHeaders(xhr); + } catch (e) { + __DEBUG_BUILD__ && logger.log('Could not extract headers from response'); + } + + requestHeaders = headers; + } + + const event = this._createEvent({ + url: xhr.responseURL, + method: method, + status: xhr.status, + requestHeaders, + // Can't access request cookies from XHR + responseHeaders, + responseCookies, + }); + + hub.captureEvent(event); + } + } + + /** + * Extracts response size from `Content-Length` header when possible + * + * @param headers + * @returns The response size in bytes or undefined + */ + private _getResponseSizeFromHeaders(headers?: Record): number | undefined { + if (headers) { + const contentLength = headers['Content-Length'] || headers['content-length']; + + if (contentLength) { + return parseInt(contentLength, 10); + } + } + + return undefined; + } + + /** + * Creates an object containing cookies from the given cookie string + * + * @param cookieString The cookie string to parse + * @returns The parsed cookies + */ + private _parseCookieString(cookieString: string): Record { + return cookieString.split('; ').reduce((acc: Record, cookie: string) => { + const [key, value] = cookie.split('='); + acc[key] = value; + return acc; + }, {}); + } + + /** + * Extracts the headers as an object from the given Fetch API request or response object + * + * @param headers The headers to extract + * @returns The extracted headers as an object + */ + private _extractFetchHeaders(headers: Headers): Record { + const result: Record = {}; + + headers.forEach((value, key) => { + result[key] = value; + }); + + return result; + } + + /** + * Extracts the response headers as an object from the given XHR object + * + * @param xhr The XHR object to extract the response headers from + * @returns The response headers as an object + */ + private _getXHRResponseHeaders(xhr: XMLHttpRequest): Record { + const headers = xhr.getAllResponseHeaders(); + + if (!headers) { + return {}; + } + + return headers.split('\r\n').reduce((acc: Record, line: string) => { + const [key, value] = line.split(': '); + acc[key] = value; + return acc; + }, {}); + } + + /** + * Checks if the given target url is in the given list of targets + * + * @param target The target url to check + * @returns true if the target url is in the given list of targets, false otherwise + */ + private _isInGivenRequestTargets(target: string): boolean { + if (!this._options.failedRequestTargets) { + return false; + } + + return this._options.failedRequestTargets.some((givenRequestTarget: HttpRequestTarget) => { + if (typeof givenRequestTarget === 'string') { + return target.includes(givenRequestTarget); + } + + return givenRequestTarget.test(target); + }); + } + + /** + * Checks if the given status code is in the given range + * + * @param status The status code to check + * @returns true if the status code is in the given range, false otherwise + */ + private _isInGivenStatusRanges(status: number): boolean { + if (!this._options.failedRequestStatusCodes) { + return false; + } + + return this._options.failedRequestStatusCodes.some((range: HttpStatusCodeRange) => { + if (typeof range === 'number') { + return range === status; + } + + return status >= range[0] && status <= range[1]; + }); + } + + /** + * Wraps `fetch` function to capture request and response data + */ + private _wrapFetch(): void { + if (!supportsNativeFetch()) { + return; + } + + // eslint-disable-next-line @typescript-eslint/no-this-alias + const self = this; + + fill(GLOBAL_OBJ, 'fetch', function (originalFetch: (...args: unknown[]) => Promise) { + return function (this: Window, ...args: unknown[]): Promise { + const [requestInfo, requestInit] = args as [RequestInfo, RequestInit | undefined]; + const responsePromise: Promise = originalFetch.apply(this, args); + + responsePromise + .then((response: Response) => { + self._fetchResponseHandler(requestInfo, response, requestInit); + return response; + }) + .catch((error: Error) => { + throw error; + }); + + return responsePromise; + }; + }); + } + + /** + * Wraps XMLHttpRequest to capture request and response data + */ + private _wrapXHR(): void { + if (!('XMLHttpRequest' in GLOBAL_OBJ)) { + return; + } + + // eslint-disable-next-line @typescript-eslint/no-this-alias + const self = this; + + fill(XMLHttpRequest.prototype, 'open', function (originalOpen: (...openArgs: unknown[]) => void): () => void { + return function (this: XMLHttpRequest, ...openArgs: unknown[]): void { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const xhr = this; + const method = openArgs[0] as string; + const headers: Record = {}; + + // Intercepting `setRequestHeader` to access the request headers of XHR instance. + // This will only work for user/library defined headers, not for the default/browser-assigned headers. + // Request cookies are also unavailable for XHR, as `Cookie` header can't be defined by `setRequestHeader`. + fill( + xhr, + 'setRequestHeader', + // eslint-disable-next-line @typescript-eslint/ban-types + function (originalSetRequestHeader: (...setRequestHeaderArgs: unknown[]) => void): Function { + return function (...setRequestHeaderArgs: unknown[]): void { + const [header, value] = setRequestHeaderArgs as [string, string]; + + headers[header] = value; + + return originalSetRequestHeader.apply(xhr, setRequestHeaderArgs); + }; + }, + ); + + // eslint-disable-next-line @typescript-eslint/ban-types + fill(xhr, 'onloadend', function (original?: (...onloadendArgs: unknown[]) => void): Function { + return function (...onloadendArgs: unknown[]): void { + try { + self._xhrResponseHandler(xhr, method, headers); + } catch (e) { + __DEBUG_BUILD__ && logger.warn('Error while extracting response event form XHR response', e); + } + + if (original) { + return original.apply(xhr, onloadendArgs); + } + }; + }); + + return originalOpen.apply(this, openArgs); + }; + }); + } + + /** + * Checks whether given url points to Sentry server + * + * @param url url to verify + */ + private _isSentryRequest(url: string): boolean { + const client = this._getCurrentHub && this._getCurrentHub().getClient(); + + if (!client) { + return false; + } + + const dsn = client.getDsn(); + return dsn ? url.includes(dsn.host) : false; + } + + /** + * Checks whether to capture given response as an event + * + * @param status response status code + * @param url response url + */ + private _shouldCaptureResponse(status: number, url: string): boolean { + return this._isInGivenStatusRanges(status) && this._isInGivenRequestTargets(url) && !this._isSentryRequest(url); + } + + /** + * Creates a synthetic Sentry event from given response data + * + * @param data response data + * @returns event + */ + private _createEvent(data: { + url: string; + method: string; + status: number; + responseHeaders?: Record; + responseCookies?: Record; + requestHeaders?: Record; + requestCookies?: Record; + }): SentryEvent { + const message = `HTTP Client Error with status code: ${data.status}`; + + const event: SentryEvent = { + message, + exception: { + values: [ + { + type: 'Error', + value: message, + }, + ], + }, + request: { + url: data.url, + method: data.method, + headers: data.requestHeaders, + cookies: data.requestCookies, + }, + contexts: { + response: { + status_code: data.status, + headers: data.responseHeaders, + cookies: data.responseCookies, + body_size: this._getResponseSizeFromHeaders(data.responseHeaders), + }, + }, + }; + + addExceptionMechanism(event, { + type: 'http.client', + }); + + return event; + } +} diff --git a/packages/integrations/src/index.ts b/packages/integrations/src/index.ts index 9a2573ee5a44..2f3708075ac1 100644 --- a/packages/integrations/src/index.ts +++ b/packages/integrations/src/index.ts @@ -7,3 +7,4 @@ export { ReportingObserver } from './reportingobserver'; export { RewriteFrames } from './rewriteframes'; export { SessionTiming } from './sessiontiming'; export { Transaction } from './transaction'; +export { HttpClient } from './httpclient'; diff --git a/packages/types/src/context.ts b/packages/types/src/context.ts index 4b6a08585273..3a342373c3fe 100644 --- a/packages/types/src/context.ts +++ b/packages/types/src/context.ts @@ -5,6 +5,7 @@ export interface Contexts extends Record { device?: DeviceContext; os?: OsContext; culture?: CultureContext; + response?: ResponseContext; } export interface AppContext extends Record { @@ -70,3 +71,11 @@ export interface CultureContext extends Record { is_24_hour_format?: boolean; timezone?: string; } + +export interface ResponseContext extends Record { + type?: string; + cookies?: string[][] | Record; + headers?: Record; + status_code?: number; + body_size?: number; // in bytes +} diff --git a/packages/types/src/hub.ts b/packages/types/src/hub.ts index a34d4372e08d..555da1ef94ab 100644 --- a/packages/types/src/hub.ts +++ b/packages/types/src/hub.ts @@ -227,4 +227,10 @@ export interface Hub { * @param endSession If set the session will be marked as exited and removed from the scope */ captureSession(endSession?: boolean): void; + + /** + * Returns if default PII should be sent to Sentry and propagated in ourgoing requests + * when Tracing is used. + */ + shouldSendDefaultPii(): boolean; } From c65597f98e30c3f72cea2cf127fb7c64105a836c Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Thu, 5 Jan 2023 15:32:05 +0100 Subject: [PATCH 007/113] ref(nextjs): Remove `instrumentSever` (#6592) --- packages/nextjs/package.json | 7 +- packages/nextjs/rollup.npm.config.js | 7 +- packages/nextjs/src/index.server.ts | 16 - packages/nextjs/src/utils/instrumentServer.ts | 369 ------------------ 4 files changed, 2 insertions(+), 397 deletions(-) delete mode 100644 packages/nextjs/src/utils/instrumentServer.ts diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 43d1483e5007..453cc7327238 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -77,10 +77,5 @@ }, "volta": { "extends": "../../package.json" - }, - "sideEffects": [ - "./cjs/index.server.js", - "./esm/index.server.js", - "./src/index.server.ts" - ] + } } diff --git a/packages/nextjs/rollup.npm.config.js b/packages/nextjs/rollup.npm.config.js index 32100ef278be..f9bdfbf1bcbc 100644 --- a/packages/nextjs/rollup.npm.config.js +++ b/packages/nextjs/rollup.npm.config.js @@ -5,12 +5,7 @@ export default [ makeBaseNPMConfig({ // We need to include `instrumentServer.ts` separately because it's only conditionally required, and so rollup // doesn't automatically include it when calculating the module dependency tree. - entrypoints: [ - 'src/index.server.ts', - 'src/index.client.ts', - 'src/utils/instrumentServer.ts', - 'src/config/webpack.ts', - ], + entrypoints: ['src/index.server.ts', 'src/index.client.ts', 'src/config/webpack.ts'], // prevent this internal nextjs code from ending up in our built package (this doesn't happen automatially because // the name doesn't match an SDK dependency) diff --git a/packages/nextjs/src/index.server.ts b/packages/nextjs/src/index.server.ts index 0c5982dc747d..922be7aa09b6 100644 --- a/packages/nextjs/src/index.server.ts +++ b/packages/nextjs/src/index.server.ts @@ -175,19 +175,3 @@ export { withSentryAPI, withSentry, } from './config/wrappers'; - -// Wrap various server methods to enable error monitoring and tracing. (Note: This only happens for non-Vercel -// deployments, because the current method of doing the wrapping a) crashes Next 12 apps deployed to Vercel and -// b) doesn't work on those apps anyway. We also don't do it during build, because there's no server running in that -// phase.) -if (!IS_BUILD && !IS_VERCEL) { - // Dynamically require the file because even importing from it causes Next 12 to crash on Vercel. - // In environments where the JS file doesn't exist, such as testing, import the TS file. - try { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const { instrumentServer } = require('./utils/instrumentServer.js'); - instrumentServer(); - } catch (err) { - __DEBUG_BUILD__ && logger.warn(`Error: Unable to instrument server for tracing. Got ${err}.`); - } -} diff --git a/packages/nextjs/src/utils/instrumentServer.ts b/packages/nextjs/src/utils/instrumentServer.ts deleted file mode 100644 index 393c85fed014..000000000000 --- a/packages/nextjs/src/utils/instrumentServer.ts +++ /dev/null @@ -1,369 +0,0 @@ -/* eslint-disable max-lines */ -import { captureException, configureScope, deepReadDirSync, getCurrentHub, startTransaction } from '@sentry/node'; -import { extractTraceparentData, getActiveTransaction, hasTracingEnabled } from '@sentry/tracing'; -import { - addExceptionMechanism, - baggageHeaderToDynamicSamplingContext, - fill, - isString, - logger, - stripUrlQueryAndFragment, -} from '@sentry/utils'; -import * as domain from 'domain'; -import * as http from 'http'; -import { default as createNextServer } from 'next'; -import * as querystring from 'querystring'; -import * as url from 'url'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type PlainObject = { [key: string]: T }; - -// class used by `next` as a proxy to the real server; see -// https://github.com/vercel/next.js/blob/4443d6f3d36b107e833376c2720c1e206eee720d/packages/next/server/next.ts#L32 -interface NextServer { - server: Server; - getServer: () => Promise; - createServer: (options: PlainObject) => Server; -} - -// `next`'s main server class; see -// https://github.com/vercel/next.js/blob/4443d6f3d36b107e833376c2720c1e206eee720d/packages/next/next-server/server/next-server.ts#L132 -interface Server { - dir: string; - publicDir: string; -} - -export type NextRequest = ( - | http.IncomingMessage // in nextjs versions < 12.0.9, `NextRequest` extends `http.IncomingMessage` - | { - _req: http.IncomingMessage; // in nextjs versions >= 12.0.9, `NextRequest` wraps `http.IncomingMessage` - } -) & { - cookies: Record; - url: string; - query: { [key: string]: string }; - headers: { [key: string]: string }; - body: string | { [key: string]: unknown }; - method: string; -}; - -type NextResponse = - // in nextjs versions < 12.0.9, `NextResponse` extends `http.ServerResponse` - | http.ServerResponse - // in nextjs versions >= 12.0.9, `NextResponse` wraps `http.ServerResponse` - | { - _res: http.ServerResponse; - }; - -// the methods we'll wrap -type HandlerGetter = () => Promise; -type ReqHandler = (nextReq: NextRequest, nextRes: NextResponse, parsedUrl?: url.UrlWithParsedQuery) => Promise; -type ErrorLogger = (err: Error) => void; -type ApiPageEnsurer = (path: string) => Promise; -type PageComponentFinder = ( - pathname: string, - query: querystring.ParsedUrlQuery, - params: { [key: string]: unknown } | null, -) => Promise<{ [key: string]: unknown } | null>; - -// these aliases are purely to make the function signatures more easily understandable -type WrappedHandlerGetter = HandlerGetter; -type WrappedErrorLogger = ErrorLogger; -type WrappedReqHandler = ReqHandler; -type WrappedApiPageEnsurer = ApiPageEnsurer; -type WrappedPageComponentFinder = PageComponentFinder; - -let liveServer: Server; -let sdkSetupComplete = false; - -const globalWithInjectedValues = global as typeof global & { - __sentryRewritesTunnelPath__?: string; -}; - -/** - * Do the monkeypatching and wrapping necessary to catch errors in page routes and record transactions for both page and - * API routes. - */ -export function instrumentServer(): void { - // The full implementation here involves a lot of indirection and multiple layers of callbacks and wrapping, and is - // therefore potentially a little hard to follow. Here's the overall idea: - - // Next.js uses two server classes, `NextServer` and `Server`, with the former proxying calls to the latter, which - // then does the all real work. The only access we have to either is through Next's default export, - // `createNextServer()`, which returns a `NextServer` instance. - - // At server startup: - // `next.config.js` imports SDK -> - // SDK's `index.ts` runs -> - // `instrumentServer()` (the function we're in right now) -> - // `createNextServer()` -> - // `NextServer` instance -> - // `NextServer` prototype -> - // Wrap `NextServer.getServerRequestHandler()`, purely to get us to the next step - - // At time of first request: - // Wrapped `getServerRequestHandler` runs for the first time -> - // Live `NextServer` instance(via`this`) -> - // Live `Server` instance (via `NextServer.server`) -> - // `Server` prototype -> - // Wrap `Server.logError`, `Server.handleRequest`, `Server.ensureApiPage`, and `Server.findPageComponents` methods, - // then fulfill original purpose of function by passing wrapped version of `handleRequest` to caller - - // Whenever caller of `NextServer.getServerRequestHandler` calls the wrapped `Server.handleRequest`: - // Trace request - - // Whenever something calls the wrapped `Server.logError`: - // Capture error - - // Whenever an API request is handled and the wrapped `Server.ensureApiPage` is called, or whenever a page request is - // handled and the wrapped `Server.findPageComponents` is called: - // Replace URL in transaction name with parameterized version - - const nextServerPrototype = Object.getPrototypeOf(createNextServer({})); - fill(nextServerPrototype, 'getServerRequestHandler', makeWrappedHandlerGetter); -} - -/** - * Create a wrapped version of Nextjs's `NextServer.getServerRequestHandler` method, as a way to access the running - * `Server` instance and monkeypatch its prototype. - * - * @param origHandlerGetter Nextjs's `NextServer.getServerRequestHandler` method - * @returns A wrapped version of the same method, to monkeypatch in at server startup - */ -function makeWrappedHandlerGetter(origHandlerGetter: HandlerGetter): WrappedHandlerGetter { - // We wrap this purely in order to be able to grab data and do further monkeypatching the first time it runs. - // Otherwise, it's just a pass-through to the original method. - const wrappedHandlerGetter = async function (this: NextServer): Promise { - if (!sdkSetupComplete) { - // stash this in the closure so that `makeWrappedReqHandler` can use it - liveServer = await this.getServer(); - const serverPrototype = Object.getPrototypeOf(liveServer); - - // Wrap for error capturing (`logError` gets called by `next` for all server-side errors) - fill(serverPrototype, 'logError', makeWrappedErrorLogger); - - // Wrap for request transaction creation (`handleRequest` is called for all incoming requests, and dispatches them - // to the appropriate handlers) - fill(serverPrototype, 'handleRequest', makeWrappedReqHandler); - - // Wrap as a way to grab the parameterized request URL to use as the transaction name for API requests and page - // requests, respectively. These methods are chosen because they're the first spot in the request-handling process - // where the parameterized path is provided as an argument, so it's easy to grab. - fill(serverPrototype, 'ensureApiPage', makeWrappedMethodForGettingParameterizedPath); - fill(serverPrototype, 'findPageComponents', makeWrappedMethodForGettingParameterizedPath); - - sdkSetupComplete = true; - } - - return origHandlerGetter.call(this); - }; - - return wrappedHandlerGetter; -} - -/** - * Wrap the error logger used by the server to capture exceptions which arise from functions like `getServerSideProps`. - * - * @param origErrorLogger The original logger from the `Server` class - * @returns A wrapped version of that logger - */ -function makeWrappedErrorLogger(origErrorLogger: ErrorLogger): WrappedErrorLogger { - return function (this: Server, err: Error): void { - // TODO add more context data here - - // We can use `configureScope` rather than `withScope` here because we're using domains to ensure that each request - // gets its own scope. (`configureScope` has the advantage of not creating a clone of the current scope before - // modifying it, which in this case is unnecessary.) - configureScope(scope => { - scope.addEventProcessor(event => { - addExceptionMechanism(event, { - type: 'instrument', - handled: true, - data: { - function: 'logError', - }, - }); - return event; - }); - }); - - captureException(err); - - return origErrorLogger.call(this, err); - }; -} - -// inspired by next's public file routing; see -// https://github.com/vercel/next.js/blob/4443d6f3d36b107e833376c2720c1e206eee720d/packages/next/next-server/server/next-server.ts#L1166 -function getPublicDirFiles(): Set { - try { - // we need the paths here to match the format of a request url, which means they must: - // - start with a slash - // - use forward slashes rather than backslashes - // - be URL-encoded - const dirContents = deepReadDirSync(liveServer.publicDir).map(filepath => - encodeURI(`/${filepath.replace(/\\/g, '/')}`), - ); - return new Set(dirContents); - } catch (_) { - return new Set(); - } -} - -/** - * Wrap the server's request handler to be able to create request transactions. - * - * @param origReqHandler The original request handler from the `Server` class - * @returns A wrapped version of that handler - */ -function makeWrappedReqHandler(origReqHandler: ReqHandler): WrappedReqHandler { - const publicDirFiles = getPublicDirFiles(); - // add transaction start and stop to the normal request handling - const wrappedReqHandler = async function ( - this: Server, - nextReq: NextRequest, - nextRes: NextResponse, - parsedUrl?: url.UrlWithParsedQuery, - ): Promise { - // Starting with version 12.0.9, nextjs wraps the incoming request in a `NodeNextRequest` object and the outgoing - // response in a `NodeNextResponse` object before passing them to the handler. (This is necessary here but not in - // `withSentry` because by the time nextjs passes them to an API handler, it's unwrapped them again.) - const req = '_req' in nextReq ? nextReq._req : nextReq; - const res = '_res' in nextRes ? nextRes._res : nextRes; - - // wrap everything in a domain in order to prevent scope bleed between requests - const local = domain.create(); - local.add(req); - local.add(res); - // TODO could this replace wrapping the error logger? - // local.on('error', Sentry.captureException); - - local.run(() => { - const currentScope = getCurrentHub().getScope(); - - if (currentScope) { - // Store the request on the scope so we can pull data from it and add it to the event - currentScope.setSDKProcessingMetadata({ request: req }); - - // We only want to record page and API requests - if (hasTracingEnabled() && shouldTraceRequest(nextReq.url, publicDirFiles)) { - // If there is a trace header set, extract the data from it (parentSpanId, traceId, and sampling decision) - let traceparentData; - if (nextReq.headers && isString(nextReq.headers['sentry-trace'])) { - traceparentData = extractTraceparentData(nextReq.headers['sentry-trace']); - __DEBUG_BUILD__ && logger.log(`[Tracing] Continuing trace ${traceparentData?.traceId}.`); - } - - const baggageHeader = nextReq.headers && nextReq.headers.baggage; - const dynamicSamplingContext = baggageHeaderToDynamicSamplingContext(baggageHeader); - - // pull off query string, if any - const reqPath = stripUrlQueryAndFragment(nextReq.url); - - // requests for pages will only ever be GET requests, so don't bother to include the method in the transaction - // name; requests to API routes could be GET, POST, PUT, etc, so do include it there - const namePrefix = nextReq.url.startsWith('/api') ? `${nextReq.method.toUpperCase()} ` : ''; - - const transaction = startTransaction( - { - name: `${namePrefix}${reqPath}`, - op: 'http.server', - metadata: { - dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext, - requestPath: reqPath, - // TODO: Investigate if there's a way to tell if this is a dynamic route, so that we can make this more - // like `source: isDynamicRoute? 'url' : 'route'` - // TODO: What happens when `withSentry` is used also? Which values of `name` and `source` win? - source: 'url', - request: req, - }, - ...traceparentData, - }, - // Extra context passed to the `tracesSampler` (Note: We're combining `nextReq` and `req` this way in order - // to not break people's `tracesSampler` functions, even though the format of `nextReq` has changed (see - // note above re: nextjs 12.0.9). If `nextReq === req` (pre 12.0.9), then spreading `req` is a no-op - we're - // just spreading the same stuff twice. If `nextReq` contains `req` (12.0.9 and later), then spreading `req` - // mimics the old format by flattening the data.) - { request: { ...nextReq, ...req } }, - ); - - currentScope.setSpan(transaction); - - res.once('finish', () => { - const transaction = getActiveTransaction(); - if (transaction) { - transaction.setHttpStatus(res.statusCode); - - // Push `transaction.finish` to the next event loop so open spans have a chance to finish before the - // transaction closes - setImmediate(() => { - transaction.finish(); - }); - } - }); - } - } - - return origReqHandler.call(this, nextReq, nextRes, parsedUrl); - }); - }; - - return wrappedReqHandler; -} - -/** - * Wrap the given method in order to use the parameterized path passed to it in the transaction name. - * - * @param origMethod Either `ensureApiPage` (called for every API request) or `findPageComponents` (called for every - * page request), both from the `Server` class - * @returns A wrapped version of the given method - */ -function makeWrappedMethodForGettingParameterizedPath( - origMethod: ApiPageEnsurer | PageComponentFinder, -): WrappedApiPageEnsurer | WrappedPageComponentFinder { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const wrappedMethod = async function ( - this: Server, - parameterizedPath: - | string // `ensureAPIPage`, `findPageComponents` before nextjs 12.2.6 - | { pathname: string }, // `findPageComponents` from nextjs 12.2.6 onward - ...args: any[] - ): Promise { - const transaction = getActiveTransaction(); - - // replace specific URL with parameterized version - if (transaction && transaction.metadata.requestPath) { - const origPath = transaction.metadata.requestPath; - const newPath = typeof parameterizedPath === 'string' ? parameterizedPath : parameterizedPath.pathname; - if (newPath) { - const newName = transaction.name.replace(origPath, newPath); - transaction.setName(newName, 'route'); - } - } - - return origMethod.call(this, parameterizedPath, ...args); - }; - - return wrappedMethod; -} - -/** - * Determine if the request should be traced, by filtering out requests for internal next files and static resources. - * - * @param url The URL of the request - * @param publicDirFiles A set containing relative paths to all available static resources (note that this does not - * include static *pages*, but rather images and the like) - * @returns false if the URL is for an internal or static resource - */ -function shouldTraceRequest(url: string, publicDirFiles: Set): boolean { - // Don't trace tunneled sentry events - const tunnelPath = globalWithInjectedValues.__sentryRewritesTunnelPath__; - const pathname = new URL(url, 'http://example.com/').pathname; // `url` is relative so we need to define a base to be able to parse with URL - if (tunnelPath && pathname === tunnelPath) { - __DEBUG_BUILD__ && logger.log(`Tunneling Sentry event received on "${url}"`); - return false; - } - - // `static` is a deprecated but still-functional location for static resources - return !url.startsWith('/_next/') && !url.startsWith('/static/') && !publicDirFiles.has(url); -} From 46a4c3c87cf80370a10e5dcbbf958ffbcbc4250e Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Thu, 5 Jan 2023 15:59:01 +0100 Subject: [PATCH 008/113] ref(nextjs): Simplify `isBuild` logic (#6594) --- packages/nextjs/src/utils/isBuild.ts | 24 +--------- packages/nextjs/test/utils/isBuild.test.ts | 51 ---------------------- 2 files changed, 1 insertion(+), 74 deletions(-) delete mode 100644 packages/nextjs/test/utils/isBuild.test.ts diff --git a/packages/nextjs/src/utils/isBuild.ts b/packages/nextjs/src/utils/isBuild.ts index 49dd6d103543..0e6f6691bf44 100644 --- a/packages/nextjs/src/utils/isBuild.ts +++ b/packages/nextjs/src/utils/isBuild.ts @@ -4,27 +4,5 @@ import { NEXT_PHASE_PRODUCTION_BUILD } from './phases'; * Decide if the currently running process is part of the build phase or happening at runtime. */ export function isBuild(): boolean { - if ( - // During build, the main process is invoked by - // `node next build` - // and child processes are invoked as - // `node /node_modules/.../jest-worker/processChild.js`. - // The former is (obviously) easy to recognize, but the latter could happen at runtime as well. Fortunately, the main - // process hits this file before any of the child processes do, so we're able to set an env variable which the child - // processes can then check. During runtime, the main process is invoked as - // `node next start` - // or - // `node /var/runtime/index.js`, - // so we never drop into the `if` in the first place. - process.argv.includes('build') || - process.env.SENTRY_BUILD_PHASE || - // This is set by next, but not until partway through the build process, which is why we need the above checks. That - // said, in case this function isn't called until we're in a child process, it can serve as a good backup. - process.env.NEXT_PHASE === NEXT_PHASE_PRODUCTION_BUILD - ) { - process.env.SENTRY_BUILD_PHASE = 'true'; - return true; - } - - return false; + return process.env.NEXT_PHASE === NEXT_PHASE_PRODUCTION_BUILD; } diff --git a/packages/nextjs/test/utils/isBuild.test.ts b/packages/nextjs/test/utils/isBuild.test.ts deleted file mode 100644 index 0bc1523b041a..000000000000 --- a/packages/nextjs/test/utils/isBuild.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { isBuild } from '../../src/utils/isBuild'; - -let originalEnv: typeof process.env; -let originalArgv: typeof process.argv; - -function assertNoMagicValues(): void { - if (Object.keys(process.env).includes('SENTRY_BUILD_PHASE') || process.argv.includes('build')) { - throw new Error('Not starting test with a clean setup'); - } -} - -describe('isBuild()', () => { - beforeEach(() => { - assertNoMagicValues(); - originalEnv = { ...process.env }; - originalArgv = [...process.argv]; - }); - - afterEach(() => { - process.env = originalEnv; - process.argv = originalArgv; - assertNoMagicValues(); - }); - - it("detects 'build' in argv", () => { - // the result of calling `next build` - process.argv = ['/abs/path/to/node', '/abs/path/to/nextjs/excecutable', 'build']; - expect(isBuild()).toBe(true); - }); - - it("sets env var when 'build' in argv", () => { - // the result of calling `next build` - process.argv = ['/abs/path/to/node', '/abs/path/to/nextjs/excecutable', 'build']; - isBuild(); - expect(Object.keys(process.env).includes('SENTRY_BUILD_PHASE')).toBe(true); - }); - - it("does not set env var when 'build' not in argv", () => { - isBuild(); - expect(Object.keys(process.env).includes('SENTRY_BUILD_PHASE')).toBe(false); - }); - - it('detects env var', () => { - process.env.SENTRY_BUILD_PHASE = 'true'; - expect(isBuild()).toBe(true); - }); - - it("returns false when 'build' not in `argv` and env var not present", () => { - expect(isBuild()).toBe(false); - }); -}); From 8291249ad644fc75bfb3b80556243d2c67475dd1 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Thu, 5 Jan 2023 15:09:07 +0000 Subject: [PATCH 009/113] feat(transport): Return result through Transport send (#6626) --- packages/core/src/transports/base.ts | 12 ++++--- packages/node/test/transports/http.test.ts | 40 +++++++++------------ packages/node/test/transports/https.test.ts | 29 +++++++-------- packages/replay/src/replay.ts | 4 +-- packages/types/src/transport.ts | 3 +- 5 files changed, 41 insertions(+), 47 deletions(-) diff --git a/packages/core/src/transports/base.ts b/packages/core/src/transports/base.ts index 23a7d00d70df..3c7603b024ff 100644 --- a/packages/core/src/transports/base.ts +++ b/packages/core/src/transports/base.ts @@ -7,6 +7,7 @@ import { EventItem, InternalBaseTransportOptions, Transport, + TransportMakeRequestResponse, TransportRequestExecutor, } from '@sentry/types'; import { @@ -35,13 +36,15 @@ export const DEFAULT_TRANSPORT_BUFFER_SIZE = 30; export function createTransport( options: InternalBaseTransportOptions, makeRequest: TransportRequestExecutor, - buffer: PromiseBuffer = makePromiseBuffer(options.bufferSize || DEFAULT_TRANSPORT_BUFFER_SIZE), + buffer: PromiseBuffer = makePromiseBuffer( + options.bufferSize || DEFAULT_TRANSPORT_BUFFER_SIZE, + ), ): Transport { let rateLimits: RateLimits = {}; const flush = (timeout?: number): PromiseLike => buffer.drain(timeout); - function send(envelope: Envelope): PromiseLike { + function send(envelope: Envelope): PromiseLike { const filteredEnvelopeItems: EnvelopeItem[] = []; // Drop rate limited items from envelope @@ -71,7 +74,7 @@ export function createTransport( }); }; - const requestTask = (): PromiseLike => + const requestTask = (): PromiseLike => makeRequest({ body: serializeEnvelope(filteredEnvelope, options.textEncoder) }).then( response => { // We don't want to throw on NOK responses, but we want to at least log them @@ -80,10 +83,11 @@ export function createTransport( } rateLimits = updateRateLimits(rateLimits, response); + return response; }, error => { - __DEBUG_BUILD__ && logger.error('Failed while sending event:', error); recordEnvelopeLoss('network_error'); + throw error; }, ); diff --git a/packages/node/test/transports/http.test.ts b/packages/node/test/transports/http.test.ts index 0bf61118b9e9..80ab74b9f673 100644 --- a/packages/node/test/transports/http.test.ts +++ b/packages/node/test/transports/http.test.ts @@ -7,6 +7,8 @@ import { createGunzip } from 'zlib'; import { makeNodeTransport } from '../../src/transports'; +const textEncoder = new TextEncoder(); + jest.mock('@sentry/core', () => { const actualCore = jest.requireActual('@sentry/core'); return { @@ -70,22 +72,19 @@ const EVENT_ENVELOPE = createEnvelope({ event_id: 'aa3ff046696b4b [{ type: 'event' }, { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' }] as EventItem, ]); -const SERIALIZED_EVENT_ENVELOPE = serializeEnvelope(EVENT_ENVELOPE, new TextEncoder()); +const SERIALIZED_EVENT_ENVELOPE = serializeEnvelope(EVENT_ENVELOPE, textEncoder); const ATTACHMENT_ITEM = createAttachmentEnvelopeItem( { filename: 'empty-file.bin', data: new Uint8Array(50_000) }, - new TextEncoder(), + textEncoder, ); const EVENT_ATTACHMENT_ENVELOPE = addItemToEnvelope(EVENT_ENVELOPE, ATTACHMENT_ITEM); -const SERIALIZED_EVENT_ATTACHMENT_ENVELOPE = serializeEnvelope( - EVENT_ATTACHMENT_ENVELOPE, - new TextEncoder(), -) as Uint8Array; +const SERIALIZED_EVENT_ATTACHMENT_ENVELOPE = serializeEnvelope(EVENT_ATTACHMENT_ENVELOPE, textEncoder) as Uint8Array; const defaultOptions = { url: TEST_SERVER_URL, recordDroppedEvent: () => undefined, - textEncoder: new TextEncoder(), + textEncoder, }; describe('makeNewHttpTransport()', () => { @@ -151,7 +150,9 @@ describe('makeNewHttpTransport()', () => { const transport = makeNodeTransport(defaultOptions); - await expect(transport.send(EVENT_ENVELOPE)).resolves.toBeUndefined(); + await expect(transport.send(EVENT_ENVELOPE)).resolves.toEqual( + expect.objectContaining({ statusCode: serverStatusCode }), + ); }, ); @@ -165,20 +166,13 @@ describe('makeNewHttpTransport()', () => { }); const transport = makeNodeTransport(defaultOptions); - await expect(transport.send(EVENT_ENVELOPE)).resolves.toBeUndefined(); - }); - - it('should resolve when server responds with rate limit header and status code 200', async () => { - await setupTestServer({ + await expect(transport.send(EVENT_ENVELOPE)).resolves.toEqual({ statusCode: SUCCESS, - responseHeaders: { - 'Retry-After': '2700', - 'X-Sentry-Rate-Limits': '60::organization, 2700::organization', + headers: { + 'retry-after': '2700', + 'x-sentry-rate-limits': '60::organization, 2700::organization', }, }); - - const transport = makeNodeTransport(defaultOptions); - await transport.send(EVENT_ENVELOPE); }); }); @@ -308,7 +302,7 @@ describe('makeNewHttpTransport()', () => { const registeredRequestExecutor = (createTransport as jest.Mock).mock.calls[0][1]; const executorResult = registeredRequestExecutor({ - body: serializeEnvelope(EVENT_ENVELOPE, new TextEncoder()), + body: serializeEnvelope(EVENT_ENVELOPE, textEncoder), category: 'error', }); @@ -328,7 +322,7 @@ describe('makeNewHttpTransport()', () => { const registeredRequestExecutor = (createTransport as jest.Mock).mock.calls[0][1]; const executorResult = registeredRequestExecutor({ - body: serializeEnvelope(EVENT_ENVELOPE, new TextEncoder()), + body: serializeEnvelope(EVENT_ENVELOPE, textEncoder), category: 'error', }); @@ -356,7 +350,7 @@ describe('makeNewHttpTransport()', () => { const registeredRequestExecutor = (createTransport as jest.Mock).mock.calls[0][1]; const executorResult = registeredRequestExecutor({ - body: serializeEnvelope(EVENT_ENVELOPE, new TextEncoder()), + body: serializeEnvelope(EVENT_ENVELOPE, textEncoder), category: 'error', }); @@ -384,7 +378,7 @@ describe('makeNewHttpTransport()', () => { const registeredRequestExecutor = (createTransport as jest.Mock).mock.calls[0][1]; const executorResult = registeredRequestExecutor({ - body: serializeEnvelope(EVENT_ENVELOPE, new TextEncoder()), + body: serializeEnvelope(EVENT_ENVELOPE, textEncoder), category: 'error', }); diff --git a/packages/node/test/transports/https.test.ts b/packages/node/test/transports/https.test.ts index cf7051b54fe4..77286e32cbb6 100644 --- a/packages/node/test/transports/https.test.ts +++ b/packages/node/test/transports/https.test.ts @@ -9,6 +9,8 @@ import { makeNodeTransport } from '../../src/transports'; import { HTTPModule, HTTPModuleRequestIncomingMessage } from '../../src/transports/http-module'; import testServerCerts from './test-server-certs'; +const textEncoder = new TextEncoder(); + jest.mock('@sentry/core', () => { const actualCore = jest.requireActual('@sentry/core'); return { @@ -70,7 +72,7 @@ const EVENT_ENVELOPE = createEnvelope({ event_id: 'aa3ff046696b4b [{ type: 'event' }, { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' }] as EventItem, ]); -const SERIALIZED_EVENT_ENVELOPE = serializeEnvelope(EVENT_ENVELOPE, new TextEncoder()); +const SERIALIZED_EVENT_ENVELOPE = serializeEnvelope(EVENT_ENVELOPE, textEncoder); const unsafeHttpsModule: HTTPModule = { request: jest @@ -84,7 +86,7 @@ const defaultOptions = { httpModule: unsafeHttpsModule, url: TEST_SERVER_URL, recordDroppedEvent: () => undefined, // noop - textEncoder: new TextEncoder(), + textEncoder, }; describe('makeNewHttpsTransport()', () => { @@ -151,20 +153,13 @@ describe('makeNewHttpsTransport()', () => { }); const transport = makeNodeTransport(defaultOptions); - await expect(transport.send(EVENT_ENVELOPE)).resolves.toBeUndefined(); - }); - - it('should resolve when server responds with rate limit header and status code 200', async () => { - await setupTestServer({ + await expect(transport.send(EVENT_ENVELOPE)).resolves.toEqual({ statusCode: SUCCESS, - responseHeaders: { - 'Retry-After': '2700', - 'X-Sentry-Rate-Limits': '60::organization, 2700::organization', + headers: { + 'retry-after': '2700', + 'x-sentry-rate-limits': '60::organization, 2700::organization', }, }); - - const transport = makeNodeTransport(defaultOptions); - await expect(transport.send(EVENT_ENVELOPE)).resolves.toBeUndefined(); }); it('should use `caCerts` option', async () => { @@ -299,7 +294,7 @@ describe('makeNewHttpsTransport()', () => { const registeredRequestExecutor = (createTransport as jest.Mock).mock.calls[0][1]; const executorResult = registeredRequestExecutor({ - body: serializeEnvelope(EVENT_ENVELOPE, new TextEncoder()), + body: serializeEnvelope(EVENT_ENVELOPE, textEncoder), category: 'error', }); @@ -319,7 +314,7 @@ describe('makeNewHttpsTransport()', () => { const registeredRequestExecutor = (createTransport as jest.Mock).mock.calls[0][1]; const executorResult = registeredRequestExecutor({ - body: serializeEnvelope(EVENT_ENVELOPE, new TextEncoder()), + body: serializeEnvelope(EVENT_ENVELOPE, textEncoder), category: 'error', }); @@ -347,7 +342,7 @@ describe('makeNewHttpsTransport()', () => { const registeredRequestExecutor = (createTransport as jest.Mock).mock.calls[0][1]; const executorResult = registeredRequestExecutor({ - body: serializeEnvelope(EVENT_ENVELOPE, new TextEncoder()), + body: serializeEnvelope(EVENT_ENVELOPE, textEncoder), category: 'error', }); @@ -375,7 +370,7 @@ describe('makeNewHttpsTransport()', () => { const registeredRequestExecutor = (createTransport as jest.Mock).mock.calls[0][1]; const executorResult = registeredRequestExecutor({ - body: serializeEnvelope(EVENT_ENVELOPE, new TextEncoder()), + body: serializeEnvelope(EVENT_ENVELOPE, textEncoder), category: 'error', }); diff --git a/packages/replay/src/replay.ts b/packages/replay/src/replay.ts index 50f34beb2f45..1cbfc40cec94 100644 --- a/packages/replay/src/replay.ts +++ b/packages/replay/src/replay.ts @@ -1,6 +1,6 @@ /* eslint-disable max-lines */ // TODO: We might want to split this file up import { addGlobalEventProcessor, captureException, getCurrentHub, setContext } from '@sentry/core'; -import { Breadcrumb, ReplayEvent } from '@sentry/types'; +import { Breadcrumb, ReplayEvent, TransportMakeRequestResponse } from '@sentry/types'; import { addInstrumentationHandler, logger } from '@sentry/utils'; import { EventType, record } from 'rrweb'; @@ -904,7 +904,7 @@ export class ReplayContainer implements ReplayContainerInterface { segmentId: segment_id, includeReplayStartTimestamp, eventContext, - }: SendReplay): Promise { + }: SendReplay): Promise { const payloadWithSequence = createPayload({ events, headers: { diff --git a/packages/types/src/transport.ts b/packages/types/src/transport.ts index b9beb2148f20..b254edecc6df 100644 --- a/packages/types/src/transport.ts +++ b/packages/types/src/transport.ts @@ -29,7 +29,8 @@ export interface BaseTransportOptions extends InternalBaseTransportOptions { } export interface Transport { - send(request: Envelope): PromiseLike; + // TODO (v8) Remove void from return as it was only retained to avoid a breaking change + send(request: Envelope): PromiseLike; flush(timeout?: number): PromiseLike; } From 7408b0a261d3ad966cbddae1d9e1b4ec2fddf482 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Fri, 6 Jan 2023 16:15:57 +0100 Subject: [PATCH 010/113] feat(replay): Remove `replayType` from tags and into `replay_event` (#6658) Moves `replayType` from tags to part of `replay_event`. Added in backend in https://github.com/getsentry/replay-backend/issues/210 --- packages/replay/src/replay.ts | 4 ++-- packages/replay/test/unit/index-errorSampleRate.test.ts | 4 ++-- packages/replay/test/unit/index.test.ts | 2 +- .../replay/test/unit/util/createReplayEnvelope.test.ts | 8 +++++--- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/replay/src/replay.ts b/packages/replay/src/replay.ts index 1cbfc40cec94..0e0546c853a8 100644 --- a/packages/replay/src/replay.ts +++ b/packages/replay/src/replay.ts @@ -936,6 +936,7 @@ export class ReplayContainer implements ReplayContainerInterface { urls, replay_id: replayId, segment_id, + replay_type: this.session?.sampled, }; const replayEvent = await getReplayEvent({ scope, client, event: baseEvent }); @@ -951,7 +952,6 @@ export class ReplayContainer implements ReplayContainerInterface { ...replayEvent.tags, sessionSampleRate: this._options.sessionSampleRate, errorSampleRate: this._options.errorSampleRate, - replayType: this.session?.sampled, }; /* @@ -970,6 +970,7 @@ export class ReplayContainer implements ReplayContainerInterface { ], "replay_id": "eventId", "segment_id": 3, + "replay_type": "error", "platform": "javascript", "event_id": "generated-uuid", "environment": "production", @@ -985,7 +986,6 @@ export class ReplayContainer implements ReplayContainerInterface { "tags": { "sessionSampleRate": 1, "errorSampleRate": 0, - "replayType": "error" } } */ diff --git a/packages/replay/test/unit/index-errorSampleRate.test.ts b/packages/replay/test/unit/index-errorSampleRate.test.ts index d6c848923118..02c715f7cb01 100644 --- a/packages/replay/test/unit/index-errorSampleRate.test.ts +++ b/packages/replay/test/unit/index-errorSampleRate.test.ts @@ -60,9 +60,9 @@ describe('Replay (errorSampleRate)', () => { expect(replay).toHaveSentReplay({ recordingPayloadHeader: { segment_id: 0 }, replayEventPayload: expect.objectContaining({ + replay_type: 'error', tags: expect.objectContaining({ errorSampleRate: 1, - replayType: 'error', sessionSampleRate: 0, }), }), @@ -90,9 +90,9 @@ describe('Replay (errorSampleRate)', () => { expect(replay).toHaveLastSentReplay({ recordingPayloadHeader: { segment_id: 1 }, replayEventPayload: expect.objectContaining({ + replay_type: 'error', tags: expect.objectContaining({ errorSampleRate: 1, - replayType: 'error', sessionSampleRate: 0, }), }), diff --git a/packages/replay/test/unit/index.test.ts b/packages/replay/test/unit/index.test.ts index b7f26d2d3a83..2bd6d61852ac 100644 --- a/packages/replay/test/unit/index.test.ts +++ b/packages/replay/test/unit/index.test.ts @@ -789,9 +789,9 @@ describe('Replay', () => { replayEventPayload: expect.objectContaining({ replay_start_timestamp: (BASE_TIMESTAMP - 10000) / 1000, urls: ['http://localhost/'], // this doesn't truly test if we are capturing the right URL as we don't change URLs, but good enough + replay_type: 'session', tags: expect.objectContaining({ errorSampleRate: 0, - replayType: 'session', sessionSampleRate: 1, }), }), diff --git a/packages/replay/test/unit/util/createReplayEnvelope.test.ts b/packages/replay/test/unit/util/createReplayEnvelope.test.ts index 456c0863e3fc..1e5ec5334784 100644 --- a/packages/replay/test/unit/util/createReplayEnvelope.test.ts +++ b/packages/replay/test/unit/util/createReplayEnvelope.test.ts @@ -23,10 +23,10 @@ describe('createReplayEnvelope', () => { name: 'sentry.javascript.browser', version: '7.25.0', }, + replay_type: 'error', tags: { sessionSampleRate: 1, errorSampleRate: 0, - replayType: 'error', }, }; @@ -59,9 +59,10 @@ describe('createReplayEnvelope', () => { event_id: REPLAY_ID, platform: 'javascript', replay_id: REPLAY_ID, + replay_type: 'error', sdk: { integrations: ['BrowserTracing', 'Replay'], name: 'sentry.javascript.browser', version: '7.25.0' }, segment_id: 3, - tags: { errorSampleRate: 0, replayType: 'error', sessionSampleRate: 1 }, + tags: { errorSampleRate: 0, sessionSampleRate: 1 }, timestamp: 1670837008.634, trace_ids: ['traceId'], type: 'replay_event', @@ -94,7 +95,8 @@ describe('createReplayEnvelope', () => { replay_id: REPLAY_ID, sdk: { integrations: ['BrowserTracing', 'Replay'], name: 'sentry.javascript.browser', version: '7.25.0' }, segment_id: 3, - tags: { errorSampleRate: 0, replayType: 'error', sessionSampleRate: 1 }, + replay_type: 'error', + tags: { errorSampleRate: 0, sessionSampleRate: 1 }, timestamp: 1670837008.634, trace_ids: ['traceId'], type: 'replay_event', From eef9ba6e8e5648967e026ca53ad460f26cd9d7e8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Jan 2023 08:18:37 +0000 Subject: [PATCH 011/113] build(deps): bump json5 in /packages/node/test/manual/webpack-domain Bumps [json5](https://github.com/json5/json5) from 1.0.1 to 1.0.2. - [Release notes](https://github.com/json5/json5/releases) - [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md) - [Commits](https://github.com/json5/json5/compare/v1.0.1...v1.0.2) --- updated-dependencies: - dependency-name: json5 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- packages/node/test/manual/webpack-domain/yarn.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/node/test/manual/webpack-domain/yarn.lock b/packages/node/test/manual/webpack-domain/yarn.lock index 211372cbec81..4c93e5750bcf 100644 --- a/packages/node/test/manual/webpack-domain/yarn.lock +++ b/packages/node/test/manual/webpack-domain/yarn.lock @@ -1391,9 +1391,9 @@ json-schema-traverse@^0.4.1: integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== json5@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" - integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== + version "1.0.2" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" + integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== dependencies: minimist "^1.2.0" @@ -1577,9 +1577,9 @@ minimatch@^3.0.4: brace-expansion "^1.1.7" minimist@^1.2.0, minimist@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" - integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + version "1.2.7" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" + integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== mississippi@^3.0.0: version "3.0.0" From 5a0a1f9e459a347b6aa9f400d22d034f8792a8bc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Jan 2023 08:19:01 +0000 Subject: [PATCH 012/113] build(deps): bump json5 from 1.0.1 to 1.0.2 in /packages/replay/demo Bumps [json5](https://github.com/json5/json5) from 1.0.1 to 1.0.2. - [Release notes](https://github.com/json5/json5/releases) - [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md) - [Commits](https://github.com/json5/json5/compare/v1.0.1...v1.0.2) --- updated-dependencies: - dependency-name: json5 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- packages/replay/demo/yarn.lock | 52 +++++++++++++++------------------- 1 file changed, 23 insertions(+), 29 deletions(-) diff --git a/packages/replay/demo/yarn.lock b/packages/replay/demo/yarn.lock index 4bc70e61d74f..3e9509ff43c9 100644 --- a/packages/replay/demo/yarn.lock +++ b/packages/replay/demo/yarn.lock @@ -1493,13 +1493,13 @@ "@sentry/utils" "7.1.1" tslib "^1.9.3" -"@sentry/core@7.24.0": - version "7.24.0" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.24.0.tgz#e856a071c702279854d6e5221957d61cd48036cc" - integrity sha512-QVRtmnaWEI0/MHIfBozgsMfh+7WU6OfpvUd72x1Dpk3Zk6Zs7Hqq0YfxfeBd7ApjNjGogPl1beaHcHAHlr3IyA== +"@sentry/core@7.28.1": + version "7.28.1" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.28.1.tgz#c712ce17469b18b01606108817be24a99ed2116e" + integrity sha512-7wvnuvn/mrAfcugWoCG/3pqDIrUgH5t+HisMJMGw0h9Tc33KqrmqMDCQVvjlrr2pWrw/vuUCFdm8CbUHJ832oQ== dependencies: - "@sentry/types" "7.24.0" - "@sentry/utils" "7.24.0" + "@sentry/types" "7.28.1" + "@sentry/utils" "7.28.1" tslib "^1.9.3" "@sentry/hub@7.1.1": @@ -1512,12 +1512,11 @@ tslib "^1.9.3" "@sentry/replay@file:..": - version "7.24.0" + version "7.28.1" dependencies: - "@sentry/core" "7.24.0" - "@sentry/types" "7.24.0" - "@sentry/utils" "7.24.0" - lodash.debounce "^4.0.8" + "@sentry/core" "7.28.1" + "@sentry/types" "7.28.1" + "@sentry/utils" "7.28.1" "@sentry/tracing@^7.1.1": version "7.1.1" @@ -1534,10 +1533,10 @@ resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.1.1.tgz#63aa68e7be36d63cc305d01af9119a4cdb186ae3" integrity sha512-5N1UMd2SqvUXprcIUMyDEju3H9lJY2oWfWQBGo0lG6Amn/lGAPAYlchg+4vQCLutDQMyd8K9zPwcbKn4u6gHdw== -"@sentry/types@7.24.0": - version "7.24.0" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.24.0.tgz#1acc841efe7bd56fc3eb647a613dba92631e8413" - integrity sha512-Xs4r9esBPieJUA6cGmMqfSQiinILdlhScjM+NqDSzxOo8+LRCJzckTLhUttBGVlaAoa4hjCEsfkHA1tVV1DycA== +"@sentry/types@7.28.1": + version "7.28.1" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.28.1.tgz#9018b4c152b475de9bedd267237393d3c9b1253d" + integrity sha512-DvSplMVrVEmOzR2M161V5+B8Up3vR71xMqJOpWTzE9TqtFJRGPtqT/5OBsNJJw1+/j2ssMcnKwbEo9Q2EGeS6g== "@sentry/utils@7.1.1": version "7.1.1" @@ -1547,12 +1546,12 @@ "@sentry/types" "7.1.1" tslib "^1.9.3" -"@sentry/utils@7.24.0": - version "7.24.0" - resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.24.0.tgz#73f100dc8942e73353473eaf3168e5052e2f45be" - integrity sha512-baaRDhHWHTyhmR6V8YKSo0NvN+D17pIKRDmb2vpWHVpTjobKCivNBLRoy3VhnIMS/24XyZnL028QLwkUNLg1Ug== +"@sentry/utils@7.28.1": + version "7.28.1" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.28.1.tgz#0a7b6aa4b09e91e4d1aded2a8c8dbaf818cee96e" + integrity sha512-75/jzLUO9HH09iC9TslNimGbxOP3jgn89P+q7uR+rp2fJfRExHVeKJZQdK0Ij4/SmE7TJ3Uh2r154N0INZEx1g== dependencies: - "@sentry/types" "7.24.0" + "@sentry/types" "7.28.1" tslib "^1.9.3" "@sinonjs/commons@^1.7.0": @@ -5548,9 +5547,9 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" json5@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" - integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== + version "1.0.2" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" + integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== dependencies: minimist "^1.2.0" @@ -5879,12 +5878,7 @@ minimatch@^5.0.1: dependencies: brace-expansion "^2.0.1" -minimist@^1.1.1, minimist@^1.2.5: - version "1.2.6" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" - integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== - -minimist@^1.2.0: +minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5: version "1.2.7" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== From 2fbd83587d8806fcb9e8a08d98ed4998656809a1 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Mon, 9 Jan 2023 10:09:06 +0100 Subject: [PATCH 013/113] Revert "fix(replay): Generate uuids for replay events (#6650)" (#6668) This reverts commit e6350d6e05d871fd91e6dd0134e03c5b351d18e9. --- packages/replay/src/replay.ts | 4 ++-- packages/replay/src/util/getReplayEvent.ts | 4 +++- packages/replay/test/unit/util/getReplayEvent.test.ts | 6 +++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/replay/src/replay.ts b/packages/replay/src/replay.ts index 0e0546c853a8..4645f151b847 100644 --- a/packages/replay/src/replay.ts +++ b/packages/replay/src/replay.ts @@ -939,7 +939,7 @@ export class ReplayContainer implements ReplayContainerInterface { replay_type: this.session?.sampled, }; - const replayEvent = await getReplayEvent({ scope, client, event: baseEvent }); + const replayEvent = await getReplayEvent({ scope, client, replayId, event: baseEvent }); if (!replayEvent) { // Taken from baseclient's `_processEvent` method, where this is handled for errors/transactions @@ -972,7 +972,7 @@ export class ReplayContainer implements ReplayContainerInterface { "segment_id": 3, "replay_type": "error", "platform": "javascript", - "event_id": "generated-uuid", + "event_id": "eventId", "environment": "production", "sdk": { "integrations": [ diff --git a/packages/replay/src/util/getReplayEvent.ts b/packages/replay/src/util/getReplayEvent.ts index f41c6f549515..5a0cfd0ac8ff 100644 --- a/packages/replay/src/util/getReplayEvent.ts +++ b/packages/replay/src/util/getReplayEvent.ts @@ -4,13 +4,15 @@ import { Client, ReplayEvent } from '@sentry/types'; export async function getReplayEvent({ client, scope, + replayId: event_id, event, }: { client: Client; scope: Scope; + replayId: string; event: ReplayEvent; }): Promise { - const preparedEvent = (await prepareEvent(client.getOptions(), event, {}, scope)) as ReplayEvent | null; + const preparedEvent = (await prepareEvent(client.getOptions(), event, { event_id }, scope)) as ReplayEvent | null; // If e.g. a global event processor returned null if (!preparedEvent) { diff --git a/packages/replay/test/unit/util/getReplayEvent.test.ts b/packages/replay/test/unit/util/getReplayEvent.test.ts index 2ac30c1a053c..31ab71747771 100644 --- a/packages/replay/test/unit/util/getReplayEvent.test.ts +++ b/packages/replay/test/unit/util/getReplayEvent.test.ts @@ -33,10 +33,11 @@ describe('getReplayEvent', () => { trace_ids: ['trace-ID'], urls: ['https://sentry.io/'], replay_id: replayId, + event_id: replayId, segment_id: 3, }; - const replayEvent = await getReplayEvent({ scope, client, event }); + const replayEvent = await getReplayEvent({ scope, client, replayId, event }); expect(replayEvent).toEqual({ type: 'replay_event', @@ -47,8 +48,7 @@ describe('getReplayEvent', () => { replay_id: 'replay-ID', segment_id: 3, platform: 'javascript', - // generated uuid with 32 chars - event_id: expect.stringMatching(/^\w{32}$/), + event_id: 'replay-ID', environment: 'production', sdk: { name: 'sentry.javascript.browser', From 012ff3461c5d40535de7cebed8efa7c6498f28b3 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Mon, 9 Jan 2023 10:32:24 +0100 Subject: [PATCH 014/113] fix(replay): Fix ts errors with `replay_type` (#6681) Co-authored-by: Francesco Novy --- packages/replay/src/replay.ts | 7 +++---- packages/replay/src/types.ts | 4 +--- packages/replay/test/unit/util/getReplayEvent.test.ts | 2 ++ packages/types/src/index.ts | 2 +- packages/types/src/replay.ts | 7 +++++++ 5 files changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/replay/src/replay.ts b/packages/replay/src/replay.ts index 4645f151b847..84465de40de5 100644 --- a/packages/replay/src/replay.ts +++ b/packages/replay/src/replay.ts @@ -1,6 +1,6 @@ /* eslint-disable max-lines */ // TODO: We might want to split this file up import { addGlobalEventProcessor, captureException, getCurrentHub, setContext } from '@sentry/core'; -import { Breadcrumb, ReplayEvent, TransportMakeRequestResponse } from '@sentry/types'; +import type { Breadcrumb, ReplayEvent, ReplayRecordingMode, TransportMakeRequestResponse } from '@sentry/types'; import { addInstrumentationHandler, logger } from '@sentry/utils'; import { EventType, record } from 'rrweb'; @@ -34,7 +34,6 @@ import type { RecordingOptions, ReplayContainer as ReplayContainerInterface, ReplayPluginOptions, - ReplayRecordingMode, SendReplay, Session, } from './types'; @@ -922,7 +921,7 @@ export class ReplayContainer implements ReplayContainerInterface { const transport = client && client.getTransport(); const dsn = client?.getDsn(); - if (!client || !scope || !transport || !dsn) { + if (!client || !scope || !transport || !dsn || !this.session || !this.session.sampled) { return; } @@ -936,7 +935,7 @@ export class ReplayContainer implements ReplayContainerInterface { urls, replay_id: replayId, segment_id, - replay_type: this.session?.sampled, + replay_type: this.session.sampled, }; const replayEvent = await getReplayEvent({ scope, client, replayId, event: baseEvent }); diff --git a/packages/replay/src/types.ts b/packages/replay/src/types.ts index c331273b135e..df35aefd2c60 100644 --- a/packages/replay/src/types.ts +++ b/packages/replay/src/types.ts @@ -1,4 +1,4 @@ -import { ReplayRecordingData } from '@sentry/types'; +import type { ReplayRecordingData, ReplayRecordingMode } from '@sentry/types'; import type { eventWithTime, recordOptions } from './types/rrweb'; @@ -9,8 +9,6 @@ export type RecordedEvents = Uint8Array | string; export type AllPerformanceEntry = PerformancePaintTiming | PerformanceResourceTiming | PerformanceNavigationTiming; -export type ReplayRecordingMode = 'session' | 'error'; - export interface SendReplay { events: RecordedEvents; replayId: string; diff --git a/packages/replay/test/unit/util/getReplayEvent.test.ts b/packages/replay/test/unit/util/getReplayEvent.test.ts index 31ab71747771..1949b722c545 100644 --- a/packages/replay/test/unit/util/getReplayEvent.test.ts +++ b/packages/replay/test/unit/util/getReplayEvent.test.ts @@ -34,6 +34,7 @@ describe('getReplayEvent', () => { urls: ['https://sentry.io/'], replay_id: replayId, event_id: replayId, + replay_type: 'session', segment_id: 3, }; @@ -46,6 +47,7 @@ describe('getReplayEvent', () => { trace_ids: ['trace-ID'], urls: ['https://sentry.io/'], replay_id: 'replay-ID', + replay_type: 'session', segment_id: 3, platform: 'javascript', event_id: 'replay-ID', diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 37857ce12155..a3437df77f1f 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -40,7 +40,7 @@ export type { ExtractedNodeRequestData, HttpHeaderValue, Primitive, WorkerLocati export type { ClientOptions, Options } from './options'; export type { Package } from './package'; export type { PolymorphicEvent, PolymorphicRequest } from './polymorphics'; -export type { ReplayEvent, ReplayRecordingData } from './replay'; +export type { ReplayEvent, ReplayRecordingData, ReplayRecordingMode } from './replay'; export type { QueryParams, Request } from './request'; export type { Runtime } from './runtime'; export type { CaptureContext, Scope, ScopeContext } from './scope'; diff --git a/packages/types/src/replay.ts b/packages/types/src/replay.ts index 1bec7202ecdc..2027a373bb33 100644 --- a/packages/types/src/replay.ts +++ b/packages/types/src/replay.ts @@ -10,6 +10,7 @@ export interface ReplayEvent extends Event { trace_ids: string[]; replay_id: string; segment_id: number; + replay_type: ReplayRecordingMode; } /** @@ -17,3 +18,9 @@ export interface ReplayEvent extends Event { * @hidden */ export type ReplayRecordingData = string | Uint8Array; + +/** + * NOTE: These types are still considered Beta and subject to change. + * @hidden + */ +export type ReplayRecordingMode = 'session' | 'error'; From 41dad559787554cdbd4acea3b0a892e0603158cf Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 9 Jan 2023 11:30:48 +0100 Subject: [PATCH 015/113] fix(nextjs): Don't write to `res.end` to fix `next export` (#6682) --- packages/nextjs/src/config/wrappers/utils/responseEnd.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/nextjs/src/config/wrappers/utils/responseEnd.ts b/packages/nextjs/src/config/wrappers/utils/responseEnd.ts index 21d5f5d850a5..82b6fb69ea28 100644 --- a/packages/nextjs/src/config/wrappers/utils/responseEnd.ts +++ b/packages/nextjs/src/config/wrappers/utils/responseEnd.ts @@ -31,7 +31,8 @@ export function autoEndTransactionOnResponseEnd(transaction: Transaction, res: S }; // Prevent double-wrapping - if (!(res.end as WrappedResponseEndMethod).__sentry_original__) { + // res.end may be undefined during build when using `next export` to statically export a Next.js app + if (res.end && !(res.end as WrappedResponseEndMethod).__sentry_original__) { fill(res, 'end', wrapEndMethod); } } From e5422c143495e5c385505228734f2b111e243794 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 9 Jan 2023 11:32:31 +0100 Subject: [PATCH 016/113] fix(nextjs): Don't wrap `res.json` and `res.send` (#6674) --- packages/nextjs/src/config/wrappers/types.ts | 5 ++++ .../src/config/wrappers/withSentryAPI.ts | 28 +++---------------- .../pages/api/doubleEndMethodOnVercel.ts | 11 ++++++++ .../test/server/doubleEndMethodOnVercel.js | 10 +++++++ 4 files changed, 30 insertions(+), 24 deletions(-) create mode 100644 packages/nextjs/test/integration/pages/api/doubleEndMethodOnVercel.ts create mode 100644 packages/nextjs/test/integration/test/server/doubleEndMethodOnVercel.js diff --git a/packages/nextjs/src/config/wrappers/types.ts b/packages/nextjs/src/config/wrappers/types.ts index 7ad042cc2474..a411c4ea62cf 100644 --- a/packages/nextjs/src/config/wrappers/types.ts +++ b/packages/nextjs/src/config/wrappers/types.ts @@ -25,6 +25,11 @@ import type { NextApiRequest, NextApiResponse } from 'next'; export type NextApiHandler = { (req: NextApiRequest, res: NextApiResponse): void | Promise | unknown | Promise; __sentry_route__?: string; + + /** + * A property we set in our integration tests to simulate running an API route on platforms that don't support streaming. + */ + __sentry_test_doesnt_support_streaming__?: true; }; export type WrappedNextApiHandler = { diff --git a/packages/nextjs/src/config/wrappers/withSentryAPI.ts b/packages/nextjs/src/config/wrappers/withSentryAPI.ts index e047be166ed4..561939685aa0 100644 --- a/packages/nextjs/src/config/wrappers/withSentryAPI.ts +++ b/packages/nextjs/src/config/wrappers/withSentryAPI.ts @@ -130,31 +130,11 @@ export function withSentry(origHandler: NextApiHandler, parameterizedRoute?: str ); currentScope.setSpan(transaction); - if (platformSupportsStreaming()) { + if (platformSupportsStreaming() && !origHandler.__sentry_test_doesnt_support_streaming__) { autoEndTransactionOnResponseEnd(transaction, res); } else { - // If we're not on a platform that supports streaming, we're blocking all response-ending methods until the - // queue is flushed. - - const origResSend = res.send; - res.send = async function (this: unknown, ...args: unknown[]) { - if (transaction) { - await finishTransaction(transaction, res); - await flushQueue(); - } - - origResSend.apply(this, args); - }; - - const origResJson = res.json; - res.json = async function (this: unknown, ...args: unknown[]) { - if (transaction) { - await finishTransaction(transaction, res); - await flushQueue(); - } - - origResJson.apply(this, args); - }; + // If we're not on a platform that supports streaming, we're blocking res.end() until the queue is flushed. + // res.json() and res.send() will implicitly call res.end(), so it is enough to wrap res.end(). // eslint-disable-next-line @typescript-eslint/unbound-method const origResEnd = res.end; @@ -223,7 +203,7 @@ export function withSentry(origHandler: NextApiHandler, parameterizedRoute?: str // moment they detect an error, so it's important to get this done before rethrowing the error. Apps not // deployed serverlessly will run into this cleanup code again in `res.end(), but the transaction will already // be finished and the queue will already be empty, so effectively it'll just no-op.) - if (platformSupportsStreaming()) { + if (platformSupportsStreaming() && !origHandler.__sentry_test_doesnt_support_streaming__) { void finishTransaction(transaction, res); } else { await finishTransaction(transaction, res); diff --git a/packages/nextjs/test/integration/pages/api/doubleEndMethodOnVercel.ts b/packages/nextjs/test/integration/pages/api/doubleEndMethodOnVercel.ts new file mode 100644 index 000000000000..f32fcf55fafd --- /dev/null +++ b/packages/nextjs/test/integration/pages/api/doubleEndMethodOnVercel.ts @@ -0,0 +1,11 @@ +import { NextApiRequest, NextApiResponse } from 'next'; + +const handler = async (_req: NextApiRequest, res: NextApiResponse): Promise => { + // This handler calls .end twice. We test this to verify that this still doesn't throw because we're wrapping `.end`. + res.status(200).json({ success: true }); + res.end(); +}; + +handler.__sentry_test_doesnt_support_streaming__ = true; + +export default handler; diff --git a/packages/nextjs/test/integration/test/server/doubleEndMethodOnVercel.js b/packages/nextjs/test/integration/test/server/doubleEndMethodOnVercel.js new file mode 100644 index 000000000000..fa2b0e7cbeb4 --- /dev/null +++ b/packages/nextjs/test/integration/test/server/doubleEndMethodOnVercel.js @@ -0,0 +1,10 @@ +const assert = require('assert'); +const { getAsync } = require('../utils/server'); + +// This test asserts that our wrapping of `res.end` doesn't break API routes on Vercel if people call `res.json` or +// `res.send` multiple times in one request handler. +// https://github.com/getsentry/sentry-javascript/issues/6670 +module.exports = async ({ url: urlBase }) => { + const response = await getAsync(`${urlBase}/api/doubleEndMethodOnVercel`); + assert.equal(response, '{"success":true}'); +}; From 1b4d9daffbc39946bcf0fa3cde259379ff66f227 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 9 Jan 2023 11:48:55 +0100 Subject: [PATCH 017/113] ref(replay): Add jsdoc to all replay modules (#6654) This is one more step of https://github.com/getsentry/sentry-javascript/issues/6323. Note that this also renames/moves a few things to make more sense. --- docs/event-sending.md | 4 +- packages/replay/.eslintrc.js | 4 +- .../src/coreHandlers/breadcrumbHandler.ts | 3 + packages/replay/src/coreHandlers/handleDom.ts | 3 + .../replay/src/coreHandlers/handleFetch.ts | 3 +- .../replay/src/coreHandlers/handleHistory.ts | 3 +- .../replay/src/coreHandlers/handleScope.ts | 3 + packages/replay/src/coreHandlers/handleXhr.ts | 3 +- packages/replay/src/createPerformanceEntry.ts | 85 ++++--------------- packages/replay/src/eventBuffer.ts | 24 +++++- packages/replay/src/integration.ts | 6 ++ packages/replay/src/replay.ts | 16 ++-- packages/replay/src/session/saveSession.ts | 3 + packages/replay/src/types.ts | 27 ++++++ packages/replay/src/util/addMemoryEntry.ts | 30 ++++++- packages/replay/src/util/createBreadcrumb.ts | 3 + .../replay/src/util/createPerformanceSpans.ts | 3 +- ...reatePayload.ts => createRecordingData.ts} | 5 +- .../replay/src/util/createReplayEnvelope.ts | 4 + packages/replay/src/util/isBrowser.ts | 3 + packages/replay/src/util/isRrwebError.ts | 3 + .../src/util/monkeyPatchRecordDroppedEvent.ts | 6 ++ ...etReplayEvent.ts => prepareReplayEvent.ts} | 5 +- ...ent.test.ts => prepareReplayEvent.test.ts} | 7 +- 24 files changed, 161 insertions(+), 95 deletions(-) rename packages/replay/src/util/{createPayload.ts => createRecordingData.ts} (90%) rename packages/replay/src/util/{getReplayEvent.ts => prepareReplayEvent.ts} (90%) rename packages/replay/test/unit/util/{getReplayEvent.test.ts => prepareReplayEvent.test.ts} (88%) diff --git a/docs/event-sending.md b/docs/event-sending.md index 06077e807ed3..f014e1f2f94e 100644 --- a/docs/event-sending.md +++ b/docs/event-sending.md @@ -77,8 +77,8 @@ This document gives an outline for how event sending works, and which which plac ## Replay (WIP) * `replay.sendReplayRequest()` - * `createPayload()` - * `getReplayEvent()` + * `createRecordingData()` + * `prepareReplayEvent()` * `client._prepareEvent()` (see baseclient) * `baseclient._applyClientOptions()` * `baseclient._applyIntegrationsMetadata()` diff --git a/packages/replay/.eslintrc.js b/packages/replay/.eslintrc.js index 9df1f4dffa5f..cd80e893576c 100644 --- a/packages/replay/.eslintrc.js +++ b/packages/replay/.eslintrc.js @@ -25,10 +25,8 @@ module.exports = { rules: { // TODO (high-prio): Re-enable this after migration '@typescript-eslint/explicit-member-accessibility': 'off', - // TODO (high-prio): Re-enable this after migration + // Since we target only es6 here, we can leave this off '@sentry-internal/sdk/no-async-await': 'off', - // TODO (medium-prio): Re-enable this after migration - 'jsdoc/require-jsdoc': 'off', }, }, { diff --git a/packages/replay/src/coreHandlers/breadcrumbHandler.ts b/packages/replay/src/coreHandlers/breadcrumbHandler.ts index fe0504b0230f..f790e28db3c1 100644 --- a/packages/replay/src/coreHandlers/breadcrumbHandler.ts +++ b/packages/replay/src/coreHandlers/breadcrumbHandler.ts @@ -4,6 +4,9 @@ import type { InstrumentationTypeBreadcrumb } from '../types'; import { DomHandlerData, handleDom } from './handleDom'; import { handleScope } from './handleScope'; +/** + * An event handler to react to breadcrumbs. + */ export function breadcrumbHandler(type: InstrumentationTypeBreadcrumb, handlerData: unknown): Breadcrumb | null { if (type === 'scope') { return handleScope(handlerData as Scope); diff --git a/packages/replay/src/coreHandlers/handleDom.ts b/packages/replay/src/coreHandlers/handleDom.ts index 412bbb6cefd9..976adf7761c7 100644 --- a/packages/replay/src/coreHandlers/handleDom.ts +++ b/packages/replay/src/coreHandlers/handleDom.ts @@ -9,6 +9,9 @@ export interface DomHandlerData { event: Node | { target: Node }; } +/** + * An event handler to react to DOM events. + */ export function handleDom(handlerData: DomHandlerData): Breadcrumb | null { // Taken from https://github.com/getsentry/sentry-javascript/blob/master/packages/browser/src/integrations/breadcrumbs.ts#L112 let target; diff --git a/packages/replay/src/coreHandlers/handleFetch.ts b/packages/replay/src/coreHandlers/handleFetch.ts index 961a18b638d6..290a58d4531d 100644 --- a/packages/replay/src/coreHandlers/handleFetch.ts +++ b/packages/replay/src/coreHandlers/handleFetch.ts @@ -1,5 +1,4 @@ -import type { ReplayPerformanceEntry } from '../createPerformanceEntry'; -import type { ReplayContainer } from '../types'; +import type { ReplayContainer, ReplayPerformanceEntry } from '../types'; import { createPerformanceSpans } from '../util/createPerformanceSpans'; import { shouldFilterRequest } from '../util/shouldFilterRequest'; diff --git a/packages/replay/src/coreHandlers/handleHistory.ts b/packages/replay/src/coreHandlers/handleHistory.ts index f806d2d3c75b..4732c0118831 100644 --- a/packages/replay/src/coreHandlers/handleHistory.ts +++ b/packages/replay/src/coreHandlers/handleHistory.ts @@ -1,5 +1,4 @@ -import { ReplayPerformanceEntry } from '../createPerformanceEntry'; -import type { ReplayContainer } from '../types'; +import type { ReplayContainer, ReplayPerformanceEntry } from '../types'; import { createPerformanceSpans } from '../util/createPerformanceSpans'; interface HistoryHandlerData { diff --git a/packages/replay/src/coreHandlers/handleScope.ts b/packages/replay/src/coreHandlers/handleScope.ts index 41cc4a6d4e02..429fc9d9a3bb 100644 --- a/packages/replay/src/coreHandlers/handleScope.ts +++ b/packages/replay/src/coreHandlers/handleScope.ts @@ -4,6 +4,9 @@ import { createBreadcrumb } from '../util/createBreadcrumb'; let _LAST_BREADCRUMB: null | Breadcrumb = null; +/** + * An event handler to handle scope changes. + */ export function handleScope(scope: Scope): Breadcrumb | null { const newBreadcrumb = scope.getLastBreadcrumb(); diff --git a/packages/replay/src/coreHandlers/handleXhr.ts b/packages/replay/src/coreHandlers/handleXhr.ts index a225345afe2f..883201d825e9 100644 --- a/packages/replay/src/coreHandlers/handleXhr.ts +++ b/packages/replay/src/coreHandlers/handleXhr.ts @@ -1,5 +1,4 @@ -import { ReplayPerformanceEntry } from '../createPerformanceEntry'; -import type { ReplayContainer } from '../types'; +import type { ReplayContainer, ReplayPerformanceEntry } from '../types'; import { createPerformanceSpans } from '../util/createPerformanceSpans'; import { shouldFilterRequest } from '../util/shouldFilterRequest'; diff --git a/packages/replay/src/createPerformanceEntry.ts b/packages/replay/src/createPerformanceEntry.ts index c4fb293429dd..e502e4d96247 100644 --- a/packages/replay/src/createPerformanceEntry.ts +++ b/packages/replay/src/createPerformanceEntry.ts @@ -2,40 +2,12 @@ import { browserPerformanceTimeOrigin } from '@sentry/utils'; import { record } from 'rrweb'; import { WINDOW } from './constants'; -import type { AllPerformanceEntry, PerformanceNavigationTiming, PerformancePaintTiming } from './types'; - -export interface ReplayPerformanceEntry { - /** - * One of these types https://developer.mozilla.org/en-US/docs/Web/API/PerformanceEntry/entryType - */ - type: string; - - /** - * A more specific description of the performance entry - */ - name: string; - - /** - * The start timestamp in seconds - */ - start: number; - - /** - * The end timestamp in seconds - */ - end: number; - - /** - * Additional unstructured data to be included - */ - data?: Record; -} - -interface MemoryInfo { - jsHeapSizeLimit: number; - totalJSHeapSize: number; - usedJSHeapSize: number; -} +import type { + AllPerformanceEntry, + PerformanceNavigationTiming, + PerformancePaintTiming, + ReplayPerformanceEntry, +} from './types'; // Map entryType -> function to normalize data for event // @ts-ignore TODO: entry type does not fit the create* functions entry type @@ -46,9 +18,12 @@ const ENTRY_TYPES: Record null | ReplayP // @ts-ignore TODO: entry type does not fit the create* functions entry type navigation: createNavigationEntry, // @ts-ignore TODO: entry type does not fit the create* functions entry type - ['largest-contentful-paint']: createLargestContentfulPaint, + 'largest-contentful-paint': createLargestContentfulPaint, }; +/** + * Create replay performance entries from the browser performance entries. + */ export function createPerformanceEntries(entries: AllPerformanceEntry[]): ReplayPerformanceEntry[] { return entries.map(createPerformanceEntry).filter(Boolean) as ReplayPerformanceEntry[]; } @@ -67,9 +42,7 @@ function getAbsoluteTime(time: number): number { return ((browserPerformanceTimeOrigin || WINDOW.performance.timeOrigin) + time) / 1000; } -// TODO: type definition! -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -function createPaintEntry(entry: PerformancePaintTiming) { +function createPaintEntry(entry: PerformancePaintTiming): ReplayPerformanceEntry { const { duration, entryType, name, startTime } = entry; const start = getAbsoluteTime(startTime); @@ -81,9 +54,7 @@ function createPaintEntry(entry: PerformancePaintTiming) { }; } -// TODO: type definition! -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -function createNavigationEntry(entry: PerformanceNavigationTiming) { +function createNavigationEntry(entry: PerformanceNavigationTiming): ReplayPerformanceEntry | null { // TODO: There looks to be some more interesting bits in here (domComplete, domContentLoaded) const { entryType, name, duration, domComplete, startTime, transferSize, type } = entry; @@ -104,9 +75,7 @@ function createNavigationEntry(entry: PerformanceNavigationTiming) { }; } -// TODO: type definition! -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -function createResourceEntry(entry: PerformanceResourceTiming) { +function createResourceEntry(entry: PerformanceResourceTiming): ReplayPerformanceEntry | null { const { entryType, initiatorType, name, responseEnd, startTime, encodedBodySize, transferSize } = entry; // Core SDK handles these @@ -126,9 +95,9 @@ function createResourceEntry(entry: PerformanceResourceTiming) { }; } -// TODO: type definition! -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -function createLargestContentfulPaint(entry: PerformanceEntry & { size: number; element: Node }) { +function createLargestContentfulPaint( + entry: PerformanceEntry & { size: number; element: Node }, +): ReplayPerformanceEntry { const { duration, entryType, startTime, size } = entry; const start = getAbsoluteTime(startTime); @@ -147,25 +116,3 @@ function createLargestContentfulPaint(entry: PerformanceEntry & { size: number; }, }; } - -type ReplayMemoryEntry = ReplayPerformanceEntry & { data: { memory: MemoryInfo } }; - -export function createMemoryEntry(memoryEntry: MemoryInfo): ReplayMemoryEntry { - const { jsHeapSizeLimit, totalJSHeapSize, usedJSHeapSize } = memoryEntry; - // we don't want to use `getAbsoluteTime` because it adds the event time to the - // time origin, so we get the current timestamp instead - const time = new Date().getTime() / 1000; - return { - type: 'memory', - name: 'memory', - start: time, - end: time, - data: { - memory: { - jsHeapSizeLimit, - totalJSHeapSize, - usedJSHeapSize, - }, - }, - }; -} diff --git a/packages/replay/src/eventBuffer.ts b/packages/replay/src/eventBuffer.ts index f5fbfb2497ff..a755ab0676e8 100644 --- a/packages/replay/src/eventBuffer.ts +++ b/packages/replay/src/eventBuffer.ts @@ -12,6 +12,9 @@ interface CreateEventBufferParams { useCompression: boolean; } +/** + * Create an event buffer for replays. + */ export function createEventBuffer({ useCompression }: CreateEventBufferParams): EventBuffer { // eslint-disable-next-line no-restricted-globals if (useCompression && window.Worker) { @@ -72,7 +75,10 @@ class EventBufferArray implements EventBuffer { } } -// exporting for testing +/** + * Event buffer that uses a web worker to compress events. + * Exported only for testing. + */ export class EventBufferCompressionWorker implements EventBuffer { private _worker: null | Worker; private _eventBufferItemLength: number = 0; @@ -90,12 +96,18 @@ export class EventBufferCompressionWorker implements EventBuffer { return this._eventBufferItemLength; } + /** + * Destroy the event buffer. + */ public destroy(): void { __DEBUG_BUILD__ && logger.log('[Replay] Destroying compression worker'); this._worker?.terminate(); this._worker = null; } + /** + * Add an event to the event buffer. + */ public async addEvent(event: RecordingEvent, isCheckout?: boolean): Promise { if (isCheckout) { // This event is a checkout, make sure worker buffer is cleared before @@ -110,6 +122,9 @@ export class EventBufferCompressionWorker implements EventBuffer { return this._sendEventToWorker(event); } + /** + * Finish the event buffer and return the compressed data. + */ public finish(): Promise { return this._finishRequest(this._getAndIncrementId()); } @@ -160,6 +175,9 @@ export class EventBufferCompressionWorker implements EventBuffer { }); } + /** + * Send the event to the worker. + */ private _sendEventToWorker(event: RecordingEvent): Promise { const promise = this._postMessage({ id: this._getAndIncrementId(), @@ -173,6 +191,9 @@ export class EventBufferCompressionWorker implements EventBuffer { return promise; } + /** + * Finish the request and return the compressed data from the worker. + */ private async _finishRequest(id: number): Promise { const promise = this._postMessage({ id, method: 'finish', args: [] }); @@ -182,6 +203,7 @@ export class EventBufferCompressionWorker implements EventBuffer { return promise as Promise; } + /** Get the current ID and increment it for the next call. */ private _getAndIncrementId(): number { return this._id++; } diff --git a/packages/replay/src/integration.ts b/packages/replay/src/integration.ts index d5fe1275f7bd..865472c1571c 100644 --- a/packages/replay/src/integration.ts +++ b/packages/replay/src/integration.ts @@ -18,6 +18,9 @@ const MEDIA_SELECTORS = 'img,image,svg,path,rect,area,video,object,picture,embed let _initialized = false; +/** + * The main replay integration class, to be passed to `init({ integrations: [] })`. + */ export class Replay implements Integration { /** * @inheritDoc @@ -126,10 +129,12 @@ Sentry.init({ replaysOnErrorSampleRate: ${errorSampleRate} })`, this._isInitialized = true; } + /** If replay has already been initialized */ protected get _isInitialized(): boolean { return _initialized; } + /** Update _isInitialized */ protected set _isInitialized(value: boolean) { _initialized = value; } @@ -181,6 +186,7 @@ Sentry.init({ replaysOnErrorSampleRate: ${errorSampleRate} })`, this._replay.stop(); } + /** Setup the integration. */ private _setup(): void { // Client is not available in constructor, so we need to wait until setupOnce this._loadReplayOptionsFromClient(); diff --git a/packages/replay/src/replay.ts b/packages/replay/src/replay.ts index 84465de40de5..a54a5453e689 100644 --- a/packages/replay/src/replay.ts +++ b/packages/replay/src/replay.ts @@ -40,14 +40,14 @@ import type { import { addEvent } from './util/addEvent'; import { addMemoryEntry } from './util/addMemoryEntry'; import { createBreadcrumb } from './util/createBreadcrumb'; -import { createPayload } from './util/createPayload'; import { createPerformanceSpans } from './util/createPerformanceSpans'; +import { createRecordingData } from './util/createRecordingData'; import { createReplayEnvelope } from './util/createReplayEnvelope'; import { debounce } from './util/debounce'; -import { getReplayEvent } from './util/getReplayEvent'; import { isExpired } from './util/isExpired'; import { isSessionExpired } from './util/isSessionExpired'; import { overwriteRecordDroppedEvent, restoreRecordDroppedEvent } from './util/monkeyPatchRecordDroppedEvent'; +import { prepareReplayEvent } from './util/prepareReplayEvent'; /** * Returns true to return control to calling function, otherwise continue with normal batching @@ -56,6 +56,9 @@ import { overwriteRecordDroppedEvent, restoreRecordDroppedEvent } from './util/m const BASE_RETRY_INTERVAL = 5000; const MAX_RETRY_COUNT = 3; +/** + * The main replay container class, which holds all the state and methods for recording and sending replays. + */ export class ReplayContainer implements ReplayContainerInterface { public eventBuffer: EventBuffer | null = null; @@ -904,7 +907,7 @@ export class ReplayContainer implements ReplayContainerInterface { includeReplayStartTimestamp, eventContext, }: SendReplay): Promise { - const payloadWithSequence = createPayload({ + const recordingData = createRecordingData({ events, headers: { segment_id, @@ -938,7 +941,7 @@ export class ReplayContainer implements ReplayContainerInterface { replay_type: this.session.sampled, }; - const replayEvent = await getReplayEvent({ scope, client, replayId, event: baseEvent }); + const replayEvent = await prepareReplayEvent({ scope, client, replayId, event: baseEvent }); if (!replayEvent) { // Taken from baseclient's `_processEvent` method, where this is handled for errors/transactions @@ -989,7 +992,7 @@ export class ReplayContainer implements ReplayContainerInterface { } */ - const envelope = createReplayEnvelope(replayEvent, payloadWithSequence, dsn, client.getOptions().tunnel); + const envelope = createReplayEnvelope(replayEvent, recordingData, dsn, client.getOptions().tunnel); try { return await transport.send(envelope); @@ -998,6 +1001,9 @@ export class ReplayContainer implements ReplayContainerInterface { } } + /** + * Reset the counter of retries for sending replays. + */ resetRetries(): void { this._retryCount = 0; this._retryInterval = BASE_RETRY_INTERVAL; diff --git a/packages/replay/src/session/saveSession.ts b/packages/replay/src/session/saveSession.ts index a506625436f8..8f75d0ab50ed 100644 --- a/packages/replay/src/session/saveSession.ts +++ b/packages/replay/src/session/saveSession.ts @@ -1,6 +1,9 @@ import { REPLAY_SESSION_KEY, WINDOW } from '../constants'; import type { Session } from '../types'; +/** + * Save a session to session storage. + */ export function saveSession(session: Session): void { const hasSessionStorage = 'sessionStorage' in WINDOW; if (!hasSessionStorage) { diff --git a/packages/replay/src/types.ts b/packages/replay/src/types.ts index df35aefd2c60..029bd109c31f 100644 --- a/packages/replay/src/types.ts +++ b/packages/replay/src/types.ts @@ -235,3 +235,30 @@ export interface ReplayContainer { addUpdate(cb: AddUpdateCallback): void; getOptions(): ReplayPluginOptions; } + +export interface ReplayPerformanceEntry { + /** + * One of these types https://developer.mozilla.org/en-US/docs/Web/API/PerformanceEntry/entryType + */ + type: string; + + /** + * A more specific description of the performance entry + */ + name: string; + + /** + * The start timestamp in seconds + */ + start: number; + + /** + * The end timestamp in seconds + */ + end: number; + + /** + * Additional unstructured data to be included + */ + data?: Record; +} diff --git a/packages/replay/src/util/addMemoryEntry.ts b/packages/replay/src/util/addMemoryEntry.ts index bac07200cd6c..0b847a25a42d 100644 --- a/packages/replay/src/util/addMemoryEntry.ts +++ b/packages/replay/src/util/addMemoryEntry.ts @@ -1,7 +1,15 @@ import { WINDOW } from '../constants'; -import type { ReplayContainer } from '../types'; +import type { ReplayContainer, ReplayPerformanceEntry } from '../types'; import { createPerformanceSpans } from './createPerformanceSpans'; +type ReplayMemoryEntry = ReplayPerformanceEntry & { data: { memory: MemoryInfo } }; + +interface MemoryInfo { + jsHeapSizeLimit: number; + totalJSHeapSize: number; + usedJSHeapSize: number; +} + /** * Create a "span" for the total amount of memory being used by JS objects * (including v8 internal objects). @@ -17,3 +25,23 @@ export function addMemoryEntry(replay: ReplayContainer): void { // Do nothing } } + +function createMemoryEntry(memoryEntry: MemoryInfo): ReplayMemoryEntry { + const { jsHeapSizeLimit, totalJSHeapSize, usedJSHeapSize } = memoryEntry; + // we don't want to use `getAbsoluteTime` because it adds the event time to the + // time origin, so we get the current timestamp instead + const time = new Date().getTime() / 1000; + return { + type: 'memory', + name: 'memory', + start: time, + end: time, + data: { + memory: { + jsHeapSizeLimit, + totalJSHeapSize, + usedJSHeapSize, + }, + }, + }; +} diff --git a/packages/replay/src/util/createBreadcrumb.ts b/packages/replay/src/util/createBreadcrumb.ts index bb1d2eb49ec1..b9f7527b0180 100644 --- a/packages/replay/src/util/createBreadcrumb.ts +++ b/packages/replay/src/util/createBreadcrumb.ts @@ -2,6 +2,9 @@ import type { Breadcrumb } from '@sentry/types'; type RequiredProperties = 'category' | 'message'; +/** + * Create a breadcrumb for a replay. + */ export function createBreadcrumb( breadcrumb: Pick & Partial>, ): Breadcrumb { diff --git a/packages/replay/src/util/createPerformanceSpans.ts b/packages/replay/src/util/createPerformanceSpans.ts index 9bb999a0faa3..f7327ff488b0 100644 --- a/packages/replay/src/util/createPerformanceSpans.ts +++ b/packages/replay/src/util/createPerformanceSpans.ts @@ -1,7 +1,6 @@ import { EventType } from 'rrweb'; -import { ReplayPerformanceEntry } from '../createPerformanceEntry'; -import type { ReplayContainer } from '../types'; +import type { ReplayContainer, ReplayPerformanceEntry } from '../types'; import { addEvent } from './addEvent'; /** diff --git a/packages/replay/src/util/createPayload.ts b/packages/replay/src/util/createRecordingData.ts similarity index 90% rename from packages/replay/src/util/createPayload.ts rename to packages/replay/src/util/createRecordingData.ts index b3b6615b1b40..63c1db5f6e7b 100644 --- a/packages/replay/src/util/createPayload.ts +++ b/packages/replay/src/util/createRecordingData.ts @@ -2,7 +2,10 @@ import { ReplayRecordingData } from '@sentry/types'; import type { RecordedEvents } from '../types'; -export function createPayload({ +/** + * Create the recording data ready to be sent. + */ +export function createRecordingData({ events, headers, }: { diff --git a/packages/replay/src/util/createReplayEnvelope.ts b/packages/replay/src/util/createReplayEnvelope.ts index 3d950e06cb29..3a052f32fdd6 100644 --- a/packages/replay/src/util/createReplayEnvelope.ts +++ b/packages/replay/src/util/createReplayEnvelope.ts @@ -1,6 +1,10 @@ import { DsnComponents, ReplayEnvelope, ReplayEvent, ReplayRecordingData } from '@sentry/types'; import { createEnvelope, createEventEnvelopeHeaders, getSdkMetadataForEnvelopeHeader } from '@sentry/utils'; +/** + * Create a replay envelope ready to be sent. + * This includes both the replay event, as well as the recording data. + */ export function createReplayEnvelope( replayEvent: ReplayEvent, recordingData: ReplayRecordingData, diff --git a/packages/replay/src/util/isBrowser.ts b/packages/replay/src/util/isBrowser.ts index 3ad78dce93a5..6a64317ba3fa 100644 --- a/packages/replay/src/util/isBrowser.ts +++ b/packages/replay/src/util/isBrowser.ts @@ -1,5 +1,8 @@ import { isNodeEnv } from '@sentry/utils'; +/** + * Returns true if we are in the browser. + */ export function isBrowser(): boolean { // eslint-disable-next-line no-restricted-globals return typeof window !== 'undefined' && (!isNodeEnv() || isElectronNodeRenderer()); diff --git a/packages/replay/src/util/isRrwebError.ts b/packages/replay/src/util/isRrwebError.ts index d9b065857062..1607f55255ef 100644 --- a/packages/replay/src/util/isRrwebError.ts +++ b/packages/replay/src/util/isRrwebError.ts @@ -1,5 +1,8 @@ import { Event } from '@sentry/types'; +/** + * Returns true if we think the given event is an error originating inside of rrweb. + */ export function isRrwebError(event: Event): boolean { if (event.type || !event.exception?.values?.length) { return false; diff --git a/packages/replay/src/util/monkeyPatchRecordDroppedEvent.ts b/packages/replay/src/util/monkeyPatchRecordDroppedEvent.ts index 76825f727e6b..70cf11faf450 100644 --- a/packages/replay/src/util/monkeyPatchRecordDroppedEvent.ts +++ b/packages/replay/src/util/monkeyPatchRecordDroppedEvent.ts @@ -3,6 +3,9 @@ import { Client, DataCategory, Event, EventDropReason } from '@sentry/types'; let _originalRecordDroppedEvent: Client['recordDroppedEvent'] | undefined; +/** + * Overwrite the `recordDroppedEvent` method on the client, so we can find out which events were dropped. + * */ export function overwriteRecordDroppedEvent(errorIds: Set): void { const client = getCurrentHub().getClient(); @@ -28,6 +31,9 @@ export function overwriteRecordDroppedEvent(errorIds: Set): void { _originalRecordDroppedEvent = _originalCallback; } +/** + * Restore the original method. + * */ export function restoreRecordDroppedEvent(): void { const client = getCurrentHub().getClient(); diff --git a/packages/replay/src/util/getReplayEvent.ts b/packages/replay/src/util/prepareReplayEvent.ts similarity index 90% rename from packages/replay/src/util/getReplayEvent.ts rename to packages/replay/src/util/prepareReplayEvent.ts index 5a0cfd0ac8ff..a7e2fd6da995 100644 --- a/packages/replay/src/util/getReplayEvent.ts +++ b/packages/replay/src/util/prepareReplayEvent.ts @@ -1,7 +1,10 @@ import { prepareEvent, Scope } from '@sentry/core'; import { Client, ReplayEvent } from '@sentry/types'; -export async function getReplayEvent({ +/** + * Prepare a replay event & enrich it with the SDK metadata. + */ +export async function prepareReplayEvent({ client, scope, replayId: event_id, diff --git a/packages/replay/test/unit/util/getReplayEvent.test.ts b/packages/replay/test/unit/util/prepareReplayEvent.test.ts similarity index 88% rename from packages/replay/test/unit/util/getReplayEvent.test.ts rename to packages/replay/test/unit/util/prepareReplayEvent.test.ts index 1949b722c545..4c7785bf3735 100644 --- a/packages/replay/test/unit/util/getReplayEvent.test.ts +++ b/packages/replay/test/unit/util/prepareReplayEvent.test.ts @@ -3,10 +3,10 @@ import { getCurrentHub, Hub, Scope } from '@sentry/core'; import { Client, ReplayEvent } from '@sentry/types'; import { REPLAY_EVENT_NAME } from '../../../src/constants'; -import { getReplayEvent } from '../../../src/util/getReplayEvent'; +import { prepareReplayEvent } from '../../../src/util/prepareReplayEvent'; import { getDefaultBrowserClientOptions } from '../../utils/getDefaultBrowserClientOptions'; -describe('getReplayEvent', () => { +describe('prepareReplayEvent', () => { let hub: Hub; let client: Client; let scope: Scope; @@ -33,12 +33,11 @@ describe('getReplayEvent', () => { trace_ids: ['trace-ID'], urls: ['https://sentry.io/'], replay_id: replayId, - event_id: replayId, replay_type: 'session', segment_id: 3, }; - const replayEvent = await getReplayEvent({ scope, client, replayId, event }); + const replayEvent = await prepareReplayEvent({ scope, client, replayId, event }); expect(replayEvent).toEqual({ type: 'replay_event', From 7316b850988df64467a7052082db84461f8a2cd2 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 9 Jan 2023 11:57:37 +0100 Subject: [PATCH 018/113] fix(nextjs): Exclude SDK from Edge runtime bundles (#6683) --- .../nextjs/src/config/loaders/proxyLoader.ts | 7 ++++ .../templates/apiProxyLoaderTemplate.ts | 18 ++--------- packages/nextjs/src/config/types.ts | 2 +- packages/nextjs/src/config/webpack.ts | 32 +++++++++++++++++-- packages/nextjs/src/index.client.ts | 15 --------- packages/nextjs/src/index.server.ts | 11 ------- 6 files changed, 40 insertions(+), 45 deletions(-) diff --git a/packages/nextjs/src/config/loaders/proxyLoader.ts b/packages/nextjs/src/config/loaders/proxyLoader.ts index c6bee0ec9f3a..42aa1a7a230f 100644 --- a/packages/nextjs/src/config/loaders/proxyLoader.ts +++ b/packages/nextjs/src/config/loaders/proxyLoader.ts @@ -9,6 +9,7 @@ type LoaderOptions = { pagesDir: string; pageExtensionRegex: string; excludeServerRoutes: Array; + isEdgeRuntime: boolean; }; /** @@ -22,8 +23,14 @@ export default async function proxyLoader(this: LoaderThis, userC pagesDir, pageExtensionRegex, excludeServerRoutes = [], + isEdgeRuntime, } = 'getOptions' in this ? this.getOptions() : this.query; + // We currently don't support the edge runtime + if (isEdgeRuntime) { + return userCode; + } + // Get the parameterized route name from this page's filepath const parameterizedRoute = path // Get the path of the file insde of the pages directory diff --git a/packages/nextjs/src/config/templates/apiProxyLoaderTemplate.ts b/packages/nextjs/src/config/templates/apiProxyLoaderTemplate.ts index 230eb55e457f..1cd7c40181eb 100644 --- a/packages/nextjs/src/config/templates/apiProxyLoaderTemplate.ts +++ b/packages/nextjs/src/config/templates/apiProxyLoaderTemplate.ts @@ -23,7 +23,7 @@ type NextApiModule = ( } // CJS export | NextApiHandler -) & { config?: PageConfig & { runtime?: string } }; +) & { config?: PageConfig }; const userApiModule = origModule as NextApiModule; @@ -53,21 +53,7 @@ export const config = { }, }; -// This is a variable that Next.js will string replace during build with a string if run in an edge runtime from Next.js -// v12.2.1-canary.3 onwards: -// https://github.com/vercel/next.js/blob/166e5fb9b92f64c4b5d1f6560a05e2b9778c16fb/packages/next/build/webpack-config.ts#L206 -// https://edge-runtime.vercel.sh/features/available-apis#addressing-the-runtime -declare const EdgeRuntime: string | undefined; - -let exportedHandler; - -if (typeof EdgeRuntime === 'string') { - exportedHandler = userProvidedHandler; -} else { - exportedHandler = userProvidedHandler ? Sentry.withSentryAPI(userProvidedHandler, '__ROUTE__') : undefined; -} - -export default exportedHandler; +export default userProvidedHandler ? Sentry.withSentryAPI(userProvidedHandler, '__ROUTE__') : undefined; // Re-export anything exported by the page module we're wrapping. When processing this code, Rollup is smart enough to // not include anything whose name matchs something we've explicitly exported above. diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index f596252fd472..6a815664e527 100644 --- a/packages/nextjs/src/config/types.ts +++ b/packages/nextjs/src/config/types.ts @@ -137,7 +137,7 @@ export type BuildContext = { // eslint-disable-next-line @typescript-eslint/no-explicit-any defaultLoaders: any; totalPages: number; - nextRuntime?: 'nodejs' | 'edge'; + nextRuntime?: 'nodejs' | 'edge'; // Added in Next.js 12+ }; /** diff --git a/packages/nextjs/src/config/webpack.ts b/packages/nextjs/src/config/webpack.ts index 988c6cc8b4b3..15cf51e2b742 100644 --- a/packages/nextjs/src/config/webpack.ts +++ b/packages/nextjs/src/config/webpack.ts @@ -85,6 +85,13 @@ export function constructWebpackConfigFunction( // Add a loader which will inject code that sets global values addValueInjectionLoader(newConfig, userNextConfig, userSentryOptions); + if (buildContext.nextRuntime === 'edge') { + // eslint-disable-next-line no-console + console.warn( + '[@sentry/nextjs] You are using edge functions or middleware. Please note that Sentry does not yet support error monitoring for these features.', + ); + } + if (isServer) { if (userSentryOptions.autoInstrumentServerFunctions !== false) { const pagesDir = newConfig.resolve?.alias?.['private-next-pages'] as string; @@ -102,6 +109,7 @@ export function constructWebpackConfigFunction( pagesDir, pageExtensionRegex, excludeServerRoutes: userSentryOptions.excludeServerRoutes, + isEdgeRuntime: buildContext.nextRuntime === 'edge', }, }, ], @@ -305,7 +313,15 @@ async function addSentryToEntryProperty( // inject into all entry points which might contain user's code for (const entryPointName in newEntryProperty) { - if (shouldAddSentryToEntryPoint(entryPointName, isServer, userSentryOptions.excludeServerRoutes, isDev)) { + if ( + shouldAddSentryToEntryPoint( + entryPointName, + isServer, + userSentryOptions.excludeServerRoutes, + isDev, + buildContext.nextRuntime === 'edge', + ) + ) { addFilesToExistingEntryPoint(newEntryProperty, entryPointName, filesToInject); } else { if ( @@ -432,7 +448,13 @@ function shouldAddSentryToEntryPoint( isServer: boolean, excludeServerRoutes: Array = [], isDev: boolean, + isEdgeRuntime: boolean, ): boolean { + // We don't support the Edge runtime yet + if (isEdgeRuntime) { + return false; + } + // On the server side, by default we inject the `Sentry.init()` code into every page (with a few exceptions). if (isServer) { const entryPointRoute = entryPointName.replace(/^pages/, ''); @@ -529,7 +551,13 @@ export function getWebpackPluginOptions( stripPrefix: ['webpack://_N_E/'], urlPrefix, entries: (entryPointName: string) => - shouldAddSentryToEntryPoint(entryPointName, isServer, userSentryOptions.excludeServerRoutes, isDev), + shouldAddSentryToEntryPoint( + entryPointName, + isServer, + userSentryOptions.excludeServerRoutes, + isDev, + buildContext.nextRuntime === 'edge', + ), release: getSentryRelease(buildId), dryRun: isDev, }); diff --git a/packages/nextjs/src/index.client.ts b/packages/nextjs/src/index.client.ts index 2d9e29d48ac8..98ea9928b8ed 100644 --- a/packages/nextjs/src/index.client.ts +++ b/packages/nextjs/src/index.client.ts @@ -2,7 +2,6 @@ import { RewriteFrames } from '@sentry/integrations'; import { configureScope, init as reactInit, Integrations } from '@sentry/react'; import { BrowserTracing, defaultRequestInstrumentationOptions, hasTracingEnabled } from '@sentry/tracing'; import { EventProcessor } from '@sentry/types'; -import { logger } from '@sentry/utils'; import { nextRouterInstrumentation } from './performance/client'; import { buildMetadata } from './utils/metadata'; @@ -31,26 +30,12 @@ export { BrowserTracing }; // Treeshakable guard to remove all code related to tracing declare const __SENTRY_TRACING__: boolean; -// This is a variable that Next.js will string replace during build with a string if run in an edge runtime from Next.js -// v12.2.1-canary.3 onwards: -// https://github.com/vercel/next.js/blob/166e5fb9b92f64c4b5d1f6560a05e2b9778c16fb/packages/next/build/webpack-config.ts#L206 -// https://edge-runtime.vercel.sh/features/available-apis#addressing-the-runtime -declare const EdgeRuntime: string | undefined; - const globalWithInjectedValues = global as typeof global & { __rewriteFramesAssetPrefixPath__: string; }; /** Inits the Sentry NextJS SDK on the browser with the React SDK. */ export function init(options: NextjsOptions): void { - if (typeof EdgeRuntime === 'string') { - // If the SDK is imported when using the Vercel Edge Runtime, it will import the browser SDK, even though it is - // running the server part of a Next.js application. We can use the `EdgeRuntime` to check for that case and make - // the init call a no-op. This will prevent the SDK from crashing on the Edge Runtime. - __DEBUG_BUILD__ && logger.log('Vercel Edge Runtime detected. Will not initialize SDK.'); - return; - } - applyTunnelRouteOption(options); buildMetadata(options, ['nextjs', 'react']); options.environment = options.environment || process.env.NODE_ENV; diff --git a/packages/nextjs/src/index.server.ts b/packages/nextjs/src/index.server.ts index 922be7aa09b6..0500bd802300 100644 --- a/packages/nextjs/src/index.server.ts +++ b/packages/nextjs/src/index.server.ts @@ -30,12 +30,6 @@ const globalWithInjectedValues = global as typeof global & { const domain = domainModule as typeof domainModule & { active: (domainModule.Domain & Carrier) | null }; -// This is a variable that Next.js will string replace during build with a string if run in an edge runtime from Next.js -// v12.2.1-canary.3 onwards: -// https://github.com/vercel/next.js/blob/166e5fb9b92f64c4b5d1f6560a05e2b9778c16fb/packages/next/build/webpack-config.ts#L206 -// https://edge-runtime.vercel.sh/features/available-apis#addressing-the-runtime -declare const EdgeRuntime: string | undefined; - // Exporting this constant means we can compute it without the linter complaining, even if we stop directly using it in // this file. It's important that it be computed as early as possible, because one of its indicators is seeing 'build' // (as in the CLI command `next build`) in `process.argv`. Later on in the build process, everything's been spun out @@ -51,11 +45,6 @@ export function init(options: NextjsOptions): void { logger.enable(); } - if (typeof EdgeRuntime === 'string') { - __DEBUG_BUILD__ && logger.log('Vercel Edge Runtime detected. Will not initialize SDK.'); - return; - } - __DEBUG_BUILD__ && logger.log('Initializing SDK...'); if (sdkAlreadyInitialized()) { From f05239460073918a23bce8b2bb03bab2d2b93143 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20FIDRY?= <5175937+theofidry@users.noreply.github.com> Date: Mon, 9 Jan 2023 12:16:13 +0100 Subject: [PATCH 019/113] ref(angular) Add error-like objects handling (#6446) It is more beneficial to handle error shaped objects instead of just instances of `Error`s --- packages/angular/src/errorhandler.ts | 39 ++++++++++++++++++---- packages/angular/test/errorhandler.test.ts | 33 +++++------------- 2 files changed, 41 insertions(+), 31 deletions(-) diff --git a/packages/angular/src/errorhandler.ts b/packages/angular/src/errorhandler.ts index 8cfe2427f409..cf89b0e69eec 100644 --- a/packages/angular/src/errorhandler.ts +++ b/packages/angular/src/errorhandler.ts @@ -2,7 +2,7 @@ import { HttpErrorResponse } from '@angular/common/http'; import { ErrorHandler as AngularErrorHandler, Inject, Injectable } from '@angular/core'; import * as Sentry from '@sentry/browser'; import { captureException } from '@sentry/browser'; -import { addExceptionMechanism } from '@sentry/utils'; +import { addExceptionMechanism, isString } from '@sentry/utils'; import { runOutsideAngular } from './zone'; @@ -32,7 +32,7 @@ function tryToUnwrapZonejsError(error: unknown): unknown | Error { function extractHttpModuleError(error: HttpErrorResponse): string | Error { // The `error` property of http exception can be either an `Error` object, which we can use directly... - if (error.error instanceof Error) { + if (isErrorOrErrorLikeObject(error.error)) { return error.error; } @@ -50,6 +50,31 @@ function extractHttpModuleError(error: HttpErrorResponse): string | Error { return error.message; } +type ErrorCandidate = { + name?: unknown; + message?: unknown; + stack?: unknown; +}; + +function isErrorOrErrorLikeObject(value: unknown): value is Error { + if (value instanceof Error) { + return true; + } + + if (value === null || typeof value !== 'object') { + return false; + } + + const candidate = value as ErrorCandidate; + + return ( + isString(candidate.name) && + isString(candidate.name) && + isString(candidate.message) && + (undefined === candidate.stack || isString(candidate.stack)) + ); +} + /** * Implementation of Angular's ErrorHandler provider that can be used as a drop-in replacement for the stock one. */ @@ -117,16 +142,16 @@ class SentryErrorHandler implements AngularErrorHandler { protected _defaultExtractor(errorCandidate: unknown): unknown { const error = tryToUnwrapZonejsError(errorCandidate); - // We can handle messages and Error objects directly. - if (typeof error === 'string' || error instanceof Error) { - return error; - } - // If it's http module error, extract as much information from it as we can. if (error instanceof HttpErrorResponse) { return extractHttpModuleError(error); } + // We can handle messages and Error objects directly. + if (typeof error === 'string' || isErrorOrErrorLikeObject(error)) { + return error; + } + // Nothing was extracted, fallback to default error message. return null; } diff --git a/packages/angular/test/errorhandler.test.ts b/packages/angular/test/errorhandler.test.ts index df28e809bb26..e4398fd8aa70 100644 --- a/packages/angular/test/errorhandler.test.ts +++ b/packages/angular/test/errorhandler.test.ts @@ -33,7 +33,7 @@ class CustomError extends Error { } class ErrorLikeShapedClass implements Partial { - constructor(public message: string) {} + constructor(public name: string, public message: string) {} } function createErrorEvent(message: string, innerError: any): ErrorEvent { @@ -118,8 +118,7 @@ describe('SentryErrorHandler', () => { createErrorHandler().handleError(errorLikeWithoutStack); expect(captureExceptionSpy).toHaveBeenCalledTimes(1); - // TODO: to be changed; see https://github.com/getsentry/sentry-javascript/issues/6332 - expect(captureExceptionSpy).toHaveBeenCalledWith('Handled unknown error', expect.any(Function)); + expect(captureExceptionSpy).toHaveBeenCalledWith(errorLikeWithoutStack, expect.any(Function)); }); it('extracts an error-like object with a stack', () => { @@ -132,8 +131,7 @@ describe('SentryErrorHandler', () => { createErrorHandler().handleError(errorLikeWithStack); expect(captureExceptionSpy).toHaveBeenCalledTimes(1); - // TODO: to be changed; see https://github.com/getsentry/sentry-javascript/issues/6332 - expect(captureExceptionSpy).toHaveBeenCalledWith('Handled unknown error', expect.any(Function)); + expect(captureExceptionSpy).toHaveBeenCalledWith(errorLikeWithStack, expect.any(Function)); }); it('extracts an object that could look like an error but is not (does not have a message)', () => { @@ -150,7 +148,6 @@ describe('SentryErrorHandler', () => { it('extracts an object that could look like an error but is not (does not have an explicit name)', () => { const notErr: Partial = { - // missing name; but actually is always there as part of the Object prototype message: 'something failed.', }; @@ -194,12 +191,12 @@ describe('SentryErrorHandler', () => { }); it('extracts an instance of class not extending Error but that has an error-like shape', () => { - const err = new ErrorLikeShapedClass('something happened'); + const err = new ErrorLikeShapedClass('sentry-error', 'something happened'); createErrorHandler().handleError(err); expect(captureExceptionSpy).toHaveBeenCalledTimes(1); - expect(captureExceptionSpy).toHaveBeenCalledWith('Handled unknown error', expect.any(Function)); + expect(captureExceptionSpy).toHaveBeenCalledWith(err, expect.any(Function)); }); it('extracts an instance of a class that does not extend Error and does not have an error-like shape', () => { @@ -304,11 +301,7 @@ describe('SentryErrorHandler', () => { createErrorHandler().handleError(err); expect(captureExceptionSpy).toHaveBeenCalledTimes(1); - // TODO: to be changed; see https://github.com/getsentry/sentry-javascript/issues/6332 - expect(captureExceptionSpy).toHaveBeenCalledWith( - 'Http failure response for (unknown url): undefined undefined', - expect.any(Function), - ); + expect(captureExceptionSpy).toHaveBeenCalledWith(errorLikeWithoutStack, expect.any(Function)); }); it('extracts an `HttpErrorResponse` with error-like object with a stack', () => { @@ -322,11 +315,7 @@ describe('SentryErrorHandler', () => { createErrorHandler().handleError(err); expect(captureExceptionSpy).toHaveBeenCalledTimes(1); - // TODO: to be changed; see https://github.com/getsentry/sentry-javascript/issues/6332 - expect(captureExceptionSpy).toHaveBeenCalledWith( - 'Http failure response for (unknown url): undefined undefined', - expect.any(Function), - ); + expect(captureExceptionSpy).toHaveBeenCalledWith(errorLikeWithStack, expect.any(Function)); }); it('extracts an `HttpErrorResponse` with an object that could look like an error but is not (does not have a message)', () => { @@ -347,7 +336,6 @@ describe('SentryErrorHandler', () => { it('extracts an `HttpErrorResponse` with an object that could look like an error but is not (does not have an explicit name)', () => { const notErr: Partial = { - // missing name; but actually is always there as part of the Object prototype message: 'something failed.', }; const err = new HttpErrorResponse({ error: notErr }); @@ -453,16 +441,13 @@ describe('SentryErrorHandler', () => { }); it('extracts an `HttpErrorResponse` with an instance of class not extending Error but that has an error-like shape', () => { - const innerErr = new ErrorLikeShapedClass('something happened'); + const innerErr = new ErrorLikeShapedClass('sentry-error', 'something happened'); const err = new HttpErrorResponse({ error: innerErr }); createErrorHandler().handleError(err); expect(captureExceptionSpy).toHaveBeenCalledTimes(1); - expect(captureExceptionSpy).toHaveBeenCalledWith( - 'Http failure response for (unknown url): undefined undefined', - expect.any(Function), - ); + expect(captureExceptionSpy).toHaveBeenCalledWith(innerErr, expect.any(Function)); }); it('extracts an `HttpErrorResponse` with an instance of a class that does not extend Error and does not have an error-like shape', () => { From 57c7e7b7c92f3f02b004796b2b58260786c1acc2 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Mon, 9 Jan 2023 12:32:53 +0000 Subject: [PATCH 020/113] feat(node): Add `LocalVariables` integration to capture local variables to stack frames (#6478) --- .../LocalVariables/local-variables.js | 35 + .../LocalVariables/no-local-variables.js | 34 + .../suites/public-api/LocalVariables/test.ts | 54 + packages/node/src/integrations/index.ts | 1 + packages/node/src/integrations/inspector.d.ts | 3359 +++++++++++++++++ .../node/src/integrations/localvariables.ts | 278 ++ packages/node/src/sdk.ts | 2 + .../test/integrations/localvariables.test.ts | 269 ++ 8 files changed, 4032 insertions(+) create mode 100644 packages/node-integration-tests/suites/public-api/LocalVariables/local-variables.js create mode 100644 packages/node-integration-tests/suites/public-api/LocalVariables/no-local-variables.js create mode 100644 packages/node-integration-tests/suites/public-api/LocalVariables/test.ts create mode 100644 packages/node/src/integrations/inspector.d.ts create mode 100644 packages/node/src/integrations/localvariables.ts create mode 100644 packages/node/test/integrations/localvariables.test.ts diff --git a/packages/node-integration-tests/suites/public-api/LocalVariables/local-variables.js b/packages/node-integration-tests/suites/public-api/LocalVariables/local-variables.js new file mode 100644 index 000000000000..db24a014c5a2 --- /dev/null +++ b/packages/node-integration-tests/suites/public-api/LocalVariables/local-variables.js @@ -0,0 +1,35 @@ +/* eslint-disable no-unused-vars */ +const Sentry = require('@sentry/node'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + _experiments: { includeStackLocals: true }, + beforeSend: event => { + // eslint-disable-next-line no-console + console.log(JSON.stringify(event)); + }, +}); + +process.on('uncaughtException', () => { + // do nothing - this will prevent the Error below from closing this process +}); + +class Some { + two(name) { + throw new Error('Enough!'); + } +} + +function one(name) { + const arr = [1, '2', null]; + const obj = { + name, + num: 5, + }; + + const ty = new Some(); + + ty.two(name); +} + +one('some name'); diff --git a/packages/node-integration-tests/suites/public-api/LocalVariables/no-local-variables.js b/packages/node-integration-tests/suites/public-api/LocalVariables/no-local-variables.js new file mode 100644 index 000000000000..03c9254efea8 --- /dev/null +++ b/packages/node-integration-tests/suites/public-api/LocalVariables/no-local-variables.js @@ -0,0 +1,34 @@ +/* eslint-disable no-unused-vars */ +const Sentry = require('@sentry/node'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + beforeSend: event => { + // eslint-disable-next-line no-console + console.log(JSON.stringify(event)); + }, +}); + +process.on('uncaughtException', () => { + // do nothing - this will prevent the Error below from closing this process +}); + +class Some { + two(name) { + throw new Error('Enough!'); + } +} + +function one(name) { + const arr = [1, '2', null]; + const obj = { + name, + num: 5, + }; + + const ty = new Some(); + + ty.two(name); +} + +one('some name'); diff --git a/packages/node-integration-tests/suites/public-api/LocalVariables/test.ts b/packages/node-integration-tests/suites/public-api/LocalVariables/test.ts new file mode 100644 index 000000000000..37f155e534a6 --- /dev/null +++ b/packages/node-integration-tests/suites/public-api/LocalVariables/test.ts @@ -0,0 +1,54 @@ +import { Event } from '@sentry/node'; +import * as childProcess from 'child_process'; +import * as path from 'path'; + +describe('LocalVariables integration', () => { + test('Should not include local variables by default', done => { + expect.assertions(2); + + const testScriptPath = path.resolve(__dirname, 'no-local-variables.js'); + + childProcess.exec(`node ${testScriptPath}`, { encoding: 'utf8' }, (_, stdout) => { + const event = JSON.parse(stdout) as Event; + + const frames = event.exception?.values?.[0].stacktrace?.frames || []; + const lastFrame = frames[frames.length - 1]; + + expect(lastFrame.vars).toBeUndefined(); + + const penultimateFrame = frames[frames.length - 2]; + + expect(penultimateFrame.vars).toBeUndefined(); + + done(); + }); + }); + + test('Should include local variables when enabled', done => { + expect.assertions(4); + + const testScriptPath = path.resolve(__dirname, 'local-variables.js'); + + childProcess.exec(`node ${testScriptPath}`, { encoding: 'utf8' }, (_, stdout) => { + const event = JSON.parse(stdout) as Event; + + const frames = event.exception?.values?.[0].stacktrace?.frames || []; + const lastFrame = frames[frames.length - 1]; + + expect(lastFrame.function).toBe('Some.two'); + expect(lastFrame.vars).toEqual({ name: 'some name' }); + + const penultimateFrame = frames[frames.length - 2]; + + expect(penultimateFrame.function).toBe('one'); + expect(penultimateFrame.vars).toEqual({ + name: 'some name', + arr: [1, '2', null], + obj: { name: 'some name', num: 5 }, + ty: '', + }); + + done(); + }); + }); +}); diff --git a/packages/node/src/integrations/index.ts b/packages/node/src/integrations/index.ts index cdb5145b0ea1..167a482e5b5f 100644 --- a/packages/node/src/integrations/index.ts +++ b/packages/node/src/integrations/index.ts @@ -7,3 +7,4 @@ export { Modules } from './modules'; export { ContextLines } from './contextlines'; export { Context } from './context'; export { RequestData } from './requestdata'; +export { LocalVariables } from './localvariables'; diff --git a/packages/node/src/integrations/inspector.d.ts b/packages/node/src/integrations/inspector.d.ts new file mode 100644 index 000000000000..527006910ee9 --- /dev/null +++ b/packages/node/src/integrations/inspector.d.ts @@ -0,0 +1,3359 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable @typescript-eslint/unified-signatures */ +/* eslint-disable @typescript-eslint/explicit-member-accessibility */ +/* eslint-disable max-lines */ +/* eslint-disable @typescript-eslint/ban-types */ +// Type definitions for inspector + +// These definitions were copied from: +// https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/d37bf642ed2f3fe403e405892e2eb4240a191bb0/types/node/inspector.d.ts + +/** + * The `inspector` module provides an API for interacting with the V8 inspector. + * + * It can be accessed using: + * + * ```js + * const inspector = require('inspector'); + * ``` + * @see [source](https://github.com/nodejs/node/blob/v18.0.0/lib/inspector.js) + */ +declare module 'inspector' { + import EventEmitter = require('node:events'); + interface InspectorNotification { + method: string; + params: T; + } + namespace Schema { + /** + * Description of the protocol domain. + */ + interface Domain { + /** + * Domain name. + */ + name: string; + /** + * Domain version. + */ + version: string; + } + interface GetDomainsReturnType { + /** + * List of supported domains. + */ + domains: Domain[]; + } + } + namespace Runtime { + /** + * Unique script identifier. + */ + type ScriptId = string; + /** + * Unique object identifier. + */ + type RemoteObjectId = string; + /** + * Primitive value which cannot be JSON-stringified. + */ + type UnserializableValue = string; + /** + * Mirror object referencing original JavaScript object. + */ + interface RemoteObject { + /** + * Object type. + */ + type: string; + /** + * Object subtype hint. Specified for object type values only. + */ + subtype?: string | undefined; + /** + * Object class (constructor) name. Specified for object type values only. + */ + className?: string | undefined; + /** + * Remote object value in case of primitive values or JSON values (if it was requested). + */ + value?: any; + /** + * Primitive value which can not be JSON-stringified does not have value, but gets this property. + */ + unserializableValue?: UnserializableValue | undefined; + /** + * String representation of the object. + */ + description?: string | undefined; + /** + * Unique object identifier (for non-primitive values). + */ + objectId?: RemoteObjectId | undefined; + /** + * Preview containing abbreviated property values. Specified for object type values only. + * @experimental + */ + preview?: ObjectPreview | undefined; + /** + * @experimental + */ + customPreview?: CustomPreview | undefined; + } + /** + * @experimental + */ + interface CustomPreview { + header: string; + hasBody: boolean; + formatterObjectId: RemoteObjectId; + bindRemoteObjectFunctionId: RemoteObjectId; + configObjectId?: RemoteObjectId | undefined; + } + /** + * Object containing abbreviated remote object value. + * @experimental + */ + interface ObjectPreview { + /** + * Object type. + */ + type: string; + /** + * Object subtype hint. Specified for object type values only. + */ + subtype?: string | undefined; + /** + * String representation of the object. + */ + description?: string | undefined; + /** + * True iff some of the properties or entries of the original object did not fit. + */ + overflow: boolean; + /** + * List of the properties. + */ + properties: PropertyPreview[]; + /** + * List of the entries. Specified for map and set subtype values only. + */ + entries?: EntryPreview[] | undefined; + } + /** + * @experimental + */ + interface PropertyPreview { + /** + * Property name. + */ + name: string; + /** + * Object type. Accessor means that the property itself is an accessor property. + */ + type: string; + /** + * User-friendly property value string. + */ + value?: string | undefined; + /** + * Nested value preview. + */ + valuePreview?: ObjectPreview | undefined; + /** + * Object subtype hint. Specified for object type values only. + */ + subtype?: string | undefined; + } + /** + * @experimental + */ + interface EntryPreview { + /** + * Preview of the key. Specified for map-like collection entries. + */ + key?: ObjectPreview | undefined; + /** + * Preview of the value. + */ + value: ObjectPreview; + } + /** + * Object property descriptor. + */ + interface PropertyDescriptor { + /** + * Property name or symbol description. + */ + name: string; + /** + * The value associated with the property. + */ + value?: RemoteObject | undefined; + /** + * True if the value associated with the property may be changed (data descriptors only). + */ + writable?: boolean | undefined; + /** + * A function which serves as a getter for the property, or undefined if there is no getter (accessor descriptors only). + */ + get?: RemoteObject | undefined; + /** + * A function which serves as a setter for the property, or undefined if there is no setter (accessor descriptors only). + */ + set?: RemoteObject | undefined; + /** + * True if the type of this property descriptor may be changed and if the property may be deleted from the corresponding object. + */ + configurable: boolean; + /** + * True if this property shows up during enumeration of the properties on the corresponding object. + */ + enumerable: boolean; + /** + * True if the result was thrown during the evaluation. + */ + wasThrown?: boolean | undefined; + /** + * True if the property is owned for the object. + */ + isOwn?: boolean | undefined; + /** + * Property symbol object, if the property is of the symbol type. + */ + symbol?: RemoteObject | undefined; + } + /** + * Object internal property descriptor. This property isn't normally visible in JavaScript code. + */ + interface InternalPropertyDescriptor { + /** + * Conventional property name. + */ + name: string; + /** + * The value associated with the property. + */ + value?: RemoteObject | undefined; + } + /** + * Represents function call argument. Either remote object id objectId, primitive value, unserializable primitive value or neither of (for undefined) them should be specified. + */ + interface CallArgument { + /** + * Primitive value or serializable javascript object. + */ + value?: any; + /** + * Primitive value which can not be JSON-stringified. + */ + unserializableValue?: UnserializableValue | undefined; + /** + * Remote object handle. + */ + objectId?: RemoteObjectId | undefined; + } + /** + * Id of an execution context. + */ + type ExecutionContextId = number; + /** + * Description of an isolated world. + */ + interface ExecutionContextDescription { + /** + * Unique id of the execution context. It can be used to specify in which execution context script evaluation should be performed. + */ + id: ExecutionContextId; + /** + * Execution context origin. + */ + origin: string; + /** + * Human readable name describing given context. + */ + name: string; + /** + * Embedder-specific auxiliary data. + */ + auxData?: {} | undefined; + } + /** + * Detailed information about exception (or error) that was thrown during script compilation or execution. + */ + interface ExceptionDetails { + /** + * Exception id. + */ + exceptionId: number; + /** + * Exception text, which should be used together with exception object when available. + */ + text: string; + /** + * Line number of the exception location (0-based). + */ + lineNumber: number; + /** + * Column number of the exception location (0-based). + */ + columnNumber: number; + /** + * Script ID of the exception location. + */ + scriptId?: ScriptId | undefined; + /** + * URL of the exception location, to be used when the script was not reported. + */ + url?: string | undefined; + /** + * JavaScript stack trace if available. + */ + stackTrace?: StackTrace | undefined; + /** + * Exception object if available. + */ + exception?: RemoteObject | undefined; + /** + * Identifier of the context where exception happened. + */ + executionContextId?: ExecutionContextId | undefined; + } + /** + * Number of milliseconds since epoch. + */ + type Timestamp = number; + /** + * Stack entry for runtime errors and assertions. + */ + interface CallFrame { + /** + * JavaScript function name. + */ + functionName: string; + /** + * JavaScript script id. + */ + scriptId: ScriptId; + /** + * JavaScript script name or url. + */ + url: string; + /** + * JavaScript script line number (0-based). + */ + lineNumber: number; + /** + * JavaScript script column number (0-based). + */ + columnNumber: number; + } + /** + * Call frames for assertions or error messages. + */ + interface StackTrace { + /** + * String label of this stack trace. For async traces this may be a name of the function that initiated the async call. + */ + description?: string | undefined; + /** + * JavaScript function name. + */ + callFrames: CallFrame[]; + /** + * Asynchronous JavaScript stack trace that preceded this stack, if available. + */ + parent?: StackTrace | undefined; + /** + * Asynchronous JavaScript stack trace that preceded this stack, if available. + * @experimental + */ + parentId?: StackTraceId | undefined; + } + /** + * Unique identifier of current debugger. + * @experimental + */ + type UniqueDebuggerId = string; + /** + * If debuggerId is set stack trace comes from another debugger and can be resolved there. This allows to track cross-debugger calls. See Runtime.StackTrace and Debugger.paused for usages. + * @experimental + */ + interface StackTraceId { + id: string; + debuggerId?: UniqueDebuggerId | undefined; + } + interface EvaluateParameterType { + /** + * Expression to evaluate. + */ + expression: string; + /** + * Symbolic group name that can be used to release multiple objects. + */ + objectGroup?: string | undefined; + /** + * Determines whether Command Line API should be available during the evaluation. + */ + includeCommandLineAPI?: boolean | undefined; + /** + * In silent mode exceptions thrown during evaluation are not reported and do not pause execution. Overrides setPauseOnException state. + */ + silent?: boolean | undefined; + /** + * Specifies in which execution context to perform evaluation. If the parameter is omitted the evaluation will be performed in the context of the inspected page. + */ + contextId?: ExecutionContextId | undefined; + /** + * Whether the result is expected to be a JSON object that should be sent by value. + */ + returnByValue?: boolean | undefined; + /** + * Whether preview should be generated for the result. + * @experimental + */ + generatePreview?: boolean | undefined; + /** + * Whether execution should be treated as initiated by user in the UI. + */ + userGesture?: boolean | undefined; + /** + * Whether execution should await for resulting value and return once awaited promise is resolved. + */ + awaitPromise?: boolean | undefined; + } + interface AwaitPromiseParameterType { + /** + * Identifier of the promise. + */ + promiseObjectId: RemoteObjectId; + /** + * Whether the result is expected to be a JSON object that should be sent by value. + */ + returnByValue?: boolean | undefined; + /** + * Whether preview should be generated for the result. + */ + generatePreview?: boolean | undefined; + } + interface CallFunctionOnParameterType { + /** + * Declaration of the function to call. + */ + functionDeclaration: string; + /** + * Identifier of the object to call function on. Either objectId or executionContextId should be specified. + */ + objectId?: RemoteObjectId | undefined; + /** + * Call arguments. All call arguments must belong to the same JavaScript world as the target object. + */ + arguments?: CallArgument[] | undefined; + /** + * In silent mode exceptions thrown during evaluation are not reported and do not pause execution. Overrides setPauseOnException state. + */ + silent?: boolean | undefined; + /** + * Whether the result is expected to be a JSON object which should be sent by value. + */ + returnByValue?: boolean | undefined; + /** + * Whether preview should be generated for the result. + * @experimental + */ + generatePreview?: boolean | undefined; + /** + * Whether execution should be treated as initiated by user in the UI. + */ + userGesture?: boolean | undefined; + /** + * Whether execution should await for resulting value and return once awaited promise is resolved. + */ + awaitPromise?: boolean | undefined; + /** + * Specifies execution context which global object will be used to call function on. Either executionContextId or objectId should be specified. + */ + executionContextId?: ExecutionContextId | undefined; + /** + * Symbolic group name that can be used to release multiple objects. If objectGroup is not specified and objectId is, objectGroup will be inherited from object. + */ + objectGroup?: string | undefined; + } + interface GetPropertiesParameterType { + /** + * Identifier of the object to return properties for. + */ + objectId: RemoteObjectId; + /** + * If true, returns properties belonging only to the element itself, not to its prototype chain. + */ + ownProperties?: boolean | undefined; + /** + * If true, returns accessor properties (with getter/setter) only; internal properties are not returned either. + * @experimental + */ + accessorPropertiesOnly?: boolean | undefined; + /** + * Whether preview should be generated for the results. + * @experimental + */ + generatePreview?: boolean | undefined; + } + interface ReleaseObjectParameterType { + /** + * Identifier of the object to release. + */ + objectId: RemoteObjectId; + } + interface ReleaseObjectGroupParameterType { + /** + * Symbolic object group name. + */ + objectGroup: string; + } + interface SetCustomObjectFormatterEnabledParameterType { + enabled: boolean; + } + interface CompileScriptParameterType { + /** + * Expression to compile. + */ + expression: string; + /** + * Source url to be set for the script. + */ + sourceURL: string; + /** + * Specifies whether the compiled script should be persisted. + */ + persistScript: boolean; + /** + * Specifies in which execution context to perform script run. If the parameter is omitted the evaluation will be performed in the context of the inspected page. + */ + executionContextId?: ExecutionContextId | undefined; + } + interface RunScriptParameterType { + /** + * Id of the script to run. + */ + scriptId: ScriptId; + /** + * Specifies in which execution context to perform script run. If the parameter is omitted the evaluation will be performed in the context of the inspected page. + */ + executionContextId?: ExecutionContextId | undefined; + /** + * Symbolic group name that can be used to release multiple objects. + */ + objectGroup?: string | undefined; + /** + * In silent mode exceptions thrown during evaluation are not reported and do not pause execution. Overrides setPauseOnException state. + */ + silent?: boolean | undefined; + /** + * Determines whether Command Line API should be available during the evaluation. + */ + includeCommandLineAPI?: boolean | undefined; + /** + * Whether the result is expected to be a JSON object which should be sent by value. + */ + returnByValue?: boolean | undefined; + /** + * Whether preview should be generated for the result. + */ + generatePreview?: boolean | undefined; + /** + * Whether execution should await for resulting value and return once awaited promise is resolved. + */ + awaitPromise?: boolean | undefined; + } + interface QueryObjectsParameterType { + /** + * Identifier of the prototype to return objects for. + */ + prototypeObjectId: RemoteObjectId; + } + interface GlobalLexicalScopeNamesParameterType { + /** + * Specifies in which execution context to lookup global scope variables. + */ + executionContextId?: ExecutionContextId | undefined; + } + interface EvaluateReturnType { + /** + * Evaluation result. + */ + result: RemoteObject; + /** + * Exception details. + */ + exceptionDetails?: ExceptionDetails | undefined; + } + interface AwaitPromiseReturnType { + /** + * Promise result. Will contain rejected value if promise was rejected. + */ + result: RemoteObject; + /** + * Exception details if stack strace is available. + */ + exceptionDetails?: ExceptionDetails | undefined; + } + interface CallFunctionOnReturnType { + /** + * Call result. + */ + result: RemoteObject; + /** + * Exception details. + */ + exceptionDetails?: ExceptionDetails | undefined; + } + interface GetPropertiesReturnType { + /** + * Object properties. + */ + result: PropertyDescriptor[]; + /** + * Internal object properties (only of the element itself). + */ + internalProperties?: InternalPropertyDescriptor[] | undefined; + /** + * Exception details. + */ + exceptionDetails?: ExceptionDetails | undefined; + } + interface CompileScriptReturnType { + /** + * Id of the script. + */ + scriptId?: ScriptId | undefined; + /** + * Exception details. + */ + exceptionDetails?: ExceptionDetails | undefined; + } + interface RunScriptReturnType { + /** + * Run result. + */ + result: RemoteObject; + /** + * Exception details. + */ + exceptionDetails?: ExceptionDetails | undefined; + } + interface QueryObjectsReturnType { + /** + * Array with objects. + */ + objects: RemoteObject; + } + interface GlobalLexicalScopeNamesReturnType { + names: string[]; + } + interface ExecutionContextCreatedEventDataType { + /** + * A newly created execution context. + */ + context: ExecutionContextDescription; + } + interface ExecutionContextDestroyedEventDataType { + /** + * Id of the destroyed context + */ + executionContextId: ExecutionContextId; + } + interface ExceptionThrownEventDataType { + /** + * Timestamp of the exception. + */ + timestamp: Timestamp; + exceptionDetails: ExceptionDetails; + } + interface ExceptionRevokedEventDataType { + /** + * Reason describing why exception was revoked. + */ + reason: string; + /** + * The id of revoked exception, as reported in exceptionThrown. + */ + exceptionId: number; + } + interface ConsoleAPICalledEventDataType { + /** + * Type of the call. + */ + type: string; + /** + * Call arguments. + */ + args: RemoteObject[]; + /** + * Identifier of the context where the call was made. + */ + executionContextId: ExecutionContextId; + /** + * Call timestamp. + */ + timestamp: Timestamp; + /** + * Stack trace captured when the call was made. + */ + stackTrace?: StackTrace | undefined; + /** + * Console context descriptor for calls on non-default console context (not console.*): 'anonymous#unique-logger-id' for call on unnamed context, 'name#unique-logger-id' for call on named context. + * @experimental + */ + context?: string | undefined; + } + interface InspectRequestedEventDataType { + object: RemoteObject; + hints: {}; + } + } + namespace Debugger { + /** + * Breakpoint identifier. + */ + type BreakpointId = string; + /** + * Call frame identifier. + */ + type CallFrameId = string; + /** + * Location in the source code. + */ + interface Location { + /** + * Script identifier as reported in the Debugger.scriptParsed. + */ + scriptId: Runtime.ScriptId; + /** + * Line number in the script (0-based). + */ + lineNumber: number; + /** + * Column number in the script (0-based). + */ + columnNumber?: number | undefined; + } + /** + * Location in the source code. + * @experimental + */ + interface ScriptPosition { + lineNumber: number; + columnNumber: number; + } + /** + * JavaScript call frame. Array of call frames form the call stack. + */ + interface CallFrame { + /** + * Call frame identifier. This identifier is only valid while the virtual machine is paused. + */ + callFrameId: CallFrameId; + /** + * Name of the JavaScript function called on this call frame. + */ + functionName: string; + /** + * Location in the source code. + */ + functionLocation?: Location | undefined; + /** + * Location in the source code. + */ + location: Location; + /** + * JavaScript script name or url. + */ + url: string; + /** + * Scope chain for this call frame. + */ + scopeChain: Scope[]; + /** + * this object for this call frame. + */ + this: Runtime.RemoteObject; + /** + * The value being returned, if the function is at return point. + */ + returnValue?: Runtime.RemoteObject | undefined; + } + /** + * Scope description. + */ + interface Scope { + /** + * Scope type. + */ + type: string; + /** + * Object representing the scope. For global and with scopes it represents the actual object; for the rest of the scopes, it is artificial transient object enumerating scope variables as its properties. + */ + object: Runtime.RemoteObject; + name?: string | undefined; + /** + * Location in the source code where scope starts + */ + startLocation?: Location | undefined; + /** + * Location in the source code where scope ends + */ + endLocation?: Location | undefined; + } + /** + * Search match for resource. + */ + interface SearchMatch { + /** + * Line number in resource content. + */ + lineNumber: number; + /** + * Line with match content. + */ + lineContent: string; + } + interface BreakLocation { + /** + * Script identifier as reported in the Debugger.scriptParsed. + */ + scriptId: Runtime.ScriptId; + /** + * Line number in the script (0-based). + */ + lineNumber: number; + /** + * Column number in the script (0-based). + */ + columnNumber?: number | undefined; + type?: string | undefined; + } + interface SetBreakpointsActiveParameterType { + /** + * New value for breakpoints active state. + */ + active: boolean; + } + interface SetSkipAllPausesParameterType { + /** + * New value for skip pauses state. + */ + skip: boolean; + } + interface SetBreakpointByUrlParameterType { + /** + * Line number to set breakpoint at. + */ + lineNumber: number; + /** + * URL of the resources to set breakpoint on. + */ + url?: string | undefined; + /** + * Regex pattern for the URLs of the resources to set breakpoints on. Either url or urlRegex must be specified. + */ + urlRegex?: string | undefined; + /** + * Script hash of the resources to set breakpoint on. + */ + scriptHash?: string | undefined; + /** + * Offset in the line to set breakpoint at. + */ + columnNumber?: number | undefined; + /** + * Expression to use as a breakpoint condition. When specified, debugger will only stop on the breakpoint if this expression evaluates to true. + */ + condition?: string | undefined; + } + interface SetBreakpointParameterType { + /** + * Location to set breakpoint in. + */ + location: Location; + /** + * Expression to use as a breakpoint condition. When specified, debugger will only stop on the breakpoint if this expression evaluates to true. + */ + condition?: string | undefined; + } + interface RemoveBreakpointParameterType { + breakpointId: BreakpointId; + } + interface GetPossibleBreakpointsParameterType { + /** + * Start of range to search possible breakpoint locations in. + */ + start: Location; + /** + * End of range to search possible breakpoint locations in (excluding). When not specified, end of scripts is used as end of range. + */ + end?: Location | undefined; + /** + * Only consider locations which are in the same (non-nested) function as start. + */ + restrictToFunction?: boolean | undefined; + } + interface ContinueToLocationParameterType { + /** + * Location to continue to. + */ + location: Location; + targetCallFrames?: string | undefined; + } + interface PauseOnAsyncCallParameterType { + /** + * Debugger will pause when async call with given stack trace is started. + */ + parentStackTraceId: Runtime.StackTraceId; + } + interface StepIntoParameterType { + /** + * Debugger will issue additional Debugger.paused notification if any async task is scheduled before next pause. + * @experimental + */ + breakOnAsyncCall?: boolean | undefined; + } + interface GetStackTraceParameterType { + stackTraceId: Runtime.StackTraceId; + } + interface SearchInContentParameterType { + /** + * Id of the script to search in. + */ + scriptId: Runtime.ScriptId; + /** + * String to search for. + */ + query: string; + /** + * If true, search is case sensitive. + */ + caseSensitive?: boolean | undefined; + /** + * If true, treats string parameter as regex. + */ + isRegex?: boolean | undefined; + } + interface SetScriptSourceParameterType { + /** + * Id of the script to edit. + */ + scriptId: Runtime.ScriptId; + /** + * New content of the script. + */ + scriptSource: string; + /** + * If true the change will not actually be applied. Dry run may be used to get result description without actually modifying the code. + */ + dryRun?: boolean | undefined; + } + interface RestartFrameParameterType { + /** + * Call frame identifier to evaluate on. + */ + callFrameId: CallFrameId; + } + interface GetScriptSourceParameterType { + /** + * Id of the script to get source for. + */ + scriptId: Runtime.ScriptId; + } + interface SetPauseOnExceptionsParameterType { + /** + * Pause on exceptions mode. + */ + state: string; + } + interface EvaluateOnCallFrameParameterType { + /** + * Call frame identifier to evaluate on. + */ + callFrameId: CallFrameId; + /** + * Expression to evaluate. + */ + expression: string; + /** + * String object group name to put result into (allows rapid releasing resulting object handles using releaseObjectGroup). + */ + objectGroup?: string | undefined; + /** + * Specifies whether command line API should be available to the evaluated expression, defaults to false. + */ + includeCommandLineAPI?: boolean | undefined; + /** + * In silent mode exceptions thrown during evaluation are not reported and do not pause execution. Overrides setPauseOnException state. + */ + silent?: boolean | undefined; + /** + * Whether the result is expected to be a JSON object that should be sent by value. + */ + returnByValue?: boolean | undefined; + /** + * Whether preview should be generated for the result. + * @experimental + */ + generatePreview?: boolean | undefined; + /** + * Whether to throw an exception if side effect cannot be ruled out during evaluation. + */ + throwOnSideEffect?: boolean | undefined; + } + interface SetVariableValueParameterType { + /** + * 0-based number of scope as was listed in scope chain. Only 'local', 'closure' and 'catch' scope types are allowed. Other scopes could be manipulated manually. + */ + scopeNumber: number; + /** + * Variable name. + */ + variableName: string; + /** + * New variable value. + */ + newValue: Runtime.CallArgument; + /** + * Id of callframe that holds variable. + */ + callFrameId: CallFrameId; + } + interface SetReturnValueParameterType { + /** + * New return value. + */ + newValue: Runtime.CallArgument; + } + interface SetAsyncCallStackDepthParameterType { + /** + * Maximum depth of async call stacks. Setting to 0 will effectively disable collecting async call stacks (default). + */ + maxDepth: number; + } + interface SetBlackboxPatternsParameterType { + /** + * Array of regexps that will be used to check script url for blackbox state. + */ + patterns: string[]; + } + interface SetBlackboxedRangesParameterType { + /** + * Id of the script. + */ + scriptId: Runtime.ScriptId; + positions: ScriptPosition[]; + } + interface EnableReturnType { + /** + * Unique identifier of the debugger. + * @experimental + */ + debuggerId: Runtime.UniqueDebuggerId; + } + interface SetBreakpointByUrlReturnType { + /** + * Id of the created breakpoint for further reference. + */ + breakpointId: BreakpointId; + /** + * List of the locations this breakpoint resolved into upon addition. + */ + locations: Location[]; + } + interface SetBreakpointReturnType { + /** + * Id of the created breakpoint for further reference. + */ + breakpointId: BreakpointId; + /** + * Location this breakpoint resolved into. + */ + actualLocation: Location; + } + interface GetPossibleBreakpointsReturnType { + /** + * List of the possible breakpoint locations. + */ + locations: BreakLocation[]; + } + interface GetStackTraceReturnType { + stackTrace: Runtime.StackTrace; + } + interface SearchInContentReturnType { + /** + * List of search matches. + */ + result: SearchMatch[]; + } + interface SetScriptSourceReturnType { + /** + * New stack trace in case editing has happened while VM was stopped. + */ + callFrames?: CallFrame[] | undefined; + /** + * Whether current call stack was modified after applying the changes. + */ + stackChanged?: boolean | undefined; + /** + * Async stack trace, if any. + */ + asyncStackTrace?: Runtime.StackTrace | undefined; + /** + * Async stack trace, if any. + * @experimental + */ + asyncStackTraceId?: Runtime.StackTraceId | undefined; + /** + * Exception details if any. + */ + exceptionDetails?: Runtime.ExceptionDetails | undefined; + } + interface RestartFrameReturnType { + /** + * New stack trace. + */ + callFrames: CallFrame[]; + /** + * Async stack trace, if any. + */ + asyncStackTrace?: Runtime.StackTrace | undefined; + /** + * Async stack trace, if any. + * @experimental + */ + asyncStackTraceId?: Runtime.StackTraceId | undefined; + } + interface GetScriptSourceReturnType { + /** + * Script source. + */ + scriptSource: string; + } + interface EvaluateOnCallFrameReturnType { + /** + * Object wrapper for the evaluation result. + */ + result: Runtime.RemoteObject; + /** + * Exception details. + */ + exceptionDetails?: Runtime.ExceptionDetails | undefined; + } + interface ScriptParsedEventDataType { + /** + * Identifier of the script parsed. + */ + scriptId: Runtime.ScriptId; + /** + * URL or name of the script parsed (if any). + */ + url: string; + /** + * Line offset of the script within the resource with given URL (for script tags). + */ + startLine: number; + /** + * Column offset of the script within the resource with given URL. + */ + startColumn: number; + /** + * Last line of the script. + */ + endLine: number; + /** + * Length of the last line of the script. + */ + endColumn: number; + /** + * Specifies script creation context. + */ + executionContextId: Runtime.ExecutionContextId; + /** + * Content hash of the script. + */ + hash: string; + /** + * Embedder-specific auxiliary data. + */ + executionContextAuxData?: {} | undefined; + /** + * True, if this script is generated as a result of the live edit operation. + * @experimental + */ + isLiveEdit?: boolean | undefined; + /** + * URL of source map associated with script (if any). + */ + sourceMapURL?: string | undefined; + /** + * True, if this script has sourceURL. + */ + hasSourceURL?: boolean | undefined; + /** + * True, if this script is ES6 module. + */ + isModule?: boolean | undefined; + /** + * This script length. + */ + length?: number | undefined; + /** + * JavaScript top stack frame of where the script parsed event was triggered if available. + * @experimental + */ + stackTrace?: Runtime.StackTrace | undefined; + } + interface ScriptFailedToParseEventDataType { + /** + * Identifier of the script parsed. + */ + scriptId: Runtime.ScriptId; + /** + * URL or name of the script parsed (if any). + */ + url: string; + /** + * Line offset of the script within the resource with given URL (for script tags). + */ + startLine: number; + /** + * Column offset of the script within the resource with given URL. + */ + startColumn: number; + /** + * Last line of the script. + */ + endLine: number; + /** + * Length of the last line of the script. + */ + endColumn: number; + /** + * Specifies script creation context. + */ + executionContextId: Runtime.ExecutionContextId; + /** + * Content hash of the script. + */ + hash: string; + /** + * Embedder-specific auxiliary data. + */ + executionContextAuxData?: {} | undefined; + /** + * URL of source map associated with script (if any). + */ + sourceMapURL?: string | undefined; + /** + * True, if this script has sourceURL. + */ + hasSourceURL?: boolean | undefined; + /** + * True, if this script is ES6 module. + */ + isModule?: boolean | undefined; + /** + * This script length. + */ + length?: number | undefined; + /** + * JavaScript top stack frame of where the script parsed event was triggered if available. + * @experimental + */ + stackTrace?: Runtime.StackTrace | undefined; + } + interface BreakpointResolvedEventDataType { + /** + * Breakpoint unique identifier. + */ + breakpointId: BreakpointId; + /** + * Actual breakpoint location. + */ + location: Location; + } + interface PausedEventDataType { + /** + * Call stack the virtual machine stopped on. + */ + callFrames: CallFrame[]; + /** + * Pause reason. + */ + reason: string; + /** + * Object containing break-specific auxiliary properties. + */ + data?: {} | undefined; + /** + * Hit breakpoints IDs + */ + hitBreakpoints?: string[] | undefined; + /** + * Async stack trace, if any. + */ + asyncStackTrace?: Runtime.StackTrace | undefined; + /** + * Async stack trace, if any. + * @experimental + */ + asyncStackTraceId?: Runtime.StackTraceId | undefined; + /** + * Just scheduled async call will have this stack trace as parent stack during async execution. This field is available only after Debugger.stepInto call with breakOnAsynCall flag. + * @experimental + */ + asyncCallStackTraceId?: Runtime.StackTraceId | undefined; + } + } + namespace Console { + /** + * Console message. + */ + interface ConsoleMessage { + /** + * Message source. + */ + source: string; + /** + * Message severity. + */ + level: string; + /** + * Message text. + */ + text: string; + /** + * URL of the message origin. + */ + url?: string | undefined; + /** + * Line number in the resource that generated this message (1-based). + */ + line?: number | undefined; + /** + * Column number in the resource that generated this message (1-based). + */ + column?: number | undefined; + } + interface MessageAddedEventDataType { + /** + * Console message that has been added. + */ + message: ConsoleMessage; + } + } + namespace Profiler { + /** + * Profile node. Holds callsite information, execution statistics and child nodes. + */ + interface ProfileNode { + /** + * Unique id of the node. + */ + id: number; + /** + * Function location. + */ + callFrame: Runtime.CallFrame; + /** + * Number of samples where this node was on top of the call stack. + */ + hitCount?: number | undefined; + /** + * Child node ids. + */ + children?: number[] | undefined; + /** + * The reason of being not optimized. The function may be deoptimized or marked as don't optimize. + */ + deoptReason?: string | undefined; + /** + * An array of source position ticks. + */ + positionTicks?: PositionTickInfo[] | undefined; + } + /** + * Profile. + */ + interface Profile { + /** + * The list of profile nodes. First item is the root node. + */ + nodes: ProfileNode[]; + /** + * Profiling start timestamp in microseconds. + */ + startTime: number; + /** + * Profiling end timestamp in microseconds. + */ + endTime: number; + /** + * Ids of samples top nodes. + */ + samples?: number[] | undefined; + /** + * Time intervals between adjacent samples in microseconds. The first delta is relative to the profile startTime. + */ + timeDeltas?: number[] | undefined; + } + /** + * Specifies a number of samples attributed to a certain source position. + */ + interface PositionTickInfo { + /** + * Source line number (1-based). + */ + line: number; + /** + * Number of samples attributed to the source line. + */ + ticks: number; + } + /** + * Coverage data for a source range. + */ + interface CoverageRange { + /** + * JavaScript script source offset for the range start. + */ + startOffset: number; + /** + * JavaScript script source offset for the range end. + */ + endOffset: number; + /** + * Collected execution count of the source range. + */ + count: number; + } + /** + * Coverage data for a JavaScript function. + */ + interface FunctionCoverage { + /** + * JavaScript function name. + */ + functionName: string; + /** + * Source ranges inside the function with coverage data. + */ + ranges: CoverageRange[]; + /** + * Whether coverage data for this function has block granularity. + */ + isBlockCoverage: boolean; + } + /** + * Coverage data for a JavaScript script. + */ + interface ScriptCoverage { + /** + * JavaScript script id. + */ + scriptId: Runtime.ScriptId; + /** + * JavaScript script name or url. + */ + url: string; + /** + * Functions contained in the script that has coverage data. + */ + functions: FunctionCoverage[]; + } + /** + * Describes a type collected during runtime. + * @experimental + */ + interface TypeObject { + /** + * Name of a type collected with type profiling. + */ + name: string; + } + /** + * Source offset and types for a parameter or return value. + * @experimental + */ + interface TypeProfileEntry { + /** + * Source offset of the parameter or end of function for return values. + */ + offset: number; + /** + * The types for this parameter or return value. + */ + types: TypeObject[]; + } + /** + * Type profile data collected during runtime for a JavaScript script. + * @experimental + */ + interface ScriptTypeProfile { + /** + * JavaScript script id. + */ + scriptId: Runtime.ScriptId; + /** + * JavaScript script name or url. + */ + url: string; + /** + * Type profile entries for parameters and return values of the functions in the script. + */ + entries: TypeProfileEntry[]; + } + interface SetSamplingIntervalParameterType { + /** + * New sampling interval in microseconds. + */ + interval: number; + } + interface StartPreciseCoverageParameterType { + /** + * Collect accurate call counts beyond simple 'covered' or 'not covered'. + */ + callCount?: boolean | undefined; + /** + * Collect block-based coverage. + */ + detailed?: boolean | undefined; + } + interface StopReturnType { + /** + * Recorded profile. + */ + profile: Profile; + } + interface TakePreciseCoverageReturnType { + /** + * Coverage data for the current isolate. + */ + result: ScriptCoverage[]; + } + interface GetBestEffortCoverageReturnType { + /** + * Coverage data for the current isolate. + */ + result: ScriptCoverage[]; + } + interface TakeTypeProfileReturnType { + /** + * Type profile for all scripts since startTypeProfile() was turned on. + */ + result: ScriptTypeProfile[]; + } + interface ConsoleProfileStartedEventDataType { + id: string; + /** + * Location of console.profile(). + */ + location: Debugger.Location; + /** + * Profile title passed as an argument to console.profile(). + */ + title?: string | undefined; + } + interface ConsoleProfileFinishedEventDataType { + id: string; + /** + * Location of console.profileEnd(). + */ + location: Debugger.Location; + profile: Profile; + /** + * Profile title passed as an argument to console.profile(). + */ + title?: string | undefined; + } + } + namespace HeapProfiler { + /** + * Heap snapshot object id. + */ + type HeapSnapshotObjectId = string; + /** + * Sampling Heap Profile node. Holds callsite information, allocation statistics and child nodes. + */ + interface SamplingHeapProfileNode { + /** + * Function location. + */ + callFrame: Runtime.CallFrame; + /** + * Allocations size in bytes for the node excluding children. + */ + selfSize: number; + /** + * Child nodes. + */ + children: SamplingHeapProfileNode[]; + } + /** + * Profile. + */ + interface SamplingHeapProfile { + head: SamplingHeapProfileNode; + } + interface StartTrackingHeapObjectsParameterType { + trackAllocations?: boolean | undefined; + } + interface StopTrackingHeapObjectsParameterType { + /** + * If true 'reportHeapSnapshotProgress' events will be generated while snapshot is being taken when the tracking is stopped. + */ + reportProgress?: boolean | undefined; + } + interface TakeHeapSnapshotParameterType { + /** + * If true 'reportHeapSnapshotProgress' events will be generated while snapshot is being taken. + */ + reportProgress?: boolean | undefined; + } + interface GetObjectByHeapObjectIdParameterType { + objectId: HeapSnapshotObjectId; + /** + * Symbolic group name that can be used to release multiple objects. + */ + objectGroup?: string | undefined; + } + interface AddInspectedHeapObjectParameterType { + /** + * Heap snapshot object id to be accessible by means of $x command line API. + */ + heapObjectId: HeapSnapshotObjectId; + } + interface GetHeapObjectIdParameterType { + /** + * Identifier of the object to get heap object id for. + */ + objectId: Runtime.RemoteObjectId; + } + interface StartSamplingParameterType { + /** + * Average sample interval in bytes. Poisson distribution is used for the intervals. The default value is 32768 bytes. + */ + samplingInterval?: number | undefined; + } + interface GetObjectByHeapObjectIdReturnType { + /** + * Evaluation result. + */ + result: Runtime.RemoteObject; + } + interface GetHeapObjectIdReturnType { + /** + * Id of the heap snapshot object corresponding to the passed remote object id. + */ + heapSnapshotObjectId: HeapSnapshotObjectId; + } + interface StopSamplingReturnType { + /** + * Recorded sampling heap profile. + */ + profile: SamplingHeapProfile; + } + interface GetSamplingProfileReturnType { + /** + * Return the sampling profile being collected. + */ + profile: SamplingHeapProfile; + } + interface AddHeapSnapshotChunkEventDataType { + chunk: string; + } + interface ReportHeapSnapshotProgressEventDataType { + done: number; + total: number; + finished?: boolean | undefined; + } + interface LastSeenObjectIdEventDataType { + lastSeenObjectId: number; + timestamp: number; + } + interface HeapStatsUpdateEventDataType { + /** + * An array of triplets. Each triplet describes a fragment. The first integer is the fragment index, the second integer is a total count of objects for the fragment, the third integer is a total size of the objects for the fragment. + */ + statsUpdate: number[]; + } + } + namespace NodeTracing { + interface TraceConfig { + /** + * Controls how the trace buffer stores data. + */ + recordMode?: string | undefined; + /** + * Included category filters. + */ + includedCategories: string[]; + } + interface StartParameterType { + traceConfig: TraceConfig; + } + interface GetCategoriesReturnType { + /** + * A list of supported tracing categories. + */ + categories: string[]; + } + interface DataCollectedEventDataType { + value: Array<{}>; + } + } + namespace NodeWorker { + type WorkerID = string; + /** + * Unique identifier of attached debugging session. + */ + type SessionID = string; + interface WorkerInfo { + workerId: WorkerID; + type: string; + title: string; + url: string; + } + interface SendMessageToWorkerParameterType { + message: string; + /** + * Identifier of the session. + */ + sessionId: SessionID; + } + interface EnableParameterType { + /** + * Whether to new workers should be paused until the frontend sends `Runtime.runIfWaitingForDebugger` + * message to run them. + */ + waitForDebuggerOnStart: boolean; + } + interface DetachParameterType { + sessionId: SessionID; + } + interface AttachedToWorkerEventDataType { + /** + * Identifier assigned to the session used to send/receive messages. + */ + sessionId: SessionID; + workerInfo: WorkerInfo; + waitingForDebugger: boolean; + } + interface DetachedFromWorkerEventDataType { + /** + * Detached session identifier. + */ + sessionId: SessionID; + } + interface ReceivedMessageFromWorkerEventDataType { + /** + * Identifier of a session which sends a message. + */ + sessionId: SessionID; + message: string; + } + } + namespace NodeRuntime { + interface NotifyWhenWaitingForDisconnectParameterType { + enabled: boolean; + } + } + /** + * The `inspector.Session` is used for dispatching messages to the V8 inspector + * back-end and receiving message responses and notifications. + */ + class Session extends EventEmitter { + /** + * Create a new instance of the inspector.Session class. + * The inspector session needs to be connected through session.connect() before the messages can be dispatched to the inspector backend. + */ + constructor(); + /** + * Connects a session to the inspector back-end. + * @since v8.0.0 + */ + connect(): void; + /** + * Immediately close the session. All pending message callbacks will be called + * with an error. `session.connect()` will need to be called to be able to send + * messages again. Reconnected session will lose all inspector state, such as + * enabled agents or configured breakpoints. + * @since v8.0.0 + */ + disconnect(): void; + /** + * Posts a message to the inspector back-end. `callback` will be notified when + * a response is received. `callback` is a function that accepts two optional + * arguments: error and message-specific result. + * + * ```js + * session.post('Runtime.evaluate', { expression: '2 + 2' }, + * (error, { result }) => console.log(result)); + * // Output: { type: 'number', value: 4, description: '4' } + * ``` + * + * The latest version of the V8 inspector protocol is published on the [Chrome DevTools Protocol Viewer](https://chromedevtools.github.io/devtools-protocol/v8/). + * + * Node.js inspector supports all the Chrome DevTools Protocol domains declared + * by V8\. Chrome DevTools Protocol domain provides an interface for interacting + * with one of the runtime agents used to inspect the application state and listen + * to the run-time events. + * + * ## Example usage + * + * Apart from the debugger, various V8 Profilers are available through the DevTools + * protocol. + * @since v8.0.0 + */ + post(method: string, params?: {}, callback?: (err: Error | null, params?: {}) => void): void; + post(method: string, callback?: (err: Error | null, params?: {}) => void): void; + /** + * Returns supported domains. + */ + post( + method: 'Schema.getDomains', + callback?: (err: Error | null, params: Schema.GetDomainsReturnType) => void, + ): void; + /** + * Evaluates expression on global object. + */ + post( + method: 'Runtime.evaluate', + params?: Runtime.EvaluateParameterType, + callback?: (err: Error | null, params: Runtime.EvaluateReturnType) => void, + ): void; + post(method: 'Runtime.evaluate', callback?: (err: Error | null, params: Runtime.EvaluateReturnType) => void): void; + /** + * Add handler to promise with given promise object id. + */ + post( + method: 'Runtime.awaitPromise', + params?: Runtime.AwaitPromiseParameterType, + callback?: (err: Error | null, params: Runtime.AwaitPromiseReturnType) => void, + ): void; + post( + method: 'Runtime.awaitPromise', + callback?: (err: Error | null, params: Runtime.AwaitPromiseReturnType) => void, + ): void; + /** + * Calls function with given declaration on the given object. Object group of the result is inherited from the target object. + */ + post( + method: 'Runtime.callFunctionOn', + params?: Runtime.CallFunctionOnParameterType, + callback?: (err: Error | null, params: Runtime.CallFunctionOnReturnType) => void, + ): void; + post( + method: 'Runtime.callFunctionOn', + callback?: (err: Error | null, params: Runtime.CallFunctionOnReturnType) => void, + ): void; + /** + * Returns properties of a given object. Object group of the result is inherited from the target object. + */ + post( + method: 'Runtime.getProperties', + params?: Runtime.GetPropertiesParameterType, + callback?: (err: Error | null, params: Runtime.GetPropertiesReturnType) => void, + ): void; + post( + method: 'Runtime.getProperties', + callback?: (err: Error | null, params: Runtime.GetPropertiesReturnType) => void, + ): void; + /** + * Releases remote object with given id. + */ + post( + method: 'Runtime.releaseObject', + params?: Runtime.ReleaseObjectParameterType, + callback?: (err: Error | null) => void, + ): void; + post(method: 'Runtime.releaseObject', callback?: (err: Error | null) => void): void; + /** + * Releases all remote objects that belong to a given group. + */ + post( + method: 'Runtime.releaseObjectGroup', + params?: Runtime.ReleaseObjectGroupParameterType, + callback?: (err: Error | null) => void, + ): void; + post(method: 'Runtime.releaseObjectGroup', callback?: (err: Error | null) => void): void; + /** + * Tells inspected instance to run if it was waiting for debugger to attach. + */ + post(method: 'Runtime.runIfWaitingForDebugger', callback?: (err: Error | null) => void): void; + /** + * Enables reporting of execution contexts creation by means of executionContextCreated event. When the reporting gets enabled the event will be sent immediately for each existing execution context. + */ + post(method: 'Runtime.enable', callback?: (err: Error | null) => void): void; + /** + * Disables reporting of execution contexts creation. + */ + post(method: 'Runtime.disable', callback?: (err: Error | null) => void): void; + /** + * Discards collected exceptions and console API calls. + */ + post(method: 'Runtime.discardConsoleEntries', callback?: (err: Error | null) => void): void; + /** + * @experimental + */ + post( + method: 'Runtime.setCustomObjectFormatterEnabled', + params?: Runtime.SetCustomObjectFormatterEnabledParameterType, + callback?: (err: Error | null) => void, + ): void; + post(method: 'Runtime.setCustomObjectFormatterEnabled', callback?: (err: Error | null) => void): void; + /** + * Compiles expression. + */ + post( + method: 'Runtime.compileScript', + params?: Runtime.CompileScriptParameterType, + callback?: (err: Error | null, params: Runtime.CompileScriptReturnType) => void, + ): void; + post( + method: 'Runtime.compileScript', + callback?: (err: Error | null, params: Runtime.CompileScriptReturnType) => void, + ): void; + /** + * Runs script with given id in a given context. + */ + post( + method: 'Runtime.runScript', + params?: Runtime.RunScriptParameterType, + callback?: (err: Error | null, params: Runtime.RunScriptReturnType) => void, + ): void; + post( + method: 'Runtime.runScript', + callback?: (err: Error | null, params: Runtime.RunScriptReturnType) => void, + ): void; + post( + method: 'Runtime.queryObjects', + params?: Runtime.QueryObjectsParameterType, + callback?: (err: Error | null, params: Runtime.QueryObjectsReturnType) => void, + ): void; + post( + method: 'Runtime.queryObjects', + callback?: (err: Error | null, params: Runtime.QueryObjectsReturnType) => void, + ): void; + /** + * Returns all let, const and class variables from global scope. + */ + post( + method: 'Runtime.globalLexicalScopeNames', + params?: Runtime.GlobalLexicalScopeNamesParameterType, + callback?: (err: Error | null, params: Runtime.GlobalLexicalScopeNamesReturnType) => void, + ): void; + post( + method: 'Runtime.globalLexicalScopeNames', + callback?: (err: Error | null, params: Runtime.GlobalLexicalScopeNamesReturnType) => void, + ): void; + /** + * Enables debugger for the given page. Clients should not assume that the debugging has been enabled until the result for this command is received. + */ + post(method: 'Debugger.enable', callback?: (err: Error | null, params: Debugger.EnableReturnType) => void): void; + /** + * Disables debugger for given page. + */ + post(method: 'Debugger.disable', callback?: (err: Error | null) => void): void; + /** + * Activates / deactivates all breakpoints on the page. + */ + post( + method: 'Debugger.setBreakpointsActive', + params?: Debugger.SetBreakpointsActiveParameterType, + callback?: (err: Error | null) => void, + ): void; + post(method: 'Debugger.setBreakpointsActive', callback?: (err: Error | null) => void): void; + /** + * Makes page not interrupt on any pauses (breakpoint, exception, dom exception etc). + */ + post( + method: 'Debugger.setSkipAllPauses', + params?: Debugger.SetSkipAllPausesParameterType, + callback?: (err: Error | null) => void, + ): void; + post(method: 'Debugger.setSkipAllPauses', callback?: (err: Error | null) => void): void; + /** + * Sets JavaScript breakpoint at given location specified either by URL or URL regex. Once this command is issued, all existing parsed scripts will have breakpoints resolved and returned in locations property. Further matching script parsing will result in subsequent breakpointResolved events issued. This logical breakpoint will survive page reloads. + */ + post( + method: 'Debugger.setBreakpointByUrl', + params?: Debugger.SetBreakpointByUrlParameterType, + callback?: (err: Error | null, params: Debugger.SetBreakpointByUrlReturnType) => void, + ): void; + post( + method: 'Debugger.setBreakpointByUrl', + callback?: (err: Error | null, params: Debugger.SetBreakpointByUrlReturnType) => void, + ): void; + /** + * Sets JavaScript breakpoint at a given location. + */ + post( + method: 'Debugger.setBreakpoint', + params?: Debugger.SetBreakpointParameterType, + callback?: (err: Error | null, params: Debugger.SetBreakpointReturnType) => void, + ): void; + post( + method: 'Debugger.setBreakpoint', + callback?: (err: Error | null, params: Debugger.SetBreakpointReturnType) => void, + ): void; + /** + * Removes JavaScript breakpoint. + */ + post( + method: 'Debugger.removeBreakpoint', + params?: Debugger.RemoveBreakpointParameterType, + callback?: (err: Error | null) => void, + ): void; + post(method: 'Debugger.removeBreakpoint', callback?: (err: Error | null) => void): void; + /** + * Returns possible locations for breakpoint. scriptId in start and end range locations should be the same. + */ + post( + method: 'Debugger.getPossibleBreakpoints', + params?: Debugger.GetPossibleBreakpointsParameterType, + callback?: (err: Error | null, params: Debugger.GetPossibleBreakpointsReturnType) => void, + ): void; + post( + method: 'Debugger.getPossibleBreakpoints', + callback?: (err: Error | null, params: Debugger.GetPossibleBreakpointsReturnType) => void, + ): void; + /** + * Continues execution until specific location is reached. + */ + post( + method: 'Debugger.continueToLocation', + params?: Debugger.ContinueToLocationParameterType, + callback?: (err: Error | null) => void, + ): void; + post(method: 'Debugger.continueToLocation', callback?: (err: Error | null) => void): void; + /** + * @experimental + */ + post( + method: 'Debugger.pauseOnAsyncCall', + params?: Debugger.PauseOnAsyncCallParameterType, + callback?: (err: Error | null) => void, + ): void; + post(method: 'Debugger.pauseOnAsyncCall', callback?: (err: Error | null) => void): void; + /** + * Steps over the statement. + */ + post(method: 'Debugger.stepOver', callback?: (err: Error | null) => void): void; + /** + * Steps into the function call. + */ + post( + method: 'Debugger.stepInto', + params?: Debugger.StepIntoParameterType, + callback?: (err: Error | null) => void, + ): void; + post(method: 'Debugger.stepInto', callback?: (err: Error | null) => void): void; + /** + * Steps out of the function call. + */ + post(method: 'Debugger.stepOut', callback?: (err: Error | null) => void): void; + /** + * Stops on the next JavaScript statement. + */ + post(method: 'Debugger.pause', callback?: (err: Error | null) => void): void; + /** + * This method is deprecated - use Debugger.stepInto with breakOnAsyncCall and Debugger.pauseOnAsyncTask instead. Steps into next scheduled async task if any is scheduled before next pause. Returns success when async task is actually scheduled, returns error if no task were scheduled or another scheduleStepIntoAsync was called. + * @experimental + */ + post(method: 'Debugger.scheduleStepIntoAsync', callback?: (err: Error | null) => void): void; + /** + * Resumes JavaScript execution. + */ + post(method: 'Debugger.resume', callback?: (err: Error | null) => void): void; + /** + * Returns stack trace with given stackTraceId. + * @experimental + */ + post( + method: 'Debugger.getStackTrace', + params?: Debugger.GetStackTraceParameterType, + callback?: (err: Error | null, params: Debugger.GetStackTraceReturnType) => void, + ): void; + post( + method: 'Debugger.getStackTrace', + callback?: (err: Error | null, params: Debugger.GetStackTraceReturnType) => void, + ): void; + /** + * Searches for given string in script content. + */ + post( + method: 'Debugger.searchInContent', + params?: Debugger.SearchInContentParameterType, + callback?: (err: Error | null, params: Debugger.SearchInContentReturnType) => void, + ): void; + post( + method: 'Debugger.searchInContent', + callback?: (err: Error | null, params: Debugger.SearchInContentReturnType) => void, + ): void; + /** + * Edits JavaScript source live. + */ + post( + method: 'Debugger.setScriptSource', + params?: Debugger.SetScriptSourceParameterType, + callback?: (err: Error | null, params: Debugger.SetScriptSourceReturnType) => void, + ): void; + post( + method: 'Debugger.setScriptSource', + callback?: (err: Error | null, params: Debugger.SetScriptSourceReturnType) => void, + ): void; + /** + * Restarts particular call frame from the beginning. + */ + post( + method: 'Debugger.restartFrame', + params?: Debugger.RestartFrameParameterType, + callback?: (err: Error | null, params: Debugger.RestartFrameReturnType) => void, + ): void; + post( + method: 'Debugger.restartFrame', + callback?: (err: Error | null, params: Debugger.RestartFrameReturnType) => void, + ): void; + /** + * Returns source for the script with given id. + */ + post( + method: 'Debugger.getScriptSource', + params?: Debugger.GetScriptSourceParameterType, + callback?: (err: Error | null, params: Debugger.GetScriptSourceReturnType) => void, + ): void; + post( + method: 'Debugger.getScriptSource', + callback?: (err: Error | null, params: Debugger.GetScriptSourceReturnType) => void, + ): void; + /** + * Defines pause on exceptions state. Can be set to stop on all exceptions, uncaught exceptions or no exceptions. Initial pause on exceptions state is none. + */ + post( + method: 'Debugger.setPauseOnExceptions', + params?: Debugger.SetPauseOnExceptionsParameterType, + callback?: (err: Error | null) => void, + ): void; + post(method: 'Debugger.setPauseOnExceptions', callback?: (err: Error | null) => void): void; + /** + * Evaluates expression on a given call frame. + */ + post( + method: 'Debugger.evaluateOnCallFrame', + params?: Debugger.EvaluateOnCallFrameParameterType, + callback?: (err: Error | null, params: Debugger.EvaluateOnCallFrameReturnType) => void, + ): void; + post( + method: 'Debugger.evaluateOnCallFrame', + callback?: (err: Error | null, params: Debugger.EvaluateOnCallFrameReturnType) => void, + ): void; + /** + * Changes value of variable in a callframe. Object-based scopes are not supported and must be mutated manually. + */ + post( + method: 'Debugger.setVariableValue', + params?: Debugger.SetVariableValueParameterType, + callback?: (err: Error | null) => void, + ): void; + post(method: 'Debugger.setVariableValue', callback?: (err: Error | null) => void): void; + /** + * Changes return value in top frame. Available only at return break position. + * @experimental + */ + post( + method: 'Debugger.setReturnValue', + params?: Debugger.SetReturnValueParameterType, + callback?: (err: Error | null) => void, + ): void; + post(method: 'Debugger.setReturnValue', callback?: (err: Error | null) => void): void; + /** + * Enables or disables async call stacks tracking. + */ + post( + method: 'Debugger.setAsyncCallStackDepth', + params?: Debugger.SetAsyncCallStackDepthParameterType, + callback?: (err: Error | null) => void, + ): void; + post(method: 'Debugger.setAsyncCallStackDepth', callback?: (err: Error | null) => void): void; + /** + * Replace previous blackbox patterns with passed ones. Forces backend to skip stepping/pausing in scripts with url matching one of the patterns. VM will try to leave blackboxed script by performing 'step in' several times, finally resorting to 'step out' if unsuccessful. + * @experimental + */ + post( + method: 'Debugger.setBlackboxPatterns', + params?: Debugger.SetBlackboxPatternsParameterType, + callback?: (err: Error | null) => void, + ): void; + post(method: 'Debugger.setBlackboxPatterns', callback?: (err: Error | null) => void): void; + /** + * Makes backend skip steps in the script in blackboxed ranges. VM will try leave blacklisted scripts by performing 'step in' several times, finally resorting to 'step out' if unsuccessful. Positions array contains positions where blackbox state is changed. First interval isn't blackboxed. Array should be sorted. + * @experimental + */ + post( + method: 'Debugger.setBlackboxedRanges', + params?: Debugger.SetBlackboxedRangesParameterType, + callback?: (err: Error | null) => void, + ): void; + post(method: 'Debugger.setBlackboxedRanges', callback?: (err: Error | null) => void): void; + /** + * Enables console domain, sends the messages collected so far to the client by means of the messageAdded notification. + */ + post(method: 'Console.enable', callback?: (err: Error | null) => void): void; + /** + * Disables console domain, prevents further console messages from being reported to the client. + */ + post(method: 'Console.disable', callback?: (err: Error | null) => void): void; + /** + * Does nothing. + */ + post(method: 'Console.clearMessages', callback?: (err: Error | null) => void): void; + post(method: 'Profiler.enable', callback?: (err: Error | null) => void): void; + post(method: 'Profiler.disable', callback?: (err: Error | null) => void): void; + /** + * Changes CPU profiler sampling interval. Must be called before CPU profiles recording started. + */ + post( + method: 'Profiler.setSamplingInterval', + params?: Profiler.SetSamplingIntervalParameterType, + callback?: (err: Error | null) => void, + ): void; + post(method: 'Profiler.setSamplingInterval', callback?: (err: Error | null) => void): void; + post(method: 'Profiler.start', callback?: (err: Error | null) => void): void; + post(method: 'Profiler.stop', callback?: (err: Error | null, params: Profiler.StopReturnType) => void): void; + /** + * Enable precise code coverage. Coverage data for JavaScript executed before enabling precise code coverage may be incomplete. Enabling prevents running optimized code and resets execution counters. + */ + post( + method: 'Profiler.startPreciseCoverage', + params?: Profiler.StartPreciseCoverageParameterType, + callback?: (err: Error | null) => void, + ): void; + post(method: 'Profiler.startPreciseCoverage', callback?: (err: Error | null) => void): void; + /** + * Disable precise code coverage. Disabling releases unnecessary execution count records and allows executing optimized code. + */ + post(method: 'Profiler.stopPreciseCoverage', callback?: (err: Error | null) => void): void; + /** + * Collect coverage data for the current isolate, and resets execution counters. Precise code coverage needs to have started. + */ + post( + method: 'Profiler.takePreciseCoverage', + callback?: (err: Error | null, params: Profiler.TakePreciseCoverageReturnType) => void, + ): void; + /** + * Collect coverage data for the current isolate. The coverage data may be incomplete due to garbage collection. + */ + post( + method: 'Profiler.getBestEffortCoverage', + callback?: (err: Error | null, params: Profiler.GetBestEffortCoverageReturnType) => void, + ): void; + /** + * Enable type profile. + * @experimental + */ + post(method: 'Profiler.startTypeProfile', callback?: (err: Error | null) => void): void; + /** + * Disable type profile. Disabling releases type profile data collected so far. + * @experimental + */ + post(method: 'Profiler.stopTypeProfile', callback?: (err: Error | null) => void): void; + /** + * Collect type profile. + * @experimental + */ + post( + method: 'Profiler.takeTypeProfile', + callback?: (err: Error | null, params: Profiler.TakeTypeProfileReturnType) => void, + ): void; + post(method: 'HeapProfiler.enable', callback?: (err: Error | null) => void): void; + post(method: 'HeapProfiler.disable', callback?: (err: Error | null) => void): void; + post( + method: 'HeapProfiler.startTrackingHeapObjects', + params?: HeapProfiler.StartTrackingHeapObjectsParameterType, + callback?: (err: Error | null) => void, + ): void; + post(method: 'HeapProfiler.startTrackingHeapObjects', callback?: (err: Error | null) => void): void; + post( + method: 'HeapProfiler.stopTrackingHeapObjects', + params?: HeapProfiler.StopTrackingHeapObjectsParameterType, + callback?: (err: Error | null) => void, + ): void; + post(method: 'HeapProfiler.stopTrackingHeapObjects', callback?: (err: Error | null) => void): void; + post( + method: 'HeapProfiler.takeHeapSnapshot', + params?: HeapProfiler.TakeHeapSnapshotParameterType, + callback?: (err: Error | null) => void, + ): void; + post(method: 'HeapProfiler.takeHeapSnapshot', callback?: (err: Error | null) => void): void; + post(method: 'HeapProfiler.collectGarbage', callback?: (err: Error | null) => void): void; + post( + method: 'HeapProfiler.getObjectByHeapObjectId', + params?: HeapProfiler.GetObjectByHeapObjectIdParameterType, + callback?: (err: Error | null, params: HeapProfiler.GetObjectByHeapObjectIdReturnType) => void, + ): void; + post( + method: 'HeapProfiler.getObjectByHeapObjectId', + callback?: (err: Error | null, params: HeapProfiler.GetObjectByHeapObjectIdReturnType) => void, + ): void; + /** + * Enables console to refer to the node with given id via $x (see Command Line API for more details $x functions). + */ + post( + method: 'HeapProfiler.addInspectedHeapObject', + params?: HeapProfiler.AddInspectedHeapObjectParameterType, + callback?: (err: Error | null) => void, + ): void; + post(method: 'HeapProfiler.addInspectedHeapObject', callback?: (err: Error | null) => void): void; + post( + method: 'HeapProfiler.getHeapObjectId', + params?: HeapProfiler.GetHeapObjectIdParameterType, + callback?: (err: Error | null, params: HeapProfiler.GetHeapObjectIdReturnType) => void, + ): void; + post( + method: 'HeapProfiler.getHeapObjectId', + callback?: (err: Error | null, params: HeapProfiler.GetHeapObjectIdReturnType) => void, + ): void; + post( + method: 'HeapProfiler.startSampling', + params?: HeapProfiler.StartSamplingParameterType, + callback?: (err: Error | null) => void, + ): void; + post(method: 'HeapProfiler.startSampling', callback?: (err: Error | null) => void): void; + post( + method: 'HeapProfiler.stopSampling', + callback?: (err: Error | null, params: HeapProfiler.StopSamplingReturnType) => void, + ): void; + post( + method: 'HeapProfiler.getSamplingProfile', + callback?: (err: Error | null, params: HeapProfiler.GetSamplingProfileReturnType) => void, + ): void; + /** + * Gets supported tracing categories. + */ + post( + method: 'NodeTracing.getCategories', + callback?: (err: Error | null, params: NodeTracing.GetCategoriesReturnType) => void, + ): void; + /** + * Start trace events collection. + */ + post( + method: 'NodeTracing.start', + params?: NodeTracing.StartParameterType, + callback?: (err: Error | null) => void, + ): void; + post(method: 'NodeTracing.start', callback?: (err: Error | null) => void): void; + /** + * Stop trace events collection. Remaining collected events will be sent as a sequence of + * dataCollected events followed by tracingComplete event. + */ + post(method: 'NodeTracing.stop', callback?: (err: Error | null) => void): void; + /** + * Sends protocol message over session with given id. + */ + post( + method: 'NodeWorker.sendMessageToWorker', + params?: NodeWorker.SendMessageToWorkerParameterType, + callback?: (err: Error | null) => void, + ): void; + post(method: 'NodeWorker.sendMessageToWorker', callback?: (err: Error | null) => void): void; + /** + * Instructs the inspector to attach to running workers. Will also attach to new workers + * as they start + */ + post( + method: 'NodeWorker.enable', + params?: NodeWorker.EnableParameterType, + callback?: (err: Error | null) => void, + ): void; + post(method: 'NodeWorker.enable', callback?: (err: Error | null) => void): void; + /** + * Detaches from all running workers and disables attaching to new workers as they are started. + */ + post(method: 'NodeWorker.disable', callback?: (err: Error | null) => void): void; + /** + * Detached from the worker with given sessionId. + */ + post( + method: 'NodeWorker.detach', + params?: NodeWorker.DetachParameterType, + callback?: (err: Error | null) => void, + ): void; + post(method: 'NodeWorker.detach', callback?: (err: Error | null) => void): void; + /** + * Enable the `NodeRuntime.waitingForDisconnect`. + */ + post( + method: 'NodeRuntime.notifyWhenWaitingForDisconnect', + params?: NodeRuntime.NotifyWhenWaitingForDisconnectParameterType, + callback?: (err: Error | null) => void, + ): void; + post(method: 'NodeRuntime.notifyWhenWaitingForDisconnect', callback?: (err: Error | null) => void): void; + // Events + addListener(event: string, listener: (...args: any[]) => void): this; + /** + * Emitted when any notification from the V8 Inspector is received. + */ + addListener(event: 'inspectorNotification', listener: (message: InspectorNotification<{}>) => void): this; + /** + * Issued when new execution context is created. + */ + addListener( + event: 'Runtime.executionContextCreated', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Issued when execution context is destroyed. + */ + addListener( + event: 'Runtime.executionContextDestroyed', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Issued when all executionContexts were cleared in browser + */ + addListener(event: 'Runtime.executionContextsCleared', listener: () => void): this; + /** + * Issued when exception was thrown and unhandled. + */ + addListener( + event: 'Runtime.exceptionThrown', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Issued when unhandled exception was revoked. + */ + addListener( + event: 'Runtime.exceptionRevoked', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Issued when console API was called. + */ + addListener( + event: 'Runtime.consoleAPICalled', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Issued when object should be inspected (for example, as a result of inspect() command line API call). + */ + addListener( + event: 'Runtime.inspectRequested', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Fired when virtual machine parses script. This event is also fired for all known and uncollected scripts upon enabling debugger. + */ + addListener( + event: 'Debugger.scriptParsed', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Fired when virtual machine fails to parse the script. + */ + addListener( + event: 'Debugger.scriptFailedToParse', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Fired when breakpoint is resolved to an actual script and location. + */ + addListener( + event: 'Debugger.breakpointResolved', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Fired when the virtual machine stopped on breakpoint or exception or any other stop criteria. + */ + addListener( + event: 'Debugger.paused', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Fired when the virtual machine resumed execution. + */ + addListener(event: 'Debugger.resumed', listener: () => void): this; + /** + * Issued when new console message is added. + */ + addListener( + event: 'Console.messageAdded', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Sent when new profile recording is started using console.profile() call. + */ + addListener( + event: 'Profiler.consoleProfileStarted', + listener: (message: InspectorNotification) => void, + ): this; + addListener( + event: 'Profiler.consoleProfileFinished', + listener: (message: InspectorNotification) => void, + ): this; + addListener( + event: 'HeapProfiler.addHeapSnapshotChunk', + listener: (message: InspectorNotification) => void, + ): this; + addListener(event: 'HeapProfiler.resetProfiles', listener: () => void): this; + addListener( + event: 'HeapProfiler.reportHeapSnapshotProgress', + listener: (message: InspectorNotification) => void, + ): this; + /** + * If heap objects tracking has been started then backend regularly sends a current value for last seen object id and corresponding timestamp. If the were changes in the heap since last event then one or more heapStatsUpdate events will be sent before a new lastSeenObjectId event. + */ + addListener( + event: 'HeapProfiler.lastSeenObjectId', + listener: (message: InspectorNotification) => void, + ): this; + /** + * If heap objects tracking has been started then backend may send update for one or more fragments + */ + addListener( + event: 'HeapProfiler.heapStatsUpdate', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Contains an bucket of collected trace events. + */ + addListener( + event: 'NodeTracing.dataCollected', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Signals that tracing is stopped and there is no trace buffers pending flush, all data were + * delivered via dataCollected events. + */ + addListener(event: 'NodeTracing.tracingComplete', listener: () => void): this; + /** + * Issued when attached to a worker. + */ + addListener( + event: 'NodeWorker.attachedToWorker', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Issued when detached from the worker. + */ + addListener( + event: 'NodeWorker.detachedFromWorker', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Notifies about a new protocol message received from the session + * (session ID is provided in attachedToWorker notification). + */ + addListener( + event: 'NodeWorker.receivedMessageFromWorker', + listener: (message: InspectorNotification) => void, + ): this; + /** + * This event is fired instead of `Runtime.executionContextDestroyed` when + * enabled. + * It is fired when the Node process finished all code execution and is + * waiting for all frontends to disconnect. + */ + addListener(event: 'NodeRuntime.waitingForDisconnect', listener: () => void): this; + emit(event: string | symbol, ...args: any[]): boolean; + emit(event: 'inspectorNotification', message: InspectorNotification<{}>): boolean; + emit( + event: 'Runtime.executionContextCreated', + message: InspectorNotification, + ): boolean; + emit( + event: 'Runtime.executionContextDestroyed', + message: InspectorNotification, + ): boolean; + emit(event: 'Runtime.executionContextsCleared'): boolean; + emit( + event: 'Runtime.exceptionThrown', + message: InspectorNotification, + ): boolean; + emit( + event: 'Runtime.exceptionRevoked', + message: InspectorNotification, + ): boolean; + emit( + event: 'Runtime.consoleAPICalled', + message: InspectorNotification, + ): boolean; + emit( + event: 'Runtime.inspectRequested', + message: InspectorNotification, + ): boolean; + emit(event: 'Debugger.scriptParsed', message: InspectorNotification): boolean; + emit( + event: 'Debugger.scriptFailedToParse', + message: InspectorNotification, + ): boolean; + emit( + event: 'Debugger.breakpointResolved', + message: InspectorNotification, + ): boolean; + emit(event: 'Debugger.paused', message: InspectorNotification): boolean; + emit(event: 'Debugger.resumed'): boolean; + emit(event: 'Console.messageAdded', message: InspectorNotification): boolean; + emit( + event: 'Profiler.consoleProfileStarted', + message: InspectorNotification, + ): boolean; + emit( + event: 'Profiler.consoleProfileFinished', + message: InspectorNotification, + ): boolean; + emit( + event: 'HeapProfiler.addHeapSnapshotChunk', + message: InspectorNotification, + ): boolean; + emit(event: 'HeapProfiler.resetProfiles'): boolean; + emit( + event: 'HeapProfiler.reportHeapSnapshotProgress', + message: InspectorNotification, + ): boolean; + emit( + event: 'HeapProfiler.lastSeenObjectId', + message: InspectorNotification, + ): boolean; + emit( + event: 'HeapProfiler.heapStatsUpdate', + message: InspectorNotification, + ): boolean; + emit( + event: 'NodeTracing.dataCollected', + message: InspectorNotification, + ): boolean; + emit(event: 'NodeTracing.tracingComplete'): boolean; + emit( + event: 'NodeWorker.attachedToWorker', + message: InspectorNotification, + ): boolean; + emit( + event: 'NodeWorker.detachedFromWorker', + message: InspectorNotification, + ): boolean; + emit( + event: 'NodeWorker.receivedMessageFromWorker', + message: InspectorNotification, + ): boolean; + emit(event: 'NodeRuntime.waitingForDisconnect'): boolean; + on(event: string, listener: (...args: any[]) => void): this; + /** + * Emitted when any notification from the V8 Inspector is received. + */ + on(event: 'inspectorNotification', listener: (message: InspectorNotification<{}>) => void): this; + /** + * Issued when new execution context is created. + */ + on( + event: 'Runtime.executionContextCreated', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Issued when execution context is destroyed. + */ + on( + event: 'Runtime.executionContextDestroyed', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Issued when all executionContexts were cleared in browser + */ + on(event: 'Runtime.executionContextsCleared', listener: () => void): this; + /** + * Issued when exception was thrown and unhandled. + */ + on( + event: 'Runtime.exceptionThrown', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Issued when unhandled exception was revoked. + */ + on( + event: 'Runtime.exceptionRevoked', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Issued when console API was called. + */ + on( + event: 'Runtime.consoleAPICalled', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Issued when object should be inspected (for example, as a result of inspect() command line API call). + */ + on( + event: 'Runtime.inspectRequested', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Fired when virtual machine parses script. This event is also fired for all known and uncollected scripts upon enabling debugger. + */ + on( + event: 'Debugger.scriptParsed', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Fired when virtual machine fails to parse the script. + */ + on( + event: 'Debugger.scriptFailedToParse', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Fired when breakpoint is resolved to an actual script and location. + */ + on( + event: 'Debugger.breakpointResolved', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Fired when the virtual machine stopped on breakpoint or exception or any other stop criteria. + */ + on( + event: 'Debugger.paused', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Fired when the virtual machine resumed execution. + */ + on(event: 'Debugger.resumed', listener: () => void): this; + /** + * Issued when new console message is added. + */ + on( + event: 'Console.messageAdded', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Sent when new profile recording is started using console.profile() call. + */ + on( + event: 'Profiler.consoleProfileStarted', + listener: (message: InspectorNotification) => void, + ): this; + on( + event: 'Profiler.consoleProfileFinished', + listener: (message: InspectorNotification) => void, + ): this; + on( + event: 'HeapProfiler.addHeapSnapshotChunk', + listener: (message: InspectorNotification) => void, + ): this; + on(event: 'HeapProfiler.resetProfiles', listener: () => void): this; + on( + event: 'HeapProfiler.reportHeapSnapshotProgress', + listener: (message: InspectorNotification) => void, + ): this; + /** + * If heap objects tracking has been started then backend regularly sends a current value for last seen object id and corresponding timestamp. If the were changes in the heap since last event then one or more heapStatsUpdate events will be sent before a new lastSeenObjectId event. + */ + on( + event: 'HeapProfiler.lastSeenObjectId', + listener: (message: InspectorNotification) => void, + ): this; + /** + * If heap objects tracking has been started then backend may send update for one or more fragments + */ + on( + event: 'HeapProfiler.heapStatsUpdate', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Contains an bucket of collected trace events. + */ + on( + event: 'NodeTracing.dataCollected', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Signals that tracing is stopped and there is no trace buffers pending flush, all data were + * delivered via dataCollected events. + */ + on(event: 'NodeTracing.tracingComplete', listener: () => void): this; + /** + * Issued when attached to a worker. + */ + on( + event: 'NodeWorker.attachedToWorker', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Issued when detached from the worker. + */ + on( + event: 'NodeWorker.detachedFromWorker', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Notifies about a new protocol message received from the session + * (session ID is provided in attachedToWorker notification). + */ + on( + event: 'NodeWorker.receivedMessageFromWorker', + listener: (message: InspectorNotification) => void, + ): this; + /** + * This event is fired instead of `Runtime.executionContextDestroyed` when + * enabled. + * It is fired when the Node process finished all code execution and is + * waiting for all frontends to disconnect. + */ + on(event: 'NodeRuntime.waitingForDisconnect', listener: () => void): this; + once(event: string, listener: (...args: any[]) => void): this; + /** + * Emitted when any notification from the V8 Inspector is received. + */ + once(event: 'inspectorNotification', listener: (message: InspectorNotification<{}>) => void): this; + /** + * Issued when new execution context is created. + */ + once( + event: 'Runtime.executionContextCreated', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Issued when execution context is destroyed. + */ + once( + event: 'Runtime.executionContextDestroyed', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Issued when all executionContexts were cleared in browser + */ + once(event: 'Runtime.executionContextsCleared', listener: () => void): this; + /** + * Issued when exception was thrown and unhandled. + */ + once( + event: 'Runtime.exceptionThrown', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Issued when unhandled exception was revoked. + */ + once( + event: 'Runtime.exceptionRevoked', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Issued when console API was called. + */ + once( + event: 'Runtime.consoleAPICalled', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Issued when object should be inspected (for example, as a result of inspect() command line API call). + */ + once( + event: 'Runtime.inspectRequested', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Fired when virtual machine parses script. This event is also fired for all known and uncollected scripts upon enabling debugger. + */ + once( + event: 'Debugger.scriptParsed', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Fired when virtual machine fails to parse the script. + */ + once( + event: 'Debugger.scriptFailedToParse', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Fired when breakpoint is resolved to an actual script and location. + */ + once( + event: 'Debugger.breakpointResolved', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Fired when the virtual machine stopped on breakpoint or exception or any other stop criteria. + */ + once( + event: 'Debugger.paused', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Fired when the virtual machine resumed execution. + */ + once(event: 'Debugger.resumed', listener: () => void): this; + /** + * Issued when new console message is added. + */ + once( + event: 'Console.messageAdded', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Sent when new profile recording is started using console.profile() call. + */ + once( + event: 'Profiler.consoleProfileStarted', + listener: (message: InspectorNotification) => void, + ): this; + once( + event: 'Profiler.consoleProfileFinished', + listener: (message: InspectorNotification) => void, + ): this; + once( + event: 'HeapProfiler.addHeapSnapshotChunk', + listener: (message: InspectorNotification) => void, + ): this; + once(event: 'HeapProfiler.resetProfiles', listener: () => void): this; + once( + event: 'HeapProfiler.reportHeapSnapshotProgress', + listener: (message: InspectorNotification) => void, + ): this; + /** + * If heap objects tracking has been started then backend regularly sends a current value for last seen object id and corresponding timestamp. If the were changes in the heap since last event then one or more heapStatsUpdate events will be sent before a new lastSeenObjectId event. + */ + once( + event: 'HeapProfiler.lastSeenObjectId', + listener: (message: InspectorNotification) => void, + ): this; + /** + * If heap objects tracking has been started then backend may send update for one or more fragments + */ + once( + event: 'HeapProfiler.heapStatsUpdate', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Contains an bucket of collected trace events. + */ + once( + event: 'NodeTracing.dataCollected', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Signals that tracing is stopped and there is no trace buffers pending flush, all data were + * delivered via dataCollected events. + */ + once(event: 'NodeTracing.tracingComplete', listener: () => void): this; + /** + * Issued when attached to a worker. + */ + once( + event: 'NodeWorker.attachedToWorker', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Issued when detached from the worker. + */ + once( + event: 'NodeWorker.detachedFromWorker', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Notifies about a new protocol message received from the session + * (session ID is provided in attachedToWorker notification). + */ + once( + event: 'NodeWorker.receivedMessageFromWorker', + listener: (message: InspectorNotification) => void, + ): this; + /** + * This event is fired instead of `Runtime.executionContextDestroyed` when + * enabled. + * It is fired when the Node process finished all code execution and is + * waiting for all frontends to disconnect. + */ + once(event: 'NodeRuntime.waitingForDisconnect', listener: () => void): this; + prependListener(event: string, listener: (...args: any[]) => void): this; + /** + * Emitted when any notification from the V8 Inspector is received. + */ + prependListener(event: 'inspectorNotification', listener: (message: InspectorNotification<{}>) => void): this; + /** + * Issued when new execution context is created. + */ + prependListener( + event: 'Runtime.executionContextCreated', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Issued when execution context is destroyed. + */ + prependListener( + event: 'Runtime.executionContextDestroyed', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Issued when all executionContexts were cleared in browser + */ + prependListener(event: 'Runtime.executionContextsCleared', listener: () => void): this; + /** + * Issued when exception was thrown and unhandled. + */ + prependListener( + event: 'Runtime.exceptionThrown', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Issued when unhandled exception was revoked. + */ + prependListener( + event: 'Runtime.exceptionRevoked', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Issued when console API was called. + */ + prependListener( + event: 'Runtime.consoleAPICalled', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Issued when object should be inspected (for example, as a result of inspect() command line API call). + */ + prependListener( + event: 'Runtime.inspectRequested', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Fired when virtual machine parses script. This event is also fired for all known and uncollected scripts upon enabling debugger. + */ + prependListener( + event: 'Debugger.scriptParsed', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Fired when virtual machine fails to parse the script. + */ + prependListener( + event: 'Debugger.scriptFailedToParse', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Fired when breakpoint is resolved to an actual script and location. + */ + prependListener( + event: 'Debugger.breakpointResolved', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Fired when the virtual machine stopped on breakpoint or exception or any other stop criteria. + */ + prependListener( + event: 'Debugger.paused', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Fired when the virtual machine resumed execution. + */ + prependListener(event: 'Debugger.resumed', listener: () => void): this; + /** + * Issued when new console message is added. + */ + prependListener( + event: 'Console.messageAdded', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Sent when new profile recording is started using console.profile() call. + */ + prependListener( + event: 'Profiler.consoleProfileStarted', + listener: (message: InspectorNotification) => void, + ): this; + prependListener( + event: 'Profiler.consoleProfileFinished', + listener: (message: InspectorNotification) => void, + ): this; + prependListener( + event: 'HeapProfiler.addHeapSnapshotChunk', + listener: (message: InspectorNotification) => void, + ): this; + prependListener(event: 'HeapProfiler.resetProfiles', listener: () => void): this; + prependListener( + event: 'HeapProfiler.reportHeapSnapshotProgress', + listener: (message: InspectorNotification) => void, + ): this; + /** + * If heap objects tracking has been started then backend regularly sends a current value for last seen object id and corresponding timestamp. If the were changes in the heap since last event then one or more heapStatsUpdate events will be sent before a new lastSeenObjectId event. + */ + prependListener( + event: 'HeapProfiler.lastSeenObjectId', + listener: (message: InspectorNotification) => void, + ): this; + /** + * If heap objects tracking has been started then backend may send update for one or more fragments + */ + prependListener( + event: 'HeapProfiler.heapStatsUpdate', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Contains an bucket of collected trace events. + */ + prependListener( + event: 'NodeTracing.dataCollected', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Signals that tracing is stopped and there is no trace buffers pending flush, all data were + * delivered via dataCollected events. + */ + prependListener(event: 'NodeTracing.tracingComplete', listener: () => void): this; + /** + * Issued when attached to a worker. + */ + prependListener( + event: 'NodeWorker.attachedToWorker', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Issued when detached from the worker. + */ + prependListener( + event: 'NodeWorker.detachedFromWorker', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Notifies about a new protocol message received from the session + * (session ID is provided in attachedToWorker notification). + */ + prependListener( + event: 'NodeWorker.receivedMessageFromWorker', + listener: (message: InspectorNotification) => void, + ): this; + /** + * This event is fired instead of `Runtime.executionContextDestroyed` when + * enabled. + * It is fired when the Node process finished all code execution and is + * waiting for all frontends to disconnect. + */ + prependListener(event: 'NodeRuntime.waitingForDisconnect', listener: () => void): this; + prependOnceListener(event: string, listener: (...args: any[]) => void): this; + /** + * Emitted when any notification from the V8 Inspector is received. + */ + prependOnceListener(event: 'inspectorNotification', listener: (message: InspectorNotification<{}>) => void): this; + /** + * Issued when new execution context is created. + */ + prependOnceListener( + event: 'Runtime.executionContextCreated', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Issued when execution context is destroyed. + */ + prependOnceListener( + event: 'Runtime.executionContextDestroyed', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Issued when all executionContexts were cleared in browser + */ + prependOnceListener(event: 'Runtime.executionContextsCleared', listener: () => void): this; + /** + * Issued when exception was thrown and unhandled. + */ + prependOnceListener( + event: 'Runtime.exceptionThrown', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Issued when unhandled exception was revoked. + */ + prependOnceListener( + event: 'Runtime.exceptionRevoked', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Issued when console API was called. + */ + prependOnceListener( + event: 'Runtime.consoleAPICalled', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Issued when object should be inspected (for example, as a result of inspect() command line API call). + */ + prependOnceListener( + event: 'Runtime.inspectRequested', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Fired when virtual machine parses script. This event is also fired for all known and uncollected scripts upon enabling debugger. + */ + prependOnceListener( + event: 'Debugger.scriptParsed', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Fired when virtual machine fails to parse the script. + */ + prependOnceListener( + event: 'Debugger.scriptFailedToParse', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Fired when breakpoint is resolved to an actual script and location. + */ + prependOnceListener( + event: 'Debugger.breakpointResolved', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Fired when the virtual machine stopped on breakpoint or exception or any other stop criteria. + */ + prependOnceListener( + event: 'Debugger.paused', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Fired when the virtual machine resumed execution. + */ + prependOnceListener(event: 'Debugger.resumed', listener: () => void): this; + /** + * Issued when new console message is added. + */ + prependOnceListener( + event: 'Console.messageAdded', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Sent when new profile recording is started using console.profile() call. + */ + prependOnceListener( + event: 'Profiler.consoleProfileStarted', + listener: (message: InspectorNotification) => void, + ): this; + prependOnceListener( + event: 'Profiler.consoleProfileFinished', + listener: (message: InspectorNotification) => void, + ): this; + prependOnceListener( + event: 'HeapProfiler.addHeapSnapshotChunk', + listener: (message: InspectorNotification) => void, + ): this; + prependOnceListener(event: 'HeapProfiler.resetProfiles', listener: () => void): this; + prependOnceListener( + event: 'HeapProfiler.reportHeapSnapshotProgress', + listener: (message: InspectorNotification) => void, + ): this; + /** + * If heap objects tracking has been started then backend regularly sends a current value for last seen object id and corresponding timestamp. If the were changes in the heap since last event then one or more heapStatsUpdate events will be sent before a new lastSeenObjectId event. + */ + prependOnceListener( + event: 'HeapProfiler.lastSeenObjectId', + listener: (message: InspectorNotification) => void, + ): this; + /** + * If heap objects tracking has been started then backend may send update for one or more fragments + */ + prependOnceListener( + event: 'HeapProfiler.heapStatsUpdate', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Contains an bucket of collected trace events. + */ + prependOnceListener( + event: 'NodeTracing.dataCollected', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Signals that tracing is stopped and there is no trace buffers pending flush, all data were + * delivered via dataCollected events. + */ + prependOnceListener(event: 'NodeTracing.tracingComplete', listener: () => void): this; + /** + * Issued when attached to a worker. + */ + prependOnceListener( + event: 'NodeWorker.attachedToWorker', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Issued when detached from the worker. + */ + prependOnceListener( + event: 'NodeWorker.detachedFromWorker', + listener: (message: InspectorNotification) => void, + ): this; + /** + * Notifies about a new protocol message received from the session + * (session ID is provided in attachedToWorker notification). + */ + prependOnceListener( + event: 'NodeWorker.receivedMessageFromWorker', + listener: (message: InspectorNotification) => void, + ): this; + /** + * This event is fired instead of `Runtime.executionContextDestroyed` when + * enabled. + * It is fired when the Node process finished all code execution and is + * waiting for all frontends to disconnect. + */ + prependOnceListener(event: 'NodeRuntime.waitingForDisconnect', listener: () => void): this; + } + /** + * Activate inspector on host and port. Equivalent to`node --inspect=[[host:]port]`, but can be done programmatically after node has + * started. + * + * If wait is `true`, will block until a client has connected to the inspect port + * and flow control has been passed to the debugger client. + * + * See the `security warning` regarding the `host`parameter usage. + * @param [port='what was specified on the CLI'] Port to listen on for inspector connections. Optional. + * @param [host='what was specified on the CLI'] Host to listen on for inspector connections. Optional. + * @param [wait=false] Block until a client has connected. Optional. + */ + function open(port?: number, host?: string, wait?: boolean): void; + /** + * Deactivate the inspector. Blocks until there are no active connections. + */ + function close(): void; + /** + * Return the URL of the active inspector, or `undefined` if there is none. + * + * ```console + * $ node --inspect -p 'inspector.url()' + * Debugger listening on ws://127.0.0.1:9229/166e272e-7a30-4d09-97ce-f1c012b43c34 + * For help, see: https://nodejs.org/en/docs/inspector + * ws://127.0.0.1:9229/166e272e-7a30-4d09-97ce-f1c012b43c34 + * + * $ node --inspect=localhost:3000 -p 'inspector.url()' + * Debugger listening on ws://localhost:3000/51cf8d0e-3c36-4c59-8efd-54519839e56a + * For help, see: https://nodejs.org/en/docs/inspector + * ws://localhost:3000/51cf8d0e-3c36-4c59-8efd-54519839e56a + * + * $ node -p 'inspector.url()' + * undefined + * ``` + */ + function url(): string | undefined; + /** + * Blocks until a client (existing or connected later) has sent`Runtime.runIfWaitingForDebugger` command. + * + * An exception will be thrown if there is no active inspector. + * @since v12.7.0 + */ + function waitForDebugger(): void; +} +/** + * The inspector module provides an API for interacting with the V8 inspector. + */ +declare module 'node:inspector' { + import inspector = require('inspector'); + export = inspector; +} diff --git a/packages/node/src/integrations/localvariables.ts b/packages/node/src/integrations/localvariables.ts new file mode 100644 index 000000000000..fc82e1a611f0 --- /dev/null +++ b/packages/node/src/integrations/localvariables.ts @@ -0,0 +1,278 @@ +import { + ClientOptions, + Event, + EventProcessor, + Exception, + Hub, + Integration, + StackFrame, + StackParser, +} from '@sentry/types'; +import { Debugger, InspectorNotification, Runtime, Session } from 'inspector'; +import { LRUMap } from 'lru_map'; + +export interface DebugSession { + /** Configures and connects to the debug session */ + configureAndConnect(onPause: (message: InspectorNotification) => void): void; + /** Gets local variables for an objectId */ + getLocalVariables(objectId: string): Promise>; +} + +/** + * Promise API is available as `Experimental` and in Node 19 only. + * + * Callback-based API is `Stable` since v14 and `Experimental` since v8. + * Because of that, we are creating our own `AsyncSession` class. + * + * https://nodejs.org/docs/latest-v19.x/api/inspector.html#promises-api + * https://nodejs.org/docs/latest-v14.x/api/inspector.html + */ +class AsyncSession extends Session implements DebugSession { + /** @inheritdoc */ + public configureAndConnect(onPause: (message: InspectorNotification) => void): void { + this.connect(); + this.on('Debugger.paused', onPause); + this.post('Debugger.enable'); + // We only want to pause on uncaught exceptions + this.post('Debugger.setPauseOnExceptions', { state: 'uncaught' }); + } + + /** @inheritdoc */ + public async getLocalVariables(objectId: string): Promise> { + const props = await this._getProperties(objectId); + const unrolled: Record = {}; + + for (const prop of props) { + if (prop?.value?.objectId && prop?.value.className === 'Array') { + unrolled[prop.name] = await this._unrollArray(prop.value.objectId); + } else if (prop?.value?.objectId && prop?.value?.className === 'Object') { + unrolled[prop.name] = await this._unrollObject(prop.value.objectId); + } else if (prop?.value?.value || prop?.value?.description) { + unrolled[prop.name] = prop.value.value || `<${prop.value.description}>`; + } + } + + return unrolled; + } + + /** + * Gets all the PropertyDescriptors of an object + */ + private _getProperties(objectId: string): Promise { + return new Promise((resolve, reject) => { + this.post( + 'Runtime.getProperties', + { + objectId, + ownProperties: true, + }, + (err, params) => { + if (err) { + reject(err); + } else { + resolve(params.result); + } + }, + ); + }); + } + + /** + * Unrolls an array property + */ + private async _unrollArray(objectId: string): Promise { + const props = await this._getProperties(objectId); + return props + .filter(v => v.name !== 'length' && !isNaN(parseInt(v.name, 10))) + .sort((a, b) => parseInt(a.name, 10) - parseInt(b.name, 10)) + .map(v => v?.value?.value); + } + + /** + * Unrolls an object property + */ + private async _unrollObject(objectId: string): Promise> { + const props = await this._getProperties(objectId); + return props + .map<[string, unknown]>(v => [v.name, v?.value?.value]) + .reduce((obj, [key, val]) => { + obj[key] = val; + return obj; + }, {} as Record); + } +} + +// Add types for the exception event data +type PausedExceptionEvent = Debugger.PausedEventDataType & { + data: { + // This contains error.stack + description: string; + }; +}; + +/** Could this be an anonymous function? */ +function isAnonymous(name: string | undefined): boolean { + return name !== undefined && ['', '?', ''].includes(name); +} + +/** Do the function names appear to match? */ +function functionNamesMatch(a: string | undefined, b: string | undefined): boolean { + return a === b || (isAnonymous(a) && isAnonymous(b)); +} + +/** Creates a unique hash from stack frames */ +function hashFrames(frames: StackFrame[] | undefined): string | undefined { + if (frames === undefined) { + return; + } + + // Only hash the 10 most recent frames (ie. the last 10) + return frames.slice(-10).reduce((acc, frame) => `${acc},${frame.function},${frame.lineno},${frame.colno}`, ''); +} + +/** + * We use the stack parser to create a unique hash from the exception stack trace + * This is used to lookup vars when the exception passes through the event processor + */ +function hashFromStack(stackParser: StackParser, stack: string | undefined): string | undefined { + if (stack === undefined) { + return undefined; + } + + return hashFrames(stackParser(stack, 1)); +} + +export interface FrameVariables { + function: string; + vars?: Record; +} + +/** There are no options yet. This allows them to be added later without breaking changes */ +// eslint-disable-next-line @typescript-eslint/no-empty-interface +interface Options {} + +/** + * Adds local variables to exception frames + */ +export class LocalVariables implements Integration { + public static id: string = 'LocalVariables'; + + public readonly name: string = LocalVariables.id; + + private readonly _cachedFrames: LRUMap> = new LRUMap(20); + + public constructor(_options: Options = {}, private readonly _session: DebugSession = new AsyncSession()) {} + + /** + * @inheritDoc + */ + public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { + this._setup(addGlobalEventProcessor, getCurrentHub().getClient()?.getOptions()); + } + + /** Setup in a way that's easier to call from tests */ + private _setup( + addGlobalEventProcessor: (callback: EventProcessor) => void, + clientOptions: ClientOptions | undefined, + ): void { + if (clientOptions?._experiments?.includeStackLocals) { + this._session.configureAndConnect(ev => + this._handlePaused(clientOptions.stackParser, ev as InspectorNotification), + ); + + addGlobalEventProcessor(async event => this._addLocalVariables(event)); + } + } + + /** + * Handle the pause event + */ + private async _handlePaused( + stackParser: StackParser, + { params: { reason, data, callFrames } }: InspectorNotification, + ): Promise { + if (reason !== 'exception' && reason !== 'promiseRejection') { + return; + } + + // data.description contains the original error.stack + const exceptionHash = hashFromStack(stackParser, data?.description); + + if (exceptionHash == undefined) { + return; + } + + const framePromises = callFrames.map(async ({ scopeChain, functionName, this: obj }) => { + const localScope = scopeChain.find(scope => scope.type === 'local'); + + const fn = obj.className === 'global' ? functionName : `${obj.className}.${functionName}`; + + if (localScope?.object.objectId === undefined) { + return { function: fn }; + } + + const vars = await this._session.getLocalVariables(localScope.object.objectId); + + return { function: fn, vars }; + }); + + // We add the un-awaited promise to the cache rather than await here otherwise the event processor + // can be called before we're finished getting all the vars + this._cachedFrames.set(exceptionHash, Promise.all(framePromises)); + } + + /** + * Adds local variables event stack frames. + */ + private async _addLocalVariables(event: Event): Promise { + for (const exception of event?.exception?.values || []) { + await this._addLocalVariablesToException(exception); + } + + return event; + } + + /** + * Adds local variables to the exception stack frames. + */ + private async _addLocalVariablesToException(exception: Exception): Promise { + const hash = hashFrames(exception?.stacktrace?.frames); + + if (hash === undefined) { + return; + } + + // Check if we have local variables for an exception that matches the hash + // delete is identical to get but also removes the entry from the cache + const cachedFrames = await this._cachedFrames.delete(hash); + + if (cachedFrames === undefined) { + return; + } + + const frameCount = exception.stacktrace?.frames?.length || 0; + + for (let i = 0; i < frameCount; i++) { + // Sentry frames are in reverse order + const frameIndex = frameCount - i - 1; + + // Drop out if we run out of frames to match up + if (!exception?.stacktrace?.frames?.[frameIndex] || !cachedFrames[i]) { + break; + } + + if ( + // We need to have vars to add + cachedFrames[i].vars === undefined || + // We're not interested in frames that are not in_app because the vars are not relevant + exception.stacktrace.frames[frameIndex].in_app === false || + // The function names need to match + !functionNamesMatch(exception.stacktrace.frames[frameIndex].function, cachedFrames[i].function) + ) { + continue; + } + + exception.stacktrace.frames[frameIndex].vars = cachedFrames[i].vars; + } + } +} diff --git a/packages/node/src/sdk.ts b/packages/node/src/sdk.ts index 42751525fbaa..d70d8ec73180 100644 --- a/packages/node/src/sdk.ts +++ b/packages/node/src/sdk.ts @@ -24,6 +24,7 @@ import { ContextLines, Http, LinkedErrors, + LocalVariables, Modules, OnUncaughtException, OnUnhandledRejection, @@ -45,6 +46,7 @@ export const defaultIntegrations = [ new OnUnhandledRejection(), // Event Info new ContextLines(), + new LocalVariables(), new Context(), new Modules(), new RequestData(), diff --git a/packages/node/test/integrations/localvariables.test.ts b/packages/node/test/integrations/localvariables.test.ts new file mode 100644 index 000000000000..a9756e11d623 --- /dev/null +++ b/packages/node/test/integrations/localvariables.test.ts @@ -0,0 +1,269 @@ +import { ClientOptions, EventProcessor } from '@sentry/types'; +import { Debugger, InspectorNotification } from 'inspector'; +import { LRUMap } from 'lru_map'; + +import { defaultStackParser } from '../../src'; +import { DebugSession, FrameVariables, LocalVariables } from '../../src/integrations/localvariables'; +import { getDefaultNodeClientOptions } from '../../test/helper/node-client-options'; + +interface ThrowOn { + configureAndConnect?: boolean; + getLocalVariables?: boolean; +} + +class MockDebugSession implements DebugSession { + private _onPause?: (message: InspectorNotification) => void; + + constructor(private readonly _vars: Record>, private readonly _throwOn?: ThrowOn) {} + + public configureAndConnect(onPause: (message: InspectorNotification) => void): void { + if (this._throwOn?.configureAndConnect) { + throw new Error('configureAndConnect should not be called'); + } + + this._onPause = onPause; + } + + public async getLocalVariables(objectId: string): Promise> { + if (this._throwOn?.getLocalVariables) { + throw new Error('getLocalVariables should not be called'); + } + + return this._vars[objectId]; + } + + public runPause(message: InspectorNotification) { + this._onPause?.(message); + } +} + +interface LocalVariablesPrivate { + _cachedFrames: LRUMap>; + _setup(addGlobalEventProcessor: (callback: EventProcessor) => void, clientOptions: ClientOptions): void; +} + +const exceptionEvent = { + method: 'Debugger.paused', + params: { + reason: 'exception', + data: { + description: + 'Error: Some error\n' + + ' at two (/dist/javascript/src/main.js:23:9)\n' + + ' at one (/dist/javascript/src/main.js:19:3)\n' + + ' at Timeout._onTimeout (/dist/javascript/src/main.js:40:5)\n' + + ' at listOnTimeout (node:internal/timers:559:17)\n' + + ' at process.processTimers (node:internal/timers:502:7)', + }, + callFrames: [ + { + callFrameId: '-6224981551105448869.1.0', + functionName: 'two', + location: { scriptId: '134', lineNumber: 22 }, + url: '', + scopeChain: [ + { + type: 'local', + object: { + type: 'object', + className: 'Object', + objectId: '-6224981551105448869.1.2', + }, + name: 'two', + }, + ], + this: { + type: 'object', + className: 'global', + }, + }, + { + callFrameId: '-6224981551105448869.1.1', + functionName: 'one', + location: { scriptId: '134', lineNumber: 18 }, + url: '', + scopeChain: [ + { + type: 'local', + object: { + type: 'object', + className: 'Object', + objectId: '-6224981551105448869.1.6', + }, + name: 'one', + }, + ], + this: { + type: 'object', + className: 'global', + }, + }, + ], + }, +}; + +describe('LocalVariables', () => { + it('Adds local variables to stack frames', async () => { + expect.assertions(7); + + const session = new MockDebugSession({ + '-6224981551105448869.1.2': { name: 'tim' }, + '-6224981551105448869.1.6': { arr: [1, 2, 3] }, + }); + const localVariables = new LocalVariables({}, session); + const options = getDefaultNodeClientOptions({ + stackParser: defaultStackParser, + _experiments: { includeStackLocals: true }, + }); + + let eventProcessor: EventProcessor | undefined; + + (localVariables as unknown as LocalVariablesPrivate)._setup(callback => { + eventProcessor = callback; + }, options); + + expect(eventProcessor).toBeDefined(); + + session.runPause(exceptionEvent); + + expect((localVariables as unknown as LocalVariablesPrivate)._cachedFrames.size).toBe(1); + + let frames: Promise | undefined; + + (localVariables as unknown as LocalVariablesPrivate)._cachedFrames.forEach(promise => { + frames = promise; + }); + + expect(frames).toBeDefined(); + + const vars = await (frames as Promise); + + expect(vars).toEqual([ + { function: 'two', vars: { name: 'tim' } }, + { function: 'one', vars: { arr: [1, 2, 3] } }, + ]); + + const event = await eventProcessor?.( + { + event_id: '9cbf882ade9a415986632ac4e16918eb', + platform: 'node', + timestamp: 1671113680.306, + level: 'fatal', + exception: { + values: [ + { + type: 'Error', + value: 'Some error', + stacktrace: { + frames: [ + { + function: 'process.processTimers', + lineno: 502, + colno: 7, + in_app: false, + }, + { + function: 'listOnTimeout', + lineno: 559, + colno: 17, + in_app: false, + }, + { + function: 'Timeout._onTimeout', + lineno: 40, + colno: 5, + in_app: true, + }, + { + function: 'one', + lineno: 19, + colno: 3, + in_app: true, + }, + { + function: 'two', + lineno: 23, + colno: 9, + in_app: true, + }, + ], + }, + mechanism: { type: 'generic', handled: true }, + }, + ], + }, + }, + {}, + ); + + expect(event?.exception?.values?.[0].stacktrace?.frames?.[3]?.vars).toEqual({ arr: [1, 2, 3] }); + expect(event?.exception?.values?.[0].stacktrace?.frames?.[4]?.vars).toEqual({ name: 'tim' }); + + expect((localVariables as unknown as LocalVariablesPrivate)._cachedFrames.size).toBe(0); + }); + + it('Should not lookup variables for non-exception reasons', async () => { + expect.assertions(1); + + const session = new MockDebugSession({}, { getLocalVariables: true }); + const localVariables = new LocalVariables({}, session); + const options = getDefaultNodeClientOptions({ + stackParser: defaultStackParser, + _experiments: { includeStackLocals: true }, + }); + + (localVariables as unknown as LocalVariablesPrivate)._setup(_ => {}, options); + + const nonExceptionEvent = { + method: exceptionEvent.method, + params: { ...exceptionEvent.params, reason: 'non-exception-reason' }, + }; + + session.runPause(nonExceptionEvent); + + expect((localVariables as unknown as LocalVariablesPrivate)._cachedFrames.size).toBe(0); + }); + + it('Should not initialize when disabled', async () => { + expect.assertions(1); + + const session = new MockDebugSession({}, { configureAndConnect: true }); + const localVariables = new LocalVariables({}, session); + const options = getDefaultNodeClientOptions({ + stackParser: defaultStackParser, + _experiments: { includeStackLocals: false }, + }); + + let eventProcessor: EventProcessor | undefined; + + (localVariables as unknown as LocalVariablesPrivate)._setup(callback => { + eventProcessor = callback; + }, options); + + expect(eventProcessor).toBeUndefined(); + }); + + it.only('Should cache identical uncaught exception events', async () => { + expect.assertions(1); + + const session = new MockDebugSession({ + '-6224981551105448869.1.2': { name: 'tim' }, + '-6224981551105448869.1.6': { arr: [1, 2, 3] }, + }); + const localVariables = new LocalVariables({}, session); + const options = getDefaultNodeClientOptions({ + stackParser: defaultStackParser, + _experiments: { includeStackLocals: true }, + }); + + (localVariables as unknown as LocalVariablesPrivate)._setup(_ => {}, options); + + session.runPause(exceptionEvent); + session.runPause(exceptionEvent); + session.runPause(exceptionEvent); + session.runPause(exceptionEvent); + session.runPause(exceptionEvent); + + expect((localVariables as unknown as LocalVariablesPrivate)._cachedFrames.size).toBe(1); + }); +}); From 0adaff4faf433bb2b47491a82dd9e2f4b771c73c Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 9 Jan 2023 14:17:25 +0100 Subject: [PATCH 021/113] meta: Update changelog for 7.30.0 (#6693) --- CHANGELOG.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1cc773f11af1..7c1cda541031 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,26 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 7.30.0 + +- feat(core): Add `addIntegration` method to client (#6651) +- feat(core): Add `replay_event` type for events (#6481) +- feat(gatsby): Support Gatsby v5 (#6635) +- feat(integrations): Add HTTPClient integration (#6500) +- feat(node): Add `LocalVariables` integration to capture local variables to stack frames (#6478) +- feat(node): Check for invalid url in node transport (#6623) +- feat(replay): Remove `replayType` from tags and into `replay_event` (#6658) +- feat(transport): Return result through Transport send (#6626) +- fix(nextjs): Don't wrap `res.json` and `res.send` (#6674) +- fix(nextjs): Don't write to `res.end` to fix `next export` (#6682) +- fix(nextjs): Exclude SDK from Edge runtime bundles (#6683) +- fix(replay): Allow Replay to be used in Electron renderers with nodeIntegration enabled (#6644) +- fix(utils): Ignore stack frames over 1kb (#6627) +- ref(angular) Add error-like objects handling (#6446) +- ref(nextjs): Remove `instrumentSever` (#6592) + +Work in this release contributed by @rjoonas, @Naddiseo, and @theofidry. Thank you for your contributions! + ## 7.29.0 This update includes a change to the `@sentry/nextjs` SDK that may increase response times of requests in applications From 9f06d4843b36891de2fa7dbb1aecaff91612ef26 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Mon, 9 Jan 2023 16:27:40 +0000 Subject: [PATCH 022/113] feat(tests): Inject `@sentry/integrations` bundles to Playwright templates (#6666) --- .../integrations/httpclient/fetch/test.ts | 6 --- .../integrations/httpclient/xhr/test.ts | 6 --- .../integration-tests/utils/generatePlugin.ts | 39 ++++++++++++++++--- 3 files changed, 33 insertions(+), 18 deletions(-) diff --git a/packages/integration-tests/suites/integrations/httpclient/fetch/test.ts b/packages/integration-tests/suites/integrations/httpclient/fetch/test.ts index 1d31c868d784..d399dd209a84 100644 --- a/packages/integration-tests/suites/integrations/httpclient/fetch/test.ts +++ b/packages/integration-tests/suites/integrations/httpclient/fetch/test.ts @@ -7,12 +7,6 @@ import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; sentryTest( 'should assign request and response context from a failed 500 fetch request', async ({ getLocalTestPath, page }) => { - // Skipping this test when running in bundle mode, because `@sentry/integrations` bundle - // is not injected to the page with the current test setup. - if (process.env.PW_BUNDLE?.includes('bundle')) { - sentryTest.skip(); - } - const url = await getLocalTestPath({ testDir: __dirname }); await page.route('**/foo', route => { diff --git a/packages/integration-tests/suites/integrations/httpclient/xhr/test.ts b/packages/integration-tests/suites/integrations/httpclient/xhr/test.ts index 7d19da9854f4..72167f73ca2b 100644 --- a/packages/integration-tests/suites/integrations/httpclient/xhr/test.ts +++ b/packages/integration-tests/suites/integrations/httpclient/xhr/test.ts @@ -7,12 +7,6 @@ import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; sentryTest( 'should assign request and response context from a failed 500 XHR request', async ({ getLocalTestPath, page }) => { - // Skipping this test when running in bundle mode, because `@sentry/integrations` bundle - // is not injected to the page with the current test setup. - if (process.env.PW_BUNDLE?.includes('bundle')) { - sentryTest.skip(); - } - const url = await getLocalTestPath({ testDir: __dirname }); await page.route('**/foo', route => { diff --git a/packages/integration-tests/utils/generatePlugin.ts b/packages/integration-tests/utils/generatePlugin.ts index acaf711946f3..ebd57bf2cfb6 100644 --- a/packages/integration-tests/utils/generatePlugin.ts +++ b/packages/integration-tests/utils/generatePlugin.ts @@ -32,6 +32,14 @@ const BUNDLE_PATHS: Record> = { bundle_es6: 'build/bundles/bundle.tracing.js', bundle_es6_min: 'build/bundles/bundle.tracing.min.js', }, + integrations: { + cjs: 'build/npm/cjs/index.js', + esm: 'build/npm/esm/index.js', + bundle_es5: 'build/bundles/[INTEGRATION_NAME].es5.js', + bundle_es5_min: 'build/bundles/[INTEGRATION_NAME].es5.min.js', + bundle_es6: 'build/bundles/[INTEGRATION_NAME].js', + bundle_es6_min: 'build/bundles/[INTEGRATION_NAME].min.js', + }, }; /* @@ -78,6 +86,7 @@ function generateSentryAlias(): Record { class SentryScenarioGenerationPlugin { public requiresTracing: boolean = false; + public requiredIntegrations: string[] = []; private _name: string = 'SentryScenarioGenerationPlugin'; @@ -89,18 +98,24 @@ class SentryScenarioGenerationPlugin { // To help Webpack resolve Sentry modules in `import` statements in cases where they're provided in bundles rather than in `node_modules` '@sentry/browser': 'Sentry', '@sentry/tracing': 'Sentry', + '@sentry/integrations': 'Sentry.Integrations', } : {}; - // Checking if the current scenario has imported `@sentry/tracing`. + // Checking if the current scenario has imported `@sentry/tracing` or `@sentry/integrations`. compiler.hooks.normalModuleFactory.tap(this._name, factory => { factory.hooks.parser.for('javascript/auto').tap(this._name, parser => { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - parser.hooks.import.tap(this._name, (_statement: unknown, source: string) => { - if (source === '@sentry/tracing') { - this.requiresTracing = true; - } - }); + parser.hooks.import.tap( + this._name, + (statement: { specifiers: [{ imported: { name: string } }] }, source: string) => { + if (source === '@sentry/tracing') { + this.requiresTracing = true; + } else if (source === '@sentry/integrations') { + this.requiredIntegrations.push(statement.specifiers[0].imported.name.toLowerCase()); + } + }, + ); }); }); @@ -113,6 +128,18 @@ class SentryScenarioGenerationPlugin { src: path.resolve(PACKAGES_DIR, bundleName, BUNDLE_PATHS[bundleName][bundleKey]), }); + this.requiredIntegrations.forEach(integration => { + const integrationObject = createHtmlTagObject('script', { + src: path.resolve( + PACKAGES_DIR, + 'integrations', + BUNDLE_PATHS['integrations'][bundleKey].replace('[INTEGRATION_NAME]', integration), + ), + }); + + data.assetTags.scripts.unshift(integrationObject); + }); + data.assetTags.scripts.unshift(bundleObject); } From c8c972befde2e033db95e466c73565836b8b72fc Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Tue, 10 Jan 2023 10:06:32 +0100 Subject: [PATCH 023/113] fix(ci): Update package lists before downloading playwright deps (#6694) --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 66bf558f473a..20b723d90ca0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -546,6 +546,7 @@ jobs: PW_TRACING_ONLY: ${{ matrix.tracing_only }} run: | cd packages/integration-tests + sudo apt-get update yarn run playwright install-deps webkit yarn test:ci From 10a80d5cbff367022ca4fc2d2e9b4cf76a1fc319 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Tue, 10 Jan 2023 13:34:43 +0100 Subject: [PATCH 024/113] ci: Fix CI failing on master (#6703) --- .github/workflows/build.yml | 1 - packages/integration-tests/package.json | 4 ++-- packages/remix/test/integration/package.json | 10 ++++----- yarn.lock | 22 +++++++++++++++----- 4 files changed, 24 insertions(+), 13 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 20b723d90ca0..66bf558f473a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -546,7 +546,6 @@ jobs: PW_TRACING_ONLY: ${{ matrix.tracing_only }} run: | cd packages/integration-tests - sudo apt-get update yarn run playwright install-deps webkit yarn test:ci diff --git a/packages/integration-tests/package.json b/packages/integration-tests/package.json index 5c30315831d7..08dfeaf9c610 100644 --- a/packages/integration-tests/package.json +++ b/packages/integration-tests/package.json @@ -26,10 +26,10 @@ }, "dependencies": { "@babel/preset-typescript": "^7.16.7", - "@playwright/test": "^1.27.1", + "@playwright/test": "^1.29.2", "babel-loader": "^8.2.2", "html-webpack-plugin": "^5.5.0", - "playwright": "^1.27.1", + "playwright": "^1.29.2", "typescript": "^4.5.2", "webpack": "^5.52.0" }, diff --git a/packages/remix/test/integration/package.json b/packages/remix/test/integration/package.json index 724ee95c1d61..102a7844b1f9 100644 --- a/packages/remix/test/integration/package.json +++ b/packages/remix/test/integration/package.json @@ -7,16 +7,16 @@ "start": "remix-serve build" }, "dependencies": { - "@remix-run/express": "^1.6.5", - "@remix-run/node": "^1.6.5", - "@remix-run/react": "^1.6.5", - "@remix-run/serve": "^1.6.5", + "@remix-run/express": "1.9.0", + "@remix-run/node": "1.9.0", + "@remix-run/react": "1.9.0", + "@remix-run/serve": "1.9.0", "@sentry/remix": "file:../..", "react": "^17.0.2", "react-dom": "^17.0.2" }, "devDependencies": { - "@remix-run/dev": "^1.6.5", + "@remix-run/dev": "1.9.0", "@types/react": "^17.0.47", "@types/react-dom": "^17.0.17", "nock": "^13.1.0", diff --git a/yarn.lock b/yarn.lock index d1bfebbebeba..aebbc2871d55 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3225,13 +3225,13 @@ "@opentelemetry/resources" "^0.12.0" "@opentelemetry/semantic-conventions" "^0.12.0" -"@playwright/test@^1.27.1": - version "1.28.1" - resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.28.1.tgz#e5be297e024a3256610cac2baaa9347fd57c7860" - integrity sha512-xN6spdqrNlwSn9KabIhqfZR7IWjPpFK1835tFNgjrlysaSezuX8PYUwaz38V/yI8TJLG9PkAMEXoHRXYXlpTPQ== +"@playwright/test@^1.29.2": + version "1.29.2" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.29.2.tgz#c48184721d0f0b7627a886e2ec42f1efb2be339d" + integrity sha512-+3/GPwOgcoF0xLz/opTnahel1/y42PdcgZ4hs+BZGIUjtmEFSXGg+nFoaH3NSmuc7a6GSFwXDJ5L7VXpqzigNg== dependencies: "@types/node" "*" - playwright-core "1.28.1" + playwright-core "1.29.2" "@polka/url@^1.0.0-next.9": version "1.0.0-next.12" @@ -19107,6 +19107,11 @@ playwright-core@1.28.1: resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.28.1.tgz#8400be9f4a8d1c0489abdb9e75a4cc0ffc3c00cb" integrity sha512-3PixLnGPno0E8rSBJjtwqTwJe3Yw72QwBBBxNoukIj3lEeBNXwbNiKrNuB1oyQgTBw5QHUhNO3SteEtHaMK6ag== +playwright-core@1.29.2: + version "1.29.2" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.29.2.tgz#2e8347e7e8522409f22b244e600e703b64022406" + integrity sha512-94QXm4PMgFoHAhlCuoWyaBYKb92yOcGVHdQLoxQ7Wjlc7Flg4aC/jbFW7xMR52OfXMVkWicue4WXE7QEegbIRA== + playwright@^1.27.1: version "1.28.1" resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.28.1.tgz#f23247f1de466ff73d7230d94df96271e5da6583" @@ -19114,6 +19119,13 @@ playwright@^1.27.1: dependencies: playwright-core "1.28.1" +playwright@^1.29.2: + version "1.29.2" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.29.2.tgz#d6a0a3e8e44f023f7956ed19ffa8af915a042769" + integrity sha512-hKBYJUtdmYzcjdhYDkP9WGtORwwZBBKAW8+Lz7sr0ZMxtJr04ASXVzH5eBWtDkdb0c3LLFsehfPBTRfvlfKJOA== + dependencies: + playwright-core "1.29.2" + plugin-error@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/plugin-error/-/plugin-error-1.0.1.tgz#77016bd8919d0ac377fdcdd0322328953ca5781c" From 19ec8c8b502738f516fc06f83c6bde5304f648ad Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Tue, 10 Jan 2023 12:38:46 +0000 Subject: [PATCH 025/113] release: 7.30.0 --- lerna.json | 2 +- packages/angular/package.json | 8 ++++---- packages/browser/package.json | 10 +++++----- packages/core/package.json | 6 +++--- packages/core/src/version.ts | 2 +- packages/e2e-tests/package.json | 2 +- packages/ember/package.json | 10 +++++----- packages/eslint-config-sdk/package.json | 6 +++--- packages/eslint-plugin-sdk/package.json | 2 +- packages/gatsby/package.json | 10 +++++----- packages/hub/package.json | 8 ++++---- packages/integration-tests/package.json | 2 +- packages/integrations/package.json | 6 +++--- packages/nextjs/package.json | 18 +++++++++--------- packages/node-integration-tests/package.json | 2 +- packages/node/package.json | 8 ++++---- packages/opentelemetry-node/package.json | 12 ++++++------ packages/react/package.json | 8 ++++---- packages/remix/package.json | 16 ++++++++-------- packages/replay/package.json | 8 ++++---- packages/serverless/package.json | 10 +++++----- packages/svelte/package.json | 8 ++++---- packages/tracing/package.json | 10 +++++----- packages/types/package.json | 2 +- packages/typescript/package.json | 2 +- packages/utils/package.json | 4 ++-- packages/vue/package.json | 10 +++++----- packages/wasm/package.json | 8 ++++---- 28 files changed, 100 insertions(+), 100 deletions(-) diff --git a/lerna.json b/lerna.json index e4acd69f4123..408b79d9c474 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "lerna": "3.4.0", - "version": "7.29.0", + "version": "7.30.0", "packages": "packages/*", "npmClient": "yarn", "useWorkspaces": true diff --git a/packages/angular/package.json b/packages/angular/package.json index 7fb82c86056d..9d97dc49a0f6 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/angular", - "version": "7.29.0", + "version": "7.30.0", "description": "Official Sentry SDK for Angular", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/angular", @@ -21,9 +21,9 @@ "rxjs": "^6.5.5 || ^7.x" }, "dependencies": { - "@sentry/browser": "7.29.0", - "@sentry/types": "7.29.0", - "@sentry/utils": "7.29.0", + "@sentry/browser": "7.30.0", + "@sentry/types": "7.30.0", + "@sentry/utils": "7.30.0", "tslib": "^2.0.0" }, "devDependencies": { diff --git a/packages/browser/package.json b/packages/browser/package.json index 660866ac5c96..e45e29b81451 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/browser", - "version": "7.29.0", + "version": "7.30.0", "description": "Official Sentry SDK for browsers", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/browser", @@ -16,10 +16,10 @@ "access": "public" }, "dependencies": { - "@sentry/core": "7.29.0", - "@sentry/replay": "7.29.0", - "@sentry/types": "7.29.0", - "@sentry/utils": "7.29.0", + "@sentry/core": "7.30.0", + "@sentry/replay": "7.30.0", + "@sentry/types": "7.30.0", + "@sentry/utils": "7.30.0", "tslib": "^1.9.3" }, "devDependencies": { diff --git a/packages/core/package.json b/packages/core/package.json index 555c83a2379a..fb0244a7993d 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/core", - "version": "7.29.0", + "version": "7.30.0", "description": "Base implementation for all Sentry JavaScript SDKs", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/core", @@ -16,8 +16,8 @@ "access": "public" }, "dependencies": { - "@sentry/types": "7.29.0", - "@sentry/utils": "7.29.0", + "@sentry/types": "7.30.0", + "@sentry/utils": "7.30.0", "tslib": "^1.9.3" }, "scripts": { diff --git a/packages/core/src/version.ts b/packages/core/src/version.ts index 624d4bc7a47b..a90c364596fe 100644 --- a/packages/core/src/version.ts +++ b/packages/core/src/version.ts @@ -1 +1 @@ -export const SDK_VERSION = '7.29.0'; +export const SDK_VERSION = '7.30.0'; diff --git a/packages/e2e-tests/package.json b/packages/e2e-tests/package.json index a59169ef61ce..3c70a4163955 100644 --- a/packages/e2e-tests/package.json +++ b/packages/e2e-tests/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/e2e-tests", - "version": "7.29.0", + "version": "7.30.0", "license": "MIT", "engines": { "node": ">=10" diff --git a/packages/ember/package.json b/packages/ember/package.json index 70f05202afac..a2703b03fcb1 100644 --- a/packages/ember/package.json +++ b/packages/ember/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/ember", - "version": "7.29.0", + "version": "7.30.0", "description": "Official Sentry SDK for Ember.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/ember", @@ -30,10 +30,10 @@ }, "dependencies": { "@embroider/macros": "^1.9.0", - "@sentry/browser": "7.29.0", - "@sentry/tracing": "7.29.0", - "@sentry/types": "7.29.0", - "@sentry/utils": "7.29.0", + "@sentry/browser": "7.30.0", + "@sentry/tracing": "7.30.0", + "@sentry/types": "7.30.0", + "@sentry/utils": "7.30.0", "ember-auto-import": "^1.12.1 || ^2.4.3", "ember-cli-babel": "^7.26.11", "ember-cli-htmlbars": "^6.1.1", diff --git a/packages/eslint-config-sdk/package.json b/packages/eslint-config-sdk/package.json index a03cc033bb9d..08fb32ce9883 100644 --- a/packages/eslint-config-sdk/package.json +++ b/packages/eslint-config-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/eslint-config-sdk", - "version": "7.29.0", + "version": "7.30.0", "description": "Official Sentry SDK eslint config", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/eslint-config-sdk", @@ -19,8 +19,8 @@ "access": "public" }, "dependencies": { - "@sentry-internal/eslint-plugin-sdk": "7.29.0", - "@sentry-internal/typescript": "7.29.0", + "@sentry-internal/eslint-plugin-sdk": "7.30.0", + "@sentry-internal/typescript": "7.30.0", "@typescript-eslint/eslint-plugin": "^5.48.0", "@typescript-eslint/parser": "^5.48.0", "eslint-config-prettier": "^6.11.0", diff --git a/packages/eslint-plugin-sdk/package.json b/packages/eslint-plugin-sdk/package.json index bef126ca3150..9ce9602d56aa 100644 --- a/packages/eslint-plugin-sdk/package.json +++ b/packages/eslint-plugin-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/eslint-plugin-sdk", - "version": "7.29.0", + "version": "7.30.0", "description": "Official Sentry SDK eslint plugin", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/eslint-plugin-sdk", diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json index 508ba3fed1e5..27c50112150e 100644 --- a/packages/gatsby/package.json +++ b/packages/gatsby/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/gatsby", - "version": "7.29.0", + "version": "7.30.0", "description": "Official Sentry SDK for Gatsby.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/gatsby", @@ -20,10 +20,10 @@ "access": "public" }, "dependencies": { - "@sentry/react": "7.29.0", - "@sentry/tracing": "7.29.0", - "@sentry/types": "7.29.0", - "@sentry/utils": "7.29.0", + "@sentry/react": "7.30.0", + "@sentry/tracing": "7.30.0", + "@sentry/types": "7.30.0", + "@sentry/utils": "7.30.0", "@sentry/webpack-plugin": "1.19.0" }, "peerDependencies": { diff --git a/packages/hub/package.json b/packages/hub/package.json index f37f53689d47..d28265f9131d 100644 --- a/packages/hub/package.json +++ b/packages/hub/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/hub", - "version": "7.29.0", + "version": "7.30.0", "description": "Sentry hub which handles global state managment.", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/hub", @@ -16,9 +16,9 @@ "access": "public" }, "dependencies": { - "@sentry/core": "7.29.0", - "@sentry/types": "7.29.0", - "@sentry/utils": "7.29.0", + "@sentry/core": "7.30.0", + "@sentry/types": "7.30.0", + "@sentry/utils": "7.30.0", "tslib": "^1.9.3" }, "scripts": { diff --git a/packages/integration-tests/package.json b/packages/integration-tests/package.json index 08dfeaf9c610..9117874b84a4 100644 --- a/packages/integration-tests/package.json +++ b/packages/integration-tests/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/browser-integration-tests", - "version": "7.29.0", + "version": "7.30.0", "main": "index.js", "license": "MIT", "engines": { diff --git a/packages/integrations/package.json b/packages/integrations/package.json index 2839e69abb2b..2b6f9b1180ee 100644 --- a/packages/integrations/package.json +++ b/packages/integrations/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/integrations", - "version": "7.29.0", + "version": "7.30.0", "description": "Pluggable integrations that can be used to enhance JS SDKs", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/integrations", @@ -16,8 +16,8 @@ "module": "build/npm/esm/index.js", "types": "build/npm/types/index.d.ts", "dependencies": { - "@sentry/types": "7.29.0", - "@sentry/utils": "7.29.0", + "@sentry/types": "7.30.0", + "@sentry/utils": "7.30.0", "localforage": "^1.8.1", "tslib": "^1.9.3" }, diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 453cc7327238..a5a60ed0dd33 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/nextjs", - "version": "7.29.0", + "version": "7.30.0", "description": "Official Sentry SDK for Next.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/nextjs", @@ -19,20 +19,20 @@ "dependencies": { "@rollup/plugin-sucrase": "4.0.4", "@rollup/plugin-virtual": "3.0.0", - "@sentry/core": "7.29.0", - "@sentry/integrations": "7.29.0", - "@sentry/node": "7.29.0", - "@sentry/react": "7.29.0", - "@sentry/tracing": "7.29.0", - "@sentry/types": "7.29.0", - "@sentry/utils": "7.29.0", + "@sentry/core": "7.30.0", + "@sentry/integrations": "7.30.0", + "@sentry/node": "7.30.0", + "@sentry/react": "7.30.0", + "@sentry/tracing": "7.30.0", + "@sentry/types": "7.30.0", + "@sentry/utils": "7.30.0", "@sentry/webpack-plugin": "1.20.0", "chalk": "3.0.0", "rollup": "2.78.0", "tslib": "^1.9.3" }, "devDependencies": { - "@sentry/nextjs": "7.29.0", + "@sentry/nextjs": "7.30.0", "@types/webpack": "^4.41.31", "eslint-plugin-react": "^7.31.11", "next": "10.1.3" diff --git a/packages/node-integration-tests/package.json b/packages/node-integration-tests/package.json index 797ccb250ec6..1ed9cb62e8fd 100644 --- a/packages/node-integration-tests/package.json +++ b/packages/node-integration-tests/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/node-integration-tests", - "version": "7.29.0", + "version": "7.30.0", "license": "MIT", "engines": { "node": ">=10" diff --git a/packages/node/package.json b/packages/node/package.json index 5e1cee3e64fb..ddb098e08f74 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/node", - "version": "7.29.0", + "version": "7.30.0", "description": "Official Sentry SDK for Node.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/node", @@ -16,9 +16,9 @@ "access": "public" }, "dependencies": { - "@sentry/core": "7.29.0", - "@sentry/types": "7.29.0", - "@sentry/utils": "7.29.0", + "@sentry/core": "7.30.0", + "@sentry/types": "7.30.0", + "@sentry/utils": "7.30.0", "cookie": "^0.4.1", "https-proxy-agent": "^5.0.0", "lru_map": "^0.3.3", diff --git a/packages/opentelemetry-node/package.json b/packages/opentelemetry-node/package.json index b7c2d1359448..af2181f02eb1 100644 --- a/packages/opentelemetry-node/package.json +++ b/packages/opentelemetry-node/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/opentelemetry-node", - "version": "7.29.0", + "version": "7.30.0", "description": "Official Sentry SDK for OpenTelemetry Node.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/opentelemetry-node", @@ -16,10 +16,10 @@ "access": "public" }, "dependencies": { - "@sentry/core": "7.29.0", - "@sentry/tracing": "7.29.0", - "@sentry/types": "7.29.0", - "@sentry/utils": "7.29.0" + "@sentry/core": "7.30.0", + "@sentry/tracing": "7.30.0", + "@sentry/types": "7.30.0", + "@sentry/utils": "7.30.0" }, "peerDependencies": { "@opentelemetry/api": "1.x", @@ -33,7 +33,7 @@ "@opentelemetry/sdk-trace-base": "^1.7.0", "@opentelemetry/sdk-trace-node": "^1.7.0", "@opentelemetry/semantic-conventions": "^1.7.0", - "@sentry/node": "7.29.0" + "@sentry/node": "7.30.0" }, "scripts": { "build": "run-p build:rollup build:types", diff --git a/packages/react/package.json b/packages/react/package.json index 48d8008547e0..cf262e1d4aa0 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/react", - "version": "7.29.0", + "version": "7.30.0", "description": "Official Sentry SDK for React.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/react", @@ -16,9 +16,9 @@ "access": "public" }, "dependencies": { - "@sentry/browser": "7.29.0", - "@sentry/types": "7.29.0", - "@sentry/utils": "7.29.0", + "@sentry/browser": "7.30.0", + "@sentry/types": "7.30.0", + "@sentry/utils": "7.30.0", "hoist-non-react-statics": "^3.3.2", "tslib": "^1.9.3" }, diff --git a/packages/remix/package.json b/packages/remix/package.json index b48366671d16..319438487765 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/remix", - "version": "7.29.0", + "version": "7.30.0", "description": "Official Sentry SDK for Remix", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/remix", @@ -21,13 +21,13 @@ }, "dependencies": { "@sentry/cli": "2.2.0", - "@sentry/core": "7.29.0", - "@sentry/integrations": "7.29.0", - "@sentry/node": "7.29.0", - "@sentry/react": "7.29.0", - "@sentry/tracing": "7.29.0", - "@sentry/types": "7.29.0", - "@sentry/utils": "7.29.0", + "@sentry/core": "7.30.0", + "@sentry/integrations": "7.30.0", + "@sentry/node": "7.30.0", + "@sentry/react": "7.30.0", + "@sentry/tracing": "7.30.0", + "@sentry/types": "7.30.0", + "@sentry/utils": "7.30.0", "@sentry/webpack-plugin": "1.19.0", "tslib": "^1.9.3", "yargs": "^17.6.0" diff --git a/packages/replay/package.json b/packages/replay/package.json index c01ec6848e77..9f2a3beffde7 100644 --- a/packages/replay/package.json +++ b/packages/replay/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/replay", - "version": "7.29.0", + "version": "7.30.0", "description": "User replays for Sentry", "main": "build/npm/cjs/index.js", "module": "build/npm/esm/index.js", @@ -53,9 +53,9 @@ "tslib": "^1.9.3" }, "dependencies": { - "@sentry/core": "7.29.0", - "@sentry/types": "7.29.0", - "@sentry/utils": "7.29.0" + "@sentry/core": "7.30.0", + "@sentry/types": "7.30.0", + "@sentry/utils": "7.30.0" }, "peerDependencies": { "@sentry/browser": ">=7.24.0" diff --git a/packages/serverless/package.json b/packages/serverless/package.json index de02c4f88ad8..58ff37e7397a 100644 --- a/packages/serverless/package.json +++ b/packages/serverless/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/serverless", - "version": "7.29.0", + "version": "7.30.0", "description": "Official Sentry SDK for various serverless solutions", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/serverless", @@ -16,10 +16,10 @@ "access": "public" }, "dependencies": { - "@sentry/node": "7.29.0", - "@sentry/tracing": "7.29.0", - "@sentry/types": "7.29.0", - "@sentry/utils": "7.29.0", + "@sentry/node": "7.30.0", + "@sentry/tracing": "7.30.0", + "@sentry/types": "7.30.0", + "@sentry/utils": "7.30.0", "@types/aws-lambda": "^8.10.62", "@types/express": "^4.17.14", "tslib": "^1.9.3" diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 929e035cdc9e..36e1be5eb435 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/svelte", - "version": "7.29.0", + "version": "7.30.0", "description": "Official Sentry SDK for Svelte", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/svelte", @@ -16,9 +16,9 @@ "access": "public" }, "dependencies": { - "@sentry/browser": "7.29.0", - "@sentry/types": "7.29.0", - "@sentry/utils": "7.29.0", + "@sentry/browser": "7.30.0", + "@sentry/types": "7.30.0", + "@sentry/utils": "7.30.0", "magic-string": "^0.26.2", "tslib": "^1.9.3" }, diff --git a/packages/tracing/package.json b/packages/tracing/package.json index 6abfbad9a9a0..d05ae83a50cb 100644 --- a/packages/tracing/package.json +++ b/packages/tracing/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/tracing", - "version": "7.29.0", + "version": "7.30.0", "description": "Extensions for Sentry AM", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/tracing", @@ -16,13 +16,13 @@ "access": "public" }, "dependencies": { - "@sentry/core": "7.29.0", - "@sentry/types": "7.29.0", - "@sentry/utils": "7.29.0", + "@sentry/core": "7.30.0", + "@sentry/types": "7.30.0", + "@sentry/utils": "7.30.0", "tslib": "^1.9.3" }, "devDependencies": { - "@sentry/browser": "7.29.0", + "@sentry/browser": "7.30.0", "@types/express": "^4.17.14" }, "scripts": { diff --git a/packages/types/package.json b/packages/types/package.json index 3c4b5f250412..4187d401db82 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/types", - "version": "7.29.0", + "version": "7.30.0", "description": "Types for all Sentry JavaScript SDKs", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/types", diff --git a/packages/typescript/package.json b/packages/typescript/package.json index ed9ad431eb84..ac39dca30034 100644 --- a/packages/typescript/package.json +++ b/packages/typescript/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/typescript", - "version": "7.29.0", + "version": "7.30.0", "description": "Typescript configuration used at Sentry", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/typescript", diff --git a/packages/utils/package.json b/packages/utils/package.json index 5211adfc448a..9b02fdc55023 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/utils", - "version": "7.29.0", + "version": "7.30.0", "description": "Utilities for all Sentry JavaScript SDKs", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/utils", @@ -16,7 +16,7 @@ "access": "public" }, "dependencies": { - "@sentry/types": "7.29.0", + "@sentry/types": "7.30.0", "tslib": "^1.9.3" }, "devDependencies": { diff --git a/packages/vue/package.json b/packages/vue/package.json index 7051e307b30f..8f639697486e 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/vue", - "version": "7.29.0", + "version": "7.30.0", "description": "Official Sentry SDK for Vue.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/vue", @@ -16,10 +16,10 @@ "access": "public" }, "dependencies": { - "@sentry/browser": "7.29.0", - "@sentry/core": "7.29.0", - "@sentry/types": "7.29.0", - "@sentry/utils": "7.29.0", + "@sentry/browser": "7.30.0", + "@sentry/core": "7.30.0", + "@sentry/types": "7.30.0", + "@sentry/utils": "7.30.0", "tslib": "^1.9.3" }, "peerDependencies": { diff --git a/packages/wasm/package.json b/packages/wasm/package.json index f3d1fc801aca..7f8cd3abdfb8 100644 --- a/packages/wasm/package.json +++ b/packages/wasm/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/wasm", - "version": "7.29.0", + "version": "7.30.0", "description": "Support for WASM.", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/wasm", @@ -16,9 +16,9 @@ "access": "public" }, "dependencies": { - "@sentry/browser": "7.29.0", - "@sentry/types": "7.29.0", - "@sentry/utils": "7.29.0", + "@sentry/browser": "7.30.0", + "@sentry/types": "7.30.0", + "@sentry/utils": "7.30.0", "tslib": "^1.9.3" }, "devDependencies": { From 5938288b4fabd53f0712e90a654e43487f9578bc Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Tue, 10 Jan 2023 15:39:33 +0100 Subject: [PATCH 026/113] ci: Add `fail-fast: false` to playwright tests (#6712) --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 66bf558f473a..2830b56b9eb4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -505,6 +505,7 @@ jobs: if: needs.job_get_metadata.outputs.changed_browser_integration == 'true' || github.event_name != 'pull_request' runs-on: ubuntu-20.04 strategy: + fail-fast: false matrix: bundle: - esm From f140888000f25ef2e65cf9d853234fac67da3e7a Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Tue, 10 Jan 2023 16:14:27 +0100 Subject: [PATCH 027/113] ref(nextjs): Use bundling instead of proxying to wrap pages and API routes (#6685) --- packages/nextjs/package.json | 3 +- packages/nextjs/rollup.npm.config.js | 11 +- packages/nextjs/src/config/loaders/index.ts | 2 +- .../nextjs/src/config/loaders/proxyLoader.ts | 98 --------- packages/nextjs/src/config/loaders/rollup.ts | 104 --------- packages/nextjs/src/config/loaders/types.ts | 8 + .../src/config/loaders/wrappingLoader.ts | 197 ++++++++++++++++++ ...oaderTemplate.ts => apiWrapperTemplate.ts} | 4 +- ...aderTemplate.ts => pageWrapperTemplate.ts} | 4 +- packages/nextjs/src/config/webpack.ts | 5 +- yarn.lock | 37 +++- 11 files changed, 246 insertions(+), 227 deletions(-) delete mode 100644 packages/nextjs/src/config/loaders/proxyLoader.ts delete mode 100644 packages/nextjs/src/config/loaders/rollup.ts create mode 100644 packages/nextjs/src/config/loaders/wrappingLoader.ts rename packages/nextjs/src/config/templates/{apiProxyLoaderTemplate.ts => apiWrapperTemplate.ts} (96%) rename packages/nextjs/src/config/templates/{pageProxyLoaderTemplate.ts => pageWrapperTemplate.ts} (95%) diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index a5a60ed0dd33..c7990d1fdb77 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -17,8 +17,7 @@ "access": "public" }, "dependencies": { - "@rollup/plugin-sucrase": "4.0.4", - "@rollup/plugin-virtual": "3.0.0", + "@rollup/plugin-commonjs": "24.0.0", "@sentry/core": "7.30.0", "@sentry/integrations": "7.30.0", "@sentry/node": "7.30.0", diff --git a/packages/nextjs/rollup.npm.config.js b/packages/nextjs/rollup.npm.config.js index f9bdfbf1bcbc..d605dc65f3eb 100644 --- a/packages/nextjs/rollup.npm.config.js +++ b/packages/nextjs/rollup.npm.config.js @@ -14,10 +14,7 @@ export default [ ), ...makeNPMConfigVariants( makeBaseNPMConfig({ - entrypoints: [ - 'src/config/templates/pageProxyLoaderTemplate.ts', - 'src/config/templates/apiProxyLoaderTemplate.ts', - ], + entrypoints: ['src/config/templates/pageWrapperTemplate.ts', 'src/config/templates/apiWrapperTemplate.ts'], packageSpecificConfig: { output: { @@ -32,15 +29,13 @@ export default [ // make it so Rollup calms down about the fact that we're combining default and named exports exports: 'named', }, - external: ['@sentry/nextjs', '__RESOURCE_PATH__'], + external: ['@sentry/nextjs', '__SENTRY_WRAPPING_TARGET__'], }, }), ), ...makeNPMConfigVariants( makeBaseNPMConfig({ entrypoints: ['src/config/loaders/index.ts'], - // Needed in order to successfully import sucrase - esModuleInterop: true, packageSpecificConfig: { output: { @@ -50,7 +45,7 @@ export default [ // make it so Rollup calms down about the fact that we're combining default and named exports exports: 'named', }, - external: ['@rollup/plugin-sucrase', 'rollup'], + external: ['@rollup/plugin-commonjs', 'rollup'], }, }), ), diff --git a/packages/nextjs/src/config/loaders/index.ts b/packages/nextjs/src/config/loaders/index.ts index 150e00bf1ca4..322567c1495b 100644 --- a/packages/nextjs/src/config/loaders/index.ts +++ b/packages/nextjs/src/config/loaders/index.ts @@ -1,3 +1,3 @@ export { default as valueInjectionLoader } from './valueInjectionLoader'; export { default as prefixLoader } from './prefixLoader'; -export { default as proxyLoader } from './proxyLoader'; +export { default as wrappingLoader } from './wrappingLoader'; diff --git a/packages/nextjs/src/config/loaders/proxyLoader.ts b/packages/nextjs/src/config/loaders/proxyLoader.ts deleted file mode 100644 index 42aa1a7a230f..000000000000 --- a/packages/nextjs/src/config/loaders/proxyLoader.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { escapeStringForRegex, logger, stringMatchesSomePattern } from '@sentry/utils'; -import * as fs from 'fs'; -import * as path from 'path'; - -import { rollupize } from './rollup'; -import { LoaderThis } from './types'; - -type LoaderOptions = { - pagesDir: string; - pageExtensionRegex: string; - excludeServerRoutes: Array; - isEdgeRuntime: boolean; -}; - -/** - * Replace the loaded file with a proxy module "wrapping" the original file. In the proxy, the original file is loaded, - * any data-fetching functions (`getInitialProps`, `getStaticProps`, and `getServerSideProps`) it contains are wrapped, - * and then everything is re-exported. - */ -export default async function proxyLoader(this: LoaderThis, userCode: string): Promise { - // We know one or the other will be defined, depending on the version of webpack being used - const { - pagesDir, - pageExtensionRegex, - excludeServerRoutes = [], - isEdgeRuntime, - } = 'getOptions' in this ? this.getOptions() : this.query; - - // We currently don't support the edge runtime - if (isEdgeRuntime) { - return userCode; - } - - // Get the parameterized route name from this page's filepath - const parameterizedRoute = path - // Get the path of the file insde of the pages directory - .relative(pagesDir, this.resourcePath) - // Add a slash at the beginning - .replace(/(.*)/, '/$1') - // Pull off the file extension - .replace(new RegExp(`\\.(${pageExtensionRegex})`), '') - // Any page file named `index` corresponds to root of the directory its in, URL-wise, so turn `/xyz/index` into - // just `/xyz` - .replace(/\/index$/, '') - // In case all of the above have left us with an empty string (which will happen if we're dealing with the - // homepage), sub back in the root route - .replace(/^$/, '/'); - - // Skip explicitly-ignored pages - if (stringMatchesSomePattern(parameterizedRoute, excludeServerRoutes, true)) { - return userCode; - } - - // We don't want to wrap twice (or infinitely), so in the proxy we add this query string onto references to the - // wrapped file, so that we know that it's already been processed. (Adding this query string is also necessary to - // convince webpack that it's a different file than the one it's in the middle of loading now, so that the originals - // themselves will have a chance to load.) - if (this.resourceQuery.includes('__sentry_wrapped__')) { - return userCode; - } - - const templateFile = parameterizedRoute.startsWith('/api') - ? 'apiProxyLoaderTemplate.js' - : 'pageProxyLoaderTemplate.js'; - const templatePath = path.resolve(__dirname, `../templates/${templateFile}`); - let templateCode = fs.readFileSync(templatePath).toString(); - // Make sure the template is included when running `webpack watch` - this.addDependency(templatePath); - - // Inject the route and the path to the file we're wrapping into the template - templateCode = templateCode.replace(/__ROUTE__/g, parameterizedRoute.replace(/\\/g, '\\\\')); - templateCode = templateCode.replace(/__RESOURCE_PATH__/g, this.resourcePath.replace(/\\/g, '\\\\')); - - // Run the proxy module code through Rollup, in order to split the `export * from ''` out into - // individual exports (which nextjs seems to require). - let proxyCode; - try { - proxyCode = await rollupize(templateCode, this.resourcePath); - } catch (err) { - __DEBUG_BUILD__ && - logger.warn( - `Could not wrap ${this.resourcePath}. An error occurred while processing the proxy module template:\n${err}`, - ); - return userCode; - } - - // Add a query string onto all references to the wrapped file, so that webpack will consider it different from the - // non-query-stringged version (which we're already in the middle of loading as we speak), and load it separately from - // this. When the second load happens this loader will run again, but we'll be able to see the query string and will - // know to immediately return without processing. This avoids an infinite loop. - const resourceFilename = path.basename(this.resourcePath); - proxyCode = proxyCode.replace( - new RegExp(`/${escapeStringForRegex(resourceFilename)}'`, 'g'), - `/${resourceFilename}?__sentry_wrapped__'`, - ); - - return proxyCode; -} diff --git a/packages/nextjs/src/config/loaders/rollup.ts b/packages/nextjs/src/config/loaders/rollup.ts deleted file mode 100644 index 6c7861827b95..000000000000 --- a/packages/nextjs/src/config/loaders/rollup.ts +++ /dev/null @@ -1,104 +0,0 @@ -import sucrase from '@rollup/plugin-sucrase'; -import virtual from '@rollup/plugin-virtual'; -import { escapeStringForRegex } from '@sentry/utils'; -import * as path from 'path'; -import type { InputOptions as RollupInputOptions, OutputOptions as RollupOutputOptions } from 'rollup'; -import { rollup } from 'rollup'; - -const SENTRY_PROXY_MODULE_NAME = 'sentry-proxy-module'; - -const getRollupInputOptions = (templateCode: string, userModulePath: string): RollupInputOptions => ({ - input: SENTRY_PROXY_MODULE_NAME, - - plugins: [ - virtual({ - [SENTRY_PROXY_MODULE_NAME]: templateCode, - }), - - sucrase({ - transforms: ['jsx', 'typescript'], - }), - ], - - // We want to process as few files as possible, so as not to slow down the build any more than we have to. We need the - // proxy module (living in the temporary file we've created) and the file we're wrapping not to be external, because - // otherwise they won't be processed. (We need Rollup to process the former so that we can use the code, and we need - // it to process the latter so it knows what exports to re-export from the proxy module.) Past that, we don't care, so - // don't bother to process anything else. - external: importPath => importPath !== SENTRY_PROXY_MODULE_NAME && importPath !== userModulePath, - - // Prevent rollup from stressing out about TS's use of global `this` when polyfilling await. (TS will polyfill if the - // user's tsconfig `target` is set to anything before `es2017`. See https://stackoverflow.com/a/72822340 and - // https://stackoverflow.com/a/60347490.) - context: 'this', - - // Rollup's path-resolution logic when handling re-exports can go wrong when wrapping pages which aren't at the root - // level of the `pages` directory. This may be a bug, as it doesn't match the behavior described in the docs, but what - // seems to happen is this: - // - // - We try to wrap `pages/xyz/userPage.js`, which contains `export { helperFunc } from '../../utils/helper'` - // - Rollup converts '../../utils/helper' into an absolute path - // - We mark the helper module as external - // - Rollup then converts it back to a relative path, but relative to `pages/` rather than `pages/xyz/`. (This is - // the part which doesn't match the docs. They say that Rollup will use the common ancestor of all modules in the - // bundle as the basis for the relative path calculation, but both our temporary file and the page being wrapped - // live in `pages/xyz/`, and they're the only two files in the bundle, so `pages/xyz/`` should be used as the - // root. Unclear why it's not.) - // - As a result of the miscalculation, our proxy module will include `export { helperFunc } from '../utils/helper'` - // rather than the expected `export { helperFunc } from '../../utils/helper'`, thereby causing a build error in - // nextjs.. - // - // Setting `makeAbsoluteExternalsRelative` to `false` prevents all of the above by causing Rollup to ignore imports of - // externals entirely, with the result that their paths remain untouched (which is what we want). - makeAbsoluteExternalsRelative: false, -}); - -const rollupOutputOptions: RollupOutputOptions = { - format: 'esm', - - // Don't create a bundle - we just want the transformed entrypoint file - preserveModules: true, -}; - -/** - * Use Rollup to process the proxy module code, in order to split its `export * from ''` call into - * individual exports (which nextjs seems to need). - * - * Note: Any errors which occur are handled by the proxy loader which calls this function. - * - * @param templateCode The proxy module code - * @param userModulePath The path to the file being wrapped - * @returns The processed proxy module code - */ -export async function rollupize(templateCode: string, userModulePath: string): Promise { - const intermediateBundle = await rollup(getRollupInputOptions(templateCode, userModulePath)); - const finalBundle = await intermediateBundle.generate(rollupOutputOptions); - - // The module at index 0 is always the entrypoint, which in this case is the proxy module. - let { code } = finalBundle.output[0]; - - // In addition to doing the desired work, Rollup also does a few things we *don't* want. Specifically, in messes up - // the path in both `import * as origModule from ''` and `export * from ''`. - // - // - It turns the square brackets surrounding each parameterized path segment into underscores. - // - It always adds `.js` to the end of the filename. - // - It converts the path from aboslute to relative, which would be fine except that when used with the virual plugin, - // it uses an incorrect (and not entirely predicable) base for that relative path. - // - // To fix this, we overwrite the messed up path with what we know it should be: `./`. (We can - // find the value of the messed up path by looking at what `import * as origModule from ''` becomes. - // Because it's the first line of the template, it's also the first line of the result, and is therefore easy to - // find.) - - const importStarStatement = code.split('\n')[0]; - // This regex should always match (we control both the input and the process which generates it, so we can guarantee - // the outcome of that processing), but just in case it somehow doesn't, we need it to throw an error so that the - // proxy loader will know to return the user's code untouched rather than returning proxy module code including a - // broken path. The non-null assertion asserts that a match has indeed been found. - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const messedUpPath = /^import \* as .* from '(.*)';$/.exec(importStarStatement)![1]; - - code = code.replace(new RegExp(escapeStringForRegex(messedUpPath), 'g'), `./${path.basename(userModulePath)}`); - - return code; -} diff --git a/packages/nextjs/src/config/loaders/types.ts b/packages/nextjs/src/config/loaders/types.ts index 5db902abeced..14766f077a12 100644 --- a/packages/nextjs/src/config/loaders/types.ts +++ b/packages/nextjs/src/config/loaders/types.ts @@ -1,3 +1,5 @@ +import type webpack from 'webpack'; + export type LoaderThis = { /** Path to the file being loaded */ resourcePath: string; @@ -7,6 +9,12 @@ export type LoaderThis = { // Function to add outside file used by loader to `watch` process addDependency: (filepath: string) => void; + + // Marks a loader as asynchronous + async: webpack.loader.LoaderContext['async']; + + // Return errors, code, and sourcemaps from an asynchronous loader + callback: webpack.loader.LoaderContext['callback']; } & ( | { // Loader options in Webpack 4 diff --git a/packages/nextjs/src/config/loaders/wrappingLoader.ts b/packages/nextjs/src/config/loaders/wrappingLoader.ts new file mode 100644 index 000000000000..f092216bbf1d --- /dev/null +++ b/packages/nextjs/src/config/loaders/wrappingLoader.ts @@ -0,0 +1,197 @@ +import commonjs from '@rollup/plugin-commonjs'; +import { stringMatchesSomePattern } from '@sentry/utils'; +import * as fs from 'fs'; +import * as path from 'path'; +import { rollup } from 'rollup'; + +import type { LoaderThis } from './types'; + +const apiWrapperTemplatePath = path.resolve(__dirname, '..', 'templates', 'apiWrapperTemplate.js'); +const apiWrapperTemplateCode = fs.readFileSync(apiWrapperTemplatePath, { encoding: 'utf8' }); + +const pageWrapperTemplatePath = path.resolve(__dirname, '..', 'templates', 'pageWrapperTemplate.js'); +const pageWrapperTemplateCode = fs.readFileSync(pageWrapperTemplatePath, { encoding: 'utf8' }); + +// Just a simple placeholder to make referencing module consistent +const SENTRY_WRAPPER_MODULE_NAME = 'sentry-wrapper-module'; + +// Needs to end in .cjs in order for the `commonjs` plugin to pick it up +const WRAPPING_TARGET_MODULE_NAME = '__SENTRY_WRAPPING_TARGET__.cjs'; + +type LoaderOptions = { + pagesDir: string; + pageExtensionRegex: string; + excludeServerRoutes: Array; + isEdgeRuntime: boolean; +}; + +/** + * Replace the loaded file with a wrapped version the original file. In the wrapped version, the original file is loaded, + * any data-fetching functions (`getInitialProps`, `getStaticProps`, and `getServerSideProps`) or API routes it contains + * are wrapped, and then everything is re-exported. + */ +export default function wrappingLoader( + this: LoaderThis, + userCode: string, + userModuleSourceMap: any, +): void | string { + // We know one or the other will be defined, depending on the version of webpack being used + const { + pagesDir, + pageExtensionRegex, + excludeServerRoutes = [], + isEdgeRuntime, + } = 'getOptions' in this ? this.getOptions() : this.query; + + // We currently don't support the edge runtime + if (isEdgeRuntime) { + return userCode; + } + + this.async(); + + // Get the parameterized route name from this page's filepath + const parameterizedRoute = path + // Get the path of the file insde of the pages directory + .relative(pagesDir, this.resourcePath) + // Add a slash at the beginning + .replace(/(.*)/, '/$1') + // Pull off the file extension + .replace(new RegExp(`\\.(${pageExtensionRegex})`), '') + // Any page file named `index` corresponds to root of the directory its in, URL-wise, so turn `/xyz/index` into + // just `/xyz` + .replace(/\/index$/, '') + // In case all of the above have left us with an empty string (which will happen if we're dealing with the + // homepage), sub back in the root route + .replace(/^$/, '/'); + + // Skip explicitly-ignored pages + if (stringMatchesSomePattern(parameterizedRoute, excludeServerRoutes, true)) { + this.callback(null, userCode, userModuleSourceMap); + return; + } + + let templateCode = parameterizedRoute.startsWith('/api') ? apiWrapperTemplateCode : pageWrapperTemplateCode; + + // Inject the route and the path to the file we're wrapping into the template + templateCode = templateCode.replace(/__ROUTE__/g, parameterizedRoute.replace(/\\/g, '\\\\')); + + // Replace the import path of the wrapping target in the template with a path that the `wrapUserCode` function will understand. + templateCode = templateCode.replace(/__SENTRY_WRAPPING_TARGET__/g, WRAPPING_TARGET_MODULE_NAME); + + // Run the proxy module code through Rollup, in order to split the `export * from ''` out into + // individual exports (which nextjs seems to require). + wrapUserCode(templateCode, userCode, userModuleSourceMap) + .then(({ code: wrappedCode, map: wrappedCodeSourceMap }) => { + this.callback(null, wrappedCode, wrappedCodeSourceMap); + }) + .catch(err => { + // eslint-disable-next-line no-console + console.warn( + `[@sentry/nextjs] Could not instrument ${this.resourcePath}. An error occurred while auto-wrapping:\n${err}`, + ); + this.callback(null, userCode, userModuleSourceMap); + return; + }); +} + +/** + * Use Rollup to process the proxy module code, in order to split its `export * from ''` call into + * individual exports (which nextjs seems to need). + * + * Wraps provided user code (located under the import defined via WRAPPING_TARGET_MODULE_NAME) with provided wrapper + * code. Under the hood, this function uses rollup to bundle the modules together. Rollup is convenient for us because + * it turns `export * from ''` (which Next.js doesn't allow) into individual named exports. + * + * Note: This function may throw in case something goes wrong while bundling. + * + * @param wrapperCode The wrapper module code + * @param userModuleCode The user module code + * @returns The wrapped user code and a source map that describes the transformations done by this function + */ +async function wrapUserCode( + wrapperCode: string, + userModuleCode: string, + userModuleSourceMap: any, +): Promise<{ code: string; map?: any }> { + const rollupBuild = await rollup({ + input: SENTRY_WRAPPER_MODULE_NAME, + + plugins: [ + // We're using a simple custom plugin that virtualizes our wrapper module and the user module, so we don't have to + // mess around with file paths and so that we can pass the original user module source map to rollup so that + // rollup gives us a bundle with correct source mapping to the original file + { + name: 'virtualize-sentry-wrapper-modules', + resolveId: id => { + if (id === SENTRY_WRAPPER_MODULE_NAME || id === WRAPPING_TARGET_MODULE_NAME) { + return id; + } else { + return null; + } + }, + load(id) { + if (id === SENTRY_WRAPPER_MODULE_NAME) { + return wrapperCode; + } else if (id === WRAPPING_TARGET_MODULE_NAME) { + return { + code: userModuleCode, + map: userModuleSourceMap, // give rollup acces to original user module source map + }; + } else { + return null; + } + }, + }, + + // People may use `module.exports` in their API routes or page files. Next.js allows that and we also need to + // handle that correctly so we let a plugin to take care of bundling cjs exports for us. + commonjs({ + transformMixedEsModules: true, + sourceMap: true, + }), + ], + + // We only want to bundle our wrapper module and the wrappee module into one, so we mark everything else as external. + external: sourceId => sourceId !== SENTRY_WRAPPER_MODULE_NAME && sourceId !== WRAPPING_TARGET_MODULE_NAME, + + // Prevent rollup from stressing out about TS's use of global `this` when polyfilling await. (TS will polyfill if the + // user's tsconfig `target` is set to anything before `es2017`. See https://stackoverflow.com/a/72822340 and + // https://stackoverflow.com/a/60347490.) + context: 'this', + + // Rollup's path-resolution logic when handling re-exports can go wrong when wrapping pages which aren't at the root + // level of the `pages` directory. This may be a bug, as it doesn't match the behavior described in the docs, but what + // seems to happen is this: + // + // - We try to wrap `pages/xyz/userPage.js`, which contains `export { helperFunc } from '../../utils/helper'` + // - Rollup converts '../../utils/helper' into an absolute path + // - We mark the helper module as external + // - Rollup then converts it back to a relative path, but relative to `pages/` rather than `pages/xyz/`. (This is + // the part which doesn't match the docs. They say that Rollup will use the common ancestor of all modules in the + // bundle as the basis for the relative path calculation, but both our temporary file and the page being wrapped + // live in `pages/xyz/`, and they're the only two files in the bundle, so `pages/xyz/`` should be used as the + // root. Unclear why it's not.) + // - As a result of the miscalculation, our proxy module will include `export { helperFunc } from '../utils/helper'` + // rather than the expected `export { helperFunc } from '../../utils/helper'`, thereby causing a build error in + // nextjs.. + // + // Setting `makeAbsoluteExternalsRelative` to `false` prevents all of the above by causing Rollup to ignore imports of + // externals entirely, with the result that their paths remain untouched (which is what we want). + makeAbsoluteExternalsRelative: false, + + onwarn: (_warning, _warn) => { + // Suppress all warnings - we don't want to bother people with this output + // Might be stuff like "you have unused imports" + // _warn(_warning); // uncomment to debug + }, + }); + + const finalBundle = await rollupBuild.generate({ + format: 'esm', + sourcemap: 'hidden', // put source map data in the bundle but don't generate a source map commment in the output + }); + + // The module at index 0 is always the entrypoint, which in this case is the proxy module. + return finalBundle.output[0]; +} diff --git a/packages/nextjs/src/config/templates/apiProxyLoaderTemplate.ts b/packages/nextjs/src/config/templates/apiWrapperTemplate.ts similarity index 96% rename from packages/nextjs/src/config/templates/apiProxyLoaderTemplate.ts rename to packages/nextjs/src/config/templates/apiWrapperTemplate.ts index 1cd7c40181eb..a16c3a0c7666 100644 --- a/packages/nextjs/src/config/templates/apiProxyLoaderTemplate.ts +++ b/packages/nextjs/src/config/templates/apiWrapperTemplate.ts @@ -8,7 +8,7 @@ // @ts-ignore See above // eslint-disable-next-line import/no-unresolved -import * as origModule from '__RESOURCE_PATH__'; +import * as origModule from '__SENTRY_WRAPPING_TARGET__'; import * as Sentry from '@sentry/nextjs'; import type { PageConfig } from 'next'; @@ -59,4 +59,4 @@ export default userProvidedHandler ? Sentry.withSentryAPI(userProvidedHandler, ' // not include anything whose name matchs something we've explicitly exported above. // @ts-ignore See above // eslint-disable-next-line import/no-unresolved -export * from '__RESOURCE_PATH__'; +export * from '__SENTRY_WRAPPING_TARGET__'; diff --git a/packages/nextjs/src/config/templates/pageProxyLoaderTemplate.ts b/packages/nextjs/src/config/templates/pageWrapperTemplate.ts similarity index 95% rename from packages/nextjs/src/config/templates/pageProxyLoaderTemplate.ts rename to packages/nextjs/src/config/templates/pageWrapperTemplate.ts index 9979ae814c49..116c0503fbef 100644 --- a/packages/nextjs/src/config/templates/pageProxyLoaderTemplate.ts +++ b/packages/nextjs/src/config/templates/pageWrapperTemplate.ts @@ -8,7 +8,7 @@ // @ts-ignore See above // eslint-disable-next-line import/no-unresolved -import * as wrapee from '__RESOURCE_PATH__'; +import * as wrapee from '__SENTRY_WRAPPING_TARGET__'; import * as Sentry from '@sentry/nextjs'; import type { GetServerSideProps, GetStaticProps, NextPage as NextPageComponent } from 'next'; @@ -53,4 +53,4 @@ export default pageComponent; // not include anything whose name matchs something we've explicitly exported above. // @ts-ignore See above // eslint-disable-next-line import/no-unresolved -export * from '__RESOURCE_PATH__'; +export * from '__SENTRY_WRAPPING_TARGET__'; diff --git a/packages/nextjs/src/config/webpack.ts b/packages/nextjs/src/config/webpack.ts index 15cf51e2b742..35c017e732e4 100644 --- a/packages/nextjs/src/config/webpack.ts +++ b/packages/nextjs/src/config/webpack.ts @@ -100,11 +100,12 @@ export function constructWebpackConfigFunction( const pageExtensions = userNextConfig.pageExtensions || ['tsx', 'ts', 'jsx', 'js']; const pageExtensionRegex = pageExtensions.map(escapeStringForRegex).join('|'); - newConfig.module.rules.push({ + // It is very important that we insert our loader at the beginning of the array because we expect any sort of transformations/transpilations (e.g. TS -> JS) to already have happened. + newConfig.module.rules.unshift({ test: new RegExp(`^${escapeStringForRegex(pagesDir)}.*\\.(${pageExtensionRegex})$`), use: [ { - loader: path.resolve(__dirname, 'loaders/proxyLoader.js'), + loader: path.resolve(__dirname, 'loaders/wrappingLoader.js'), options: { pagesDir, pageExtensionRegex, diff --git a/yarn.lock b/yarn.lock index aebbc2871d55..1e17e827e2cd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3385,6 +3385,18 @@ dependencies: web-streams-polyfill "^3.1.1" +"@rollup/plugin-commonjs@24.0.0": + version "24.0.0" + resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-24.0.0.tgz#fb7cf4a6029f07ec42b25daa535c75b05a43f75c" + integrity sha512-0w0wyykzdyRRPHOb0cQt14mIBLujfAv6GgP6g8nvg/iBxEm112t3YPPq+Buqe2+imvElTka+bjNlJ/gB56TD8g== + 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-commonjs@^15.0.0": version "15.1.0" resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-15.1.0.tgz#1e7d076c4f1b2abf7e65248570e555defc37c238" @@ -3450,7 +3462,7 @@ "@rollup/pluginutils" "^3.1.0" magic-string "^0.25.7" -"@rollup/plugin-sucrase@4.0.4", "@rollup/plugin-sucrase@^4.0.3": +"@rollup/plugin-sucrase@^4.0.3": version "4.0.4" resolved "https://registry.yarnpkg.com/@rollup/plugin-sucrase/-/plugin-sucrase-4.0.4.tgz#0a3b3d97cdc239ec3399f5a10711f751e9f95d98" integrity sha512-YH4J8yoJb5EVnLhAwWxYAQNh2SJOR+SdZ6XdgoKEv6Kxm33riYkM8MlMaggN87UoISP52qAFyZ5ey56wu6umGg== @@ -3466,11 +3478,6 @@ "@rollup/pluginutils" "^3.1.0" resolve "^1.17.0" -"@rollup/plugin-virtual@3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@rollup/plugin-virtual/-/plugin-virtual-3.0.0.tgz#8c3f54b4ab4b267d9cd3dcbaedc58d4fd1deddca" - integrity sha512-K9KORe1myM62o0lKkNR4MmCxjwuAXsZEtIHpaILfv4kILXTOrXt/R2ha7PzMcCHPYdnkWPiBZK8ed4Zr3Ll5lQ== - "@rollup/pluginutils@^3.0.8", "@rollup/pluginutils@^3.0.9", "@rollup/pluginutils@^3.1.0": version "3.1.0" resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-3.1.0.tgz#706b4524ee6dc8b103b3c995533e5ad680c02b9b" @@ -3488,6 +3495,15 @@ estree-walker "^2.0.1" picomatch "^2.2.2" +"@rollup/pluginutils@^5.0.1": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.0.2.tgz#012b8f53c71e4f6f9cb317e311df1404f56e7a33" + integrity sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA== + dependencies: + "@types/estree" "^1.0.0" + estree-walker "^2.0.2" + picomatch "^2.3.1" + "@schematics/angular@10.2.4": version "10.2.4" resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-10.2.4.tgz#3b99b9da572b57381d221e2008804e6bb9c98b82" @@ -4081,6 +4097,11 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== +"@types/estree@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.0.tgz#5fb2e536c1ae9bf35366eed879e827fa59ca41c2" + integrity sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ== + "@types/express-serve-static-core@4.17.28", "@types/express-serve-static-core@4.17.30", "@types/express-serve-static-core@^4.17.18": version "4.17.30" resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.30.tgz#0f2f99617fa8f9696170c46152ccf7500b34ac04" @@ -12742,7 +12763,7 @@ glob@7.2.0: once "^1.3.0" path-is-absolute "^1.0.0" -glob@8.0.3: +glob@8.0.3, glob@^8.0.3: version "8.0.3" resolved "https://registry.yarnpkg.com/glob/-/glob-8.0.3.tgz#415c6eb2deed9e502c68fa44a272e6da6eeca42e" integrity sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ== @@ -14359,7 +14380,7 @@ is-potential-custom-element-name@^1.0.1: resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== -is-reference@^1.2.1: +is-reference@1.2.1, is-reference@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-1.2.1.tgz#8b2dac0b371f4bc994fdeaba9eb542d03002d0b7" integrity sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ== From 9bd62965597b06c3e334c897dfcd5bbea787fc7f Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Tue, 10 Jan 2023 17:26:11 +0100 Subject: [PATCH 028/113] fix(remix): Make remix SDK type exports isomorphic (#6715) --- packages/remix/package.json | 2 +- packages/remix/src/index.types.ts | 30 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 packages/remix/src/index.types.ts diff --git a/packages/remix/package.json b/packages/remix/package.json index 319438487765..4ef43aa34551 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -15,7 +15,7 @@ "main": "build/cjs/index.server.js", "module": "build/esm/index.server.js", "browser": "build/esm/index.client.js", - "types": "build/types/index.server.d.ts", + "types": "build/types/index.types.d.ts", "publishConfig": { "access": "public" }, diff --git a/packages/remix/src/index.types.ts b/packages/remix/src/index.types.ts new file mode 100644 index 000000000000..0d3e8a8633a1 --- /dev/null +++ b/packages/remix/src/index.types.ts @@ -0,0 +1,30 @@ +/* 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, StackParser } from '@sentry/types'; + +import * as clientSdk from './index.client'; +import * as serverSdk from './index.server'; +import { RemixOptions } from './utils/remixOptions'; + +/** Initializes Sentry Remix SDK */ +export declare function init(options: RemixOptions): void; + +// We export a merged Integrations object so that users can (at least typing-wise) use all integrations everywhere. +export const Integrations = { ...clientSdk.Integrations, ...serverSdk.Integrations }; + +export declare const defaultIntegrations: Integration[]; +export declare const defaultStackParser: StackParser; + +// This variable is not a runtime variable but just a type to tell typescript that the methods below can either come +// from the client SDK or from the server SDK. TypeScript is smart enough to understand that these resolve to the same +// methods from `@sentry/core`. +declare const runtime: 'client' | 'server'; + +export const close = runtime === 'client' ? clientSdk.close : serverSdk.close; +export const flush = runtime === 'client' ? clientSdk.flush : serverSdk.flush; +export const lastEventId = runtime === 'client' ? clientSdk.lastEventId : serverSdk.lastEventId; From e332ae11cd88bf75600dbe9e2dd198c153976291 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Tue, 10 Jan 2023 17:42:51 +0100 Subject: [PATCH 029/113] feat(types): Add Trace Context type (#6714) --- packages/tracing/src/span.ts | 14 ++------------ packages/types/src/context.ts | 13 +++++++++++++ packages/types/src/index.ts | 2 +- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/packages/tracing/src/span.ts b/packages/tracing/src/span.ts index 43fbef08f623..be070c0cbfa3 100644 --- a/packages/tracing/src/span.ts +++ b/packages/tracing/src/span.ts @@ -1,5 +1,5 @@ /* eslint-disable max-lines */ -import { Instrumenter, Primitive, Span as SpanInterface, SpanContext, Transaction } from '@sentry/types'; +import { Instrumenter, Primitive, Span as SpanInterface, SpanContext, TraceContext, Transaction } from '@sentry/types'; import { dropUndefinedKeys, logger, timestampWithMs, uuid4 } from '@sentry/utils'; /** @@ -305,17 +305,7 @@ export class Span implements SpanInterface { /** * @inheritDoc */ - public getTraceContext(): { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - data?: { [key: string]: any }; - description?: string; - op?: string; - parent_span_id?: string; - span_id: string; - status?: string; - tags?: { [key: string]: Primitive }; - trace_id: string; - } { + public getTraceContext(): TraceContext { return dropUndefinedKeys({ data: Object.keys(this.data).length > 0 ? this.data : undefined, description: this.description, diff --git a/packages/types/src/context.ts b/packages/types/src/context.ts index 3a342373c3fe..120bd82cda9d 100644 --- a/packages/types/src/context.ts +++ b/packages/types/src/context.ts @@ -1,3 +1,5 @@ +import { Primitive } from './misc'; + export type Context = Record; export interface Contexts extends Record { @@ -79,3 +81,14 @@ export interface ResponseContext extends Record { status_code?: number; body_size?: number; // in bytes } + +export interface TraceContext extends Record { + data?: { [key: string]: any }; + description?: string; + op?: string; + parent_span_id?: string; + span_id: string; + status?: string; + tags?: { [key: string]: Primitive }; + trace_id: string; +} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index a3437df77f1f..0b52fac74e02 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -2,7 +2,7 @@ export type { Attachment } from './attachment'; export type { Breadcrumb, BreadcrumbHint } from './breadcrumb'; export type { Client } from './client'; export type { ClientReport, Outcome, EventDropReason } from './clientreport'; -export type { Context, Contexts, DeviceContext, OsContext, AppContext, CultureContext } from './context'; +export type { Context, Contexts, DeviceContext, OsContext, AppContext, CultureContext, TraceContext } from './context'; export type { DataCategory } from './datacategory'; export type { DsnComponents, DsnLike, DsnProtocol } from './dsn'; export type { DebugImage, DebugImageType, DebugMeta } from './debugMeta'; From 59b0bf65ead5ba48e62dffa408cb2fe4159a886a Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Tue, 10 Jan 2023 18:03:45 +0100 Subject: [PATCH 030/113] fix(nextjs): Make Next.js types isomorphic (#6707) --- .size-limit.js | 2 +- packages/nextjs/package.json | 11 ++- packages/nextjs/rollup.npm.config.js | 4 +- .../src/{index.client.ts => client/index.ts} | 19 +++-- .../client.ts => client/performance.ts} | 0 .../src/{utils => client}/tunnelRoute.ts | 5 +- .../nextjs/src/{utils => common}/_error.ts | 0 .../nextjs/src/{utils => common}/metadata.ts | 0 .../src/{utils => common}/userIntegrations.ts | 0 packages/nextjs/src/config/index.ts | 2 + .../config/templates/apiWrapperTemplate.ts | 3 +- .../config/templates/pageWrapperTemplate.ts | 1 + .../nextjs/src/config/withSentryConfig.ts | 5 +- packages/nextjs/src/config/wrappers/index.ts | 9 --- packages/nextjs/src/index.ts | 2 + packages/nextjs/src/index.types.ts | 32 +++++++++ .../src/{index.server.ts => server/index.ts} | 37 ++++------ .../src/{config/wrappers => server}/types.ts | 0 .../nextjs/src/{ => server}/utils/isBuild.ts | 4 +- .../src/{ => server}/utils/nextLogger.ts | 0 .../utils/platformSupportsStreaming.ts | 0 .../wrappers => server}/utils/responseEnd.ts | 0 .../wrappers => server/utils}/wrapperUtils.ts | 4 +- .../wrappers => server}/withSentryAPI.ts | 4 +- .../withSentryGetServerSideProps.ts | 8 ++- .../withSentryGetStaticProps.ts | 4 +- .../withSentryServerSideAppGetInitialProps.ts | 8 ++- ...SentryServerSideDocumentGetInitialProps.ts | 4 +- ...ithSentryServerSideErrorGetInitialProps.ts | 8 ++- .../withSentryServerSideGetInitialProps.ts | 8 ++- packages/nextjs/src/utils/isESM.ts | 17 ----- packages/nextjs/src/utils/nextjsOptions.ts | 5 -- packages/nextjs/src/utils/phases.ts | 3 - ...index.client.test.ts => clientSdk.test.ts} | 4 +- .../nextjs/test/config/withSentry.test.ts | 3 +- packages/nextjs/test/config/wrappers.test.ts | 11 +-- .../nextjs/test/performance/client.test.ts | 2 +- ...index.server.test.ts => serverSdk.test.ts} | 2 +- packages/nextjs/test/utils/isESM.test.ts | 69 ------------------- .../nextjs/test/utils/tunnelRoute.test.ts | 13 ++-- .../test/utils/userIntegrations.test.ts | 4 +- 41 files changed, 124 insertions(+), 193 deletions(-) rename packages/nextjs/src/{index.client.ts => client/index.ts} (83%) rename packages/nextjs/src/{performance/client.ts => client/performance.ts} (100%) rename packages/nextjs/src/{utils => client}/tunnelRoute.ts (88%) rename packages/nextjs/src/{utils => common}/_error.ts (100%) rename packages/nextjs/src/{utils => common}/metadata.ts (100%) rename packages/nextjs/src/{utils => common}/userIntegrations.ts (100%) create mode 100644 packages/nextjs/src/config/index.ts delete mode 100644 packages/nextjs/src/config/wrappers/index.ts create mode 100644 packages/nextjs/src/index.ts create mode 100644 packages/nextjs/src/index.types.ts rename packages/nextjs/src/{index.server.ts => server/index.ts} (84%) rename packages/nextjs/src/{config/wrappers => server}/types.ts (100%) rename packages/nextjs/src/{ => server}/utils/isBuild.ts (54%) rename packages/nextjs/src/{ => server}/utils/nextLogger.ts (100%) rename packages/nextjs/src/{ => server}/utils/platformSupportsStreaming.ts (100%) rename packages/nextjs/src/{config/wrappers => server}/utils/responseEnd.ts (100%) rename packages/nextjs/src/{config/wrappers => server/utils}/wrapperUtils.ts (98%) rename packages/nextjs/src/{config/wrappers => server}/withSentryAPI.ts (98%) rename packages/nextjs/src/{config/wrappers => server}/withSentryGetServerSideProps.ts (91%) rename packages/nextjs/src/{config/wrappers => server}/withSentryGetStaticProps.ts (93%) rename packages/nextjs/src/{config/wrappers => server}/withSentryServerSideAppGetInitialProps.ts (94%) rename packages/nextjs/src/{config/wrappers => server}/withSentryServerSideDocumentGetInitialProps.ts (96%) rename packages/nextjs/src/{config/wrappers => server}/withSentryServerSideErrorGetInitialProps.ts (93%) rename packages/nextjs/src/{config/wrappers => server}/withSentryServerSideGetInitialProps.ts (92%) delete mode 100644 packages/nextjs/src/utils/isESM.ts delete mode 100644 packages/nextjs/src/utils/nextjsOptions.ts delete mode 100644 packages/nextjs/src/utils/phases.ts rename packages/nextjs/test/{index.client.test.ts => clientSdk.test.ts} (98%) rename packages/nextjs/test/{index.server.test.ts => serverSdk.test.ts} (99%) delete mode 100644 packages/nextjs/test/utils/isESM.test.ts diff --git a/.size-limit.js b/.size-limit.js index 20cc71fb75f0..aee03e5a242c 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -46,7 +46,7 @@ module.exports = [ }, { name: '@sentry/nextjs Client - Webpack (gzipped + minified)', - path: 'packages/nextjs/build/esm/index.client.js', + path: 'packages/nextjs/build/esm/client/index.js', import: '{ init }', gzip: true, limit: '57 KB', diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index c7990d1fdb77..c1806fcf1ed0 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -9,10 +9,10 @@ "engines": { "node": ">=8" }, - "main": "build/cjs/index.server.js", - "module": "build/esm/index.server.js", - "browser": "build/esm/index.client.js", - "types": "build/types/index.server.d.ts", + "main": "build/cjs/index.js", + "module": "build/esm/index.js", + "browser": "build/esm/client/index.js", + "types": "build/types/index.types.d.ts", "publishConfig": { "access": "public" }, @@ -31,7 +31,6 @@ "tslib": "^1.9.3" }, "devDependencies": { - "@sentry/nextjs": "7.30.0", "@types/webpack": "^4.41.31", "eslint-plugin-react": "^7.31.11", "next": "10.1.3" @@ -56,7 +55,7 @@ "build:rollup:watch": "nodemon --ext ts --watch src scripts/buildRollup.ts", "build:types:watch": "tsc -p tsconfig.types.json --watch", "build:npm": "ts-node ../../scripts/prepack.ts && npm pack ./build", - "circularDepCheck": "madge --circular src/index.client.ts && madge --circular --exclude 'config/types\\.ts' src/index.server.ts # see https://github.com/pahen/madge/issues/306", + "circularDepCheck": "madge --circular src/index.ts && madge --circular src/client/index.ts && madge --circular src/index.types.ts", "clean": "rimraf build coverage sentry-nextjs-*.tgz", "fix": "run-s fix:eslint fix:prettier", "fix:eslint": "eslint . --format stylish --fix", diff --git a/packages/nextjs/rollup.npm.config.js b/packages/nextjs/rollup.npm.config.js index d605dc65f3eb..2329f9eafed1 100644 --- a/packages/nextjs/rollup.npm.config.js +++ b/packages/nextjs/rollup.npm.config.js @@ -5,11 +5,11 @@ export default [ makeBaseNPMConfig({ // We need to include `instrumentServer.ts` separately because it's only conditionally required, and so rollup // doesn't automatically include it when calculating the module dependency tree. - entrypoints: ['src/index.server.ts', 'src/index.client.ts', 'src/config/webpack.ts'], + entrypoints: ['src/index.ts', 'src/client/index.ts', 'src/config/webpack.ts'], // prevent this internal nextjs code from ending up in our built package (this doesn't happen automatially because // the name doesn't match an SDK dependency) - packageSpecificConfig: { external: ['next/router'] }, + packageSpecificConfig: { external: ['next/router', 'next/constants'] }, }), ), ...makeNPMConfigVariants( diff --git a/packages/nextjs/src/index.client.ts b/packages/nextjs/src/client/index.ts similarity index 83% rename from packages/nextjs/src/index.client.ts rename to packages/nextjs/src/client/index.ts index 98ea9928b8ed..08f5b113ba74 100644 --- a/packages/nextjs/src/index.client.ts +++ b/packages/nextjs/src/client/index.ts @@ -1,17 +1,16 @@ import { RewriteFrames } from '@sentry/integrations'; -import { configureScope, init as reactInit, Integrations } from '@sentry/react'; +import { BrowserOptions, configureScope, init as reactInit, Integrations } from '@sentry/react'; import { BrowserTracing, defaultRequestInstrumentationOptions, hasTracingEnabled } from '@sentry/tracing'; import { EventProcessor } from '@sentry/types'; -import { nextRouterInstrumentation } from './performance/client'; -import { buildMetadata } from './utils/metadata'; -import { NextjsOptions } from './utils/nextjsOptions'; -import { applyTunnelRouteOption } from './utils/tunnelRoute'; -import { addOrUpdateIntegration } from './utils/userIntegrations'; +import { buildMetadata } from '../common/metadata'; +import { addOrUpdateIntegration } from '../common/userIntegrations'; +import { nextRouterInstrumentation } from './performance'; +import { applyTunnelRouteOption } from './tunnelRoute'; export * from '@sentry/react'; -export { nextRouterInstrumentation } from './performance/client'; -export { captureUnderscoreErrorException } from './utils/_error'; +export { nextRouterInstrumentation } from './performance'; +export { captureUnderscoreErrorException } from '../common/_error'; export { Integrations }; @@ -35,7 +34,7 @@ const globalWithInjectedValues = global as typeof global & { }; /** Inits the Sentry NextJS SDK on the browser with the React SDK. */ -export function init(options: NextjsOptions): void { +export function init(options: BrowserOptions): void { applyTunnelRouteOption(options); buildMetadata(options, ['nextjs', 'react']); options.environment = options.environment || process.env.NODE_ENV; @@ -52,7 +51,7 @@ export function init(options: NextjsOptions): void { }); } -function addClientIntegrations(options: NextjsOptions): void { +function addClientIntegrations(options: BrowserOptions): void { let integrations = options.integrations || []; // This value is injected at build time, based on the output directory specified in the build config. Though a default diff --git a/packages/nextjs/src/performance/client.ts b/packages/nextjs/src/client/performance.ts similarity index 100% rename from packages/nextjs/src/performance/client.ts rename to packages/nextjs/src/client/performance.ts diff --git a/packages/nextjs/src/utils/tunnelRoute.ts b/packages/nextjs/src/client/tunnelRoute.ts similarity index 88% rename from packages/nextjs/src/utils/tunnelRoute.ts rename to packages/nextjs/src/client/tunnelRoute.ts index 16d4ce9b71cc..29ad48325bb9 100644 --- a/packages/nextjs/src/utils/tunnelRoute.ts +++ b/packages/nextjs/src/client/tunnelRoute.ts @@ -1,7 +1,6 @@ +import { BrowserOptions } from '@sentry/react'; import { dsnFromString, logger } from '@sentry/utils'; -import { NextjsOptions } from './nextjsOptions'; - const globalWithInjectedValues = global as typeof global & { __sentryRewritesTunnelPath__?: string; }; @@ -9,7 +8,7 @@ const globalWithInjectedValues = global as typeof global & { /** * Applies the `tunnel` option to the Next.js SDK options based on `withSentryConfig`'s `tunnelRoute` option. */ -export function applyTunnelRouteOption(options: NextjsOptions): void { +export function applyTunnelRouteOption(options: BrowserOptions): void { const tunnelRouteOption = globalWithInjectedValues.__sentryRewritesTunnelPath__; if (tunnelRouteOption && options.dsn) { const dsnComponents = dsnFromString(options.dsn); diff --git a/packages/nextjs/src/utils/_error.ts b/packages/nextjs/src/common/_error.ts similarity index 100% rename from packages/nextjs/src/utils/_error.ts rename to packages/nextjs/src/common/_error.ts diff --git a/packages/nextjs/src/utils/metadata.ts b/packages/nextjs/src/common/metadata.ts similarity index 100% rename from packages/nextjs/src/utils/metadata.ts rename to packages/nextjs/src/common/metadata.ts diff --git a/packages/nextjs/src/utils/userIntegrations.ts b/packages/nextjs/src/common/userIntegrations.ts similarity index 100% rename from packages/nextjs/src/utils/userIntegrations.ts rename to packages/nextjs/src/common/userIntegrations.ts diff --git a/packages/nextjs/src/config/index.ts b/packages/nextjs/src/config/index.ts new file mode 100644 index 000000000000..f537125a75f3 --- /dev/null +++ b/packages/nextjs/src/config/index.ts @@ -0,0 +1,2 @@ +export type { SentryWebpackPluginOptions } from './types'; +export { withSentryConfig } from './withSentryConfig'; diff --git a/packages/nextjs/src/config/templates/apiWrapperTemplate.ts b/packages/nextjs/src/config/templates/apiWrapperTemplate.ts index a16c3a0c7666..2f8dd2184301 100644 --- a/packages/nextjs/src/config/templates/apiWrapperTemplate.ts +++ b/packages/nextjs/src/config/templates/apiWrapperTemplate.ts @@ -9,12 +9,13 @@ // @ts-ignore See above // eslint-disable-next-line import/no-unresolved import * as origModule from '__SENTRY_WRAPPING_TARGET__'; +// eslint-disable-next-line import/no-extraneous-dependencies import * as Sentry from '@sentry/nextjs'; import type { PageConfig } from 'next'; // We import this from `wrappers` rather than directly from `next` because our version can work simultaneously with // multiple versions of next. See note in `wrappers/types` for more. -import type { NextApiHandler } from '../wrappers'; +import type { NextApiHandler } from '../../server/types'; type NextApiModule = ( | { diff --git a/packages/nextjs/src/config/templates/pageWrapperTemplate.ts b/packages/nextjs/src/config/templates/pageWrapperTemplate.ts index 116c0503fbef..e3b6b4e7e296 100644 --- a/packages/nextjs/src/config/templates/pageWrapperTemplate.ts +++ b/packages/nextjs/src/config/templates/pageWrapperTemplate.ts @@ -9,6 +9,7 @@ // @ts-ignore See above // eslint-disable-next-line import/no-unresolved import * as wrapee from '__SENTRY_WRAPPING_TARGET__'; +// eslint-disable-next-line import/no-extraneous-dependencies import * as Sentry from '@sentry/nextjs'; import type { GetServerSideProps, GetStaticProps, NextPage as NextPageComponent } from 'next'; diff --git a/packages/nextjs/src/config/withSentryConfig.ts b/packages/nextjs/src/config/withSentryConfig.ts index 2b8d12792470..5c16b82286c7 100644 --- a/packages/nextjs/src/config/withSentryConfig.ts +++ b/packages/nextjs/src/config/withSentryConfig.ts @@ -1,4 +1,5 @@ -import { NEXT_PHASE_DEVELOPMENT_SERVER, NEXT_PHASE_PRODUCTION_BUILD } from '../utils/phases'; +import { PHASE_DEVELOPMENT_SERVER, PHASE_PRODUCTION_BUILD } from 'next/constants'; + import type { ExportedNextConfig, NextConfigFunction, @@ -50,7 +51,7 @@ function getFinalConfigObject( // In order to prevent all of our build-time code from being bundled in people's route-handling serverless functions, // we exclude `webpack.ts` and all of its dependencies from nextjs's `@vercel/nft` filetracing. We therefore need to // make sure that we only require it at build time or in development mode. - if (phase === NEXT_PHASE_PRODUCTION_BUILD || phase === NEXT_PHASE_DEVELOPMENT_SERVER) { + if (phase === PHASE_PRODUCTION_BUILD || phase === PHASE_DEVELOPMENT_SERVER) { // eslint-disable-next-line @typescript-eslint/no-var-requires const { constructWebpackConfigFunction } = require('./webpack'); return { diff --git a/packages/nextjs/src/config/wrappers/index.ts b/packages/nextjs/src/config/wrappers/index.ts deleted file mode 100644 index aa406b970e77..000000000000 --- a/packages/nextjs/src/config/wrappers/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export type { AugmentedNextApiResponse, NextApiHandler, WrappedNextApiHandler } from './types'; - -export { withSentryGetStaticProps } from './withSentryGetStaticProps'; -export { withSentryServerSideGetInitialProps } from './withSentryServerSideGetInitialProps'; -export { withSentryServerSideAppGetInitialProps } from './withSentryServerSideAppGetInitialProps'; -export { withSentryServerSideDocumentGetInitialProps } from './withSentryServerSideDocumentGetInitialProps'; -export { withSentryServerSideErrorGetInitialProps } from './withSentryServerSideErrorGetInitialProps'; -export { withSentryGetServerSideProps } from './withSentryGetServerSideProps'; -export { withSentry, withSentryAPI } from './withSentryAPI'; diff --git a/packages/nextjs/src/index.ts b/packages/nextjs/src/index.ts new file mode 100644 index 000000000000..133b6ecf1da0 --- /dev/null +++ b/packages/nextjs/src/index.ts @@ -0,0 +1,2 @@ +export * from './config'; +export * from './server'; diff --git a/packages/nextjs/src/index.types.ts b/packages/nextjs/src/index.types.ts new file mode 100644 index 000000000000..5df30dffd580 --- /dev/null +++ b/packages/nextjs/src/index.types.ts @@ -0,0 +1,32 @@ +/* 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 './config'; +export * from './client'; +export * from './server'; + +import type { Integration, Options, StackParser } from '@sentry/types'; + +import type { BrowserOptions } from './client'; +import * as clientSdk from './client'; +import type { NodeOptions } from './server'; +import * as serverSdk from './server'; + +/** Initializes Sentry Next.js SDK */ +export declare function init(options: Options | BrowserOptions | NodeOptions): void; + +// We export a merged Integrations object so that users can (at least typing-wise) use all integrations everywhere. +export const Integrations = { ...clientSdk.Integrations, ...serverSdk.Integrations }; + +export declare const defaultIntegrations: Integration[]; +export declare const defaultStackParser: StackParser; + +// This variable is not a runtime variable but just a type to tell typescript that the methods below can either come +// from the client SDK or from the server SDK. TypeScript is smart enough to understand that these resolve to the same +// methods from `@sentry/core`. +declare const runtime: 'client' | 'server'; + +export const close = runtime === 'client' ? clientSdk.close : serverSdk.close; +export const flush = runtime === 'client' ? clientSdk.flush : serverSdk.flush; +export const lastEventId = runtime === 'client' ? clientSdk.lastEventId : serverSdk.lastEventId; diff --git a/packages/nextjs/src/index.server.ts b/packages/nextjs/src/server/index.ts similarity index 84% rename from packages/nextjs/src/index.server.ts rename to packages/nextjs/src/server/index.ts index 0500bd802300..2374b33ea519 100644 --- a/packages/nextjs/src/index.server.ts +++ b/packages/nextjs/src/server/index.ts @@ -1,24 +1,18 @@ import { Carrier, getHubFromCarrier, getMainCarrier } from '@sentry/core'; import { RewriteFrames } from '@sentry/integrations'; -import { configureScope, getCurrentHub, init as nodeInit, Integrations } from '@sentry/node'; +import { configureScope, getCurrentHub, init as nodeInit, Integrations, NodeOptions } from '@sentry/node'; import { hasTracingEnabled } from '@sentry/tracing'; import { EventProcessor } from '@sentry/types'; import { escapeStringForRegex, logger } from '@sentry/utils'; import * as domainModule from 'domain'; import * as path from 'path'; +import { buildMetadata } from '../common/metadata'; +import { addOrUpdateIntegration, IntegrationWithExclusionOption } from '../common/userIntegrations'; import { isBuild } from './utils/isBuild'; -import { buildMetadata } from './utils/metadata'; -import { NextjsOptions } from './utils/nextjsOptions'; -import { addOrUpdateIntegration, IntegrationWithExclusionOption } from './utils/userIntegrations'; export * from '@sentry/node'; -export { captureUnderscoreErrorException } from './utils/_error'; - -// Exporting the Replay integration also from index.server.ts because TS only recognizes types from index.server.ts -// If we didn't export this, TS would complain that it can't find `Sentry.Replay` in the package, -// causing a build failure, when users initialize Replay in their sentry.client.config.js/ts file. -export { Replay } from './index.client'; +export { captureUnderscoreErrorException } from '../common/_error'; // Here we want to make sure to only include what doesn't have browser specifics // because or SSR of next.js we can only use this. @@ -40,7 +34,7 @@ export const IS_BUILD = isBuild(); const IS_VERCEL = !!process.env.VERCEL; /** Inits the Sentry NextJS SDK on node. */ -export function init(options: NextjsOptions): void { +export function init(options: NodeOptions): void { if (__DEBUG_BUILD__ && options.debug) { logger.enable(); } @@ -107,7 +101,7 @@ function sdkAlreadyInitialized(): boolean { return !!hub.getClient(); } -function addServerIntegrations(options: NextjsOptions): void { +function addServerIntegrations(options: NodeOptions): void { let integrations = options.integrations || []; // This value is injected at build time, based on the output directory specified in the build config. Though a default @@ -152,15 +146,10 @@ const deprecatedIsBuild = (): boolean => isBuild(); // eslint-disable-next-line deprecation/deprecation export { deprecatedIsBuild as isBuild }; -export type { SentryWebpackPluginOptions } from './config/types'; -export { withSentryConfig } from './config/withSentryConfig'; -export { - withSentryGetServerSideProps, - withSentryGetStaticProps, - withSentryServerSideGetInitialProps, - withSentryServerSideAppGetInitialProps, - withSentryServerSideDocumentGetInitialProps, - withSentryServerSideErrorGetInitialProps, - withSentryAPI, - withSentry, -} from './config/wrappers'; +export { withSentryGetStaticProps } from './withSentryGetStaticProps'; +export { withSentryServerSideGetInitialProps } from './withSentryServerSideGetInitialProps'; +export { withSentryServerSideAppGetInitialProps } from './withSentryServerSideAppGetInitialProps'; +export { withSentryServerSideDocumentGetInitialProps } from './withSentryServerSideDocumentGetInitialProps'; +export { withSentryServerSideErrorGetInitialProps } from './withSentryServerSideErrorGetInitialProps'; +export { withSentryGetServerSideProps } from './withSentryGetServerSideProps'; +export { withSentry, withSentryAPI } from './withSentryAPI'; diff --git a/packages/nextjs/src/config/wrappers/types.ts b/packages/nextjs/src/server/types.ts similarity index 100% rename from packages/nextjs/src/config/wrappers/types.ts rename to packages/nextjs/src/server/types.ts diff --git a/packages/nextjs/src/utils/isBuild.ts b/packages/nextjs/src/server/utils/isBuild.ts similarity index 54% rename from packages/nextjs/src/utils/isBuild.ts rename to packages/nextjs/src/server/utils/isBuild.ts index 0e6f6691bf44..92b9808f75b9 100644 --- a/packages/nextjs/src/utils/isBuild.ts +++ b/packages/nextjs/src/server/utils/isBuild.ts @@ -1,8 +1,8 @@ -import { NEXT_PHASE_PRODUCTION_BUILD } from './phases'; +import { PHASE_PRODUCTION_BUILD } from 'next/constants'; /** * Decide if the currently running process is part of the build phase or happening at runtime. */ export function isBuild(): boolean { - return process.env.NEXT_PHASE === NEXT_PHASE_PRODUCTION_BUILD; + return process.env.NEXT_PHASE === PHASE_PRODUCTION_BUILD; } diff --git a/packages/nextjs/src/utils/nextLogger.ts b/packages/nextjs/src/server/utils/nextLogger.ts similarity index 100% rename from packages/nextjs/src/utils/nextLogger.ts rename to packages/nextjs/src/server/utils/nextLogger.ts diff --git a/packages/nextjs/src/utils/platformSupportsStreaming.ts b/packages/nextjs/src/server/utils/platformSupportsStreaming.ts similarity index 100% rename from packages/nextjs/src/utils/platformSupportsStreaming.ts rename to packages/nextjs/src/server/utils/platformSupportsStreaming.ts diff --git a/packages/nextjs/src/config/wrappers/utils/responseEnd.ts b/packages/nextjs/src/server/utils/responseEnd.ts similarity index 100% rename from packages/nextjs/src/config/wrappers/utils/responseEnd.ts rename to packages/nextjs/src/server/utils/responseEnd.ts diff --git a/packages/nextjs/src/config/wrappers/wrapperUtils.ts b/packages/nextjs/src/server/utils/wrapperUtils.ts similarity index 98% rename from packages/nextjs/src/config/wrappers/wrapperUtils.ts rename to packages/nextjs/src/server/utils/wrapperUtils.ts index 18665040d45d..8883f1163f57 100644 --- a/packages/nextjs/src/config/wrappers/wrapperUtils.ts +++ b/packages/nextjs/src/server/utils/wrapperUtils.ts @@ -5,8 +5,8 @@ import { baggageHeaderToDynamicSamplingContext, extractTraceparentData } from '@ import * as domain from 'domain'; import { IncomingMessage, ServerResponse } from 'http'; -import { platformSupportsStreaming } from '../../utils/platformSupportsStreaming'; -import { autoEndTransactionOnResponseEnd, flushQueue } from './utils/responseEnd'; +import { platformSupportsStreaming } from './platformSupportsStreaming'; +import { autoEndTransactionOnResponseEnd, flushQueue } from './responseEnd'; declare module 'http' { interface IncomingMessage { diff --git a/packages/nextjs/src/config/wrappers/withSentryAPI.ts b/packages/nextjs/src/server/withSentryAPI.ts similarity index 98% rename from packages/nextjs/src/config/wrappers/withSentryAPI.ts rename to packages/nextjs/src/server/withSentryAPI.ts index 561939685aa0..acb15a4c5514 100644 --- a/packages/nextjs/src/config/wrappers/withSentryAPI.ts +++ b/packages/nextjs/src/server/withSentryAPI.ts @@ -11,9 +11,9 @@ import { } from '@sentry/utils'; import * as domain from 'domain'; -import { formatAsCode, nextLogger } from '../../utils/nextLogger'; -import { platformSupportsStreaming } from '../../utils/platformSupportsStreaming'; import type { AugmentedNextApiRequest, AugmentedNextApiResponse, NextApiHandler, WrappedNextApiHandler } from './types'; +import { formatAsCode, nextLogger } from './utils/nextLogger'; +import { platformSupportsStreaming } from './utils/platformSupportsStreaming'; import { autoEndTransactionOnResponseEnd, finishTransaction, flushQueue } from './utils/responseEnd'; /** diff --git a/packages/nextjs/src/config/wrappers/withSentryGetServerSideProps.ts b/packages/nextjs/src/server/withSentryGetServerSideProps.ts similarity index 91% rename from packages/nextjs/src/config/wrappers/withSentryGetServerSideProps.ts rename to packages/nextjs/src/server/withSentryGetServerSideProps.ts index 8aa2244ea90e..76c8b848712a 100644 --- a/packages/nextjs/src/config/wrappers/withSentryGetServerSideProps.ts +++ b/packages/nextjs/src/server/withSentryGetServerSideProps.ts @@ -2,8 +2,12 @@ import { hasTracingEnabled } from '@sentry/tracing'; import { dynamicSamplingContextToSentryBaggageHeader } from '@sentry/utils'; import { GetServerSideProps } from 'next'; -import { isBuild } from '../../utils/isBuild'; -import { getTransactionFromRequest, withErrorInstrumentation, withTracedServerSideDataFetcher } from './wrapperUtils'; +import { isBuild } from './utils/isBuild'; +import { + getTransactionFromRequest, + withErrorInstrumentation, + withTracedServerSideDataFetcher, +} from './utils/wrapperUtils'; /** * Create a wrapped version of the user's exported `getServerSideProps` function diff --git a/packages/nextjs/src/config/wrappers/withSentryGetStaticProps.ts b/packages/nextjs/src/server/withSentryGetStaticProps.ts similarity index 93% rename from packages/nextjs/src/config/wrappers/withSentryGetStaticProps.ts rename to packages/nextjs/src/server/withSentryGetStaticProps.ts index 396c57c0652c..7220480b4117 100644 --- a/packages/nextjs/src/config/wrappers/withSentryGetStaticProps.ts +++ b/packages/nextjs/src/server/withSentryGetStaticProps.ts @@ -1,7 +1,7 @@ import { GetStaticProps } from 'next'; -import { isBuild } from '../../utils/isBuild'; -import { callDataFetcherTraced, withErrorInstrumentation } from './wrapperUtils'; +import { isBuild } from './utils/isBuild'; +import { callDataFetcherTraced, withErrorInstrumentation } from './utils/wrapperUtils'; type Props = { [key: string]: unknown }; diff --git a/packages/nextjs/src/config/wrappers/withSentryServerSideAppGetInitialProps.ts b/packages/nextjs/src/server/withSentryServerSideAppGetInitialProps.ts similarity index 94% rename from packages/nextjs/src/config/wrappers/withSentryServerSideAppGetInitialProps.ts rename to packages/nextjs/src/server/withSentryServerSideAppGetInitialProps.ts index a136bbcdc96a..89ed3af12dc7 100644 --- a/packages/nextjs/src/config/wrappers/withSentryServerSideAppGetInitialProps.ts +++ b/packages/nextjs/src/server/withSentryServerSideAppGetInitialProps.ts @@ -2,8 +2,12 @@ import { hasTracingEnabled } from '@sentry/tracing'; import { dynamicSamplingContextToSentryBaggageHeader } from '@sentry/utils'; import App from 'next/app'; -import { isBuild } from '../../utils/isBuild'; -import { getTransactionFromRequest, withErrorInstrumentation, withTracedServerSideDataFetcher } from './wrapperUtils'; +import { isBuild } from './utils/isBuild'; +import { + getTransactionFromRequest, + withErrorInstrumentation, + withTracedServerSideDataFetcher, +} from './utils/wrapperUtils'; type AppGetInitialProps = typeof App['getInitialProps']; diff --git a/packages/nextjs/src/config/wrappers/withSentryServerSideDocumentGetInitialProps.ts b/packages/nextjs/src/server/withSentryServerSideDocumentGetInitialProps.ts similarity index 96% rename from packages/nextjs/src/config/wrappers/withSentryServerSideDocumentGetInitialProps.ts rename to packages/nextjs/src/server/withSentryServerSideDocumentGetInitialProps.ts index a04aa11a3398..d08222b6064c 100644 --- a/packages/nextjs/src/config/wrappers/withSentryServerSideDocumentGetInitialProps.ts +++ b/packages/nextjs/src/server/withSentryServerSideDocumentGetInitialProps.ts @@ -1,8 +1,8 @@ import { hasTracingEnabled } from '@sentry/tracing'; import Document from 'next/document'; -import { isBuild } from '../../utils/isBuild'; -import { withErrorInstrumentation, withTracedServerSideDataFetcher } from './wrapperUtils'; +import { isBuild } from './utils/isBuild'; +import { withErrorInstrumentation, withTracedServerSideDataFetcher } from './utils/wrapperUtils'; type DocumentGetInitialProps = typeof Document.getInitialProps; diff --git a/packages/nextjs/src/config/wrappers/withSentryServerSideErrorGetInitialProps.ts b/packages/nextjs/src/server/withSentryServerSideErrorGetInitialProps.ts similarity index 93% rename from packages/nextjs/src/config/wrappers/withSentryServerSideErrorGetInitialProps.ts rename to packages/nextjs/src/server/withSentryServerSideErrorGetInitialProps.ts index 8ceae817e1bc..a0cadad23fa3 100644 --- a/packages/nextjs/src/config/wrappers/withSentryServerSideErrorGetInitialProps.ts +++ b/packages/nextjs/src/server/withSentryServerSideErrorGetInitialProps.ts @@ -3,8 +3,12 @@ import { dynamicSamplingContextToSentryBaggageHeader } from '@sentry/utils'; import { NextPageContext } from 'next'; import { ErrorProps } from 'next/error'; -import { isBuild } from '../../utils/isBuild'; -import { getTransactionFromRequest, withErrorInstrumentation, withTracedServerSideDataFetcher } from './wrapperUtils'; +import { isBuild } from './utils/isBuild'; +import { + getTransactionFromRequest, + withErrorInstrumentation, + withTracedServerSideDataFetcher, +} from './utils/wrapperUtils'; type ErrorGetInitialProps = (context: NextPageContext) => Promise; diff --git a/packages/nextjs/src/config/wrappers/withSentryServerSideGetInitialProps.ts b/packages/nextjs/src/server/withSentryServerSideGetInitialProps.ts similarity index 92% rename from packages/nextjs/src/config/wrappers/withSentryServerSideGetInitialProps.ts rename to packages/nextjs/src/server/withSentryServerSideGetInitialProps.ts index 06d0f7766fec..d3760d792b9d 100644 --- a/packages/nextjs/src/config/wrappers/withSentryServerSideGetInitialProps.ts +++ b/packages/nextjs/src/server/withSentryServerSideGetInitialProps.ts @@ -2,8 +2,12 @@ import { hasTracingEnabled } from '@sentry/tracing'; import { dynamicSamplingContextToSentryBaggageHeader } from '@sentry/utils'; import { NextPage } from 'next'; -import { isBuild } from '../../utils/isBuild'; -import { getTransactionFromRequest, withErrorInstrumentation, withTracedServerSideDataFetcher } from './wrapperUtils'; +import { isBuild } from './utils/isBuild'; +import { + getTransactionFromRequest, + withErrorInstrumentation, + withTracedServerSideDataFetcher, +} from './utils/wrapperUtils'; type GetInitialProps = Required['getInitialProps']; diff --git a/packages/nextjs/src/utils/isESM.ts b/packages/nextjs/src/utils/isESM.ts deleted file mode 100644 index 30ce7f4e87de..000000000000 --- a/packages/nextjs/src/utils/isESM.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Determine if the given source code represents a file written using ES6 modules. - * - * The regexes used are from https://github.com/component/is-module, which got them from - * https://github.com/formatjs/js-module-formats, which says it got them from - * https://github.com/ModuleLoader/es-module-loader, though the originals are now nowhere to be found. - * - * @param moduleSource The source code of the module - * @returns True if the module contains ESM-patterned code, false otherwise. - */ -export function isESM(moduleSource: string): boolean { - const importExportRegex = - /(?:^\s*|[}{();,\n]\s*)(import\s+['"]|(import|module)\s+[^"'()\n;]+\s+from\s+['"]|export\s+(\*|\{|default|function|var|const|let|[_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF]*))/; - const exportStarRegex = /(?:^\s*|[}{();,\n]\s*)(export\s*\*\s*from\s*(?:'([^']+)'|"([^"]+)"))/; - - return importExportRegex.test(moduleSource) || exportStarRegex.test(moduleSource); -} diff --git a/packages/nextjs/src/utils/nextjsOptions.ts b/packages/nextjs/src/utils/nextjsOptions.ts deleted file mode 100644 index ded1fe3f1d23..000000000000 --- a/packages/nextjs/src/utils/nextjsOptions.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { NodeOptions } from '@sentry/node'; -import { BrowserOptions } from '@sentry/react'; -import { Options } from '@sentry/types'; - -export type NextjsOptions = Options | BrowserOptions | NodeOptions; diff --git a/packages/nextjs/src/utils/phases.ts b/packages/nextjs/src/utils/phases.ts deleted file mode 100644 index 17e11d404be1..000000000000 --- a/packages/nextjs/src/utils/phases.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const NEXT_PHASE_PRODUCTION_BUILD = 'phase-production-build'; -export const NEXT_PHASE_PRODUCTION_SERVER = 'phase-production-server'; -export const NEXT_PHASE_DEVELOPMENT_SERVER = 'phase-development-server'; diff --git a/packages/nextjs/test/index.client.test.ts b/packages/nextjs/test/clientSdk.test.ts similarity index 98% rename from packages/nextjs/test/index.client.test.ts rename to packages/nextjs/test/clientSdk.test.ts index 9ed02519e722..4a7577c21a33 100644 --- a/packages/nextjs/test/index.client.test.ts +++ b/packages/nextjs/test/clientSdk.test.ts @@ -6,8 +6,8 @@ import { Integration } from '@sentry/types'; import { logger } from '@sentry/utils'; import { JSDOM } from 'jsdom'; -import { init, Integrations, nextRouterInstrumentation } from '../src/index.client'; -import { UserIntegrationsFunction } from '../src/utils/userIntegrations'; +import { init, Integrations, nextRouterInstrumentation } from '../src/client'; +import { UserIntegrationsFunction } from '../src/common/userIntegrations'; const { BrowserTracing } = TracingIntegrations; diff --git a/packages/nextjs/test/config/withSentry.test.ts b/packages/nextjs/test/config/withSentry.test.ts index fa468b32670b..0ff2dca6018f 100644 --- a/packages/nextjs/test/config/withSentry.test.ts +++ b/packages/nextjs/test/config/withSentry.test.ts @@ -3,7 +3,8 @@ import * as Sentry from '@sentry/node'; import { Client, ClientOptions } from '@sentry/types'; import { NextApiHandler, NextApiRequest, NextApiResponse } from 'next'; -import { AugmentedNextApiResponse, withSentry, WrappedNextApiHandler } from '../../src/config/wrappers'; +import { withSentry } from '../../src/server'; +import type { AugmentedNextApiResponse, WrappedNextApiHandler } from '../../src/server/types'; const FLUSH_DURATION = 200; diff --git a/packages/nextjs/test/config/wrappers.test.ts b/packages/nextjs/test/config/wrappers.test.ts index abbefd93c3e8..ba742869ac49 100644 --- a/packages/nextjs/test/config/wrappers.test.ts +++ b/packages/nextjs/test/config/wrappers.test.ts @@ -2,16 +2,7 @@ import * as SentryCore from '@sentry/core'; import * as SentryTracing from '@sentry/tracing'; import { IncomingMessage, ServerResponse } from 'http'; -import { - withSentryGetServerSideProps, - withSentryServerSideGetInitialProps, - // TODO: Leaving `withSentryGetStaticProps` out for now until we figure out what to do with it - // withSentryGetStaticProps, - // TODO: Leaving these out for now until we figure out pages with no data fetchers - // withSentryServerSideAppGetInitialProps, - // withSentryServerSideDocumentGetInitialProps, - // withSentryServerSideErrorGetInitialProps, -} from '../../src/config/wrappers'; +import { withSentryGetServerSideProps, withSentryServerSideGetInitialProps } from '../../src/server'; const startTransactionSpy = jest.spyOn(SentryCore, 'startTransaction'); diff --git a/packages/nextjs/test/performance/client.test.ts b/packages/nextjs/test/performance/client.test.ts index 590a8254e109..1cd7d0e787a7 100644 --- a/packages/nextjs/test/performance/client.test.ts +++ b/packages/nextjs/test/performance/client.test.ts @@ -4,7 +4,7 @@ import { JSDOM } from 'jsdom'; import { NEXT_DATA as NextData } from 'next/dist/next-server/lib/utils'; import { default as Router } from 'next/router'; -import { nextRouterInstrumentation } from '../../src/performance/client'; +import { nextRouterInstrumentation } from '../../src/client/performance'; const globalObject = WINDOW as typeof WINDOW & { __BUILD_MANIFEST?: { diff --git a/packages/nextjs/test/index.server.test.ts b/packages/nextjs/test/serverSdk.test.ts similarity index 99% rename from packages/nextjs/test/index.server.test.ts rename to packages/nextjs/test/serverSdk.test.ts index 232d3b746f1e..56ef03377df6 100644 --- a/packages/nextjs/test/index.server.test.ts +++ b/packages/nextjs/test/serverSdk.test.ts @@ -4,7 +4,7 @@ import { Integration } from '@sentry/types'; import { GLOBAL_OBJ, logger } from '@sentry/utils'; import * as domain from 'domain'; -import { init } from '../src/index.server'; +import { init } from '../src/index'; const { Integrations } = SentryNode; diff --git a/packages/nextjs/test/utils/isESM.test.ts b/packages/nextjs/test/utils/isESM.test.ts deleted file mode 100644 index 22169377e730..000000000000 --- a/packages/nextjs/test/utils/isESM.test.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { isESM } from '../../src/utils/isESM'; - -// Based on https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import -describe('import syntax', function () { - it('recognizes import syntax', function () { - expect(isESM("import dogs from 'dogs';")).toBe(true); - expect(isESM("import * as dogs from 'dogs';")).toBe(true); - expect(isESM("import { maisey } from 'dogs';")).toBe(true); - expect(isESM("import { charlie as goofball } from 'dogs';")).toBe(true); - expect(isESM("import { default as maisey } from 'dogs';")).toBe(true); - expect(isESM("import { charlie, masiey } from 'dogs';")).toBe(true); - expect(isESM("import { masiey, charlie as pickle } from 'dogs';")).toBe(true); - expect(isESM("import charlie, { maisey } from 'dogs';")).toBe(true); - expect(isESM("import maisey, * as dogs from 'dogs';")).toBe(true); - expect(isESM("import 'dogs';")).toBe(true); - }); -}); - -// Based on https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Statements/export -describe('export syntax', function () { - it('recognizes exported declarations', () => { - expect(isESM('export var maisey, charlie;')).toBe(true); - expect(isESM('export let charlie, maisey;')).toBe(true); - expect(isESM("export var maisey = 'silly', charlie = 'goofy';")).toBe(true); - expect(isESM("export let charlie = 'goofy', maisey = 'silly';")).toBe(true); - expect(isESM("export const maisey = 'silly', charlie = 'goofy';")).toBe(true); - expect(isESM('export function doDogStuff() { /* ... */ }')).toBe(true); - expect(isESM('export class Dog { /* ... */ }')).toBe(true); - expect(isESM('export function* generateWayTooManyPhotosOnMyPhone() { /* ... */ }')).toBe(true); - expect(isESM('export const { maisey, charlie } = dogObject;')).toBe(true); - expect(isESM('export const { charlie, masiey: maiseyTheDog } = dogObject;')).toBe(true); - expect(isESM('export const [ maisey, charlie ] = dogArray;')).toBe(true); - }); - - it('recognizes lists of exports', () => { - expect(isESM('export { maisey, charlie };')).toBe(true); - expect(isESM('export { charlie as charlieMcCharlerson, masiey as theMaiseyMaiseyDog };')).toBe(true); - expect(isESM('export { charlie as default };')).toBe(true); - }); - - it('recognizes default exports', () => { - expect(isESM("export default 'dogs are great';")).toBe(true); - expect(isESM('export default function doDogStuff() { /* ... */ }')).toBe(true); - expect(isESM('export default class Dog { /* ... */ }')).toBe(true); - expect(isESM('export default function* generateWayTooManyPhotosOnMyPhone() { /* ... */ }')).toBe(true); - expect(isESM('export default function () { /* ... */ }')).toBe(true); - expect(isESM('export default class { /* ... */ }')).toBe(true); - expect(isESM('export default function* () { /* ... */ }')).toBe(true); - }); - - it('recognizes exports directly from another module', () => { - expect(isESM("export * from 'dogs';")).toBe(true); - expect(isESM("export * as dogs from 'dogs';")).toBe(true); - expect(isESM("export { maisey, charlie } from 'dogs';")).toBe(true); - expect( - isESM("export { maisey as goodGirl, charlie as omgWouldYouJustPeeAlreadyIWantToGoToBed } from 'dogs';"), - ).toBe(true); - expect(isESM("export { default } from 'dogs';")).toBe(true); - expect(isESM("export { default, maisey } from 'dogs';")).toBe(true); - }); -}); - -describe('potential false positives', () => { - it("doesn't get fooled by look-alikes", () => { - expect(isESM("'this is an import statement'")).toBe(false); - expect(isESM("'this is an export statement'")).toBe(false); - expect(isESM('import(dogs)')).toBe(false); - }); -}); diff --git a/packages/nextjs/test/utils/tunnelRoute.test.ts b/packages/nextjs/test/utils/tunnelRoute.test.ts index 8a46f8b174ea..ae9b085da7ca 100644 --- a/packages/nextjs/test/utils/tunnelRoute.test.ts +++ b/packages/nextjs/test/utils/tunnelRoute.test.ts @@ -1,5 +1,6 @@ -import { NextjsOptions } from '../../src/utils/nextjsOptions'; -import { applyTunnelRouteOption } from '../../src/utils/tunnelRoute'; +import { BrowserOptions } from '@sentry/react'; + +import { applyTunnelRouteOption } from '../../src/client/tunnelRoute'; const globalWithInjectedValues = global as typeof global & { __sentryRewritesTunnelPath__?: string; @@ -14,7 +15,7 @@ describe('applyTunnelRouteOption()', () => { globalWithInjectedValues.__sentryRewritesTunnelPath__ = '/my-error-monitoring-route'; const options: any = { dsn: 'https://11111111111111111111111111111111@o2222222.ingest.sentry.io/3333333', - } as NextjsOptions; + } as BrowserOptions; applyTunnelRouteOption(options); @@ -25,7 +26,7 @@ describe('applyTunnelRouteOption()', () => { globalWithInjectedValues.__sentryRewritesTunnelPath__ = '/my-error-monitoring-route'; const options: any = { // no dsn - } as NextjsOptions; + } as BrowserOptions; applyTunnelRouteOption(options); @@ -35,7 +36,7 @@ describe('applyTunnelRouteOption()', () => { it("should not apply `tunnelRoute` option when `tunnelRoute` option wasn't injected", () => { const options: any = { dsn: 'https://11111111111111111111111111111111@o2222222.ingest.sentry.io/3333333', - } as NextjsOptions; + } as BrowserOptions; applyTunnelRouteOption(options); @@ -46,7 +47,7 @@ describe('applyTunnelRouteOption()', () => { globalWithInjectedValues.__sentryRewritesTunnelPath__ = '/my-error-monitoring-route'; const options: any = { dsn: 'https://11111111111111111111111111111111@example.com/3333333', - } as NextjsOptions; + } as BrowserOptions; applyTunnelRouteOption(options); diff --git a/packages/nextjs/test/utils/userIntegrations.test.ts b/packages/nextjs/test/utils/userIntegrations.test.ts index 7a8693a8a524..7a5d8d879fa1 100644 --- a/packages/nextjs/test/utils/userIntegrations.test.ts +++ b/packages/nextjs/test/utils/userIntegrations.test.ts @@ -1,5 +1,5 @@ -import type { IntegrationWithExclusionOption as Integration } from '../../src/utils/userIntegrations'; -import { addOrUpdateIntegration, UserIntegrations } from '../../src/utils/userIntegrations'; +import type { IntegrationWithExclusionOption as Integration } from '../../src/common/userIntegrations'; +import { addOrUpdateIntegration, UserIntegrations } from '../../src/common/userIntegrations'; type MockIntegrationOptions = { name: string; From e26e83506eb24659c31dc3f3efbb4223a3d6a8db Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Tue, 10 Jan 2023 18:16:11 +0100 Subject: [PATCH 031/113] test: Cache Playwright deps (#6716) --- .github/workflows/build.yml | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2830b56b9eb4..ca288b934fb5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -541,13 +541,34 @@ jobs: with: path: ${{ env.CACHED_BUILD_PATHS }} key: ${{ env.BUILD_CACHE_KEY }} + - name: Get npm cache directory + id: npm-cache-dir + run: | + echo "::set-output name=dir::$(npm config get cache)" + - name: Get Playwright version + id: playwright-version + run: | + echo "::set-output name=version::$(node -p "require('@playwright/test/package.json').version")" + - uses: actions/cache@v3 + name: Check if Playwright browser is cached + id: playwright-cache + with: + path: ${{ steps.npm-cache-dir.outputs.dir }} + key: ${{ runner.os }}-Playwright-${{steps.playwright-version.outputs.version}} + - name: Install Playwright browser if not cached + if: steps.playwright-cache.outputs.cache-hit != 'true' + run: npx playwright install --with-deps + env: + PLAYWRIGHT_BROWSERS_PATH: ${{steps.npm-cache-dir.outputs.dir}} + - name: Install OS dependencies of Playwright if cache hit + if: steps.playwright-cache.outputs.cache-hit == 'true' + run: npx playwright install-deps - name: Run Playwright tests env: PW_BUNDLE: ${{ matrix.bundle }} PW_TRACING_ONLY: ${{ matrix.tracing_only }} run: | cd packages/integration-tests - yarn run playwright install-deps webkit yarn test:ci job_browser_integration_tests: From aadd8556c4f39d0857d0e049625a46baae01620f Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Wed, 11 Jan 2023 10:43:32 +0100 Subject: [PATCH 032/113] fix(otel): Set trace context via Otel Span instead of Sentry span (#6724) --- .../opentelemetry-node/src/spanprocessor.ts | 23 ++++++++----------- .../test/spanprocessor.test.ts | 1 - 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/packages/opentelemetry-node/src/spanprocessor.ts b/packages/opentelemetry-node/src/spanprocessor.ts index dfb8c6f0d2b2..04cd452a1a82 100644 --- a/packages/opentelemetry-node/src/spanprocessor.ts +++ b/packages/opentelemetry-node/src/spanprocessor.ts @@ -22,25 +22,22 @@ export const SENTRY_SPAN_PROCESSOR_MAP: Map = export class SentrySpanProcessor implements OtelSpanProcessor { public constructor() { addGlobalEventProcessor(event => { - const otelSpan = trace.getActiveSpan(); + const otelSpan = trace.getActiveSpan() as OtelSpan; if (!otelSpan) { return event; } - const otelSpanId = otelSpan.spanContext().spanId; - const sentrySpan = SENTRY_SPAN_PROCESSOR_MAP.get(otelSpanId); - - if (!sentrySpan) { - return event; - } + const otelSpanContext = otelSpan.spanContext(); // If event has already set `trace` context, use that one. - // This happens in the case of transaction events. - event.contexts = { trace: sentrySpan.getTraceContext(), ...event.contexts }; - const transactionName = sentrySpan.transaction && sentrySpan.transaction.name; - if (transactionName) { - event.tags = { transaction: transactionName, ...event.tags }; - } + event.contexts = { + trace: { + trace_id: otelSpanContext.traceId, + span_id: otelSpanContext.spanId, + parent_span_id: otelSpan.parentSpanId, + }, + ...event.contexts, + }; return event; }); diff --git a/packages/opentelemetry-node/test/spanprocessor.test.ts b/packages/opentelemetry-node/test/spanprocessor.test.ts index 746f6e447c13..bd970bb737fd 100644 --- a/packages/opentelemetry-node/test/spanprocessor.test.ts +++ b/packages/opentelemetry-node/test/spanprocessor.test.ts @@ -744,7 +744,6 @@ describe('SentrySpanProcessor', () => { expect(sentryEvent).toBeDefined(); expect(sentryEvent.exception).toBeDefined(); expect(sentryEvent.contexts.trace).toEqual({ - description: otelSpan.name, parent_span_id: otelSpan.parentSpanId, span_id: otelSpan.spanContext().spanId, trace_id: otelSpan.spanContext().traceId, From 95ba49a5da4f83f37e7bbcc389d48790d0081de9 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 11 Jan 2023 11:48:33 +0100 Subject: [PATCH 033/113] build: Update Lerna to v6 and use Nx caching for builds (#6555) --- .github/workflows/build.yml | 43 +- docs/new-sdk-release-checklist.md | 2 +- lerna.json | 4 +- nx.json | 68 + package.json | 42 +- packages/angular/package.json | 11 +- packages/browser/package.json | 14 +- packages/core/package.json | 10 +- packages/e2e-tests/publish-packages.ts | 2 +- packages/ember/package.json | 2 +- packages/eslint-config-sdk/package.json | 2 +- packages/eslint-plugin-sdk/package.json | 2 +- packages/gatsby/package.json | 10 +- packages/hub/package.json | 10 +- packages/integrations/package.json | 14 +- packages/nextjs/package.json | 10 +- packages/node/package.json | 10 +- packages/opentelemetry-node/package.json | 10 +- packages/react/package.json | 10 +- packages/remix/package.json | 10 +- packages/replay/package.json | 6 +- packages/replay/workflows/build.yml | 2 +- packages/serverless/package.json | 14 +- .../serverless/scripts/buildLambdaLayer.ts | 1 + packages/svelte/package.json | 10 +- packages/tracing/package.json | 18 +- packages/types/package.json | 10 +- packages/typescript/package.json | 2 +- packages/utils/package.json | 11 +- packages/vue/package.json | 16 +- packages/wasm/package.json | 16 +- scripts/ensure-bundle-deps.ts | 4 +- scripts/{test.ts => node-unit-tests.ts} | 50 +- yarn.lock | 3317 ++++++++++------- 34 files changed, 2161 insertions(+), 1602 deletions(-) create mode 100644 nx.json rename scripts/{test.ts => node-unit-tests.ts} (75%) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ca288b934fb5..0228771337ac 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -120,6 +120,11 @@ jobs: changed_browser: ${{ steps.changed.outputs.browser }} changed_browser_integration: ${{ steps.changed.outputs.browser_integration }} changed_any_code: ${{ steps.changed.outputs.any_code }} + # Note: These next three have to be checked as strings ('true'/'false')! + is_master: ${{ github.ref == 'refs/heads/master' }} + is_release: ${{ startsWith(github.ref, 'refs/heads/release/') }} + force_skip_cache: + ${{ github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'ci-skip-cache') }} job_install_deps: name: Install Dependencies @@ -139,14 +144,19 @@ jobs: - name: Compute dependency cache key id: compute_lockfile_hash run: echo "hash=${{ hashFiles('yarn.lock') }}" >> "$GITHUB_OUTPUT" + + # When the `ci-skip-cache` label is added to a PR, we always want to skip dependency cache - name: Check dependency cache uses: actions/cache@v3 id: cache_dependencies + if: needs.job_get_metadata.outputs.force_skip_cache == 'false' with: path: ${{ env.CACHED_DEPENDENCY_PATHS }} key: ${{ steps.compute_lockfile_hash.outputs.hash }} + - name: Install dependencies - if: steps.cache_dependencies.outputs.cache-hit == '' + if: + steps.cache_dependencies.outputs.cache-hit == '' || needs.job_get_metadata.outputs.force_skip_cache == 'true' run: yarn install --ignore-engines --frozen-lockfile outputs: dependency_cache_key: ${{ steps.compute_lockfile_hash.outputs.hash }} @@ -168,12 +178,35 @@ jobs: with: path: ${{ env.CACHED_DEPENDENCY_PATHS }} key: ${{ needs.job_install_deps.outputs.dependency_cache_key }} + - name: Check build cache uses: actions/cache@v3 id: cache_built_packages + if: needs.job_get_metadata.outputs.force_skip_cache == 'false' with: path: ${{ env.CACHED_BUILD_PATHS }} key: ${{ env.BUILD_CACHE_KEY }} + + - name: NX cache + uses: actions/cache@v3 + # Disable cache when: + # - on master + # - on release branches + # - when PR has `ci-skip-cache` label + if: | + needs.job_get_metadata.outputs.is_master == 'false' && + needs.job_get_metadata.outputs.is_release == 'false' && + needs.job_get_metadata.outputs.force_skip_cache == 'false' + with: + path: node_modules/.cache/nx + key: nx-${{ runner.os }}-${{ github.ref }}-${{ env.HEAD_COMMIT }} + # GH will use the first restore-key it finds that matches + # So it will start by looking for one from the same branch, else take the newest one it can find elsewhere + restore-keys: | + nx-${{ runner.os }}-${{ github.ref }}-${{ env.HEAD_COMMIT }} + nx-${{ runner.os }}-${{ github.ref }} + nx-${{ runner.os }} + - name: Build packages # Under normal circumstances, using the git SHA as a cache key, there shouldn't ever be a cache hit on the built # packages, and so `yarn build` should always run. This `if` check is therefore only there for testing CI issues @@ -232,7 +265,7 @@ jobs: timeout-minutes: 15 runs-on: ubuntu-20.04 # Size Check will error out outside of the context of a PR - if: github.event_name == 'pull_request' || github.ref == 'refs/heads/master' + if: github.event_name == 'pull_request' || needs.job_get_metadata.outputs.is_master == 'true' steps: - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) uses: actions/checkout@v3 @@ -258,7 +291,7 @@ jobs: - name: Check bundle sizes uses: getsentry/size-limit-action@v5 # Don't run size check on release branches - at that point, we're already committed - if: ${{ !startsWith(github.ref, 'refs/heads/release/') }} + if: needs.job_get_metadata.outputs.is_release == 'false' with: github_token: ${{ secrets.GITHUB_TOKEN }} skip_step: build @@ -320,7 +353,7 @@ jobs: needs: [job_get_metadata, job_build] runs-on: ubuntu-20.04 # Build artifacts are only needed for releasing workflow. - if: startsWith(github.ref, 'refs/heads/release/') + if: needs.job_get_metadata.outputs.is_release == 'true' steps: - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) uses: actions/checkout@v3 @@ -339,7 +372,7 @@ jobs: path: ${{ env.CACHED_BUILD_PATHS }} key: ${{ env.BUILD_CACHE_KEY }} - name: Pack - run: yarn build:npm + run: yarn build:tarball - name: Archive artifacts uses: actions/upload-artifact@v3.1.1 with: diff --git a/docs/new-sdk-release-checklist.md b/docs/new-sdk-release-checklist.md index 5e795b19e18d..c9d9cec079be 100644 --- a/docs/new-sdk-release-checklist.md +++ b/docs/new-sdk-release-checklist.md @@ -21,7 +21,7 @@ This page serves as a checklist of what to do when releasing a new SDK for the f - [ ] Make sure that the `LICENSE` file exists and has the correct license (We default to the `MIT` license) - [ ] Also check, that the same license is mentioned in `package.json` -- [ ] Make sure that the tarball (`yarn build:npm`) has all the necessary contents +- [ ] Make sure that the tarball (`yarn build:tarball`) has all the necessary contents For basic SDKs, this means that the tarball has at least these files: diff --git a/lerna.json b/lerna.json index 408b79d9c474..8285520a2798 100644 --- a/lerna.json +++ b/lerna.json @@ -1,7 +1,7 @@ { - "lerna": "3.4.0", + "$schema": "node_modules/lerna/schemas/lerna-schema.json", "version": "7.30.0", - "packages": "packages/*", + "packages": ["packages/*"], "npmClient": "yarn", "useWorkspaces": true } diff --git a/nx.json b/nx.json new file mode 100644 index 000000000000..3b2b7778c325 --- /dev/null +++ b/nx.json @@ -0,0 +1,68 @@ +{ + "tasksRunnerOptions": { + "default": { + "runner": "nx/tasks-runners/default", + "options": { + "cacheableOperations": [ + "build:bundle", + "build:transpile", + "build:tarball", + "build:types" + ] + } + } + }, + "targetDefaults": { + "build:bundle": { + "dependsOn": [ + "^build:transpile", + "build:transpile", + "^build:types", + "build:types" + ], + "outputs": [ + "{projectRoot}/build/bundles" + ] + }, + "build:tarball": { + "dependsOn": [ + "^build:transpile", + "build:transpile", + "^build:types", + "build:types" + ], + "outputs": [] + }, + "build:transpile": { + "dependsOn": [ + "^build:transpile:uncached", + "^build:transpile", + "build:transpile:uncached" + ], + "outputs": [ + "{projectRoot}/build/npm", + "{projectRoot}/build/esm", + "{projectRoot}/build/cjs" + ] + }, + "build:types": { + "dependsOn": [ + "^build:types" + ], + "outputs": [ + "{projectRoot}/build/types", + "{projectRoot}/build/npm/types" + ] + } + }, + "targets": { + "@sentry/serverless": { + "build:bundle": { + "dependsOn": [], + "outputs": [ + "{projectRoot}/build/aws" + ] + } + } + } +} diff --git a/package.json b/package.json index b7ab59c0c862..296f1a1758f6 100644 --- a/package.json +++ b/package.json @@ -1,33 +1,31 @@ { "private": true, "scripts": { - "build": "node ./scripts/verify-packages-versions.js && yarn run-p build:rollup build:types build:bundle && yarn build:extras", - "build:bundle": "yarn ts-node scripts/ensure-bundle-deps.ts && yarn lerna run --parallel build:bundle", - "build:dev": "run-p build:types build:rollup", - "build:dev:filter": "lerna run --stream --concurrency 1 --sort build:dev --include-filtered-dependencies --include-filtered-dependents --scope", - "build:extras": "lerna run --parallel build:extras", - "build:rollup": "lerna run --parallel build:rollup", - "build:types": "lerna run --stream build:types", - "build:watch": "lerna run --parallel build:watch", - "build:dev:watch": "lerna run --parallel build:dev:watch", + "build": "node ./scripts/verify-packages-versions.js && run-s build:types build:transpile build:bundle", + "build:bundle": "lerna run build:bundle", + "build:dev": "run-s build:types build:transpile", + "build:dev:filter": "lerna run build:dev --include-filtered-dependencies --include-filtered-dependents --scope", + "build:transpile": "lerna run build:transpile", + "build:types": "lerna run build:types", + "build:watch": "lerna run build:watch", + "build:dev:watch": "lerna run build:dev:watch", "build:types:watch": "ts-node scripts/build-types-watch.ts", - "build:npm": "lerna run --parallel build:npm", - "circularDepCheck": "lerna run --parallel circularDepCheck", + "build:tarball": "lerna run build:tarball", + "circularDepCheck": "lerna run circularDepCheck", "clean": "run-p clean:build clean:caches", - "clean:build": "lerna run --parallel clean", + "clean:build": "lerna run clean", "clean:caches": "yarn rimraf eslintcache && yarn jest --clearCache", "clean:deps": "lerna clean --yes && rm -rf node_modules && yarn", "clean:all": "run-p clean:build clean:caches clean:deps", "codecov": "codecov", - "fix": "lerna run --parallel fix", - "link:yarn": "lerna exec --parallel yarn link", - "lint": "lerna run --parallel lint", - "lint:eslint": "lerna run --parallel lint:eslint", + "fix": "lerna run fix", + "link:yarn": "lerna exec yarn link", + "lint": "lerna run lint", + "lint:eslint": "lerna run lint:eslint", "postpublish": "lerna run --stream --concurrency 1 postpublish", - "test": "lerna run --ignore @sentry-internal/browser-integration-tests --ignore @sentry-internal/node-integration-tests --stream --concurrency 1 --sort test", - "test-ci": "ts-node ./scripts/test.ts", - "test-ci-browser": "cross-env TESTS_SKIP=node ts-node ./scripts/test.ts", - "test-ci-node": "cross-env TESTS_SKIP=browser ts-node ./scripts/test.ts", + "test": "lerna run --ignore @sentry-internal/* test", + "test-ci-browser": "lerna run test --ignore \"@sentry/{node,opentelemetry-node,serverless,nextjs,remix,gatsby}\" --ignore @sentry-internal/*", + "test-ci-node": "ts-node ./scripts/node-unit-tests.ts", "postinstall": "patch-package" }, "volta": { @@ -87,7 +85,7 @@ "jsdom": "^19.0.0", "karma-browserstack-launcher": "^1.5.1", "karma-firefox-launcher": "^1.1.0", - "lerna": "3.13.4", + "lerna": "^6.0.3", "madge": "4.0.2", "magic-string": "^0.27.0", "mocha": "^6.1.4", @@ -116,5 +114,5 @@ "@types/express-serve-static-core": "4.17.30" }, "version": "0.0.0", - "dependencies": {} + "name": "sentry-javascript" } diff --git a/packages/angular/package.json b/packages/angular/package.json index 9d97dc49a0f6..6e86b2caf79b 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -41,13 +41,12 @@ "zone.js": "^0.11.8" }, "scripts": { - "build": "yarn build:ngc", - "build:ngc": "ng build --prod", + "build": "yarn build:transpile", + "build:transpile": "ng build --prod", "build:dev": "run-s build", - "build:extras": "yarn build", - "build:watch": "run-p build:ngc:watch", - "build:ngc:watch": "ng build --prod --watch", - "build:npm": "npm pack ./build", + "build:watch": "run-p build:transpile:watch", + "build:transpile:watch": "ng build --prod --watch", + "build:tarball": "npm pack ./build", "circularDepCheck": "madge --circular src/index.ts", "clean": "rimraf build coverage sentry-angular-*.tgz", "fix": "run-s fix:eslint fix:prettier", diff --git a/packages/browser/package.json b/packages/browser/package.json index e45e29b81451..439423eea09f 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -44,17 +44,15 @@ "webpack": "^4.30.0" }, "scripts": { - "build": "run-p build:rollup build:bundle build:types", - "build:bundle": "yarn ts-node ../../scripts/ensure-bundle-deps.ts && yarn rollup --config rollup.bundle.config.js", - "build:dev": "run-p build:rollup build:types", - "build:rollup": "rollup -c rollup.npm.config.js", + "build": "run-p build:transpile build:bundle build:types", + "build:bundle": "rollup --config rollup.bundle.config.js", + "build:transpile": "rollup -c rollup.npm.config.js", "build:types": "tsc -p tsconfig.types.json", - "build:watch": "run-p build:rollup:watch build:bundle:watch build:types:watch", + "build:watch": "run-p build:transpile:watch build:bundle:watch build:types:watch", "build:bundle:watch": "rollup --config rollup.bundle.config.js --watch", - "build:dev:watch": "run-p build:rollup:watch build:types:watch", - "build:rollup:watch": "rollup -c rollup.npm.config.js --watch", + "build:transpile:watch": "rollup -c rollup.npm.config.js --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", - "build:npm": "ts-node ../../scripts/prepack.ts --bundles && npm pack ./build/npm", + "build:tarball": "ts-node ../../scripts/prepack.ts --bundles && npm pack ./build/npm", "circularDepCheck": "madge --circular src/index.ts", "clean": "rimraf build coverage .rpt2_cache sentry-browser-*.tgz", "fix": "run-s fix:eslint fix:prettier", diff --git a/packages/core/package.json b/packages/core/package.json index fb0244a7993d..35da822edaf6 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -21,15 +21,15 @@ "tslib": "^1.9.3" }, "scripts": { - "build": "run-p build:rollup build:types", + "build": "run-p build:transpile build:types", "build:dev": "run-s build", - "build:rollup": "rollup -c rollup.npm.config.js", + "build:transpile": "rollup -c rollup.npm.config.js", "build:types": "tsc -p tsconfig.types.json", - "build:watch": "run-p build:rollup:watch build:types:watch", + "build:watch": "run-p build:transpile:watch build:types:watch", "build:dev:watch": "run-s build:watch", - "build:rollup:watch": "rollup -c rollup.npm.config.js --watch", + "build:transpile:watch": "rollup -c rollup.npm.config.js --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", - "build:npm": "ts-node ../../scripts/prepack.ts && npm pack ./build", + "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", diff --git a/packages/e2e-tests/publish-packages.ts b/packages/e2e-tests/publish-packages.ts index 22341b7aec6c..31ba3a6b4eb4 100644 --- a/packages/e2e-tests/publish-packages.ts +++ b/packages/e2e-tests/publish-packages.ts @@ -6,7 +6,7 @@ import * as path from 'path'; const repositoryRoot = path.resolve(__dirname, '../..'); // Create tarballs -childProcess.execSync('yarn build:npm', { encoding: 'utf8', cwd: repositoryRoot, stdio: 'inherit' }); +childProcess.execSync('yarn build:tarball', { encoding: 'utf8', cwd: repositoryRoot, stdio: 'inherit' }); // Get absolute paths of all the packages we want to publish to the fake registry const packageTarballPaths = glob.sync('packages/*/sentry-*.tgz', { diff --git a/packages/ember/package.json b/packages/ember/package.json index a2703b03fcb1..1083b75ecc68 100644 --- a/packages/ember/package.json +++ b/packages/ember/package.json @@ -18,7 +18,7 @@ }, "scripts": { "build": "ember build --environment=production", - "build:npm": "ember ts:precompile && npm pack && ember ts:clean", + "build:tarball": "ember ts:precompile && npm pack && ember ts:clean", "clean": "yarn rimraf sentry-ember-*.tgz", "lint": "run-p lint:js lint:hbs lint:ts", "lint:hbs": "ember-template-lint .", diff --git a/packages/eslint-config-sdk/package.json b/packages/eslint-config-sdk/package.json index 08fb32ce9883..acb20a03ec7e 100644 --- a/packages/eslint-config-sdk/package.json +++ b/packages/eslint-config-sdk/package.json @@ -39,7 +39,7 @@ "clean": "yarn rimraf sentry-internal-eslint-config-sdk-*.tgz", "lint": "prettier --check \"**/*.js\"", "fix": "prettier --write \"**/*.js\"", - "build:npm": "npm pack", + "build:tarball": "npm pack", "circularDepCheck": "madge --circular src/index.js" }, "volta": { diff --git a/packages/eslint-plugin-sdk/package.json b/packages/eslint-plugin-sdk/package.json index 9ce9602d56aa..1060fcd21bab 100644 --- a/packages/eslint-plugin-sdk/package.json +++ b/packages/eslint-plugin-sdk/package.json @@ -33,7 +33,7 @@ "lint:eslint": "eslint . --cache --cache-location '../../eslintcache/' --format stylish", "lint:prettier": "prettier --check \"{src,test}/**/*.js\"", "test": "mocha test --recursive", - "build:npm": "npm pack", + "build:tarball": "npm pack", "circularDepCheck": "madge --circular src/index.js" }, "volta": { diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json index 27c50112150e..ae6b74f8da09 100644 --- a/packages/gatsby/package.json +++ b/packages/gatsby/package.json @@ -35,17 +35,17 @@ "react": "^18.0.0" }, "scripts": { - "build": "run-p build:rollup build:types && yarn build:extras", + "build": "run-p build:transpile build:types", "build:dev": "run-s build", - "build:extras": "yarn build:plugin", "build:plugin": "tsc -p tsconfig.plugin.json", + "build:transpile": "run-p build:rollup build:plugin", "build:rollup": "rollup -c rollup.npm.config.js", "build:types": "tsc -p tsconfig.types.json", - "build:watch": "run-p build:rollup:watch build:types:watch", + "build:watch": "run-p build:transpile:watch build:types:watch", "build:dev:watch": "run-s build:watch", - "build:rollup:watch": "rollup -c rollup.npm.config.js --watch", + "build:transpile:watch": "rollup -c rollup.npm.config.js --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", - "build:npm": "ts-node ../../scripts/prepack.ts && npm pack ./build", + "build:tarball": "ts-node ../../scripts/prepack.ts && npm pack ./build", "circularDepCheck": "madge --circular src/index.ts", "clean": "rimraf build coverage *.d.ts sentry-gatsby-*.tgz", "fix": "run-s fix:eslint fix:prettier", diff --git a/packages/hub/package.json b/packages/hub/package.json index d28265f9131d..318a99482f1b 100644 --- a/packages/hub/package.json +++ b/packages/hub/package.json @@ -22,15 +22,15 @@ "tslib": "^1.9.3" }, "scripts": { - "build": "run-p build:rollup build:types", + "build": "run-p build:transpile build:types", "build:dev": "run-s build", - "build:rollup": "rollup -c rollup.npm.config.js", + "build:transpile": "rollup -c rollup.npm.config.js", "build:types": "tsc -p tsconfig.types.json", - "build:watch": "run-p build:rollup:watch build:types:watch", + "build:watch": "run-p build:transpile:watch build:types:watch", "build:dev:watch": "run-s build:watch", - "build:rollup:watch": "rollup -c rollup.npm.config.js --watch", + "build:transpile:watch": "rollup -c rollup.npm.config.js --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", - "build:npm": "ts-node ../../scripts/prepack.ts && npm pack ./build", + "build:tarball": "ts-node ../../scripts/prepack.ts && npm pack ./build", "circularDepCheck": "madge --circular src/index.ts", "clean": "rimraf build coverage sentry-hub-*.tgz", "fix": "run-s fix:eslint fix:prettier", diff --git a/packages/integrations/package.json b/packages/integrations/package.json index 2b6f9b1180ee..69d50463f337 100644 --- a/packages/integrations/package.json +++ b/packages/integrations/package.json @@ -25,16 +25,16 @@ "chai": "^4.1.2" }, "scripts": { - "build": "run-p build:rollup build:types build:bundle", - "build:bundle": "ts-node ../../scripts/ensure-bundle-deps.ts && ts-node scripts/buildBundles.ts --parallel", - "build:dev": "run-p build:rollup build:types", - "build:rollup": "rollup -c rollup.npm.config.js", + "build": "run-p build:transpile build:types build:bundle", + "build:bundle": "ts-node scripts/buildBundles.ts", + "build:dev": "run-p build:transpile build:types", + "build:transpile": "rollup -c rollup.npm.config.js", "build:types": "tsc -p tsconfig.types.json", - "build:watch": "run-p build:rollup:watch build:types:watch", + "build:watch": "run-p build:transpile:watch build:types:watch", "build:dev:watch": "run-s build:watch", - "build:rollup:watch": "rollup -c rollup.npm.config.js --watch", + "build:transpile:watch": "rollup -c rollup.npm.config.js --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", - "build:npm": "ts-node ../../scripts/prepack.ts --bundles && npm pack ./build/npm", + "build:tarball": "ts-node ../../scripts/prepack.ts --bundles && npm pack ./build/npm", "circularDepCheck": "madge --circular src/index.ts", "clean": "rimraf build coverage .rpt2_cache sentry-integrations-*.tgz", "fix": "run-s fix:eslint fix:prettier", diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index c1806fcf1ed0..b6faa2670c4e 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -46,15 +46,15 @@ } }, "scripts": { - "build": "run-p build:rollup build:types", + "build": "run-p build:transpile build:types", "build:dev": "run-s build", - "build:rollup": "ts-node scripts/buildRollup.ts", + "build:transpile": "ts-node scripts/buildRollup.ts", "build:types": "tsc -p tsconfig.types.json", - "build:watch": "run-p build:rollup:watch build:types:watch", + "build:watch": "run-p build:transpile:watch build:types:watch", "build:dev:watch": "run-s build:watch", - "build:rollup:watch": "nodemon --ext ts --watch src scripts/buildRollup.ts", + "build:transpile:watch": "nodemon --ext ts --watch src scripts/buildRollup.ts", "build:types:watch": "tsc -p tsconfig.types.json --watch", - "build:npm": "ts-node ../../scripts/prepack.ts && npm pack ./build", + "build:tarball": "ts-node ../../scripts/prepack.ts && npm pack ./build", "circularDepCheck": "madge --circular src/index.ts && madge --circular src/client/index.ts && madge --circular src/index.types.ts", "clean": "rimraf build coverage sentry-nextjs-*.tgz", "fix": "run-s fix:eslint fix:prettier", diff --git a/packages/node/package.json b/packages/node/package.json index ddb098e08f74..e41b6e83f22b 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -33,15 +33,15 @@ "nock": "^13.0.5" }, "scripts": { - "build": "run-p build:rollup build:types", + "build": "run-p build:transpile build:types", "build:dev": "run-s build", - "build:rollup": "rollup -c rollup.npm.config.js", + "build:transpile": "rollup -c rollup.npm.config.js", "build:types": "tsc -p tsconfig.types.json", - "build:watch": "run-p build:rollup:watch build:types:watch", + "build:watch": "run-p build:transpile:watch build:types:watch", "build:dev:watch": "run-s build:watch", - "build:rollup:watch": "rollup -c rollup.npm.config.js --watch", + "build:transpile:watch": "rollup -c rollup.npm.config.js --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", - "build:npm": "ts-node ../../scripts/prepack.ts && npm pack ./build", + "build:tarball": "ts-node ../../scripts/prepack.ts && npm pack ./build", "circularDepCheck": "madge --circular src/index.ts", "clean": "rimraf build coverage sentry-node-*.tgz", "fix": "run-s fix:eslint fix:prettier", diff --git a/packages/opentelemetry-node/package.json b/packages/opentelemetry-node/package.json index af2181f02eb1..35982c32fd78 100644 --- a/packages/opentelemetry-node/package.json +++ b/packages/opentelemetry-node/package.json @@ -36,15 +36,15 @@ "@sentry/node": "7.30.0" }, "scripts": { - "build": "run-p build:rollup build:types", + "build": "run-p build:transpile build:types", "build:dev": "run-s build", - "build:rollup": "rollup -c rollup.npm.config.js", + "build:transpile": "rollup -c rollup.npm.config.js", "build:types": "tsc -p tsconfig.types.json", - "build:watch": "run-p build:rollup:watch build:types:watch", + "build:watch": "run-p build:transpile:watch build:types:watch", "build:dev:watch": "run-s build:watch", - "build:rollup:watch": "rollup -c rollup.npm.config.js --watch", + "build:transpile:watch": "rollup -c rollup.npm.config.js --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", - "build:npm": "ts-node ../../scripts/prepack.ts && npm pack ./build", + "build:tarball": "ts-node ../../scripts/prepack.ts && npm pack ./build", "circularDepCheck": "madge --circular src/index.ts", "clean": "rimraf build coverage sentry-node-*.tgz", "fix": "run-s fix:eslint fix:prettier", diff --git a/packages/react/package.json b/packages/react/package.json index cf262e1d4aa0..c97a29aa314c 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -51,15 +51,15 @@ "redux": "^4.0.5" }, "scripts": { - "build": "run-p build:rollup build:types", + "build": "run-p build:transpile build:types", "build:dev": "run-s build", - "build:rollup": "rollup -c rollup.npm.config.js", + "build:transpile": "rollup -c rollup.npm.config.js", "build:types": "tsc -p tsconfig.types.json", - "build:watch": "run-p build:rollup:watch build:types:watch", + "build:watch": "run-p build:transpile:watch build:types:watch", "build:dev:watch": "run-s build:watch", - "build:rollup:watch": "rollup -c rollup.npm.config.js --watch", + "build:transpile:watch": "rollup -c rollup.npm.config.js --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", - "build:npm": "ts-node ../../scripts/prepack.ts && npm pack ./build", + "build:tarball": "ts-node ../../scripts/prepack.ts && npm pack ./build", "circularDepCheck": "madge --circular src/index.ts", "clean": "rimraf build coverage sentry-react-*.tgz", "fix": "run-s fix:eslint fix:prettier", diff --git a/packages/remix/package.json b/packages/remix/package.json index 4ef43aa34551..4086b5ddab4b 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -44,15 +44,15 @@ "react": "16.x || 17.x || 18.x" }, "scripts": { - "build": "run-p build:rollup build:types", + "build": "run-p build:transpile build:types", "build:dev": "run-s build", - "build:rollup": "rollup -c rollup.npm.config.js", + "build:transpile": "rollup -c rollup.npm.config.js", "build:types": "tsc -p tsconfig.types.json", - "build:watch": "run-p build:rollup:watch build:types:watch", + "build:watch": "run-p build:transpile:watch build:types:watch", "build:dev:watch": "run-s build:watch", - "build:rollup:watch": "rollup -c rollup.npm.config.js --watch", + "build:transpile:watch": "rollup -c rollup.npm.config.js --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", - "build:npm": "ts-node ../../scripts/prepack.ts && npm pack ./build", + "build:tarball": "ts-node ../../scripts/prepack.ts && npm pack ./build", "circularDepCheck": "madge --circular src/index.server.ts", "clean": "rimraf build coverage sentry-remix-*.tgz", "fix": "run-s fix:eslint fix:prettier", diff --git a/packages/replay/package.json b/packages/replay/package.json index 9f2a3beffde7..da2f247de08b 100644 --- a/packages/replay/package.json +++ b/packages/replay/package.json @@ -8,9 +8,9 @@ "sideEffects": false, "scripts": { "build": "run-s build:worker && run-p build:core build:types build:bundle", - "build:rollup": "run-s build:worker build:core", + "build:transpile": "run-s build:worker build:core", "build:bundle": "rollup -c rollup.bundle.config.js", - "build:dev": "run-p build:worker build:rollup build:types", + "build:dev": "run-p build:worker build:transpile build:types", "build:worker": "rollup -c rollup.config.worker.js", "build:core": "rollup -c rollup.npm.config.js", "build:types": "tsc -p tsconfig.types.json", @@ -20,7 +20,7 @@ "build:worker:watch": "yarn build:worker --watch", "build:bundle:watch": "yarn build:bundle --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", - "build:npm": "ts-node ../../scripts/prepack.ts --bundles && npm pack ./build/npm", + "build:tarball": "ts-node ../../scripts/prepack.ts --bundles && npm pack ./build/npm", "circularDepCheck": "madge --circular src/index.ts", "clean": "rimraf build sentry-replay-*.tgz", "fix": "run-s fix:eslint fix:prettier", diff --git a/packages/replay/workflows/build.yml b/packages/replay/workflows/build.yml index 3048a58f6f26..849baa8629ec 100644 --- a/packages/replay/workflows/build.yml +++ b/packages/replay/workflows/build.yml @@ -26,7 +26,7 @@ jobs: yarn build - run: | - yarn build:npm + yarn build:tarball - uses: actions/upload-artifact@v3.1.1 with: diff --git a/packages/serverless/package.json b/packages/serverless/package.json index 58ff37e7397a..75bab0bec495 100644 --- a/packages/serverless/package.json +++ b/packages/serverless/package.json @@ -38,18 +38,16 @@ "read-pkg": "^5.2.0" }, "scripts": { - "build": "run-p build:rollup build:types build:bundle && yarn build:extras", - "build:awslambda-layer": "echo 'WARNING: AWS lambda layer build emporarily moved to \\`build:bundle\\`.'", + "build": "run-p build:transpile build:types build:bundle", "build:bundle": "yarn ts-node scripts/buildLambdaLayer.ts", - "build:dev": "run-p build:rollup build:types", - "build:extras": "yarn build:awslambda-layer", - "build:rollup": "rollup -c rollup.npm.config.js", + "build:dev": "run-p build:transpile build:types", + "build:transpile": "rollup -c rollup.npm.config.js", "build:types": "tsc -p tsconfig.types.json", - "build:watch": "run-p build:rollup:watch build:types:watch", + "build:watch": "run-p build:transpile:watch build:types:watch", "build:dev:watch": "run-s build:watch", - "build:rollup:watch": "rollup -c rollup.npm.config.js --watch", + "build:transpile:watch": "rollup -c rollup.npm.config.js --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", - "build:npm": "ts-node ../../scripts/prepack.ts --bundles && npm pack ./build/npm", + "build:tarball": "ts-node ../../scripts/prepack.ts --bundles && npm pack ./build/npm", "circularDepCheck": "madge --circular src/index.ts", "clean": "rimraf build dist-awslambda-layer coverage sentry-serverless-*.tgz", "fix": "run-s fix:eslint fix:prettier", diff --git a/packages/serverless/scripts/buildLambdaLayer.ts b/packages/serverless/scripts/buildLambdaLayer.ts index 1a04aafde8aa..c7e2199aedbb 100644 --- a/packages/serverless/scripts/buildLambdaLayer.ts +++ b/packages/serverless/scripts/buildLambdaLayer.ts @@ -15,6 +15,7 @@ function run(cmd: string, options?: childProcess.ExecSyncOptions): string { async function buildLambdaLayer(): Promise { // Create the main SDK bundle + // TODO: Check if we can get rid of this, after the lerna 6/nx update?? await ensureBundleBuildPrereqs({ dependencies: ['@sentry/utils', '@sentry/hub', '@sentry/core', '@sentry/tracing', '@sentry/node'], }); diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 36e1be5eb435..566ece0f9d82 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -31,15 +31,15 @@ "svelte-jester": "^2.3.2" }, "scripts": { - "build": "run-p build:rollup build:types", + "build": "run-p build:transpile build:types", "build:dev": "run-s build", - "build:rollup": "rollup -c rollup.npm.config.js", + "build:transpile": "rollup -c rollup.npm.config.js", "build:types": "tsc -p tsconfig.types.json", - "build:watch": "run-p build:rollup:watch build:types:watch", + "build:watch": "run-p build:transpile:watch build:types:watch", "build:dev:watch": "run-s build:watch", - "build:rollup:watch": "rollup -c rollup.npm.config.js --watch", + "build:transpile:watch": "rollup -c rollup.npm.config.js --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", - "build:npm": "ts-node ../../scripts/prepack.ts && npm pack ./build", + "build:tarball": "ts-node ../../scripts/prepack.ts && npm pack ./build", "circularDepCheck": "madge --circular src/index.ts", "clean": "rimraf build coverage sentry-svelte-*.tgz", "fix": "run-s fix:eslint fix:prettier", diff --git a/packages/tracing/package.json b/packages/tracing/package.json index d05ae83a50cb..7ab1417f30c0 100644 --- a/packages/tracing/package.json +++ b/packages/tracing/package.json @@ -26,19 +26,17 @@ "@types/express": "^4.17.14" }, "scripts": { - "build": "run-p build:rollup build:types build:bundle && yarn build:extras #necessary for integration tests", - "build:bundle": "yarn ts-node ../../scripts/ensure-bundle-deps.ts && yarn rollup --config rollup.bundle.config.js", - "build:dev": "run-p build:rollup build:types", - "build:extras": "yarn build:prepack", - "build:prepack": "ts-node ../../scripts/prepack.ts --bundles", - "build:rollup": "rollup -c rollup.npm.config.js", + "build": "run-p build:transpile build:types build:bundle", + "build:bundle": "yarn rollup --config rollup.bundle.config.js", + "build:dev": "run-p build:transpile build:types", + "build:transpile": "rollup -c rollup.npm.config.js", "build:types": "tsc -p tsconfig.types.json", - "build:watch": "run-p build:rollup:watch build:bundle:watch build:types:watch", + "build:watch": "run-p build:transpile:watch build:bundle:watch build:types:watch", "build:bundle:watch": "rollup --config rollup.bundle.config.js --watch", - "build:dev:watch": "run-p build:rollup:watch build:types:watch", - "build:rollup:watch": "rollup -c rollup.npm.config.js --watch", + "build:dev:watch": "run-p build:transpile:watch build:types:watch", + "build:transpile:watch": "rollup -c rollup.npm.config.js --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", - "build:npm": "ts-node ../../scripts/prepack.ts --bundles && npm pack ./build/npm", + "build:tarball": "ts-node ../../scripts/prepack.ts --bundles && npm pack ./build/npm", "clean": "rimraf build coverage sentry-tracing-*.tgz", "circularDepCheck": "madge --circular src/index.ts", "fix": "run-s fix:eslint fix:prettier", diff --git a/packages/types/package.json b/packages/types/package.json index 4187d401db82..6349b90812c6 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -16,15 +16,15 @@ "access": "public" }, "scripts": { - "build": "run-p build:rollup build:types", + "build": "run-p build:transpile build:types", "build:dev": "run-s build", - "build:rollup": "rollup -c rollup.npm.config.js", + "build:transpile": "rollup -c rollup.npm.config.js", "build:types": "tsc -p tsconfig.types.json", - "build:watch": "run-p build:rollup:watch build:types:watch", + "build:watch": "run-p build:transpile:watch build:types:watch", "build:dev:watch": "run-s build:watch", - "build:rollup:watch": "rollup -c rollup.npm.config.js --watch", + "build:transpile:watch": "rollup -c rollup.npm.config.js --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", - "build:npm": "ts-node ../../scripts/prepack.ts && npm pack ./build", + "build:tarball": "ts-node ../../scripts/prepack.ts && npm pack ./build", "clean": "rimraf build sentry-types-*.tgz", "lint": "run-s lint:prettier lint:eslint", "lint:eslint": "eslint . --cache --cache-location '../../eslintcache/' --format stylish", diff --git a/packages/typescript/package.json b/packages/typescript/package.json index ac39dca30034..b4e8dc32c416 100644 --- a/packages/typescript/package.json +++ b/packages/typescript/package.json @@ -15,7 +15,7 @@ }, "scripts": { "clean": "yarn rimraf sentry-internal-typescript-*.tgz", - "build:npm": "npm pack" + "build:tarball": "npm pack" }, "volta": { "extends": "../../package.json" diff --git a/packages/utils/package.json b/packages/utils/package.json index 9b02fdc55023..5576658642e7 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -25,15 +25,16 @@ "chai": "^4.1.2" }, "scripts": { - "build": "run-p build:rollup build:types", + "build": "run-p build:transpile build:types", "build:dev": "run-s build", - "build:rollup": "yarn ts-node scripts/buildRollup.ts", + "build:transpile": "yarn ts-node scripts/buildRollup.ts", + "build:transpile:uncached": "yarn ts-node scripts/buildRollup.ts", "build:types": "tsc -p tsconfig.types.json", - "build:watch": "run-p build:rollup:watch build:types:watch", + "build:watch": "run-p build:transpile:watch build:types:watch", "build:dev:watch": "run-s build:watch", - "build:rollup:watch": "rollup -c rollup.npm.config.js --watch", + "build:transpile:watch": "rollup -c rollup.npm.config.js --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", - "build:npm": "ts-node ../../scripts/prepack.ts && npm pack ./build", + "build:tarball": "ts-node ../../scripts/prepack.ts && npm pack ./build", "circularDepCheck": "madge --circular src/index.ts", "clean": "rimraf build coverage cjs esm sentry-utils-*.tgz", "fix": "run-s fix:eslint fix:prettier", diff --git a/packages/vue/package.json b/packages/vue/package.json index 8f639697486e..7433e18e6822 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -29,17 +29,17 @@ "vue": "~3.2.41" }, "scripts": { - "build": "run-p build:rollup build:types", - "build:bundle": "yarn ts-node ../../scripts/ensure-bundle-deps.ts && yarn rollup --config rollup.bundle.config.js", - "build:dev": "run-p build:rollup build:types", - "build:rollup": "rollup -c rollup.npm.config.js", + "build": "run-p build:transpile build:types", + "build:bundle": "rollup --config rollup.bundle.config.js", + "build:dev": "run-p build:transpile build:types", + "build:transpile": "rollup -c rollup.npm.config.js", "build:types": "tsc -p tsconfig.types.json", - "build:watch": "run-p build:rollup:watch build:types:watch", + "build:watch": "run-p build:transpile:watch build:types:watch", "build:bundle:watch": "rollup --config --watch", - "build:dev:watch": "run-p build:rollup:watch build:types:watch", - "build:rollup:watch": "rollup -c rollup.npm.config.js --watch", + "build:dev:watch": "run-p build:transpile:watch build:types:watch", + "build:transpile:watch": "rollup -c rollup.npm.config.js --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", - "build:npm": "ts-node ../../scripts/prepack.ts && npm pack ./build", + "build:tarball": "ts-node ../../scripts/prepack.ts && npm pack ./build", "circularDepCheck": "madge --circular src/index.ts", "clean": "rimraf build coverage sentry-vue-*.tgz", "fix": "run-s fix:eslint fix:prettier", diff --git a/packages/wasm/package.json b/packages/wasm/package.json index 7f8cd3abdfb8..07ab31bee05e 100644 --- a/packages/wasm/package.json +++ b/packages/wasm/package.json @@ -30,17 +30,17 @@ "puppeteer": "^5.5.0" }, "scripts": { - "build": "run-p build:rollup build:bundle build:types", - "build:bundle": "yarn ts-node ../../scripts/ensure-bundle-deps.ts && yarn rollup --config rollup.bundle.config.js", - "build:dev": "run-p build:rollup build:types", - "build:rollup": "rollup -c rollup.npm.config.js", + "build": "run-p build:transpile build:bundle build:types", + "build:bundle": "rollup --config rollup.bundle.config.js", + "build:dev": "run-p build:transpile build:types", + "build:transpile": "rollup -c rollup.npm.config.js", "build:types": "tsc -p tsconfig.types.json", - "build:watch": "run-p build:rollup:watch build:bundle:watch build:types:watch", + "build:watch": "run-p build:transpile:watch build:bundle:watch build:types:watch", "build:bundle:watch": "rollup --config rollup.bundle.config.js --watch", - "build:dev:watch": "run-p build:rollup:watch build:types:watch", - "build:rollup:watch": "rollup -c rollup.npm.config.js --watch", + "build:dev:watch": "run-p build:transpile:watch build:types:watch", + "build:transpile:watch": "rollup -c rollup.npm.config.js --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", - "build:npm": "ts-node ../../scripts/prepack.ts --bundles && npm pack ./build/npm", + "build:tarball": "ts-node ../../scripts/prepack.ts --bundles && npm pack ./build/npm", "circularDepCheck": "madge --circular src/index.ts", "clean": "rimraf build coverage sentry-wasm-*.tgz", "fix": "run-s fix:eslint fix:prettier", diff --git a/scripts/ensure-bundle-deps.ts b/scripts/ensure-bundle-deps.ts index cf14c819f509..8be6377ee4e1 100644 --- a/scripts/ensure-bundle-deps.ts +++ b/scripts/ensure-bundle-deps.ts @@ -64,7 +64,7 @@ export async function ensureBundleBuildPrereqs(options: { // dependencies. Either way, it's on us to fix the problem. // // TODO: This actually *doesn't* work for package-level `build`, not because of a flaw in this logic, but because - // `build:rollup` has similar dependency needs (it needs types rather than npm builds). We should do something like + // `build:transpile` has similar dependency needs (it needs types rather than npm builds). We should do something like // this for that at some point. if (isTopLevelBuild && yarnScript === 'build') { @@ -95,7 +95,7 @@ export async function ensureBundleBuildPrereqs(options: { for (const dependencyDir of dependencyDirs) { console.log(`\nBuilding \`${dependencyDir}\` package...`); - run('yarn build:rollup', { cwd: `${packagesDir}/${dependencyDir}` }); + run('yarn build:transpile', { cwd: `${packagesDir}/${dependencyDir}` }); } console.log('\nAll dependencies built successfully. Beginning bundle build...'); diff --git a/scripts/test.ts b/scripts/node-unit-tests.ts similarity index 75% rename from scripts/test.ts rename to scripts/node-unit-tests.ts index b1af2f918ee8..4e6efaec9d0e 100644 --- a/scripts/test.ts +++ b/scripts/node-unit-tests.ts @@ -3,25 +3,9 @@ import * as fs from 'fs'; const CURRENT_NODE_VERSION = process.version.replace('v', '').split('.')[0]; -// We run ember tests in their own job. -const DEFAULT_SKIP_TESTS_PACKAGES = ['@sentry/ember']; - -// These packages don't support Node 8 for syntax or dependency reasons. -const NODE_8_SKIP_TESTS_PACKAGES = [ +const DEFAULT_SKIP_TESTS_PACKAGES = [ '@sentry-internal/eslint-plugin-sdk', - '@sentry/react', - '@sentry/wasm', - '@sentry/gatsby', - '@sentry/serverless', - '@sentry/nextjs', - '@sentry/angular', - '@sentry/remix', - '@sentry/svelte', // svelte testing library requires Node >= 10 - '@sentry/replay', -]; - -// These can be skipped when running tests in different Node environments. -const SKIP_BROWSER_TESTS_PACKAGES = [ + '@sentry/ember', '@sentry/browser', '@sentry/vue', '@sentry/react', @@ -31,15 +15,8 @@ const SKIP_BROWSER_TESTS_PACKAGES = [ '@sentry/wasm', ]; -// These can be skipped when running tests independently of the Node version. -const SKIP_NODE_TESTS_PACKAGES = [ - '@sentry/node', - '@sentry/opentelemetry-node', - '@sentry/serverless', - '@sentry/nextjs', - '@sentry/remix', - '@sentry/gatsby', -]; +// 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']; // We have to downgrade some of our dependencies in order to run tests in Node 8 and 10. const NODE_8_LEGACY_DEPENDENCIES = [ @@ -48,12 +25,14 @@ const NODE_8_LEGACY_DEPENDENCIES = [ '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/replay']; -const NODE_10_LEGACY_DEPENDENCIES = ['jsdom@16.x']; +const NODE_10_SKIP_TESTS_PACKAGES = ['@sentry/remix']; +const NODE_10_LEGACY_DEPENDENCIES = ['jsdom@16.x', 'lerna@3.13.4']; const NODE_12_SKIP_TESTS_PACKAGES = ['@sentry/remix']; +const NODE_12_LEGACY_DEPENDENCIES = ['lerna@3.13.4']; type JSONValue = string | number | boolean | null | JSONArray | JSONObject; @@ -70,8 +49,8 @@ interface TSConfigJSON extends JSONObject { * 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) { - return childProcess.execSync(cmd, { stdio: 'inherit', ...options }); +function run(cmd: string, options?: childProcess.ExecSyncOptions): void { + childProcess.execSync(cmd, { stdio: 'inherit', ...options }); } /** @@ -135,14 +114,6 @@ function runTests(): void { DEFAULT_SKIP_TESTS_PACKAGES.forEach(dep => ignores.add(dep)); - if (process.env.TESTS_SKIP === 'browser') { - SKIP_BROWSER_TESTS_PACKAGES.forEach(dep => ignores.add(dep)); - } - - if (process.env.TESTS_SKIP === 'node') { - SKIP_NODE_TESTS_PACKAGES.forEach(dep => ignores.add(dep)); - } - switch (CURRENT_NODE_VERSION) { case '8': NODE_8_SKIP_TESTS_PACKAGES.forEach(dep => ignores.add(dep)); @@ -157,6 +128,7 @@ function runTests(): void { break; case '12': NODE_12_SKIP_TESTS_PACKAGES.forEach(dep => ignores.add(dep)); + installLegacyDeps(NODE_12_LEGACY_DEPENDENCIES); es6ifyTestTSConfig('utils'); break; } diff --git a/yarn.lock b/yarn.lock index 1e17e827e2cd..50a3eb10c310 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1589,7 +1589,7 @@ minimatch "^3.0.4" strip-json-comments "^3.1.1" -"@gar/promisify@^1.0.1": +"@gar/promisify@^1.0.1", "@gar/promisify@^1.1.3": version "1.1.3" resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw== @@ -1935,6 +1935,16 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== +"@hutson/parse-repository-url@^3.0.0": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@hutson/parse-repository-url/-/parse-repository-url-3.0.2.tgz#98c23c950a3d9b6c8f0daed06da6c3af06981340" + integrity sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q== + +"@isaacs/string-locale-compare@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@isaacs/string-locale-compare/-/string-locale-compare-1.1.0.tgz#291c227e93fd407a96ecd59879a35809120e432b" + integrity sha512-SQ7Kzhh9+D+ZW9MA0zkYv3VXhIDNx+LzM6EJ+/65I3QY+enU6Itte7E5XX7EWrqLW2FN4n06GWzBnPoC3th2aQ== + "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" @@ -2264,626 +2274,689 @@ merge-source-map "^1.1.0" schema-utils "^2.7.0" -"@lerna/add@3.13.3": - version "3.13.3" - resolved "https://registry.yarnpkg.com/@lerna/add/-/add-3.13.3.tgz#f4c1674839780e458f0426d4f7b6d0a77b9a2ae9" - integrity sha512-T3/Lsbo9ZFq+vL3ssaHxA8oKikZAPTJTGFe4CRuQgWCDd/M61+51jeWsngdaHpwzSSRDRjxg8fJTG10y10pnfA== - dependencies: - "@lerna/bootstrap" "3.13.3" - "@lerna/command" "3.13.3" - "@lerna/filter-options" "3.13.3" - "@lerna/npm-conf" "3.13.0" - "@lerna/validation-error" "3.13.0" +"@lerna/add@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/add/-/add-6.1.0.tgz#0f09495c5e1af4c4f316344af34b6d1a91b15b19" + integrity sha512-f2cAeS1mE/p7QvSRn5TCgdUXw6QVbu8PeRxaTOxTThhTdJIWdXZfY00QjAsU6jw1PdYXK1qGUSwWOPkdR16mBg== + dependencies: + "@lerna/bootstrap" "6.1.0" + "@lerna/command" "6.1.0" + "@lerna/filter-options" "6.1.0" + "@lerna/npm-conf" "6.1.0" + "@lerna/validation-error" "6.1.0" dedent "^0.7.0" - npm-package-arg "^6.1.0" - p-map "^1.2.0" - pacote "^9.5.0" - semver "^5.5.0" - -"@lerna/batch-packages@3.13.0": - version "3.13.0" - resolved "https://registry.yarnpkg.com/@lerna/batch-packages/-/batch-packages-3.13.0.tgz#697fde5be28822af9d9dca2f750250b90a89a000" - integrity sha512-TgLBTZ7ZlqilGnzJ3xh1KdAHcySfHytgNRTdG9YomfriTU6kVfp1HrXxKJYVGs7ClPUNt2CTFEOkw0tMBronjw== - dependencies: - "@lerna/package-graph" "3.13.0" - "@lerna/validation-error" "3.13.0" - npmlog "^4.1.2" + npm-package-arg "8.1.1" + p-map "^4.0.0" + pacote "^13.6.1" + semver "^7.3.4" -"@lerna/bootstrap@3.13.3": - version "3.13.3" - resolved "https://registry.yarnpkg.com/@lerna/bootstrap/-/bootstrap-3.13.3.tgz#a0e5e466de5c100b49d558d39139204fc4db5c95" - integrity sha512-2XzijnLHRZOVQh8pwS7+5GR3cG4uh+EiLrWOishCq2TVzkqgjaS3GGBoef7KMCXfWHoLqAZRr/jEdLqfETLVqg== - dependencies: - "@lerna/batch-packages" "3.13.0" - "@lerna/command" "3.13.3" - "@lerna/filter-options" "3.13.3" - "@lerna/has-npm-version" "3.13.3" - "@lerna/npm-install" "3.13.3" - "@lerna/package-graph" "3.13.0" - "@lerna/pulse-till-done" "3.13.0" - "@lerna/rimraf-dir" "3.13.3" - "@lerna/run-lifecycle" "3.13.0" - "@lerna/run-parallel-batches" "3.13.0" - "@lerna/symlink-binary" "3.13.0" - "@lerna/symlink-dependencies" "3.13.0" - "@lerna/validation-error" "3.13.0" +"@lerna/bootstrap@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/bootstrap/-/bootstrap-6.1.0.tgz#81738f32cd431814c9943dfffe28752587d90830" + integrity sha512-aDxKqgxexVj/Z0B1aPu7P1iPbPqhk1FPkl/iayCmPlkAh90pYEH0uVytGzi1hFB5iXEfG7Pa6azGQywUodx/1g== + dependencies: + "@lerna/command" "6.1.0" + "@lerna/filter-options" "6.1.0" + "@lerna/has-npm-version" "6.1.0" + "@lerna/npm-install" "6.1.0" + "@lerna/package-graph" "6.1.0" + "@lerna/pulse-till-done" "6.1.0" + "@lerna/rimraf-dir" "6.1.0" + "@lerna/run-lifecycle" "6.1.0" + "@lerna/run-topologically" "6.1.0" + "@lerna/symlink-binary" "6.1.0" + "@lerna/symlink-dependencies" "6.1.0" + "@lerna/validation-error" "6.1.0" + "@npmcli/arborist" "5.3.0" dedent "^0.7.0" - get-port "^3.2.0" - multimatch "^2.1.0" - npm-package-arg "^6.1.0" - npmlog "^4.1.2" - p-finally "^1.0.0" - p-map "^1.2.0" - p-map-series "^1.0.0" - p-waterfall "^1.0.0" - read-package-tree "^5.1.6" - semver "^5.5.0" + get-port "^5.1.1" + multimatch "^5.0.0" + npm-package-arg "8.1.1" + npmlog "^6.0.2" + p-map "^4.0.0" + p-map-series "^2.1.0" + p-waterfall "^2.1.1" + semver "^7.3.4" -"@lerna/changed@3.13.4": - version "3.13.4" - resolved "https://registry.yarnpkg.com/@lerna/changed/-/changed-3.13.4.tgz#c69d8a079999e49611dd58987f08437baee81ad4" - integrity sha512-9lfOyRVObasw6L/z7yCSfsEl1QKy0Eamb8t2Krg1deIoAt+cE3JXOdGGC1MhOSli+7f/U9LyLXjJzIOs/pc9fw== +"@lerna/changed@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/changed/-/changed-6.1.0.tgz#4fa480cbb0e7106ea9dad30d315e953975118d06" + integrity sha512-p7C2tf1scmvoUC1Osck/XIKVKXAQ8m8neL8/rfgKSYsvUVjsOB1LbF5HH1VUZntE6S4OxkRxUQGkAHVf5xrGqw== dependencies: - "@lerna/collect-updates" "3.13.3" - "@lerna/command" "3.13.3" - "@lerna/listable" "3.13.0" - "@lerna/output" "3.13.0" - "@lerna/version" "3.13.4" + "@lerna/collect-updates" "6.1.0" + "@lerna/command" "6.1.0" + "@lerna/listable" "6.1.0" + "@lerna/output" "6.1.0" -"@lerna/check-working-tree@3.13.3": - version "3.13.3" - resolved "https://registry.yarnpkg.com/@lerna/check-working-tree/-/check-working-tree-3.13.3.tgz#836a3ffd4413a29aca92ccca4a115e4f97109992" - integrity sha512-LoGZvTkne+V1WpVdCTU0XNzFKsQa2AiAFKksGRT0v8NQj6VAPp0jfVYDayTqwaWt2Ne0OGKOFE79Y5LStOuhaQ== +"@lerna/check-working-tree@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/check-working-tree/-/check-working-tree-6.1.0.tgz#b8970fd27a26449b12456d5d0ece60477aa54e15" + integrity sha512-hSciDmRqsNPevMhAD+SYbnhjatdb7UUu9W8vTyGtUXkrq2xtRZU0vAOgqovV8meirRkbC41pZePYKqyQtF0y3w== dependencies: - "@lerna/describe-ref" "3.13.3" - "@lerna/validation-error" "3.13.0" + "@lerna/collect-uncommitted" "6.1.0" + "@lerna/describe-ref" "6.1.0" + "@lerna/validation-error" "6.1.0" -"@lerna/child-process@3.13.3": - version "3.13.3" - resolved "https://registry.yarnpkg.com/@lerna/child-process/-/child-process-3.13.3.tgz#6c084ee5cca9fc9e04d6bf4fc3f743ed26ff190c" - integrity sha512-3/e2uCLnbU+bydDnDwyadpOmuzazS01EcnOleAnuj9235CU2U97DH6OyoG1EW/fU59x11J+HjIqovh5vBaMQjQ== +"@lerna/child-process@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/child-process/-/child-process-6.1.0.tgz#6361f7945cd5b36e983f819de3cd91c315707302" + integrity sha512-jhr3sCFeps6Y15SCrWEPvqE64i+QLOTSh+OzxlziCBf7ZEUu7sF0yA4n5bAqw8j43yCKhhjkf/ZLYxZe+pnl3Q== dependencies: - chalk "^2.3.1" - execa "^1.0.0" - strong-log-transformer "^2.0.0" + chalk "^4.1.0" + execa "^5.0.0" + strong-log-transformer "^2.1.0" -"@lerna/clean@3.13.3": - version "3.13.3" - resolved "https://registry.yarnpkg.com/@lerna/clean/-/clean-3.13.3.tgz#5673a1238e0712d31711e7e4e8cb9641891daaea" - integrity sha512-xmNauF1PpmDaKdtA2yuRc23Tru4q7UMO6yB1a/TTwxYPYYsAWG/CBK65bV26J7x4RlZtEv06ztYGMa9zh34UXA== - dependencies: - "@lerna/command" "3.13.3" - "@lerna/filter-options" "3.13.3" - "@lerna/prompt" "3.13.0" - "@lerna/pulse-till-done" "3.13.0" - "@lerna/rimraf-dir" "3.13.3" - p-map "^1.2.0" - p-map-series "^1.0.0" - p-waterfall "^1.0.0" - -"@lerna/cli@3.13.0": - version "3.13.0" - resolved "https://registry.yarnpkg.com/@lerna/cli/-/cli-3.13.0.tgz#3d7b357fdd7818423e9681a7b7f2abd106c8a266" - integrity sha512-HgFGlyCZbYaYrjOr3w/EsY18PdvtsTmDfpUQe8HwDjXlPeCCUgliZjXLOVBxSjiOvPeOSwvopwIHKWQmYbwywg== - dependencies: - "@lerna/global-options" "3.13.0" +"@lerna/clean@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/clean/-/clean-6.1.0.tgz#1114fd90ad82438123726e2493d3550e73abebbc" + integrity sha512-LRK2hiNUiBhPe5tmJiefOVpkaX2Yob0rp15IFNIbuteRWUJg0oERFQo62WvnxwElfzKSOhr8OGuEq/vN4bMrRA== + dependencies: + "@lerna/command" "6.1.0" + "@lerna/filter-options" "6.1.0" + "@lerna/prompt" "6.1.0" + "@lerna/pulse-till-done" "6.1.0" + "@lerna/rimraf-dir" "6.1.0" + p-map "^4.0.0" + p-map-series "^2.1.0" + p-waterfall "^2.1.1" + +"@lerna/cli@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/cli/-/cli-6.1.0.tgz#41214331fa4c1ea5f41125befdd81b009fe12640" + integrity sha512-p4G/OSPIrHiNkEl8bXrQdFOh4ORAZp2+ljvbXmAxpdf2qmopaUdr+bZYtIAxd+Z42SxRnDNz9IEyR0kOsARRQQ== + dependencies: + "@lerna/global-options" "6.1.0" dedent "^0.7.0" - npmlog "^4.1.2" - yargs "^12.0.1" + npmlog "^6.0.2" + yargs "^16.2.0" -"@lerna/collect-updates@3.13.3": - version "3.13.3" - resolved "https://registry.yarnpkg.com/@lerna/collect-updates/-/collect-updates-3.13.3.tgz#616648da59f0aff4a8e60257795cc46ca6921edd" - integrity sha512-sTpALOAxli/ZS+Mjq6fbmjU9YXqFJ2E4FrE1Ijl4wPC5stXEosg2u0Z1uPY+zVKdM+mOIhLxPVdx83rUgRS+Cg== +"@lerna/collect-uncommitted@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/collect-uncommitted/-/collect-uncommitted-6.1.0.tgz#b6ffd7adda24d73b70304210967d3518caa3529d" + integrity sha512-VvWvqDZG+OiF4PwV4Ro695r3+8ty4w+11Bnq8tbsbu5gq8qZiam8Fkc/TQLuNNqP0SPi4qmMPaIzWvSze3SmDg== + dependencies: + "@lerna/child-process" "6.1.0" + chalk "^4.1.0" + npmlog "^6.0.2" + +"@lerna/collect-updates@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/collect-updates/-/collect-updates-6.1.0.tgz#75fcc0733b5a9ac318a6484b890aa4061b7859c2" + integrity sha512-dgH7kgstwCXFctylQ4cxuCmhwSIE6VJZfHdh2bOaLuncs6ATMErKWN/mVuFHuUWEqPDRyy5Ky40Cu9S40nUq5w== dependencies: - "@lerna/child-process" "3.13.3" - "@lerna/describe-ref" "3.13.3" + "@lerna/child-process" "6.1.0" + "@lerna/describe-ref" "6.1.0" minimatch "^3.0.4" - npmlog "^4.1.2" - slash "^1.0.0" + npmlog "^6.0.2" + slash "^3.0.0" -"@lerna/command@3.13.3": - version "3.13.3" - resolved "https://registry.yarnpkg.com/@lerna/command/-/command-3.13.3.tgz#5b20b3f507224573551039e0460bc36c39f7e9d1" - integrity sha512-WHFIQCubJV0T8gSLRNr6exZUxTswrh+iAtJCb86SE0Sa+auMPklE8af7w2Yck5GJfewmxSjke3yrjNxQrstx7w== - dependencies: - "@lerna/child-process" "3.13.3" - "@lerna/package-graph" "3.13.0" - "@lerna/project" "3.13.1" - "@lerna/validation-error" "3.13.0" - "@lerna/write-log-file" "3.13.0" +"@lerna/command@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/command/-/command-6.1.0.tgz#bcb12516f2c181822b3b5be46c18eadc9b61e885" + integrity sha512-OnMqBDaEBY0C8v9CXIWFbGGKgsiUtZrnKVvQRbupMSZDKMpVGWIUd3X98Is9j9MAmk1ynhBMWE9Fwai5ML/mcA== + dependencies: + "@lerna/child-process" "6.1.0" + "@lerna/package-graph" "6.1.0" + "@lerna/project" "6.1.0" + "@lerna/validation-error" "6.1.0" + "@lerna/write-log-file" "6.1.0" + clone-deep "^4.0.1" dedent "^0.7.0" - execa "^1.0.0" - is-ci "^1.0.10" - lodash "^4.17.5" - npmlog "^4.1.2" + execa "^5.0.0" + is-ci "^2.0.0" + npmlog "^6.0.2" -"@lerna/conventional-commits@3.13.0": - version "3.13.0" - resolved "https://registry.yarnpkg.com/@lerna/conventional-commits/-/conventional-commits-3.13.0.tgz#877aa225ca34cca61c31ea02a5a6296af74e1144" - integrity sha512-BeAgcNXuocmLhPxnmKU2Vy8YkPd/Uo+vu2i/p3JGsUldzrPC8iF3IDxH7fuXpEFN2Nfogu7KHachd4tchtOppA== +"@lerna/conventional-commits@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/conventional-commits/-/conventional-commits-6.1.0.tgz#1157bb66d84d48880dc5c5026d743cedf0f47094" + integrity sha512-Tipo3cVr8mNVca4btzrCIzct59ZJWERT8/ZCZ/TQWuI4huUJZs6LRofLtB0xsGJAVZ7Vz2WRXAeH4XYgeUxutQ== dependencies: - "@lerna/validation-error" "3.13.0" - conventional-changelog-angular "^5.0.3" - conventional-changelog-core "^3.1.6" - conventional-recommended-bump "^4.0.4" - fs-extra "^7.0.0" - get-stream "^4.0.0" - npm-package-arg "^6.1.0" - npmlog "^4.1.2" - pify "^3.0.0" - semver "^5.5.0" + "@lerna/validation-error" "6.1.0" + conventional-changelog-angular "^5.0.12" + conventional-changelog-core "^4.2.4" + conventional-recommended-bump "^6.1.0" + fs-extra "^9.1.0" + get-stream "^6.0.0" + npm-package-arg "8.1.1" + npmlog "^6.0.2" + pify "^5.0.0" + semver "^7.3.4" -"@lerna/create-symlink@3.13.0": - version "3.13.0" - resolved "https://registry.yarnpkg.com/@lerna/create-symlink/-/create-symlink-3.13.0.tgz#e01133082fe040779712c960683cb3a272b67809" - integrity sha512-PTvg3jAAJSAtLFoZDsuTMv1wTOC3XYIdtg54k7uxIHsP8Ztpt+vlilY/Cni0THAqEMHvfiToel76Xdta4TU21Q== +"@lerna/create-symlink@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/create-symlink/-/create-symlink-6.1.0.tgz#d4260831f5d10abc0c70f0a8f39bea91db87e640" + integrity sha512-ulMa5OUJEwEWBHSgCUNGxrcsJllq1YMYWqhufvIigmMPJ0Zv3TV1Hha5i2MsqLJAakxtW0pNuwdutkUTtUdgxQ== dependencies: - cmd-shim "^2.0.2" - fs-extra "^7.0.0" - npmlog "^4.1.2" + cmd-shim "^5.0.0" + fs-extra "^9.1.0" + npmlog "^6.0.2" -"@lerna/create@3.13.3": - version "3.13.3" - resolved "https://registry.yarnpkg.com/@lerna/create/-/create-3.13.3.tgz#6ded142c54b7f3cea86413c3637b067027b7f55d" - integrity sha512-4M5xT1AyUMwt1gCDph4BfW3e6fZmt0KjTa3FoXkUotf/w/eqTsc2IQ+ULz2+gOFQmtuNbqIZEOK3J4P9ArJJ/A== +"@lerna/create@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/create/-/create-6.1.0.tgz#cde219da46a7c5062c558366b4ffce2134f13845" + integrity sha512-ZqlknXu0L29cV5mcfNgBLl+1RbKTWmNk8mj545zgXc7qQDgmrY+EVvrs8Cirey8C7bBpVkzP7Brzze0MSoB4rQ== dependencies: - "@lerna/child-process" "3.13.3" - "@lerna/command" "3.13.3" - "@lerna/npm-conf" "3.13.0" - "@lerna/validation-error" "3.13.0" - camelcase "^5.0.0" + "@lerna/child-process" "6.1.0" + "@lerna/command" "6.1.0" + "@lerna/npm-conf" "6.1.0" + "@lerna/validation-error" "6.1.0" dedent "^0.7.0" - fs-extra "^7.0.0" - globby "^8.0.1" - init-package-json "^1.10.3" - npm-package-arg "^6.1.0" - p-reduce "^1.0.0" - pacote "^9.5.0" - pify "^3.0.0" - semver "^5.5.0" - slash "^1.0.0" - validate-npm-package-license "^3.0.3" - validate-npm-package-name "^3.0.0" - whatwg-url "^7.0.0" + fs-extra "^9.1.0" + init-package-json "^3.0.2" + npm-package-arg "8.1.1" + p-reduce "^2.1.0" + pacote "^13.6.1" + pify "^5.0.0" + semver "^7.3.4" + slash "^3.0.0" + validate-npm-package-license "^3.0.4" + validate-npm-package-name "^4.0.0" + yargs-parser "20.2.4" -"@lerna/describe-ref@3.13.3": - version "3.13.3" - resolved "https://registry.yarnpkg.com/@lerna/describe-ref/-/describe-ref-3.13.3.tgz#13318513613f6a407d37fc5dc025ec2cfb705606" - integrity sha512-5KcLTvjdS4gU5evW8ESbZ0BF44NM5HrP3dQNtWnOUSKJRgsES8Gj0lq9AlB2+YglZfjEftFT03uOYOxnKto4Uw== +"@lerna/describe-ref@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/describe-ref/-/describe-ref-6.1.0.tgz#60f0b8297b912aa5fe5e6ab8ef6c4127813681a7" + integrity sha512-0RQAYnxBaMz1SrEb/rhfR+8VeZx5tvCNYKRee5oXIDZdQ2c6/EPyrKCp3WcqiuOWY50SfGOVfxJEcxpK8Y3FNA== dependencies: - "@lerna/child-process" "3.13.3" - npmlog "^4.1.2" + "@lerna/child-process" "6.1.0" + npmlog "^6.0.2" -"@lerna/diff@3.13.3": - version "3.13.3" - resolved "https://registry.yarnpkg.com/@lerna/diff/-/diff-3.13.3.tgz#883cb3a83a956dbfc2c17bc9a156468a5d3fae17" - integrity sha512-/DRS2keYbnKaAC+5AkDyZRGkP/kT7v1GlUS0JGZeiRDPQ1H6PzhX09EgE5X6nj0Ytrm0sUasDeN++CDVvgaI+A== +"@lerna/diff@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/diff/-/diff-6.1.0.tgz#bfa9bc35894d88a33fa0a3a5787082dea45d8cb2" + integrity sha512-GhP+jPDbcp9QcAMSAjFn4lzM8MKpLR1yt5jll+zUD831U1sL0I5t8HUosFroe5MoRNffEL/jHuI3SbC3jjqWjQ== dependencies: - "@lerna/child-process" "3.13.3" - "@lerna/command" "3.13.3" - "@lerna/validation-error" "3.13.0" - npmlog "^4.1.2" + "@lerna/child-process" "6.1.0" + "@lerna/command" "6.1.0" + "@lerna/validation-error" "6.1.0" + npmlog "^6.0.2" -"@lerna/exec@3.13.3": - version "3.13.3" - resolved "https://registry.yarnpkg.com/@lerna/exec/-/exec-3.13.3.tgz#5d2eda3f6e584f2f15b115e8a4b5bc960ba5de85" - integrity sha512-c0bD4XqM96CTPV8+lvkxzE7mkxiFyv/WNM4H01YvvbFAJzk+S4Y7cBtRkIYFTfkFZW3FLo8pEgtG1ONtIdM+tg== - dependencies: - "@lerna/batch-packages" "3.13.0" - "@lerna/child-process" "3.13.3" - "@lerna/command" "3.13.3" - "@lerna/filter-options" "3.13.3" - "@lerna/run-parallel-batches" "3.13.0" - "@lerna/validation-error" "3.13.0" +"@lerna/exec@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/exec/-/exec-6.1.0.tgz#a2d165576471ff61e33c49952d40a5dbc36fc78f" + integrity sha512-Ej6WlPHXLF6hZHsfD+J/dxeuTrnc0HIfIXR1DU//msHW5RNCdi9+I7StwreCAQH/dLEsdBjPg5chNmuj2JLQRg== + dependencies: + "@lerna/child-process" "6.1.0" + "@lerna/command" "6.1.0" + "@lerna/filter-options" "6.1.0" + "@lerna/profiler" "6.1.0" + "@lerna/run-topologically" "6.1.0" + "@lerna/validation-error" "6.1.0" + p-map "^4.0.0" -"@lerna/filter-options@3.13.3": - version "3.13.3" - resolved "https://registry.yarnpkg.com/@lerna/filter-options/-/filter-options-3.13.3.tgz#aa42a4ab78837b8a6c4278ba871d27e92d77c54f" - integrity sha512-DbtQX4eRgrBz1wCFWRP99JBD7ODykYme9ykEK79+RrKph40znhJQRlLg4idogj6IsUEzwo1OHjihCzSfnVo6Cg== +"@lerna/filter-options@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/filter-options/-/filter-options-6.1.0.tgz#f4ee65d0db0273ce490ce6c72c9dbb1d23268ca6" + integrity sha512-kPf92Z7uLsR6MUiXnyXWebaUWArLa15wLfpfTwIp5H3MNk1lTbuG7QnrxE7OxQj+ozFmBvXeV9fuwfLsYTfmOw== dependencies: - "@lerna/collect-updates" "3.13.3" - "@lerna/filter-packages" "3.13.0" + "@lerna/collect-updates" "6.1.0" + "@lerna/filter-packages" "6.1.0" dedent "^0.7.0" + npmlog "^6.0.2" -"@lerna/filter-packages@3.13.0": - version "3.13.0" - resolved "https://registry.yarnpkg.com/@lerna/filter-packages/-/filter-packages-3.13.0.tgz#f5371249e7e1a15928e5e88c544a242e0162c21c" - integrity sha512-RWiZWyGy3Mp7GRVBn//CacSnE3Kw82PxE4+H6bQ3pDUw/9atXn7NRX+gkBVQIYeKamh7HyumJtyOKq3Pp9BADQ== +"@lerna/filter-packages@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/filter-packages/-/filter-packages-6.1.0.tgz#1ddac63a6ffdf5f058d206be5adfb39ad7aaf4f9" + integrity sha512-zW2avsZHs/ITE/37AEMhegGVHjiD0rgNk9bguNDfz6zaPa90UaW6PWDH6Tf4ThPRlbkl2Go48N3bFYHYSJKbcw== dependencies: - "@lerna/validation-error" "3.13.0" - multimatch "^2.1.0" - npmlog "^4.1.2" + "@lerna/validation-error" "6.1.0" + multimatch "^5.0.0" + npmlog "^6.0.2" -"@lerna/get-npm-exec-opts@3.13.0": - version "3.13.0" - resolved "https://registry.yarnpkg.com/@lerna/get-npm-exec-opts/-/get-npm-exec-opts-3.13.0.tgz#d1b552cb0088199fc3e7e126f914e39a08df9ea5" - integrity sha512-Y0xWL0rg3boVyJk6An/vurKzubyJKtrxYv2sj4bB8Mc5zZ3tqtv0ccbOkmkXKqbzvNNF7VeUt1OJ3DRgtC/QZw== +"@lerna/get-npm-exec-opts@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/get-npm-exec-opts/-/get-npm-exec-opts-6.1.0.tgz#22351e2ebc4adbef21ca4b86187278e15e4cb38a" + integrity sha512-10Pdf+W0z7RT34o0SWlf+WVzz2/WbnTIJ1tQqXvXx6soj2L/xGLhOPvhJiKNtl4WlvUiO/zQ91yb83ESP4TZaA== dependencies: - npmlog "^4.1.2" + npmlog "^6.0.2" -"@lerna/get-packed@3.13.0": - version "3.13.0" - resolved "https://registry.yarnpkg.com/@lerna/get-packed/-/get-packed-3.13.0.tgz#335e40d77f3c1855aa248587d3e0b2d8f4b06e16" - integrity sha512-EgSim24sjIjqQDC57bgXD9l22/HCS93uQBbGpkzEOzxAVzEgpZVm7Fm1t8BVlRcT2P2zwGnRadIvxTbpQuDPTg== +"@lerna/get-packed@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/get-packed/-/get-packed-6.1.0.tgz#b6d1c1dd1e068212e784b8dfc2e5fe64741ea8db" + integrity sha512-lg0wPpV0wPekcD0mebJp619hMxsOgbZDOH5AkL/bCR217391eha0iPhQ0dU/G0Smd2vv6Cg443+J5QdI4LGRTg== dependencies: - fs-extra "^7.0.0" - ssri "^6.0.1" - tar "^4.4.8" + fs-extra "^9.1.0" + ssri "^9.0.1" + tar "^6.1.0" -"@lerna/github-client@3.13.3": - version "3.13.3" - resolved "https://registry.yarnpkg.com/@lerna/github-client/-/github-client-3.13.3.tgz#bcf9b4ff40bdd104cb40cd257322f052b41bb9ce" - integrity sha512-fcJkjab4kX0zcLLSa/DCUNvU3v8wmy2c1lhdIbL7s7gABmDcV0QZq93LhnEee3VkC9UpnJ6GKG4EkD7eIifBnA== +"@lerna/github-client@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/github-client/-/github-client-6.1.0.tgz#cd33743e4529a0b822ae6716cb4b981e1d8ffe8f" + integrity sha512-+/4PtDgsjt0VRRZtOCN2Piyu0asU/16gSZZy/opVb8dlT44lTrH/ZghrJLE4tSL8Nuv688kx0kSgbUG8BY54jQ== dependencies: - "@lerna/child-process" "3.13.3" - "@octokit/plugin-enterprise-rest" "^2.1.1" - "@octokit/rest" "^16.16.0" - git-url-parse "^11.1.2" - npmlog "^4.1.2" + "@lerna/child-process" "6.1.0" + "@octokit/plugin-enterprise-rest" "^6.0.1" + "@octokit/rest" "^19.0.3" + git-url-parse "^13.1.0" + npmlog "^6.0.2" -"@lerna/global-options@3.13.0": - version "3.13.0" - resolved "https://registry.yarnpkg.com/@lerna/global-options/-/global-options-3.13.0.tgz#217662290db06ad9cf2c49d8e3100ee28eaebae1" - integrity sha512-SlZvh1gVRRzYLVluz9fryY1nJpZ0FHDGB66U9tFfvnnxmueckRQxLopn3tXj3NU1kc3QANT2I5BsQkOqZ4TEFQ== - -"@lerna/has-npm-version@3.13.3": - version "3.13.3" - resolved "https://registry.yarnpkg.com/@lerna/has-npm-version/-/has-npm-version-3.13.3.tgz#167e3f602a2fb58f84f93cf5df39705ca6432a2d" - integrity sha512-mQzoghRw4dBg0R9FFfHrj0TH0glvXyzdEZmYZ8Isvx5BSuEEwpsryoywuZSdppcvLu8o7NAdU5Tac8cJ/mT52w== +"@lerna/gitlab-client@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/gitlab-client/-/gitlab-client-6.1.0.tgz#bbcbf80d937e5980798ac1e0edd1f769101057d8" + integrity sha512-fUI/ppXzxJafN9ceSl+FDgsYvu3iTsO6UW0WTD63pS32CfM+PiCryLQHzuc4RkyVW8WQH3aCR/GbaKCqbu52bw== dependencies: - "@lerna/child-process" "3.13.3" - semver "^5.5.0" + node-fetch "^2.6.1" + npmlog "^6.0.2" -"@lerna/import@3.13.4": - version "3.13.4" - resolved "https://registry.yarnpkg.com/@lerna/import/-/import-3.13.4.tgz#e9a1831b8fed33f3cbeab3b84c722c9371a2eaf7" - integrity sha512-dn6eNuPEljWsifBEzJ9B6NoaLwl/Zvof7PBUPA4hRyRlqG5sXRn6F9DnusMTovvSarbicmTURbOokYuotVWQQA== +"@lerna/global-options@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/global-options/-/global-options-6.1.0.tgz#268e1de924369102e47babd9288086764ec6f9e6" + integrity sha512-1OyJ/N1XJh3ZAy8S20c6th9C4yBm/k3bRIdC+z0XxpDaHwfNt8mT9kUIDt6AIFCUvVKjSwnIsMHwhzXqBnwYSA== + +"@lerna/has-npm-version@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/has-npm-version/-/has-npm-version-6.1.0.tgz#a5d960213d1a7ca5374eb3c551a17b322b9a9e62" + integrity sha512-up5PVuP6BmKQ5/UgH/t2c5B1q4HhjwW3/bqbNayX6V0qNz8OijnMYvEUbxFk8fOdeN41qVnhAk0Tb5kbdtYh2A== dependencies: - "@lerna/child-process" "3.13.3" - "@lerna/command" "3.13.3" - "@lerna/prompt" "3.13.0" - "@lerna/pulse-till-done" "3.13.0" - "@lerna/validation-error" "3.13.0" + "@lerna/child-process" "6.1.0" + semver "^7.3.4" + +"@lerna/import@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/import/-/import-6.1.0.tgz#1c64281e3431c43c9cd140b66a6a51427afe7095" + integrity sha512-xsBhiKLUavATR32dAFL+WFY0yuab0hsM1eztKtRKk4wy7lSyxRfA5EIUcNCsLXx2xaDOKoMncCTXgNcpeYuqcQ== + dependencies: + "@lerna/child-process" "6.1.0" + "@lerna/command" "6.1.0" + "@lerna/prompt" "6.1.0" + "@lerna/pulse-till-done" "6.1.0" + "@lerna/validation-error" "6.1.0" dedent "^0.7.0" - fs-extra "^7.0.0" - p-map-series "^1.0.0" + fs-extra "^9.1.0" + p-map-series "^2.1.0" -"@lerna/init@3.13.3": - version "3.13.3" - resolved "https://registry.yarnpkg.com/@lerna/init/-/init-3.13.3.tgz#ebd522fee9b9d7d3b2dacb0261eaddb4826851ff" - integrity sha512-bK/mp0sF6jT0N+c+xrbMCqN4xRoiZCXQzlYsyACxPK99KH/mpHv7hViZlTYUGlYcymtew6ZC770miv5A9wF9hA== +"@lerna/info@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/info/-/info-6.1.0.tgz#a5d66a9c1f18398dc020a6f6073c399013081587" + integrity sha512-CsrWdW/Wyb4kcvHSnrsm7KYWFvjUNItu+ryeyWBZJtWYQOv45jNmWix6j2L4/w1+mMlWMjsfLmBscg82UBrF5w== dependencies: - "@lerna/child-process" "3.13.3" - "@lerna/command" "3.13.3" - fs-extra "^7.0.0" - p-map "^1.2.0" - write-json-file "^2.3.0" + "@lerna/command" "6.1.0" + "@lerna/output" "6.1.0" + envinfo "^7.7.4" -"@lerna/link@3.13.3": - version "3.13.3" - resolved "https://registry.yarnpkg.com/@lerna/link/-/link-3.13.3.tgz#11124d4a0c8d0b79752fbda3babedfd62dd57847" - integrity sha512-IHhtdhA0KlIdevCsq6WHkI2rF3lHWHziJs2mlrEWAKniVrFczbELON1KJAgdJS1k3kAP/WeWVqmIYZ2hJDxMvg== +"@lerna/init@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/init/-/init-6.1.0.tgz#b178775693b9c38c0f3fe3300eeb574cf76e0297" + integrity sha512-z8oUeVjn+FQYAtepAw6G47cGodLyBAyNoEjO3IsJjQLWE1yH3r83L2sjyD/EckgR3o2VTEzrKo4ArhxLp2mNmg== dependencies: - "@lerna/command" "3.13.3" - "@lerna/package-graph" "3.13.0" - "@lerna/symlink-dependencies" "3.13.0" - p-map "^1.2.0" - slash "^1.0.0" + "@lerna/child-process" "6.1.0" + "@lerna/command" "6.1.0" + "@lerna/project" "6.1.0" + fs-extra "^9.1.0" + p-map "^4.0.0" + write-json-file "^4.3.0" -"@lerna/list@3.13.3": - version "3.13.3" - resolved "https://registry.yarnpkg.com/@lerna/list/-/list-3.13.3.tgz#fa93864d43cadeb4cd540a4e78a52886c57dbe74" - integrity sha512-rLRDsBCkydMq2FL6WY1J/elvnXIjxxRtb72lfKHdvDEqVdquT5Qgt9ci42hwjmcocFwWcFJgF6BZozj5pbc13A== - dependencies: - "@lerna/command" "3.13.3" - "@lerna/filter-options" "3.13.3" - "@lerna/listable" "3.13.0" - "@lerna/output" "3.13.0" - -"@lerna/listable@3.13.0": - version "3.13.0" - resolved "https://registry.yarnpkg.com/@lerna/listable/-/listable-3.13.0.tgz#babc18442c590b549cf0966d20d75fea066598d4" - integrity sha512-liYJ/WBUYP4N4MnSVZuLUgfa/jy3BZ02/1Om7xUY09xGVSuNVNEeB8uZUMSC+nHqFHIsMPZ8QK9HnmZb1E/eTA== - dependencies: - "@lerna/batch-packages" "3.13.0" - chalk "^2.3.1" - columnify "^1.5.4" - -"@lerna/log-packed@3.13.0": - version "3.13.0" - resolved "https://registry.yarnpkg.com/@lerna/log-packed/-/log-packed-3.13.0.tgz#497b5f692a8d0e3f669125da97b0dadfd9e480f3" - integrity sha512-Rmjrcz+6aM6AEcEVWmurbo8+AnHOvYtDpoeMMJh9IZ9SmZr2ClXzmD7wSvjTQc8BwOaiWjjC/ukcT0UYA2m7wg== - dependencies: - byte-size "^4.0.3" - columnify "^1.5.4" +"@lerna/link@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/link/-/link-6.1.0.tgz#f6f0cfd0b02aecdeb304ce614e4e4e89fe0a3ad5" + integrity sha512-7OD2lYNQHl6Kl1KYmplt8KoWjVHdiaqpYqwD38AwcB09YN58nGmo4aJgC12Fdx8DSNjkumgM0ROg/JOjMCTIzQ== + dependencies: + "@lerna/command" "6.1.0" + "@lerna/package-graph" "6.1.0" + "@lerna/symlink-dependencies" "6.1.0" + "@lerna/validation-error" "6.1.0" + p-map "^4.0.0" + slash "^3.0.0" + +"@lerna/list@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/list/-/list-6.1.0.tgz#a7625bceb5224c4bf1154e715c07ea29f9698bac" + integrity sha512-7/g2hjizkvVnBGpVm+qC7lUFGhZ/0GIMUbGQwnE6yXDGm8yP9aEcNVkU4JGrDWW+uIklf9oodnMHaLXd/FJe6Q== + dependencies: + "@lerna/command" "6.1.0" + "@lerna/filter-options" "6.1.0" + "@lerna/listable" "6.1.0" + "@lerna/output" "6.1.0" + +"@lerna/listable@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/listable/-/listable-6.1.0.tgz#2510045fde7bc568b18172a5d24372a719bb5c4c" + integrity sha512-3KZ9lQ9AtNfGNH/mYJYaMKCiF2EQvLLBGYkWHeIzIs6foegcZNXe0Cyv3LNXuo5WslMNr5RT4wIgy3BOoAxdtg== + dependencies: + "@lerna/query-graph" "6.1.0" + chalk "^4.1.0" + columnify "^1.6.0" + +"@lerna/log-packed@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/log-packed/-/log-packed-6.1.0.tgz#18ae946e8b7881f2fc5b973cc6682cc599b1759b" + integrity sha512-Sq2HZJAcPuoNeEHeIutcPYQCyWBxLyVGvEhgsP3xTe6XkBGQCG8piCp9wX+sc2zT+idPdpI6qLqdh85yYIMMhA== + dependencies: + byte-size "^7.0.0" + columnify "^1.6.0" has-unicode "^2.0.1" - npmlog "^4.1.2" + npmlog "^6.0.2" -"@lerna/npm-conf@3.13.0": - version "3.13.0" - resolved "https://registry.yarnpkg.com/@lerna/npm-conf/-/npm-conf-3.13.0.tgz#6b434ed75ff757e8c14381b9bbfe5d5ddec134a7" - integrity sha512-Jg2kANsGnhg+fbPEzE0X9nX5oviEAvWj0nYyOkcE+cgWuT7W0zpnPXC4hA4C5IPQGhwhhh0IxhWNNHtjTuw53g== +"@lerna/npm-conf@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/npm-conf/-/npm-conf-6.1.0.tgz#79697260c9d14ffb9d892927f37fcde75b89ec58" + integrity sha512-+RD3mmJe9XSQj7Diibs0+UafAHPcrFCd29ODpDI+tzYl4MmYZblfrlL6mbSCiVYCZQneQ8Uku3P0r+DlbYBaFw== dependencies: - config-chain "^1.1.11" - pify "^3.0.0" + config-chain "^1.1.12" + pify "^5.0.0" -"@lerna/npm-dist-tag@3.13.0": - version "3.13.0" - resolved "https://registry.yarnpkg.com/@lerna/npm-dist-tag/-/npm-dist-tag-3.13.0.tgz#49ecbe0e82cbe4ad4a8ea6de112982bf6c4e6cd4" - integrity sha512-mcuhw34JhSRFrbPn0vedbvgBTvveG52bR2lVE3M3tfE8gmR/cKS/EJFO4AUhfRKGCTFn9rjaSEzlFGYV87pemQ== +"@lerna/npm-dist-tag@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/npm-dist-tag/-/npm-dist-tag-6.1.0.tgz#29f843aa628687a29dc3a9b905dd3002db7a3820" + integrity sha512-1zo+Yww/lvWJWZnEXpke9dZSb5poDzhUM/pQNqAQYSlbZ96o18SuCR6TEi5isMPiw63Aq1MMzbUqttQfJ11EOA== dependencies: - figgy-pudding "^3.5.1" - npm-package-arg "^6.1.0" - npm-registry-fetch "^3.9.0" - npmlog "^4.1.2" + "@lerna/otplease" "6.1.0" + npm-package-arg "8.1.1" + npm-registry-fetch "^13.3.0" + npmlog "^6.0.2" -"@lerna/npm-install@3.13.3": - version "3.13.3" - resolved "https://registry.yarnpkg.com/@lerna/npm-install/-/npm-install-3.13.3.tgz#9b09852732e51c16d2e060ff2fd8bfbbb49cf7ba" - integrity sha512-7Jig9MLpwAfcsdQ5UeanAjndChUjiTjTp50zJ+UZz4CbIBIDhoBehvNMTCL2G6pOEC7sGEg6sAqJINAqred6Tg== +"@lerna/npm-install@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/npm-install/-/npm-install-6.1.0.tgz#b75d1f152540a144bd6c81586a9f6010ed7f3046" + integrity sha512-1SHmOHZA1YJuUctLQBRjA2+yMp+UNYdOBsFb3xUVT7MjWnd1Zl0toT3jxGu96RNErD9JKkk/cGo/Aq+DU3s9pg== dependencies: - "@lerna/child-process" "3.13.3" - "@lerna/get-npm-exec-opts" "3.13.0" - fs-extra "^7.0.0" - npm-package-arg "^6.1.0" - npmlog "^4.1.2" - signal-exit "^3.0.2" - write-pkg "^3.1.0" + "@lerna/child-process" "6.1.0" + "@lerna/get-npm-exec-opts" "6.1.0" + fs-extra "^9.1.0" + npm-package-arg "8.1.1" + npmlog "^6.0.2" + signal-exit "^3.0.3" + write-pkg "^4.0.0" -"@lerna/npm-publish@3.13.2": - version "3.13.2" - resolved "https://registry.yarnpkg.com/@lerna/npm-publish/-/npm-publish-3.13.2.tgz#ad713ca6f91a852687d7d0e1bda7f9c66df21768" - integrity sha512-HMucPyEYZfom5tRJL4GsKBRi47yvSS2ynMXYxL3kO0ie+j9J7cb0Ir8NmaAMEd3uJWJVFCPuQarehyfTDZsSxg== +"@lerna/npm-publish@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/npm-publish/-/npm-publish-6.1.0.tgz#8fe561e639e6a06380354271aeca7cbc39acf7dd" + integrity sha512-N0LdR1ImZQw1r4cYaKtVbBhBPtj4Zu9NbvygzizEP5HuTfxZmE1Ans3w93Kks9VTXZXob8twNbXnzBwzTyEpEA== dependencies: - "@lerna/run-lifecycle" "3.13.0" - figgy-pudding "^3.5.1" - fs-extra "^7.0.0" - libnpmpublish "^1.1.1" - npm-package-arg "^6.1.0" - npmlog "^4.1.2" - pify "^3.0.0" - read-package-json "^2.0.13" + "@lerna/otplease" "6.1.0" + "@lerna/run-lifecycle" "6.1.0" + fs-extra "^9.1.0" + libnpmpublish "^6.0.4" + npm-package-arg "8.1.1" + npmlog "^6.0.2" + pify "^5.0.0" + read-package-json "^5.0.1" -"@lerna/npm-run-script@3.13.3": - version "3.13.3" - resolved "https://registry.yarnpkg.com/@lerna/npm-run-script/-/npm-run-script-3.13.3.tgz#9bb6389ed70cd506905d6b05b6eab336b4266caf" - integrity sha512-qR4o9BFt5hI8Od5/DqLalOJydnKpiQFEeN0h9xZi7MwzuX1Ukwh3X22vqsX4YRbipIelSFtrDzleNVUm5jj0ow== +"@lerna/npm-run-script@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/npm-run-script/-/npm-run-script-6.1.0.tgz#bc5bd414ee9696168d88d8ce78f8e8b715967100" + integrity sha512-7p13mvdxdY5+VqWvvtMsMDeyCRs0PrrTmSHRO+FKuLQuGhBvUo05vevcMEOQNDvEvl/tXPrOVbeGCiGubYTCLg== dependencies: - "@lerna/child-process" "3.13.3" - "@lerna/get-npm-exec-opts" "3.13.0" - npmlog "^4.1.2" + "@lerna/child-process" "6.1.0" + "@lerna/get-npm-exec-opts" "6.1.0" + npmlog "^6.0.2" -"@lerna/output@3.13.0": - version "3.13.0" - resolved "https://registry.yarnpkg.com/@lerna/output/-/output-3.13.0.tgz#3ded7cc908b27a9872228a630d950aedae7a4989" - integrity sha512-7ZnQ9nvUDu/WD+bNsypmPG5MwZBwu86iRoiW6C1WBuXXDxM5cnIAC1m2WxHeFnjyMrYlRXM9PzOQ9VDD+C15Rg== +"@lerna/otplease@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/otplease/-/otplease-6.1.0.tgz#d25dbe2d867215b69f06de12ab4ff559d83d1d01" + integrity sha512-gqSE6IbaD4IeNJePkaDLaFLoGp0Ceu35sn7z0AHAOoHiQGGorOmvM+h1Md3xZZRSXQmY9LyJVhG5eRa38SoG4g== dependencies: - npmlog "^4.1.2" + "@lerna/prompt" "6.1.0" -"@lerna/pack-directory@3.13.1": - version "3.13.1" - resolved "https://registry.yarnpkg.com/@lerna/pack-directory/-/pack-directory-3.13.1.tgz#5ad4d0945f86a648f565e24d53c1e01bb3a912d1" - integrity sha512-kXnyqrkQbCIZOf1054N88+8h0ItC7tUN5v9ca/aWpx298gsURpxUx/1TIKqijL5TOnHMyIkj0YJmnH/PyBVLKA== +"@lerna/output@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/output/-/output-6.1.0.tgz#d470146c6ee8ee063fd416081c1ca64fb132c4d8" + integrity sha512-mgCIzLKIuroytXuxjTB689ERtpfgyNXW0rMv9WHOa6ufQc+QJPjh3L4jVsOA0l+/OxZyi97PUXotduNj+0cbnA== dependencies: - "@lerna/get-packed" "3.13.0" - "@lerna/package" "3.13.0" - "@lerna/run-lifecycle" "3.13.0" - figgy-pudding "^3.5.1" - npm-packlist "^1.4.1" - npmlog "^4.1.2" - tar "^4.4.8" - temp-write "^3.4.0" + npmlog "^6.0.2" -"@lerna/package-graph@3.13.0": - version "3.13.0" - resolved "https://registry.yarnpkg.com/@lerna/package-graph/-/package-graph-3.13.0.tgz#607062f8d2ce22b15f8d4a0623f384736e67f760" - integrity sha512-3mRF1zuqFE1HEFmMMAIggXy+f+9cvHhW/jzaPEVyrPNLKsyfJQtpTNzeI04nfRvbAh+Gd2aNksvaW/w3xGJnnw== +"@lerna/pack-directory@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/pack-directory/-/pack-directory-6.1.0.tgz#3252ba7250d826b9922238c775abf5004e7580c4" + integrity sha512-Xsixqm2nkGXs9hvq08ClbGpRlCYnlBV4TwSrLttIDL712RlyXoPe2maJzTUqo9OXBbOumFSahUEInCMT2OS05g== + dependencies: + "@lerna/get-packed" "6.1.0" + "@lerna/package" "6.1.0" + "@lerna/run-lifecycle" "6.1.0" + "@lerna/temp-write" "6.1.0" + npm-packlist "^5.1.1" + npmlog "^6.0.2" + tar "^6.1.0" + +"@lerna/package-graph@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/package-graph/-/package-graph-6.1.0.tgz#2373617605f48f53b5fa9d13188838b6c09022b0" + integrity sha512-yGyxd/eHTDjkpnBbDhTV0hwKF+i01qZc+6/ko65wOsh8xtgqpQeE6mtdgbvsLKcuMcIQ7PDy1ntyIv9phg14gQ== dependencies: - "@lerna/validation-error" "3.13.0" - npm-package-arg "^6.1.0" - semver "^5.5.0" + "@lerna/prerelease-id-from-version" "6.1.0" + "@lerna/validation-error" "6.1.0" + npm-package-arg "8.1.1" + npmlog "^6.0.2" + semver "^7.3.4" -"@lerna/package@3.13.0": - version "3.13.0" - resolved "https://registry.yarnpkg.com/@lerna/package/-/package-3.13.0.tgz#4baeebc49a57fc9b31062cc59f5ee38384429fc8" - integrity sha512-kSKO0RJQy093BufCQnkhf1jB4kZnBvL7kK5Ewolhk5gwejN+Jofjd8DGRVUDUJfQ0CkW1o6GbUeZvs8w8VIZDg== +"@lerna/package@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/package/-/package-6.1.0.tgz#e9e33876c0509a86c1b676045b19fd3f7f1c77e2" + integrity sha512-PyNFtdH2IcLasp/nyMDshmeXotriOSlhbeFIxhdl1XuGj5v1so3utMSOrJMO5kzZJQg5zyx8qQoxL+WH/hkrVQ== dependencies: - load-json-file "^4.0.0" - npm-package-arg "^6.1.0" - write-pkg "^3.1.0" + load-json-file "^6.2.0" + npm-package-arg "8.1.1" + write-pkg "^4.0.0" -"@lerna/project@3.13.1": - version "3.13.1" - resolved "https://registry.yarnpkg.com/@lerna/project/-/project-3.13.1.tgz#bce890f60187bd950bcf36c04b5260642e295e79" - integrity sha512-/GoCrpsCCTyb9sizk1+pMBrIYchtb+F1uCOn3cjn9yenyG/MfYEnlfrbV5k/UDud0Ei75YBLbmwCbigHkAKazQ== +"@lerna/prerelease-id-from-version@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/prerelease-id-from-version/-/prerelease-id-from-version-6.1.0.tgz#4ee5beeef4e81d77001e94ec5613c140b6615616" + integrity sha512-ngC4I6evvZztB6aOaSDEnhUgRTlqX3TyBXwWwLGTOXCPaCQBTPaLNokhmRdJ+ZVdZ4iHFbzEDSL07ubZrYUcmQ== + dependencies: + semver "^7.3.4" + +"@lerna/profiler@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/profiler/-/profiler-6.1.0.tgz#aae2249f1a39c79db72a548ce50bf32f86a0f3a5" + integrity sha512-WFDQNpuqPqMJLg8llvrBHF8Ib5Asgp23lMeNUe89T62NUX6gkjVBTYdjsduxM0tZH6Pa0GAGaQcha97P6fxfdQ== + dependencies: + fs-extra "^9.1.0" + npmlog "^6.0.2" + upath "^2.0.1" + +"@lerna/project@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/project/-/project-6.1.0.tgz#605afe28fb15d8b8b890fafe0ec1da2700964056" + integrity sha512-EOkfjjrTM16c3GUxGqcfYD2stV35p9mBEmkF41NPmyjfbzjol/irDF1r6Q7BsQSRsdClMJRCeZ168xdSxC2X0A== dependencies: - "@lerna/package" "3.13.0" - "@lerna/validation-error" "3.13.0" - cosmiconfig "^5.1.0" + "@lerna/package" "6.1.0" + "@lerna/validation-error" "6.1.0" + cosmiconfig "^7.0.0" dedent "^0.7.0" - dot-prop "^4.2.0" - glob-parent "^3.1.0" - globby "^8.0.1" - load-json-file "^4.0.0" - npmlog "^4.1.2" - p-map "^1.2.0" - resolve-from "^4.0.0" - write-json-file "^2.3.0" + dot-prop "^6.0.1" + glob-parent "^5.1.1" + globby "^11.0.2" + js-yaml "^4.1.0" + load-json-file "^6.2.0" + npmlog "^6.0.2" + p-map "^4.0.0" + resolve-from "^5.0.0" + write-json-file "^4.3.0" -"@lerna/prompt@3.13.0": - version "3.13.0" - resolved "https://registry.yarnpkg.com/@lerna/prompt/-/prompt-3.13.0.tgz#53571462bb3f5399cc1ca6d335a411fe093426a5" - integrity sha512-P+lWSFokdyvYpkwC3it9cE0IF2U5yy2mOUbGvvE4iDb9K7TyXGE+7lwtx2thtPvBAfIb7O13POMkv7df03HJeA== +"@lerna/prompt@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/prompt/-/prompt-6.1.0.tgz#98e228220428d33620822f77e39f592ce29c776c" + integrity sha512-981J/C53TZ2l2mFVlWJN7zynSzf5GEHKvKQa12Td9iknhASZOuwTAWb6eq46246Ant6W5tWwb0NSPu3I5qtcrA== dependencies: - inquirer "^6.2.0" - npmlog "^4.1.2" + inquirer "^8.2.4" + npmlog "^6.0.2" -"@lerna/publish@3.13.4": - version "3.13.4" - resolved "https://registry.yarnpkg.com/@lerna/publish/-/publish-3.13.4.tgz#25b678c285110897a7fc5198a35bdfa9db7f9cc1" - integrity sha512-v03pabiPlqCDwX6cVNis1PDdT6/jBgkVb5Nl4e8wcJXevIhZw3ClvtI94gSZu/wdoVFX0RMfc8QBVmaimSO0qg== - dependencies: - "@lerna/batch-packages" "3.13.0" - "@lerna/check-working-tree" "3.13.3" - "@lerna/child-process" "3.13.3" - "@lerna/collect-updates" "3.13.3" - "@lerna/command" "3.13.3" - "@lerna/describe-ref" "3.13.3" - "@lerna/log-packed" "3.13.0" - "@lerna/npm-conf" "3.13.0" - "@lerna/npm-dist-tag" "3.13.0" - "@lerna/npm-publish" "3.13.2" - "@lerna/output" "3.13.0" - "@lerna/pack-directory" "3.13.1" - "@lerna/prompt" "3.13.0" - "@lerna/pulse-till-done" "3.13.0" - "@lerna/run-lifecycle" "3.13.0" - "@lerna/run-parallel-batches" "3.13.0" - "@lerna/validation-error" "3.13.0" - "@lerna/version" "3.13.4" - figgy-pudding "^3.5.1" - fs-extra "^7.0.0" - libnpmaccess "^3.0.1" - npm-package-arg "^6.1.0" - npm-registry-fetch "^3.9.0" - npmlog "^4.1.2" - p-finally "^1.0.0" - p-map "^1.2.0" - p-pipe "^1.2.0" - p-reduce "^1.0.0" - pacote "^9.5.0" - semver "^5.5.0" +"@lerna/publish@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/publish/-/publish-6.1.0.tgz#9d62c327bc3541a0430951d726b39a2fb17b7925" + integrity sha512-XtvuydtU0IptbAapLRgoN1AZj/WJR+e3UKnx9BQ1Dwc+Fpg2oqPxR/vi+6hxAsr95pdQ5CnWBdgS+dg2wEUJ7Q== + dependencies: + "@lerna/check-working-tree" "6.1.0" + "@lerna/child-process" "6.1.0" + "@lerna/collect-updates" "6.1.0" + "@lerna/command" "6.1.0" + "@lerna/describe-ref" "6.1.0" + "@lerna/log-packed" "6.1.0" + "@lerna/npm-conf" "6.1.0" + "@lerna/npm-dist-tag" "6.1.0" + "@lerna/npm-publish" "6.1.0" + "@lerna/otplease" "6.1.0" + "@lerna/output" "6.1.0" + "@lerna/pack-directory" "6.1.0" + "@lerna/prerelease-id-from-version" "6.1.0" + "@lerna/prompt" "6.1.0" + "@lerna/pulse-till-done" "6.1.0" + "@lerna/run-lifecycle" "6.1.0" + "@lerna/run-topologically" "6.1.0" + "@lerna/validation-error" "6.1.0" + "@lerna/version" "6.1.0" + fs-extra "^9.1.0" + libnpmaccess "^6.0.3" + npm-package-arg "8.1.1" + npm-registry-fetch "^13.3.0" + npmlog "^6.0.2" + p-map "^4.0.0" + p-pipe "^3.1.0" + pacote "^13.6.1" + semver "^7.3.4" -"@lerna/pulse-till-done@3.13.0": - version "3.13.0" - resolved "https://registry.yarnpkg.com/@lerna/pulse-till-done/-/pulse-till-done-3.13.0.tgz#c8e9ce5bafaf10d930a67d7ed0ccb5d958fe0110" - integrity sha512-1SOHpy7ZNTPulzIbargrgaJX387csN7cF1cLOGZiJQA6VqnS5eWs2CIrG8i8wmaUavj2QlQ5oEbRMVVXSsGrzA== +"@lerna/pulse-till-done@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/pulse-till-done/-/pulse-till-done-6.1.0.tgz#df0112a9a5b8547b53d18742ce21104eb360d731" + integrity sha512-a2RVT82E4R9nVXtehzp2TQL6iXp0QfEM3bu8tBAR/SfI1A9ggZWQhuuUqtRyhhVCajdQDOo7rS0UG7R5JzK58w== dependencies: - npmlog "^4.1.2" + npmlog "^6.0.2" -"@lerna/resolve-symlink@3.13.0": - version "3.13.0" - resolved "https://registry.yarnpkg.com/@lerna/resolve-symlink/-/resolve-symlink-3.13.0.tgz#3e6809ef53b63fe914814bfa071cd68012e22fbb" - integrity sha512-Lc0USSFxwDxUs5JvIisS8JegjA6SHSAWJCMvi2osZx6wVRkEDlWG2B1JAfXUzCMNfHoZX0/XX9iYZ+4JIpjAtg== +"@lerna/query-graph@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/query-graph/-/query-graph-6.1.0.tgz#e78c47c78d4691231fc379570e036bc2753cf6fa" + integrity sha512-YkyCc+6aR7GlCOcZXEKPcl5o5L2v+0YUNs59JrfAS0mctFosZ/2tP7pkdu2SI4qXIi5D0PMNsh/0fRni56znsQ== dependencies: - fs-extra "^7.0.0" - npmlog "^4.1.2" - read-cmd-shim "^1.0.1" + "@lerna/package-graph" "6.1.0" -"@lerna/rimraf-dir@3.13.3": - version "3.13.3" - resolved "https://registry.yarnpkg.com/@lerna/rimraf-dir/-/rimraf-dir-3.13.3.tgz#3a8e71317fde853893ef0262bc9bba6a180b7227" - integrity sha512-d0T1Hxwu3gpYVv73ytSL+/Oy8JitsmvOYUR5ouRSABsmqS7ZZCh5t6FgVDDGVXeuhbw82+vuny1Og6Q0k4ilqw== +"@lerna/resolve-symlink@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/resolve-symlink/-/resolve-symlink-6.1.0.tgz#5a8686b99c838bc6e869930e5b5fd582607ebbe7" + integrity sha512-8ILO+h5fsE0q8MSLfdL+MT1GEsNhAB1fDyMkSsYgLRCsssN/cViZbffpclZyT/EfAhpyKfBCHZ0CmT1ZGofU1A== dependencies: - "@lerna/child-process" "3.13.3" - npmlog "^4.1.2" - path-exists "^3.0.0" - rimraf "^2.6.2" + fs-extra "^9.1.0" + npmlog "^6.0.2" + read-cmd-shim "^3.0.0" -"@lerna/run-lifecycle@3.13.0": - version "3.13.0" - resolved "https://registry.yarnpkg.com/@lerna/run-lifecycle/-/run-lifecycle-3.13.0.tgz#d8835ee83425edee40f687a55f81b502354d3261" - integrity sha512-oyiaL1biZdjpmjh6X/5C4w07wNFyiwXSSHH5GQB4Ay4BPwgq9oNhCcxRoi0UVZlZ1YwzSW8sTwLgj8emkIo3Yg== +"@lerna/rimraf-dir@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/rimraf-dir/-/rimraf-dir-6.1.0.tgz#75559585d5921563eff0e206bb9ec8ab0cc967c6" + integrity sha512-J9YeGHkCCeAIzsnKURYeGECBexiIii6HA+Bbd+rAgoKPsNCOj6ql4+qJE8Jbd7fQEFNDPQeBCYvM7JcdMc0WSA== dependencies: - "@lerna/npm-conf" "3.13.0" - figgy-pudding "^3.5.1" - npm-lifecycle "^2.1.0" - npmlog "^4.1.2" + "@lerna/child-process" "6.1.0" + npmlog "^6.0.2" + path-exists "^4.0.0" + rimraf "^3.0.2" -"@lerna/run-parallel-batches@3.13.0": - version "3.13.0" - resolved "https://registry.yarnpkg.com/@lerna/run-parallel-batches/-/run-parallel-batches-3.13.0.tgz#0276bb4e7cd0995297db82d134ca2bd08d63e311" - integrity sha512-bICFBR+cYVF1FFW+Tlm0EhWDioTUTM6dOiVziDEGE1UZha1dFkMYqzqdSf4bQzfLS31UW/KBd/2z8jy2OIjEjg== +"@lerna/run-lifecycle@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/run-lifecycle/-/run-lifecycle-6.1.0.tgz#e1fa6cd300842ef1d688af77648fed05ec2d5345" + integrity sha512-GbTdKxL+hWHEPgyBEKtqY9Nf+jFlt6YLtP5VjEVc5SdLkm+FeRquar9/YcZVUbzr3c+NJwWNgVjHuePfowdpUA== dependencies: - p-map "^1.2.0" - p-map-series "^1.0.0" + "@lerna/npm-conf" "6.1.0" + "@npmcli/run-script" "^4.1.7" + npmlog "^6.0.2" + p-queue "^6.6.2" -"@lerna/run@3.13.3": - version "3.13.3" - resolved "https://registry.yarnpkg.com/@lerna/run/-/run-3.13.3.tgz#0781c82d225ef6e85e28d3e763f7fc090a376a21" - integrity sha512-ygnLIfIYS6YY1JHWOM4CsdZiY8kTYPsDFOLAwASlRnlAXF9HiMT08GFXLmMHIblZJ8yJhsM2+QgraCB0WdxzOQ== - dependencies: - "@lerna/batch-packages" "3.13.0" - "@lerna/command" "3.13.3" - "@lerna/filter-options" "3.13.3" - "@lerna/npm-run-script" "3.13.3" - "@lerna/output" "3.13.0" - "@lerna/run-parallel-batches" "3.13.0" - "@lerna/timer" "3.13.0" - "@lerna/validation-error" "3.13.0" - p-map "^1.2.0" - -"@lerna/symlink-binary@3.13.0": - version "3.13.0" - resolved "https://registry.yarnpkg.com/@lerna/symlink-binary/-/symlink-binary-3.13.0.tgz#36a9415d468afcb8105750296902f6f000a9680d" - integrity sha512-obc4Y6jxywkdaCe+DB0uTxYqP0IQ8mFWvN+k/YMbwH4G2h7M7lCBWgPy8e7xw/50+1II9tT2sxgx+jMus1sTJg== - dependencies: - "@lerna/create-symlink" "3.13.0" - "@lerna/package" "3.13.0" - fs-extra "^7.0.0" - p-map "^1.2.0" +"@lerna/run-topologically@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/run-topologically/-/run-topologically-6.1.0.tgz#8f1a428b5d4b800bced178edabfa2262b328572f" + integrity sha512-kpTaSBKdKjtf61be8Z1e7TIaMt/aksfxswQtpFxEuKDsPsdHfR8htSkADO4d/3SZFtmcAHIHNCQj9CaNj4O4Xw== + dependencies: + "@lerna/query-graph" "6.1.0" + p-queue "^6.6.2" + +"@lerna/run@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/run/-/run-6.1.0.tgz#efaea1acc78cb7fc73b4906be70002e118628d64" + integrity sha512-vlEEKPcTloiob6EK7gxrjEdB6fQQ/LNfWhSJCGxJlvNVbrMpoWIu0Kpp20b0nE+lzX7rRJ4seWr7Wdo/Fjub4Q== + dependencies: + "@lerna/command" "6.1.0" + "@lerna/filter-options" "6.1.0" + "@lerna/npm-run-script" "6.1.0" + "@lerna/output" "6.1.0" + "@lerna/profiler" "6.1.0" + "@lerna/run-topologically" "6.1.0" + "@lerna/timer" "6.1.0" + "@lerna/validation-error" "6.1.0" + fs-extra "^9.1.0" + p-map "^4.0.0" -"@lerna/symlink-dependencies@3.13.0": - version "3.13.0" - resolved "https://registry.yarnpkg.com/@lerna/symlink-dependencies/-/symlink-dependencies-3.13.0.tgz#76c23ecabda7824db98a0561364f122b457509cf" - integrity sha512-7CyN5WYEPkbPLbqHBIQg/YiimBzb5cIGQB0E9IkLs3+racq2vmUNQZn38LOaazQacAA83seB+zWSxlI6H+eXSg== +"@lerna/symlink-binary@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/symlink-binary/-/symlink-binary-6.1.0.tgz#7d476499b86ae5fcb853c510603cff9a27acf105" + integrity sha512-DaiRNZk/dvomNxgEaTW145PyL7vIGP7rvnfXV2FO+rjX8UUSNUOjmVmHlYfs64gV9Eqx/dLfQClIbKcwYMD83A== dependencies: - "@lerna/create-symlink" "3.13.0" - "@lerna/resolve-symlink" "3.13.0" - "@lerna/symlink-binary" "3.13.0" - fs-extra "^7.0.0" - p-finally "^1.0.0" - p-map "^1.2.0" - p-map-series "^1.0.0" + "@lerna/create-symlink" "6.1.0" + "@lerna/package" "6.1.0" + fs-extra "^9.1.0" + p-map "^4.0.0" + +"@lerna/symlink-dependencies@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/symlink-dependencies/-/symlink-dependencies-6.1.0.tgz#f44d33e043fed21a366c4ced2cbde8fa8be0c5fc" + integrity sha512-hrTvtY1Ek+fLA4JjXsKsvwPjuJD0rwB/+K4WY57t00owj//BpCsJ37w3kkkS7f/PcW/5uRjCuHcY67LOEwsRxw== + dependencies: + "@lerna/create-symlink" "6.1.0" + "@lerna/resolve-symlink" "6.1.0" + "@lerna/symlink-binary" "6.1.0" + fs-extra "^9.1.0" + p-map "^4.0.0" + p-map-series "^2.1.0" + +"@lerna/temp-write@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/temp-write/-/temp-write-6.1.0.tgz#a5d532090dd7b2d4f8965fbb475376aae06b9242" + integrity sha512-ZcQl88H9HbQ/TeWUOVt+vDYwptm7kwprGvj9KkZXr9S5Bn6SiKRQOeydCCfCrQT+9Q3dm7QZXV6rWzLsACcAlQ== + dependencies: + graceful-fs "^4.1.15" + is-stream "^2.0.0" + make-dir "^3.0.0" + temp-dir "^1.0.0" + uuid "^8.3.2" -"@lerna/timer@3.13.0": - version "3.13.0" - resolved "https://registry.yarnpkg.com/@lerna/timer/-/timer-3.13.0.tgz#bcd0904551db16e08364d6c18e5e2160fc870781" - integrity sha512-RHWrDl8U4XNPqY5MQHkToWS9jHPnkLZEt5VD+uunCKTfzlxGnRCr3/zVr8VGy/uENMYpVP3wJa4RKGY6M0vkRw== +"@lerna/timer@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/timer/-/timer-6.1.0.tgz#245b02c05b2dec6d2aed2da8a0962cf0343d83d5" + integrity sha512-du+NQ9q7uO4d2nVU4AD2DSPuAZqUapA/bZKuVpFVxvY9Qhzb8dQKLsFISe4A9TjyoNAk8ZeWK0aBc/6N+Qer9A== -"@lerna/validation-error@3.13.0": - version "3.13.0" - resolved "https://registry.yarnpkg.com/@lerna/validation-error/-/validation-error-3.13.0.tgz#c86b8f07c5ab9539f775bd8a54976e926f3759c3" - integrity sha512-SiJP75nwB8GhgwLKQfdkSnDufAaCbkZWJqEDlKOUPUvVOplRGnfL+BPQZH5nvq2BYSRXsksXWZ4UHVnQZI/HYA== +"@lerna/validation-error@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/validation-error/-/validation-error-6.1.0.tgz#03bd46f6219b6db7c4420528d5aaf047f92693e3" + integrity sha512-q0c3XCi5OpyTr8AcfbisS6e3svZaJF/riCvBDqRMaQUT4A8QOPzB4fVF3/+J2u54nidBuTlIk0JZu9aOdWTUkQ== dependencies: - npmlog "^4.1.2" + npmlog "^6.0.2" -"@lerna/version@3.13.4": - version "3.13.4" - resolved "https://registry.yarnpkg.com/@lerna/version/-/version-3.13.4.tgz#ea23b264bebda425ccbfcdcd1de13ef45a390e59" - integrity sha512-pptWUEgN/lUTQZu34+gfH1g4Uhs7TDKRcdZY9A4T9k6RTOwpKC2ceLGiXdeR+ZgQJAey2C4qiE8fo5Z6Rbc6QA== - dependencies: - "@lerna/batch-packages" "3.13.0" - "@lerna/check-working-tree" "3.13.3" - "@lerna/child-process" "3.13.3" - "@lerna/collect-updates" "3.13.3" - "@lerna/command" "3.13.3" - "@lerna/conventional-commits" "3.13.0" - "@lerna/github-client" "3.13.3" - "@lerna/output" "3.13.0" - "@lerna/prompt" "3.13.0" - "@lerna/run-lifecycle" "3.13.0" - "@lerna/validation-error" "3.13.0" - chalk "^2.3.1" +"@lerna/version@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/version/-/version-6.1.0.tgz#44d8649e978df9d6a14d97c9d7631a7dcd4a9cbf" + integrity sha512-RUxVFdzHt0739lRNMrAbo6HWcFrcyG7atM1pn+Eo61fUoA5R/9N4bCk4m9xUGkJ/mOcROjuwAGe+wT1uOs58Bg== + dependencies: + "@lerna/check-working-tree" "6.1.0" + "@lerna/child-process" "6.1.0" + "@lerna/collect-updates" "6.1.0" + "@lerna/command" "6.1.0" + "@lerna/conventional-commits" "6.1.0" + "@lerna/github-client" "6.1.0" + "@lerna/gitlab-client" "6.1.0" + "@lerna/output" "6.1.0" + "@lerna/prerelease-id-from-version" "6.1.0" + "@lerna/prompt" "6.1.0" + "@lerna/run-lifecycle" "6.1.0" + "@lerna/run-topologically" "6.1.0" + "@lerna/temp-write" "6.1.0" + "@lerna/validation-error" "6.1.0" + "@nrwl/devkit" ">=14.8.6 < 16" + chalk "^4.1.0" dedent "^0.7.0" + load-json-file "^6.2.0" minimatch "^3.0.4" - npmlog "^4.1.2" - p-map "^1.2.0" - p-pipe "^1.2.0" - p-reduce "^1.0.0" - p-waterfall "^1.0.0" - semver "^5.5.0" - slash "^1.0.0" - temp-write "^3.4.0" + npmlog "^6.0.2" + p-map "^4.0.0" + p-pipe "^3.1.0" + p-reduce "^2.1.0" + p-waterfall "^2.1.1" + semver "^7.3.4" + slash "^3.0.0" + write-json-file "^4.3.0" -"@lerna/write-log-file@3.13.0": - version "3.13.0" - resolved "https://registry.yarnpkg.com/@lerna/write-log-file/-/write-log-file-3.13.0.tgz#b78d9e4cfc1349a8be64d91324c4c8199e822a26" - integrity sha512-RibeMnDPvlL8bFYW5C8cs4mbI3AHfQef73tnJCQ/SgrXZHehmHnsyWUiE7qDQCAo+B1RfTapvSyFF69iPj326A== +"@lerna/write-log-file@6.1.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@lerna/write-log-file/-/write-log-file-6.1.0.tgz#b811cffd2ea2b3be6239a756c64dac9a3795707a" + integrity sha512-09omu2w4NCt8mJH/X9ZMuToQQ3xu/KpC7EU4yDl2Qy8nxKf8HiG8Oe+YYNprngmkdsq60F5eUZvoiFDZ5JeGIg== dependencies: - npmlog "^4.1.2" - write-file-atomic "^2.3.0" + npmlog "^6.0.2" + write-file-atomic "^4.0.1" "@lint-todo/utils@^13.0.3": version "13.0.3" @@ -2898,14 +2971,6 @@ tslib "^2.3.1" upath "^2.0.1" -"@mrmlnc/readdir-enhanced@^2.2.1": - version "2.2.1" - resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde" - integrity sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g== - dependencies: - call-me-maybe "^1.0.1" - glob-to-regexp "^0.3.0" - "@next/env@10.1.3": version "10.1.3" resolved "https://registry.yarnpkg.com/@next/env/-/env-10.1.3.tgz#29e5d62919b4a7b1859f8d36169848dc3f5ddebe" @@ -2960,25 +3025,111 @@ resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz#a3f2dd61bab43b8db8fa108a121cfffe4c676655" integrity sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q== -"@nodelib/fs.stat@^1.1.2": - version "1.1.3" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b" - integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw== +"@nodelib/fs.walk@^1.2.3": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.6.tgz#cce9396b30aa5afe9e3756608f5831adcb53d063" + integrity sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow== + dependencies: + "@nodelib/fs.scandir" "2.1.4" + fastq "^1.6.0" + +"@npmcli/arborist@5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@npmcli/arborist/-/arborist-5.3.0.tgz#321d9424677bfc08569e98a5ac445ee781f32053" + integrity sha512-+rZ9zgL1lnbl8Xbb1NQdMjveOMwj4lIYfcDtyJHHi5x4X8jtR6m8SXooJMZy5vmFVZ8w7A2Bnd/oX9eTuU8w5A== + dependencies: + "@isaacs/string-locale-compare" "^1.1.0" + "@npmcli/installed-package-contents" "^1.0.7" + "@npmcli/map-workspaces" "^2.0.3" + "@npmcli/metavuln-calculator" "^3.0.1" + "@npmcli/move-file" "^2.0.0" + "@npmcli/name-from-folder" "^1.0.1" + "@npmcli/node-gyp" "^2.0.0" + "@npmcli/package-json" "^2.0.0" + "@npmcli/run-script" "^4.1.3" + bin-links "^3.0.0" + cacache "^16.0.6" + common-ancestor-path "^1.0.1" + json-parse-even-better-errors "^2.3.1" + json-stringify-nice "^1.1.4" + mkdirp "^1.0.4" + mkdirp-infer-owner "^2.0.0" + nopt "^5.0.0" + npm-install-checks "^5.0.0" + npm-package-arg "^9.0.0" + npm-pick-manifest "^7.0.0" + npm-registry-fetch "^13.0.0" + npmlog "^6.0.2" + pacote "^13.6.1" + parse-conflict-json "^2.0.1" + proc-log "^2.0.0" + promise-all-reject-late "^1.0.0" + promise-call-limit "^1.0.1" + read-package-json-fast "^2.0.2" + readdir-scoped-modules "^1.1.0" + rimraf "^3.0.2" + semver "^7.3.7" + ssri "^9.0.0" + treeverse "^2.0.0" + walk-up-path "^1.0.0" + +"@npmcli/fs@^1.0.0": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-1.1.1.tgz#72f719fe935e687c56a4faecf3c03d06ba593257" + integrity sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ== + dependencies: + "@gar/promisify" "^1.0.1" + semver "^7.3.5" + +"@npmcli/fs@^2.1.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-2.1.2.tgz#a9e2541a4a2fec2e69c29b35e6060973da79b865" + integrity sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ== + dependencies: + "@gar/promisify" "^1.1.3" + semver "^7.3.5" + +"@npmcli/git@^3.0.0": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@npmcli/git/-/git-3.0.2.tgz#5c5de6b4d70474cf2d09af149ce42e4e1dacb931" + integrity sha512-CAcd08y3DWBJqJDpfuVL0uijlq5oaXaOJEKHKc4wqrjd00gkvTZB+nFuLn+doOOKddaQS9JfqtNoFCO2LCvA3w== + dependencies: + "@npmcli/promise-spawn" "^3.0.0" + lru-cache "^7.4.4" + mkdirp "^1.0.4" + npm-pick-manifest "^7.0.0" + proc-log "^2.0.0" + promise-inflight "^1.0.1" + promise-retry "^2.0.1" + semver "^7.3.5" + which "^2.0.2" + +"@npmcli/installed-package-contents@^1.0.7": + version "1.0.7" + resolved "https://registry.yarnpkg.com/@npmcli/installed-package-contents/-/installed-package-contents-1.0.7.tgz#ab7408c6147911b970a8abe261ce512232a3f4fa" + integrity sha512-9rufe0wnJusCQoLpV9ZPKIVP55itrM5BxOXs10DmdbRfgWtHy1LDyskbwRnBghuB0PrF7pNPOqREVtpz4HqzKw== + dependencies: + npm-bundled "^1.1.1" + npm-normalize-package-bin "^1.0.1" -"@nodelib/fs.walk@^1.2.3": - version "1.2.6" - resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.6.tgz#cce9396b30aa5afe9e3756608f5831adcb53d063" - integrity sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow== +"@npmcli/map-workspaces@^2.0.3": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@npmcli/map-workspaces/-/map-workspaces-2.0.4.tgz#9e5e8ab655215a262aefabf139782b894e0504fc" + integrity sha512-bMo0aAfwhVwqoVM5UzX1DJnlvVvzDCHae821jv48L1EsrYwfOZChlqWYXEtto/+BkBXetPbEWgau++/brh4oVg== dependencies: - "@nodelib/fs.scandir" "2.1.4" - fastq "^1.6.0" + "@npmcli/name-from-folder" "^1.0.1" + glob "^8.0.1" + minimatch "^5.0.1" + read-package-json-fast "^2.0.3" -"@npmcli/fs@^1.0.0": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-1.1.1.tgz#72f719fe935e687c56a4faecf3c03d06ba593257" - integrity sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ== +"@npmcli/metavuln-calculator@^3.0.1": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@npmcli/metavuln-calculator/-/metavuln-calculator-3.1.1.tgz#9359bd72b400f8353f6a28a25c8457b562602622" + integrity sha512-n69ygIaqAedecLeVH3KnO39M6ZHiJ2dEv5A7DGvcqCB8q17BGUgW8QaanIkbWUo2aYGZqJaOORTLAlIvKjNDKA== dependencies: - "@gar/promisify" "^1.0.1" + cacache "^16.0.0" + json-parse-even-better-errors "^2.3.1" + pacote "^13.0.3" semver "^7.3.5" "@npmcli/move-file@^1.0.1": @@ -2989,119 +3140,179 @@ mkdirp "^1.0.4" rimraf "^3.0.2" -"@octokit/auth-token@^2.4.0": - version "2.4.5" - resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-2.4.5.tgz#568ccfb8cb46f36441fac094ce34f7a875b197f3" - integrity sha512-BpGYsPgJt05M7/L/5FoE1PiAbdxXFZkX/3kDYcsvd1v6UhlnE5e96dTDr0ezX/EFwciQxf3cNV0loipsURU+WA== +"@npmcli/move-file@^2.0.0": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@npmcli/move-file/-/move-file-2.0.1.tgz#26f6bdc379d87f75e55739bab89db525b06100e4" + integrity sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ== + dependencies: + mkdirp "^1.0.4" + rimraf "^3.0.2" + +"@npmcli/name-from-folder@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@npmcli/name-from-folder/-/name-from-folder-1.0.1.tgz#77ecd0a4fcb772ba6fe927e2e2e155fbec2e6b1a" + integrity sha512-qq3oEfcLFwNfEYOQ8HLimRGKlD8WSeGEdtUa7hmzpR8Sa7haL1KVQrvgO6wqMjhWFFVjgtrh1gIxDz+P8sjUaA== + +"@npmcli/node-gyp@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@npmcli/node-gyp/-/node-gyp-2.0.0.tgz#8c20e53e34e9078d18815c1d2dda6f2420d75e35" + integrity sha512-doNI35wIe3bBaEgrlPfdJPaCpUR89pJWep4Hq3aRdh6gKazIVWfs0jHttvSSoq47ZXgC7h73kDsUl8AoIQUB+A== + +"@npmcli/package-json@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@npmcli/package-json/-/package-json-2.0.0.tgz#3bbcf4677e21055adbe673d9f08c9f9cde942e4a" + integrity sha512-42jnZ6yl16GzjWSH7vtrmWyJDGVa/LXPdpN2rcUWolFjc9ON2N3uz0qdBbQACfmhuJZ2lbKYtmK5qx68ZPLHMA== + dependencies: + json-parse-even-better-errors "^2.3.1" + +"@npmcli/promise-spawn@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@npmcli/promise-spawn/-/promise-spawn-3.0.0.tgz#53283b5f18f855c6925f23c24e67c911501ef573" + integrity sha512-s9SgS+p3a9Eohe68cSI3fi+hpcZUmXq5P7w0kMlAsWVtR7XbK3ptkZqKT2cK1zLDObJ3sR+8P59sJE0w/KTL1g== + dependencies: + infer-owner "^1.0.4" + +"@npmcli/run-script@^4.1.0", "@npmcli/run-script@^4.1.3", "@npmcli/run-script@^4.1.7": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@npmcli/run-script/-/run-script-4.2.1.tgz#c07c5c71bc1c70a5f2a06b0d4da976641609b946" + integrity sha512-7dqywvVudPSrRCW5nTHpHgeWnbBtz8cFkOuKrecm6ih+oO9ciydhWt6OF7HlqupRRmB8Q/gECVdB9LMfToJbRg== + dependencies: + "@npmcli/node-gyp" "^2.0.0" + "@npmcli/promise-spawn" "^3.0.0" + node-gyp "^9.0.0" + read-package-json-fast "^2.0.3" + which "^2.0.2" + +"@nrwl/cli@15.2.1": + version "15.2.1" + resolved "https://registry.yarnpkg.com/@nrwl/cli/-/cli-15.2.1.tgz#9de75e20429315bf42516504601a785141fa6e8d" + integrity sha512-ufBJ5o3WCixdp6/TPkpn4rH3QBFJcqCMG1d14A/SvAkEnXu3vWlj3E+4GXf3CK+sGNjjf3ZGoNm7OFV+/Ml4GA== + dependencies: + nx "15.2.1" + +"@nrwl/devkit@>=14.8.6 < 16": + version "15.2.1" + resolved "https://registry.yarnpkg.com/@nrwl/devkit/-/devkit-15.2.1.tgz#3907ee18d4bcb9ab36fc950c48c9cb0466ff8ab8" + integrity sha512-si6DK0sjtr6Ln+7hh9QD50KqxnOxuP20xJ0fxMVafT/Lz/qtxuCPw9lwJWVagMHWw3pQAtN6lbkWiLOwj8+YPA== + dependencies: + "@phenomnomnominal/tsquery" "4.1.1" + ejs "^3.1.7" + ignore "^5.0.4" + semver "7.3.4" + tslib "^2.3.0" + +"@nrwl/tao@15.2.1": + version "15.2.1" + resolved "https://registry.yarnpkg.com/@nrwl/tao/-/tao-15.2.1.tgz#da6a6813eaedcb907b3917d06e4d5bbc4501dbaf" + integrity sha512-6ApglWKORasLhtjlJmqo2co/UexSSiD7mWVxT2886oKG2Y/T/5RnPFNhJgGnKJIURCNMxiSKhq7GAA+CE9zRZg== + dependencies: + nx "15.2.1" + +"@octokit/auth-token@^3.0.0": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-3.0.2.tgz#a0fc8de149fd15876e1ac78f6525c1c5ab48435f" + integrity sha512-pq7CwIMV1kmzkFTimdwjAINCXKTajZErLB4wMLYapR2nuB/Jpr66+05wOTZMSCBXP6n4DdDWT2W19Bm17vU69Q== dependencies: - "@octokit/types" "^6.0.3" + "@octokit/types" "^8.0.0" -"@octokit/endpoint@^6.0.1": - version "6.0.11" - resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-6.0.11.tgz#082adc2aebca6dcefa1fb383f5efb3ed081949d1" - integrity sha512-fUIPpx+pZyoLW4GCs3yMnlj2LfoXTWDUVPTC4V3MUEKZm48W+XYpeWSZCv+vYF1ZABUm2CqnDVf1sFtIYrj7KQ== +"@octokit/core@^4.1.0": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@octokit/core/-/core-4.1.0.tgz#b6b03a478f1716de92b3f4ec4fd64d05ba5a9251" + integrity sha512-Czz/59VefU+kKDy+ZfDwtOIYIkFjExOKf+HA92aiTZJ6EfWpFzYQWw0l54ji8bVmyhc+mGaLUbSUmXazG7z5OQ== + dependencies: + "@octokit/auth-token" "^3.0.0" + "@octokit/graphql" "^5.0.0" + "@octokit/request" "^6.0.0" + "@octokit/request-error" "^3.0.0" + "@octokit/types" "^8.0.0" + before-after-hook "^2.2.0" + universal-user-agent "^6.0.0" + +"@octokit/endpoint@^7.0.0": + version "7.0.3" + resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-7.0.3.tgz#0b96035673a9e3bedf8bab8f7335de424a2147ed" + integrity sha512-57gRlb28bwTsdNXq+O3JTQ7ERmBTuik9+LelgcLIVfYwf235VHbN9QNo4kXExtp/h8T423cR5iJThKtFYxC7Lw== dependencies: - "@octokit/types" "^6.0.3" + "@octokit/types" "^8.0.0" is-plain-object "^5.0.0" universal-user-agent "^6.0.0" -"@octokit/openapi-types@^6.0.0": - version "6.0.0" - resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-6.0.0.tgz#7da8d7d5a72d3282c1a3ff9f951c8133a707480d" - integrity sha512-CnDdK7ivHkBtJYzWzZm7gEkanA7gKH6a09Eguz7flHw//GacPJLmkHA3f3N++MJmlxD1Fl+mB7B32EEpSCwztQ== +"@octokit/graphql@^5.0.0": + version "5.0.4" + resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-5.0.4.tgz#519dd5c05123868276f3ae4e50ad565ed7dff8c8" + integrity sha512-amO1M5QUQgYQo09aStR/XO7KAl13xpigcy/kI8/N1PnZYSS69fgte+xA4+c2DISKqUZfsh0wwjc2FaCt99L41A== + dependencies: + "@octokit/request" "^6.0.0" + "@octokit/types" "^8.0.0" + universal-user-agent "^6.0.0" -"@octokit/plugin-enterprise-rest@^2.1.1": - version "2.2.2" - resolved "https://registry.yarnpkg.com/@octokit/plugin-enterprise-rest/-/plugin-enterprise-rest-2.2.2.tgz#c0e22067a043e19f96ff9c7832e2a3019f9be75c" - integrity sha512-CTZr64jZYhGWNTDGlSJ2mvIlFsm9OEO3LqWn9I/gmoHI4jRBp4kpHoFYNemG4oA75zUAcmbuWblb7jjP877YZw== +"@octokit/openapi-types@^14.0.0": + version "14.0.0" + resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-14.0.0.tgz#949c5019028c93f189abbc2fb42f333290f7134a" + integrity sha512-HNWisMYlR8VCnNurDU6os2ikx0s0VyEjDYHNS/h4cgb8DeOxQ0n72HyinUtdDVxJhFy3FWLGl0DJhfEWk3P5Iw== -"@octokit/plugin-paginate-rest@^1.1.1": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-1.1.2.tgz#004170acf8c2be535aba26727867d692f7b488fc" - integrity sha512-jbsSoi5Q1pj63sC16XIUboklNw+8tL9VOnJsWycWYR78TKss5PVpIPb1TUUcMQ+bBh7cY579cVAWmf5qG+dw+Q== +"@octokit/plugin-enterprise-rest@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@octokit/plugin-enterprise-rest/-/plugin-enterprise-rest-6.0.1.tgz#e07896739618dab8da7d4077c658003775f95437" + integrity sha512-93uGjlhUD+iNg1iWhUENAtJata6w5nE+V4urXOAlIXdco6xNZtUSfYY8dzp3Udy74aqO/B5UZL80x/YMa5PKRw== + +"@octokit/plugin-paginate-rest@^5.0.0": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-5.0.1.tgz#93d7e74f1f69d68ba554fa6b888c2a9cf1f99a83" + integrity sha512-7A+rEkS70pH36Z6JivSlR7Zqepz3KVucEFVDnSrgHXzG7WLAzYwcHZbKdfTXHwuTHbkT1vKvz7dHl1+HNf6Qyw== dependencies: - "@octokit/types" "^2.0.1" + "@octokit/types" "^8.0.0" -"@octokit/plugin-request-log@^1.0.0": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-1.0.3.tgz#70a62be213e1edc04bb8897ee48c311482f9700d" - integrity sha512-4RFU4li238jMJAzLgAwkBAw+4Loile5haQMQr+uhFq27BmyJXcXSKvoQKqh0agsZEiUlW6iSv3FAgvmGkur7OQ== +"@octokit/plugin-request-log@^1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz#5e50ed7083a613816b1e4a28aeec5fb7f1462e85" + integrity sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA== -"@octokit/plugin-rest-endpoint-methods@2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-2.4.0.tgz#3288ecf5481f68c494dd0602fc15407a59faf61e" - integrity sha512-EZi/AWhtkdfAYi01obpX0DF7U6b1VRr30QNQ5xSFPITMdLSfhcBqjamE3F+sKcxPbD7eZuMHu3Qkk2V+JGxBDQ== +"@octokit/plugin-rest-endpoint-methods@^6.7.0": + version "6.7.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-6.7.0.tgz#2f6f17f25b6babbc8b41d2bb0a95a8839672ce7c" + integrity sha512-orxQ0fAHA7IpYhG2flD2AygztPlGYNAdlzYz8yrD8NDgelPfOYoRPROfEyIe035PlxvbYrgkfUZIhSBKju/Cvw== dependencies: - "@octokit/types" "^2.0.1" + "@octokit/types" "^8.0.0" deprecation "^2.3.1" -"@octokit/request-error@^1.0.2": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-1.2.1.tgz#ede0714c773f32347576c25649dc013ae6b31801" - integrity sha512-+6yDyk1EES6WK+l3viRDElw96MvwfJxCt45GvmjDUKWjYIb3PJZQkq3i46TwGwoPD4h8NmTrENmtyA1FwbmhRA== - dependencies: - "@octokit/types" "^2.0.0" - deprecation "^2.0.0" - once "^1.4.0" - -"@octokit/request-error@^2.0.0": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-2.0.5.tgz#72cc91edc870281ad583a42619256b380c600143" - integrity sha512-T/2wcCFyM7SkXzNoyVNWjyVlUwBvW3igM3Btr/eKYiPmucXTtkxt2RBsf6gn3LTzaLSLTQtNmvg+dGsOxQrjZg== +"@octokit/request-error@^3.0.0": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-3.0.2.tgz#f74c0f163d19463b87528efe877216c41d6deb0a" + integrity sha512-WMNOFYrSaX8zXWoJg9u/pKgWPo94JXilMLb2VManNOby9EZxrQaBe/QSC4a1TzpAlpxofg2X/jMnCyZgL6y7eg== dependencies: - "@octokit/types" "^6.0.3" + "@octokit/types" "^8.0.0" deprecation "^2.0.0" once "^1.4.0" -"@octokit/request@^5.2.0": - version "5.4.14" - resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.4.14.tgz#ec5f96f78333bb2af390afa5ff66f114b063bc96" - integrity sha512-VkmtacOIQp9daSnBmDI92xNIeLuSRDOIuplp/CJomkvzt7M18NXgG044Cx/LFKLgjKt9T2tZR6AtJayba9GTSA== +"@octokit/request@^6.0.0": + version "6.2.2" + resolved "https://registry.yarnpkg.com/@octokit/request/-/request-6.2.2.tgz#a2ba5ac22bddd5dcb3f539b618faa05115c5a255" + integrity sha512-6VDqgj0HMc2FUX2awIs+sM6OwLgwHvAi4KCK3mT2H2IKRt6oH9d0fej5LluF5mck1lRR/rFWN0YIDSYXYSylbw== dependencies: - "@octokit/endpoint" "^6.0.1" - "@octokit/request-error" "^2.0.0" - "@octokit/types" "^6.7.1" - deprecation "^2.0.0" + "@octokit/endpoint" "^7.0.0" + "@octokit/request-error" "^3.0.0" + "@octokit/types" "^8.0.0" is-plain-object "^5.0.0" - node-fetch "^2.6.1" - once "^1.4.0" + node-fetch "^2.6.7" universal-user-agent "^6.0.0" -"@octokit/rest@^16.16.0": - version "16.43.2" - resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-16.43.2.tgz#c53426f1e1d1044dee967023e3279c50993dd91b" - integrity sha512-ngDBevLbBTFfrHZeiS7SAMAZ6ssuVmXuya+F/7RaVvlysgGa1JKJkKWY+jV6TCJYcW0OALfJ7nTIGXcBXzycfQ== - dependencies: - "@octokit/auth-token" "^2.4.0" - "@octokit/plugin-paginate-rest" "^1.1.1" - "@octokit/plugin-request-log" "^1.0.0" - "@octokit/plugin-rest-endpoint-methods" "2.4.0" - "@octokit/request" "^5.2.0" - "@octokit/request-error" "^1.0.2" - atob-lite "^2.0.0" - before-after-hook "^2.0.0" - btoa-lite "^1.0.0" - deprecation "^2.0.0" - lodash.get "^4.4.2" - lodash.set "^4.3.2" - lodash.uniq "^4.5.0" - octokit-pagination-methods "^1.1.0" - once "^1.4.0" - universal-user-agent "^4.0.0" - -"@octokit/types@^2.0.0", "@octokit/types@^2.0.1": - version "2.16.2" - resolved "https://registry.yarnpkg.com/@octokit/types/-/types-2.16.2.tgz#4c5f8da3c6fecf3da1811aef678fda03edac35d2" - integrity sha512-O75k56TYvJ8WpAakWwYRN8Bgu60KrmX0z1KqFp1kNiFNkgW+JW+9EBKZ+S33PU6SLvbihqd+3drvPxKK68Ee8Q== +"@octokit/rest@^19.0.3": + version "19.0.5" + resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-19.0.5.tgz#4dbde8ae69b27dca04b5f1d8119d282575818f6c" + integrity sha512-+4qdrUFq2lk7Va+Qff3ofREQWGBeoTKNqlJO+FGjFP35ZahP+nBenhZiGdu8USSgmq4Ky3IJ/i4u0xbLqHaeow== dependencies: - "@types/node" ">= 8" + "@octokit/core" "^4.1.0" + "@octokit/plugin-paginate-rest" "^5.0.0" + "@octokit/plugin-request-log" "^1.0.4" + "@octokit/plugin-rest-endpoint-methods" "^6.7.0" -"@octokit/types@^6.0.3", "@octokit/types@^6.7.1": - version "6.13.0" - resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.13.0.tgz#779e5b7566c8dde68f2f6273861dd2f0409480d0" - integrity sha512-W2J9qlVIU11jMwKHUp5/rbVUeErqelCsO5vW5PKNb7wAXQVUz87Rc+imjlEvpvbH8yUb+KHmv8NEjVZdsdpyxA== +"@octokit/types@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@octokit/types/-/types-8.0.0.tgz#93f0b865786c4153f0f6924da067fe0bb7426a9f" + integrity sha512-65/TPpOJP1i3K4lBJMnWqPUJ6zuOtzhtagDvydAWbEXpbFYA0oMKKyLb95NFZZP0lSh/4b6K+DQlzvYQJQQePg== dependencies: - "@octokit/openapi-types" "^6.0.0" + "@octokit/openapi-types" "^14.0.0" "@opentelemetry/api@0.14.0": version "0.14.0" @@ -3225,6 +3436,21 @@ "@opentelemetry/resources" "^0.12.0" "@opentelemetry/semantic-conventions" "^0.12.0" +"@parcel/watcher@2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.0.4.tgz#f300fef4cc38008ff4b8c29d92588eced3ce014b" + integrity sha512-cTDi+FUDBIUOBKEtj+nhiJ71AZVlkAsQFuGQTun5tV9mwQBQgZvhCzG+URPQc8myeN32yRVZEfVAPCs1RW+Jvg== + dependencies: + node-addon-api "^3.2.1" + node-gyp-build "^4.3.0" + +"@phenomnomnominal/tsquery@4.1.1": + version "4.1.1" + resolved "https://registry.yarnpkg.com/@phenomnomnominal/tsquery/-/tsquery-4.1.1.tgz#42971b83590e9d853d024ddb04a18085a36518df" + integrity sha512-jjMmK1tnZbm1Jq5a7fBliM4gQwjxMU7TFoRNwIyzwlO+eHPRCFv/Nv+H/Gi1jc3WR7QURG8D5d0Tn12YGrUqBQ== + dependencies: + esquery "^1.0.1" + "@playwright/test@^1.29.2": version "1.29.2" resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.29.2.tgz#c48184721d0f0b7627a886e2ec42f1efb2be339d" @@ -4327,7 +4553,7 @@ "@types/node" "*" form-data "^3.0.0" -"@types/node@*", "@types/node@>= 8", "@types/node@>=10.0.0", "@types/node@>=12.12.47", "@types/node@>=13.7.0": +"@types/node@*", "@types/node@>=10.0.0", "@types/node@>=12.12.47", "@types/node@>=13.7.0": version "17.0.38" resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.38.tgz#f8bb07c371ccb1903f3752872c89f44006132947" integrity sha512-5jY9RhV7c0Z4Jy09G+NIDTsCZ5G0L5n+Z+p+Y7t5VJHM30bgwzSjVtlcBxqAj+6L/swIlvtOSzr8rBk/aNyV2g== @@ -4352,6 +4578,11 @@ resolved "https://registry.yarnpkg.com/@types/pako/-/pako-2.0.0.tgz#12ab4c19107528452e73ac99132c875ccd43bdfb" integrity sha512-10+iaz93qR5WYxTo+PMifD5TSxiOtdRaxBf7INGGXMQgTCu8Z/7GYWYFUOS3q/G0nE5boj1r4FEB+WSy7s5gbA== +"@types/parse-json@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" + integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== + "@types/parse5@*": version "6.0.0" resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-6.0.0.tgz#38590dc2c3cf5717154064e3ee9b6947ee21b299" @@ -5155,6 +5386,21 @@ resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31" integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ== +"@yarnpkg/parsers@^3.0.0-rc.18": + version "3.0.0-rc.31" + resolved "https://registry.yarnpkg.com/@yarnpkg/parsers/-/parsers-3.0.0-rc.31.tgz#fbcce77c3783b2be8a381edf70bea3182e0b8b16" + integrity sha512-7M67TPmTM5OmtoypK0KHV3vIY9z0v4qZ6zF7flH8THLgjGuoA7naop8pEfL9x5vCtid1PDC4A4COrcym4WAZpQ== + dependencies: + js-yaml "^3.10.0" + tslib "^2.4.0" + +"@zkochan/js-yaml@0.0.6": + version "0.0.6" + resolved "https://registry.yarnpkg.com/@zkochan/js-yaml/-/js-yaml-0.0.6.tgz#975f0b306e705e28b8068a07737fa46d3fc04826" + integrity sha512-nzvgl3VfhcELQ8LyVrYOru+UtAy1nrygk2+AGbTm8a5YcO6o8lSjAT+pfg3vJWxIoZKOUhrK6UU7xW/+00kQrg== + dependencies: + argparse "^2.0.1" + "@zxing/text-encoding@0.9.0": version "0.9.0" resolved "https://registry.yarnpkg.com/@zxing/text-encoding/-/text-encoding-0.9.0.tgz#fb50ffabc6c7c66a0c96b4c03e3d9be74864b70b" @@ -5173,7 +5419,7 @@ abab@^2.0.3, abab@^2.0.5: resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a" integrity sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q== -abbrev@1: +abbrev@1, abbrev@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== @@ -5246,6 +5492,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== +add-stream@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/add-stream/-/add-stream-1.0.0.tgz#6a7990437ca736d5e1288db92bd3266d5f5cb2aa" + integrity sha512-qQLMr+8o0WC4FZGQTcJiKBVC59JylcPSrTtk6usvmIDFUOCKegapy1VHQwRbFMOFyb/inzUVqHs+eMYKDM1YeQ== + adjust-sourcemap-loader@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/adjust-sourcemap-loader/-/adjust-sourcemap-loader-3.0.0.tgz#5ae12fb5b7b1c585e80bbb5a63ec163a1a45e61e" @@ -5254,7 +5505,7 @@ adjust-sourcemap-loader@3.0.0: loader-utils "^2.0.0" regex-parser "^2.2.11" -agent-base@4, agent-base@5, agent-base@6, agent-base@^4.3.0, agent-base@~4.2.1: +agent-base@4, agent-base@5, agent-base@6, agent-base@^4.3.0, agent-base@^6.0.2, agent-base@~4.2.1: version "5.1.1" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-5.1.1.tgz#e8fb3f242959db44d63be665db7a8e739537a32c" integrity sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g== @@ -5266,6 +5517,15 @@ agentkeepalive@^3.4.1: dependencies: humanize-ms "^1.2.1" +agentkeepalive@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.2.1.tgz#a7975cbb9f83b367f06c90cc51ff28fe7d499717" + integrity sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA== + dependencies: + debug "^4.1.0" + depd "^1.1.2" + humanize-ms "^1.2.1" + aggregate-error@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" @@ -5655,10 +5915,10 @@ arr-union@^3.1.0: resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= -array-differ@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-1.0.0.tgz#eff52e3758249d33be402b8bb8e564bb2b5d4031" - integrity sha1-7/UuN1gknTO+QCuLuOVkuytdQDE= +array-differ@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-3.0.0.tgz#3cbb3d0f316810eafcc47624734237d6aee4ae6b" + integrity sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg== array-equal@^1.0.0: version "1.0.0" @@ -5771,7 +6031,7 @@ array.prototype.tosorted@^1.1.1: es-shim-unscopables "^1.0.0" get-intrinsic "^1.1.3" -arrify@^1.0.0, arrify@^1.0.1: +arrify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0= @@ -5943,6 +6203,11 @@ async@^3.0.1: resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720" integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw== +async@^3.2.3: + version "3.2.4" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" + integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== + async@~0.2.9: version "0.2.10" resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1" @@ -5958,11 +6223,6 @@ at-least-node@^1.0.0: resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== -atob-lite@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/atob-lite/-/atob-lite-2.0.0.tgz#0fef5ad46f1bd7a8502c65727f0367d5ee43d696" - integrity sha1-D+9a1G8b16hQLGVyfwNn1e5D1pY= - atob@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" @@ -6034,6 +6294,15 @@ axios@^0.27.2: follow-redirects "^1.14.9" form-data "^4.0.0" +axios@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.2.0.tgz#1cb65bd75162c70e9f8d118a905126c4a201d383" + integrity sha512-zT7wZyNYu3N5Bu0wuZ6QccIf93Qk1eV8LOewxgjOZFd2DenOs98cJ7+Y6703d0wkaXGY6/nZd4EweJaHz9uzQw== + dependencies: + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + 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" @@ -6855,10 +7124,10 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" -before-after-hook@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.2.0.tgz#09c40d92e936c64777aa385c4e9b904f8147eaf0" - integrity sha512-jH6rKQIfroBbhEXVmI7XmXe3ix5S/PgJqpzdDPnR8JGLHWNYLsYZ6tK5iWOF/Ra3oqEX0NobXGlzbiylIzVphQ== +before-after-hook@^2.2.0: + version "2.2.3" + resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.2.3.tgz#c51e809c81a4e354084422b9b26bad88249c517c" + integrity sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ== big.js@^5.2.2: version "5.2.2" @@ -6880,6 +7149,18 @@ bignumber.js@^9.0.0: resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.1.tgz#8d7ba124c882bfd8e43260c67475518d0689e4e5" integrity sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA== +bin-links@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/bin-links/-/bin-links-3.0.3.tgz#3842711ef3db2cd9f16a5f404a996a12db355a6e" + integrity sha512-zKdnMPWEdh4F5INR07/eBrodC7QrF5JKvqskjz/ZZRXg5YSAZIbn8zGhbhUrElzHBZ2fvEQdOU59RHcTG3GiwA== + dependencies: + cmd-shim "^5.0.0" + mkdirp-infer-owner "^2.0.0" + npm-normalize-package-bin "^2.0.0" + read-cmd-shim "^3.0.0" + rimraf "^3.0.0" + write-file-atomic "^4.0.0" + binary-extensions@^1.0.0: version "1.13.1" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" @@ -7668,11 +7949,6 @@ bson@^1.1.4: resolved "https://registry.yarnpkg.com/bson/-/bson-1.1.6.tgz#fb819be9a60cd677e0853aee4ca712a785d6618a" integrity sha512-EvVNVeGo4tHxwi8L6bPj3y3itEvStdwvvlojVxxbyYfoaxJ6keLgrTuKdyfEAszFK+H3olzBuafE0yoh0D1gdg== -btoa-lite@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/btoa-lite/-/btoa-lite-1.0.0.tgz#337766da15801210fdd956c22e9c6891ab9d0337" - integrity sha1-M3dm2hWAEhD92VbCLpxokaudAzc= - btoa@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/btoa/-/btoa-1.2.1.tgz#01a9909f8b2c93f6bf680ba26131eb30f7fa3d73" @@ -7755,15 +8031,10 @@ builtins@^5.0.0: dependencies: semver "^7.0.0" -byline@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/byline/-/byline-5.0.0.tgz#741c5216468eadc457b03410118ad77de8c1ddb1" - integrity sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE= - -byte-size@^4.0.3: - version "4.0.4" - resolved "https://registry.yarnpkg.com/byte-size/-/byte-size-4.0.4.tgz#29d381709f41aae0d89c631f1c81aec88cd40b23" - integrity sha512-82RPeneC6nqCdSwCX2hZUz3JPOvN5at/nTEw/CMf05Smu3Hrpo9Psb7LjN+k+XndNArG1EY8L4+BM3aTM4BCvw== +byte-size@^7.0.0: + version "7.0.1" + resolved "https://registry.yarnpkg.com/byte-size/-/byte-size-7.0.1.tgz#b1daf3386de7ab9d706b941a748dbfc71130dee3" + integrity sha512-crQdqyCwhokxwV1UyDzLZanhkugAgft7vt0qbbdt60C6Zf3CAiGmtUCylbtYwrU6loOUw3euGrNtW1J651ot1A== bytes@1: version "1.0.0" @@ -7808,26 +8079,6 @@ cacache@15.0.5: tar "^6.0.2" unique-filename "^1.1.1" -cacache@^11.3.3: - version "11.3.3" - resolved "https://registry.yarnpkg.com/cacache/-/cacache-11.3.3.tgz#8bd29df8c6a718a6ebd2d010da4d7972ae3bbadc" - integrity sha512-p8WcneCytvzPxhDvYp31PD039vi77I12W+/KfR9S8AZbaiARFBCpsPJS+9uhWfeBfeAtW7o/4vt3MUqLkbY6nA== - dependencies: - bluebird "^3.5.5" - chownr "^1.1.1" - figgy-pudding "^3.5.1" - glob "^7.1.4" - graceful-fs "^4.1.15" - lru-cache "^5.1.1" - mississippi "^3.0.0" - mkdirp "^0.5.1" - move-concurrently "^1.0.1" - promise-inflight "^1.0.1" - rimraf "^2.6.3" - ssri "^6.0.1" - unique-filename "^1.1.1" - y18n "^4.0.0" - cacache@^12.0.0, cacache@^12.0.2: version "12.0.4" resolved "https://registry.yarnpkg.com/cacache/-/cacache-12.0.4.tgz#668bcbd105aeb5f1d92fe25570ec9525c8faa40c" @@ -7873,6 +8124,30 @@ cacache@^15.0.4, cacache@^15.0.5: tar "^6.0.2" unique-filename "^1.1.1" +cacache@^16.0.0, cacache@^16.0.6, cacache@^16.1.0: + version "16.1.3" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-16.1.3.tgz#a02b9f34ecfaf9a78c9f4bc16fceb94d5d67a38e" + integrity sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ== + dependencies: + "@npmcli/fs" "^2.1.0" + "@npmcli/move-file" "^2.0.0" + chownr "^2.0.0" + fs-minipass "^2.1.0" + glob "^8.0.1" + infer-owner "^1.0.4" + lru-cache "^7.7.1" + minipass "^3.1.6" + minipass-collect "^1.0.2" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.4" + mkdirp "^1.0.4" + p-map "^4.0.0" + promise-inflight "^1.0.1" + rimraf "^3.0.2" + ssri "^9.0.0" + tar "^6.1.11" + unique-filename "^2.0.0" + cache-base@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" @@ -7929,11 +8204,6 @@ call-bind@^1.0.0, call-bind@^1.0.2: function-bind "^1.1.1" get-intrinsic "^1.0.2" -call-me-maybe@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b" - integrity sha1-JtII6onje1y95gJQoV8DHBak1ms= - caller-callsite@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134" @@ -7974,15 +8244,6 @@ camelcase-keys@^2.0.0: camelcase "^2.0.0" map-obj "^1.0.0" -camelcase-keys@^4.0.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-4.2.0.tgz#a2aa5fb1af688758259c32c141426d78923b9b77" - integrity sha1-oqpfsa9oh1glnDLBQUJteJI7m3c= - dependencies: - camelcase "^4.1.0" - map-obj "^2.0.0" - quick-lru "^1.0.0" - camelcase-keys@^6.2.2: version "6.2.2" resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-6.2.2.tgz#5e755d6ba51aa223ec7d3d52f25778210f9dc3c0" @@ -8002,11 +8263,6 @@ camelcase@^2.0.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" integrity sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8= -camelcase@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" - integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0= - camelcase@^6.0.0, camelcase@^6.1.0, camelcase@^6.2.0: version "6.3.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" @@ -8071,7 +8327,7 @@ chai@^4.1.2: pathval "^1.1.1" type-detect "^4.0.5" -chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.1, chalk@^2.4.1, chalk@^2.4.2: +chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -8096,6 +8352,14 @@ chalk@4.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +chalk@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" + integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + chalk@^1.0.0, chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" @@ -8107,7 +8371,7 @@ chalk@^1.0.0, chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" -chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: +chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -8203,11 +8467,6 @@ chrome-trace-event@^1.0.2: dependencies: tslib "^1.9.0" -ci-info@^1.5.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.6.0.tgz#2ca20dbb9ceb32d4524a683303313f0304b1e497" - integrity sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A== - ci-info@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" @@ -8300,6 +8559,13 @@ 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-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" + integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== + dependencies: + restore-cursor "^3.1.0" + cli-cursor@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" @@ -8307,14 +8573,7 @@ cli-cursor@^2.1.0: dependencies: restore-cursor "^2.0.0" -cli-cursor@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" - integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== - dependencies: - restore-cursor "^3.1.0" - -cli-spinners@^2.0.0, cli-spinners@^2.4.0, cli-spinners@^2.5.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== @@ -8345,15 +8604,6 @@ cli-width@^3.0.0: resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== -cliui@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49" - integrity sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ== - dependencies: - string-width "^2.1.1" - strip-ansi "^4.0.0" - wrap-ansi "^2.0.0" - cliui@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" @@ -8401,6 +8651,15 @@ clone-deep@^0.2.4: lazy-cache "^1.0.3" shallow-clone "^0.1.2" +clone-deep@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" + integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== + dependencies: + is-plain-object "^2.0.4" + kind-of "^6.0.2" + shallow-clone "^3.0.0" + clone-response@1.0.2, clone-response@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b" @@ -8418,13 +8677,12 @@ clone@^2.1.2: resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18= -cmd-shim@^2.0.2: - version "2.1.0" - resolved "https://registry.yarnpkg.com/cmd-shim/-/cmd-shim-2.1.0.tgz#e59a08d4248dda3bb502044083a4db4ac890579a" - integrity sha512-A5C0Cyf2H8sKsHqX0tvIWRXw5/PK++3Dc0lDbsugr90nOECLLuSPahVQBG8pgmgiXgm/TzBWMqI2rWdZwHduAw== +cmd-shim@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/cmd-shim/-/cmd-shim-5.0.0.tgz#8d0aaa1a6b0708630694c4dbde070ed94c707724" + integrity sha512-qkCtZ59BidfEwHltnJwkyVZn+XQojdAySM1D1gSeh11Z4pW1Kpolkyo53L5noc0nrxmIvyFwTmJRo4xs7FFLPw== dependencies: - graceful-fs "^4.1.2" - mkdirp "~0.5.0" + mkdirp-infer-owner "^2.0.0" co@^4.6.0: version "4.6.0" @@ -8529,12 +8787,12 @@ colors@1.4.0, colors@^1.4.0: resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== -columnify@^1.5.4: - version "1.5.4" - resolved "https://registry.yarnpkg.com/columnify/-/columnify-1.5.4.tgz#4737ddf1c7b69a8a7c340570782e947eec8e78bb" - integrity sha1-Rzfd8ce2mop8NAVweC6UfuyOeLs= +columnify@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/columnify/-/columnify-1.6.0.tgz#6989531713c9008bb29735e61e37acf5bd553cf3" + integrity sha512-lomjuFZKfM6MSAnV9aCZC9sc0qGbmZdfygNv+nCpqVkSKdCxCklLtd16O0EILGkImHw9ZpHkAnHaB+8Zxq5W6Q== dependencies: - strip-ansi "^3.0.0" + strip-ansi "^6.0.1" wcwidth "^1.0.0" combine-source-map@^0.8.0: @@ -8606,6 +8864,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" @@ -8681,10 +8944,10 @@ concat-stream@^2.0.0: readable-stream "^3.0.2" typedarray "^0.0.6" -config-chain@^1.1.11: - version "1.1.12" - resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.12.tgz#0fde8d091200eb5e808caf25fe618c02f48e4efa" - integrity sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA== +config-chain@^1.1.12: + version "1.1.13" + resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.13.tgz#fad0795aa6a6cdaff9ed1b68e9dff94372c232f4" + integrity sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ== dependencies: ini "^1.3.4" proto-list "~1.2.1" @@ -8771,47 +9034,47 @@ continuable-cache@^0.3.1: resolved "https://registry.yarnpkg.com/continuable-cache/-/continuable-cache-0.3.1.tgz#bd727a7faed77e71ff3985ac93351a912733ad0f" integrity sha1-vXJ6f67XfnH/OYWskzUakSczrQ8= -conventional-changelog-angular@^5.0.3: - version "5.0.12" - resolved "https://registry.yarnpkg.com/conventional-changelog-angular/-/conventional-changelog-angular-5.0.12.tgz#c979b8b921cbfe26402eb3da5bbfda02d865a2b9" - integrity sha512-5GLsbnkR/7A89RyHLvvoExbiGbd9xKdKqDTrArnPbOqBqG/2wIosu0fHwpeIRI8Tl94MhVNBXcLJZl92ZQ5USw== +conventional-changelog-angular@^5.0.12: + version "5.0.13" + resolved "https://registry.yarnpkg.com/conventional-changelog-angular/-/conventional-changelog-angular-5.0.13.tgz#896885d63b914a70d4934b59d2fe7bde1832b28c" + integrity sha512-i/gipMxs7s8L/QeuavPF2hLnJgH6pEZAttySB6aiQLWcX3puWDL3ACVmvBhJGxnAy52Qc15ua26BufY6KpmrVA== dependencies: compare-func "^2.0.0" q "^1.5.1" -conventional-changelog-core@^3.1.6: - version "3.2.3" - resolved "https://registry.yarnpkg.com/conventional-changelog-core/-/conventional-changelog-core-3.2.3.tgz#b31410856f431c847086a7dcb4d2ca184a7d88fb" - integrity sha512-LMMX1JlxPIq/Ez5aYAYS5CpuwbOk6QFp8O4HLAcZxe3vxoCtABkhfjetk8IYdRB9CDQGwJFLR3Dr55Za6XKgUQ== +conventional-changelog-core@^4.2.4: + version "4.2.4" + resolved "https://registry.yarnpkg.com/conventional-changelog-core/-/conventional-changelog-core-4.2.4.tgz#e50d047e8ebacf63fac3dc67bf918177001e1e9f" + integrity sha512-gDVS+zVJHE2v4SLc6B0sLsPiloR0ygU7HaDW14aNJE1v4SlqJPILPl/aJC7YdtRE4CybBf8gDwObBvKha8Xlyg== dependencies: - conventional-changelog-writer "^4.0.6" - conventional-commits-parser "^3.0.3" + add-stream "^1.0.0" + conventional-changelog-writer "^5.0.0" + conventional-commits-parser "^3.2.0" dateformat "^3.0.0" - get-pkg-repo "^1.0.0" - git-raw-commits "2.0.0" + get-pkg-repo "^4.0.0" + git-raw-commits "^2.0.8" git-remote-origin-url "^2.0.0" - git-semver-tags "^2.0.3" - lodash "^4.2.1" - normalize-package-data "^2.3.5" + git-semver-tags "^4.1.1" + lodash "^4.17.15" + normalize-package-data "^3.0.0" q "^1.5.1" read-pkg "^3.0.0" read-pkg-up "^3.0.0" - through2 "^3.0.0" + through2 "^4.0.0" -conventional-changelog-preset-loader@^2.1.1: +conventional-changelog-preset-loader@^2.3.4: version "2.3.4" resolved "https://registry.yarnpkg.com/conventional-changelog-preset-loader/-/conventional-changelog-preset-loader-2.3.4.tgz#14a855abbffd59027fd602581f1f34d9862ea44c" integrity sha512-GEKRWkrSAZeTq5+YjUZOYxdHq+ci4dNwHvpaBC3+ENalzFWuCWa9EZXSuZBpkr72sMdKB+1fyDV4takK1Lf58g== -conventional-changelog-writer@^4.0.6: - version "4.1.0" - resolved "https://registry.yarnpkg.com/conventional-changelog-writer/-/conventional-changelog-writer-4.1.0.tgz#1ca7880b75aa28695ad33312a1f2366f4b12659f" - integrity sha512-WwKcUp7WyXYGQmkLsX4QmU42AZ1lqlvRW9mqoyiQzdD+rJWbTepdWoKJuwXTS+yq79XKnQNa93/roViPQrAQgw== +conventional-changelog-writer@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/conventional-changelog-writer/-/conventional-changelog-writer-5.0.1.tgz#e0757072f045fe03d91da6343c843029e702f359" + integrity sha512-5WsuKUfxW7suLblAbFnxAcrvf6r+0b7GvNaWUwUIk0bXMnENP/PEieGKVUQrjPqwPT4o3EPAASBXiY6iHooLOQ== dependencies: - compare-func "^2.0.0" conventional-commits-filter "^2.0.7" dateformat "^3.0.0" - handlebars "^4.7.6" + handlebars "^4.7.7" json-stringify-safe "^5.0.1" lodash "^4.17.15" meow "^8.0.0" @@ -8819,7 +9082,7 @@ conventional-changelog-writer@^4.0.6: split "^1.0.0" through2 "^4.0.0" -conventional-commits-filter@^2.0.2, conventional-commits-filter@^2.0.7: +conventional-commits-filter@^2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/conventional-commits-filter/-/conventional-commits-filter-2.0.7.tgz#f8d9b4f182fce00c9af7139da49365b136c8a0b3" integrity sha512-ASS9SamOP4TbCClsRHxIHXRfcGCnIoQqkvAzCSbZzTFLfcTqJVugB0agRgsEELsqaeWgsXv513eS116wnlSSPA== @@ -8827,10 +9090,10 @@ conventional-commits-filter@^2.0.2, conventional-commits-filter@^2.0.7: lodash.ismatch "^4.4.0" modify-values "^1.0.0" -conventional-commits-parser@^3.0.2, conventional-commits-parser@^3.0.3: - version "3.2.1" - resolved "https://registry.yarnpkg.com/conventional-commits-parser/-/conventional-commits-parser-3.2.1.tgz#ba44f0b3b6588da2ee9fd8da508ebff50d116ce2" - integrity sha512-OG9kQtmMZBJD/32NEw5IhN5+HnBqVjy03eC+I71I0oQRFA5rOgA4OtPOYG7mz1GkCfCNxn3gKIX8EiHJYuf1cA== +conventional-commits-parser@^3.2.0: + version "3.2.4" + resolved "https://registry.yarnpkg.com/conventional-commits-parser/-/conventional-commits-parser-3.2.4.tgz#a7d3b77758a202a9b2293d2112a8d8052c740972" + integrity sha512-nK7sAtfi+QXbxHCYfhpZsfRtaitZLIA6889kFIouLvz6repszQDgxBu7wf2WbU+Dco7sAnNCJYERCwt54WPC2Q== dependencies: JSONStream "^1.0.4" is-text-path "^1.0.1" @@ -8838,20 +9101,19 @@ conventional-commits-parser@^3.0.2, conventional-commits-parser@^3.0.3: meow "^8.0.0" split2 "^3.0.0" through2 "^4.0.0" - trim-off-newlines "^1.0.0" -conventional-recommended-bump@^4.0.4: - version "4.1.1" - resolved "https://registry.yarnpkg.com/conventional-recommended-bump/-/conventional-recommended-bump-4.1.1.tgz#37014fadeda267d0607e2fc81124da840a585127" - integrity sha512-JT2vKfSP9kR18RXXf55BRY1O3AHG8FPg5btP3l7LYfcWJsiXI6MCf30DepQ98E8Qhowvgv7a8iev0J1bEDkTFA== +conventional-recommended-bump@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/conventional-recommended-bump/-/conventional-recommended-bump-6.1.0.tgz#cfa623285d1de554012f2ffde70d9c8a22231f55" + integrity sha512-uiApbSiNGM/kkdL9GTOLAqC4hbptObFo4wW2QRyHsKciGAfQuLU1ShZ1BIVI/+K2BE/W1AWYQMCXAsv4dyKPaw== dependencies: concat-stream "^2.0.0" - conventional-changelog-preset-loader "^2.1.1" - conventional-commits-filter "^2.0.2" - conventional-commits-parser "^3.0.2" - git-raw-commits "2.0.0" - git-semver-tags "^2.0.2" - meow "^4.0.0" + conventional-changelog-preset-loader "^2.3.4" + conventional-commits-filter "^2.0.7" + conventional-commits-parser "^3.2.0" + git-raw-commits "^2.0.8" + git-semver-tags "^4.1.1" + meow "^8.0.0" q "^1.5.1" convert-source-map@1.7.0, convert-source-map@^1.4.0, convert-source-map@^1.5.1, convert-source-map@^1.6.0, convert-source-map@^1.7.0: @@ -8979,7 +9241,7 @@ cors@^2.8.5, cors@~2.8.5: object-assign "^4" vary "^1" -cosmiconfig@^5.0.0, cosmiconfig@^5.1.0: +cosmiconfig@^5.0.0: version "5.2.1" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.2.1.tgz#040f726809c591e77a17c0a3626ca45b4f168b1a" integrity sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA== @@ -8989,6 +9251,17 @@ cosmiconfig@^5.0.0, cosmiconfig@^5.1.0: js-yaml "^3.13.1" parse-json "^4.0.0" +cosmiconfig@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6" + integrity sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.2.1" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.10.0" + create-ecdh@^4.0.0: version "4.0.4" resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e" @@ -9395,12 +9668,10 @@ dag-map@^2.0.2: resolved "https://registry.yarnpkg.com/dag-map/-/dag-map-2.0.2.tgz#9714b472de82a1843de2fba9b6876938cab44c68" integrity sha1-lxS0ct6CoYQ94vuptodpOMq0TGg= -dargs@^4.0.1: - version "4.1.0" - resolved "https://registry.yarnpkg.com/dargs/-/dargs-4.1.0.tgz#03a9dbb4b5c2f139bf14ae53f0b8a2a6a86f4e17" - integrity sha1-A6nbtLXC8Tm/FK5T8LiipqhvThc= - dependencies: - number-is-nan "^1.0.0" +dargs@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/dargs/-/dargs-7.0.0.tgz#04015c41de0bcb69ec84050f3d9be0caf8d6d5cc" + integrity sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg== dashdash@^1.12.0: version "1.14.1" @@ -9512,7 +9783,7 @@ debuglog@^1.0.1: resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI= -decamelize-keys@^1.0.0, decamelize-keys@^1.1.0: +decamelize-keys@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9" integrity sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk= @@ -9608,6 +9879,11 @@ defer-to-connect@^1.0.1: resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591" integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ== +define-lazy-prop@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" + integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== + define-properties@^1.1.2, define-properties@^1.1.3, define-properties@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.4.tgz#0b14d7bd7fbeb2f3572c3a7eda80ea5d57fb05b1" @@ -9671,10 +9947,10 @@ depd@2.0.0, depd@~2.0.0: resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== -depd@~1.1.2: +depd@^1.1.2, depd@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" - integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= + integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== dependency-graph@^0.7.2: version "0.7.2" @@ -9876,14 +10152,6 @@ diffie-hellman@^5.0.0: miller-rabin "^4.0.0" randombytes "^2.0.0" -dir-glob@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.0.0.tgz#0b205d2b6aef98238ca286598a8204d29d0a0034" - integrity sha512-37qirFDz8cA5fimp9feo43fSuRo2gHwaIn6dXL8Ber1dGwUosDrGZeCCXq57WnIqE4aQ+u3eQZzsk1yOzhdwag== - dependencies: - arrify "^1.0.1" - path-type "^3.0.0" - dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" @@ -10038,13 +10306,6 @@ dot-case@^3.0.4: no-case "^3.0.4" tslib "^2.0.3" -dot-prop@^4.2.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.1.tgz#45884194a71fc2cda71cbb4bceb3a4dd2f433ba4" - integrity sha512-l0p4+mIuJIua0mhxGoh4a+iNL9bmeK5DvnSVQa6T0OhrVmaEa1XScX5Etc673FePCJOArq/4Pa2cLGODUWTPOQ== - dependencies: - is-obj "^1.0.0" - dot-prop@^5.1.0, dot-prop@^5.2.0: version "5.3.0" resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" @@ -10052,6 +10313,18 @@ dot-prop@^5.1.0, dot-prop@^5.2.0: dependencies: is-obj "^2.0.0" +dot-prop@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-6.0.1.tgz#fc26b3cf142b9e59b74dbd39ed66ce620c681083" + integrity sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA== + dependencies: + is-obj "^2.0.0" + +dotenv@~10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81" + integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q== + duplexer3@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" @@ -10115,6 +10388,13 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= +ejs@^3.1.7: + version "3.1.8" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.8.tgz#758d32910c78047585c7ef1f92f9ee041c1c190b" + integrity sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ== + dependencies: + jake "^10.8.5" + electron-to-chromium@^1.3.47, electron-to-chromium@^1.3.634, electron-to-chromium@^1.4.251: version "1.4.284" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz#61046d1e4cab3a25238f6bf7413795270f125592" @@ -10831,7 +11111,7 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= -encoding@0.1.13, encoding@^0.1.11: +encoding@0.1.13, encoding@^0.1.11, encoding@^0.1.13: version "0.1.13" resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9" integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A== @@ -10894,7 +11174,7 @@ enhanced-resolve@^5.10.0, enhanced-resolve@^5.3.2: graceful-fs "^4.2.4" tapable "^2.2.0" -enquirer@^2.3.5: +enquirer@^2.3.5, enquirer@~2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== @@ -10926,11 +11206,26 @@ entities@~3.0.1: resolved "https://registry.yarnpkg.com/entities/-/entities-3.0.1.tgz#2b887ca62585e96db3903482d336c1006c3001d4" integrity sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q== +env-paths@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" + integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== + +envinfo@^7.7.4: + version "7.8.1" + resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.8.1.tgz#06377e3e5f4d379fea7ac592d5ad8927e0c4d475" + integrity sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw== + err-code@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/err-code/-/err-code-1.1.2.tgz#06e0116d3028f6aef4806849eb0ea6a748ae6960" integrity sha1-BuARbTAo9q70gGhJ6w6mp0iuaWA= +err-code@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9" + integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA== + errlop@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/errlop/-/errlop-2.2.0.tgz#1ff383f8f917ae328bebb802d6ca69666a42d21b" @@ -11350,7 +11645,7 @@ esprima@~3.0.0: resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.0.0.tgz#53cf247acda77313e551c3aa2e73342d3fb4f7d9" integrity sha1-U88kes2ncxPlUcOqLnM0LT+099k= -esquery@^1.4.0: +esquery@^1.0.1, esquery@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5" integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== @@ -11422,7 +11717,7 @@ event-target-shim@^5.0.0: resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== -eventemitter3@^4.0.0: +eventemitter3@^4.0.0, eventemitter3@^4.0.4: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== @@ -11690,17 +11985,16 @@ 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-glob@^2.0.2: - version "2.2.7" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.7.tgz#6953857c3afa475fff92ee6015d52da70a4cd39d" - integrity sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw== - dependencies: - "@mrmlnc/readdir-enhanced" "^2.2.1" - "@nodelib/fs.stat" "^1.1.2" - glob-parent "^3.1.0" - is-glob "^4.0.0" - merge2 "^1.2.3" - micromatch "^3.1.10" +fast-glob@3.2.7: + version "3.2.7" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.7.tgz#fd6cb7a2d7e9aa7a7846111e85a196d6b2f766a1" + integrity sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q== + 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-glob@^3.0.3, fast-glob@^3.2.11, fast-glob@^3.2.4, fast-glob@^3.2.9: version "3.2.12" @@ -11807,6 +12101,13 @@ figgy-pudding@^3.4.1, figgy-pudding@^3.5.1: resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e" integrity sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw== +figures@3.2.0, figures@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" + integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== + dependencies: + escape-string-regexp "^1.0.5" + figures@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" @@ -11814,13 +12115,6 @@ figures@^2.0.0: dependencies: escape-string-regexp "^1.0.5" -figures@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" - integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== - dependencies: - escape-string-regexp "^1.0.5" - file-entry-cache@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" @@ -11849,6 +12143,13 @@ file-uri-to-path@1.0.0: resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== +filelist@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.4.tgz#f78978a1e944775ff9e62e744424f215e58352b5" + integrity sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q== + dependencies: + minimatch "^5.0.1" + filesize@^9.0.11: version "9.0.11" resolved "https://registry.yarnpkg.com/filesize/-/filesize-9.0.11.tgz#4ac3a42c084232dd9b2a1da0107f32d42fcfa5e4" @@ -11890,11 +12191,6 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" -filter-obj@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/filter-obj/-/filter-obj-1.1.0.tgz#9b311112bc6c6127a16e016c6c5d7f19e0805c5b" - integrity sha1-mzERErxsYSehbgFsbF1/GeCAXFs= - finalhandler@1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" @@ -12127,6 +12423,11 @@ flat@^4.1.0: dependencies: is-buffer "~2.0.3" +flat@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" + integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== + flatted@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138" @@ -12155,6 +12456,11 @@ follow-redirects@^1.0.0, follow-redirects@^1.14.9: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.1.tgz#0ca6a452306c9b276e4d3127483e29575e207ad5" integrity sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA== +follow-redirects@^1.15.0: + version "1.15.2" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" + integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== + for-each@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" @@ -12355,7 +12661,7 @@ fs-minipass@^1.2.7: dependencies: minipass "^2.6.0" -fs-minipass@^2.0.0: +fs-minipass@^2.0.0, fs-minipass@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== @@ -12522,11 +12828,6 @@ get-amd-module-type@^3.0.0: ast-module-types "^2.3.2" node-source-walk "^4.0.0" -get-caller-file@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" - integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w== - get-caller-file@^2.0.1, get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" @@ -12563,21 +12864,15 @@ get-package-type@^0.1.0: resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== -get-pkg-repo@^1.0.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/get-pkg-repo/-/get-pkg-repo-1.4.0.tgz#c73b489c06d80cc5536c2c853f9e05232056972d" - integrity sha1-xztInAbYDMVTbCyFP54FIyBWly0= +get-pkg-repo@^4.0.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/get-pkg-repo/-/get-pkg-repo-4.2.1.tgz#75973e1c8050c73f48190c52047c4cee3acbf385" + integrity sha512-2+QbHjFRfGB74v/pYWjd5OhU3TDIC2Gv/YKUTk/tCvAz0pkn/Mz6P3uByuBimLOcPvN2jYdScl3xGFSrx0jEcA== dependencies: - hosted-git-info "^2.1.4" - meow "^3.3.0" - normalize-package-data "^2.3.0" - parse-github-repo-url "^1.3.0" + "@hutson/parse-repository-url" "^3.0.0" + hosted-git-info "^4.0.0" through2 "^2.0.0" - -get-port@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/get-port/-/get-port-3.2.0.tgz#dd7ce7de187c06c8bf353796ac71e099f0980ebc" - integrity sha1-3Xzn3hh8Bsi/NTeWrHHgmfCYDrw= + yargs "^16.2.0" get-port@^5.1.1: version "5.1.1" @@ -12648,16 +12943,16 @@ git-hooks-list@1.0.3: resolved "https://registry.yarnpkg.com/git-hooks-list/-/git-hooks-list-1.0.3.tgz#be5baaf78203ce342f2f844a9d2b03dba1b45156" integrity sha512-Y7wLWcrLUXwk2noSka166byGCvhMtDRpgHdzCno1UQv/n/Hegp++a2xBWJL1lJarnKD3SWaljD+0z1ztqxuKyQ== -git-raw-commits@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/git-raw-commits/-/git-raw-commits-2.0.0.tgz#d92addf74440c14bcc5c83ecce3fb7f8a79118b5" - integrity sha512-w4jFEJFgKXMQJ0H0ikBk2S+4KP2VEjhCvLCNqbNRQC8BgGWgLKNCO7a9K9LI+TVT7Gfoloje502sEnctibffgg== +git-raw-commits@^2.0.8: + version "2.0.11" + resolved "https://registry.yarnpkg.com/git-raw-commits/-/git-raw-commits-2.0.11.tgz#bc3576638071d18655e1cc60d7f524920008d723" + integrity sha512-VnctFhw+xfj8Va1xtfEqCUD2XDrbAPSJx+hSrE5K7fGdjZruW7XV+QOrN7LF/RJyvspRiD2I0asWsxFp0ya26A== dependencies: - dargs "^4.0.1" - lodash.template "^4.0.2" - meow "^4.0.0" - split2 "^2.0.0" - through2 "^2.0.0" + dargs "^7.0.0" + lodash "^4.17.15" + meow "^8.0.0" + split2 "^3.0.0" + through2 "^4.0.0" git-remote-origin-url@^2.0.0: version "2.0.0" @@ -12672,28 +12967,28 @@ git-repo-info@^2.1.1: resolved "https://registry.yarnpkg.com/git-repo-info/-/git-repo-info-2.1.1.tgz#220ffed8cbae74ef8a80e3052f2ccb5179aed058" integrity sha512-8aCohiDo4jwjOwma4FmYFd3i97urZulL8XL24nIPxuE+GZnfsAyy/g2Shqx6OjUiFKUXZM+Yy+KHnOmmA3FVcg== -git-semver-tags@^2.0.2, git-semver-tags@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/git-semver-tags/-/git-semver-tags-2.0.3.tgz#48988a718acf593800f99622a952a77c405bfa34" - integrity sha512-tj4FD4ww2RX2ae//jSrXZzrocla9db5h0V7ikPl1P/WwoZar9epdUhwR7XHXSgc+ZkNq72BEEerqQuicoEQfzA== +git-semver-tags@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/git-semver-tags/-/git-semver-tags-4.1.1.tgz#63191bcd809b0ec3e151ba4751c16c444e5b5780" + integrity sha512-OWyMt5zBe7xFs8vglMmhM9lRQzCWL3WjHtxNNfJTMngGym7pC1kh8sP6jevfydJ6LP3ZvGxfb6ABYgPUM0mtsA== dependencies: - meow "^4.0.0" + meow "^8.0.0" semver "^6.0.0" -git-up@^4.0.0: - version "4.0.2" - resolved "https://registry.yarnpkg.com/git-up/-/git-up-4.0.2.tgz#10c3d731051b366dc19d3df454bfca3f77913a7c" - integrity sha512-kbuvus1dWQB2sSW4cbfTeGpCMd8ge9jx9RKnhXhuJ7tnvT+NIrTVfYZxjtflZddQYcmdOTlkAcjmx7bor+15AQ== +git-up@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/git-up/-/git-up-7.0.0.tgz#bace30786e36f56ea341b6f69adfd83286337467" + integrity sha512-ONdIrbBCFusq1Oy0sC71F5azx8bVkvtZtMJAsv+a6lz5YAmbNnLD6HAB4gptHZVLPR8S2/kVN6Gab7lryq5+lQ== dependencies: - is-ssh "^1.3.0" - parse-url "^5.0.0" + is-ssh "^1.4.0" + parse-url "^8.1.0" -git-url-parse@^11.1.2: - version "11.4.4" - resolved "https://registry.yarnpkg.com/git-url-parse/-/git-url-parse-11.4.4.tgz#5d747debc2469c17bc385719f7d0427802d83d77" - integrity sha512-Y4o9o7vQngQDIU9IjyCmRJBin5iYjI5u9ZITnddRZpD7dcCFQj2sL2XuMNbLRE4b4B/4ENPsp2Q8P44fjAZ0Pw== +git-url-parse@^13.1.0: + version "13.1.0" + resolved "https://registry.yarnpkg.com/git-url-parse/-/git-url-parse-13.1.0.tgz#07e136b5baa08d59fabdf0e33170de425adf07b4" + integrity sha512-5FvPJP/70WkIprlUZ33bm4UAaFdjcLkJLpWft1BeZKqwR0uhhNGoKwlUaPtVb4LxCSQ++erHapRak9kWGj+FCA== dependencies: - git-up "^4.0.0" + git-up "^7.0.0" gitconfiglocal@^1.0.0: version "1.0.0" @@ -12717,11 +13012,6 @@ glob-parent@^5.1.1, glob-parent@^5.1.2, glob-parent@~5.1.0, glob-parent@~5.1.2: dependencies: is-glob "^4.0.1" -glob-to-regexp@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab" - integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs= - glob-to-regexp@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" @@ -12739,6 +13029,18 @@ glob@7.1.3: once "^1.3.0" path-is-absolute "^1.0.0" +glob@7.1.4: + version "7.1.4" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255" + integrity sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + glob@7.1.6: version "7.1.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" @@ -12763,7 +13065,7 @@ glob@7.2.0: once "^1.3.0" path-is-absolute "^1.0.0" -glob@8.0.3, glob@^8.0.3: +glob@8.0.3, glob@^8.0.1, glob@^8.0.3: version "8.0.3" resolved "https://registry.yarnpkg.com/glob/-/glob-8.0.3.tgz#415c6eb2deed9e502c68fa44a272e6da6eeca42e" integrity sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ== @@ -12912,19 +13214,6 @@ globby@^6.1.0: pify "^2.0.0" pinkie-promise "^2.0.0" -globby@^8.0.1: - version "8.0.2" - resolved "https://registry.yarnpkg.com/globby/-/globby-8.0.2.tgz#5697619ccd95c5275dbb2d6faa42087c1a941d8d" - integrity sha512-yTzMmKygLp8RUpG1Ymu2VXPSJQZjNAZPD4ywgYEaG7e4tBJeUQBO8OpXrf1RCNcEs5alsoJYPAMiIHP0cmeC7w== - dependencies: - array-union "^1.0.1" - dir-glob "2.0.0" - fast-glob "^2.0.2" - glob "^7.1.2" - ignore "^3.3.5" - pify "^3.0.0" - slash "^1.0.0" - globrex@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/globrex/-/globrex-0.1.2.tgz#dd5d9ec826232730cd6793a5e33a9302985e6098" @@ -13096,7 +13385,7 @@ handle-thing@^2.0.0: resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg== -handlebars@^4.0.1, handlebars@^4.0.4, handlebars@^4.3.1, handlebars@^4.7.3, handlebars@^4.7.6: +handlebars@^4.0.1, handlebars@^4.0.4, handlebars@^4.3.1, handlebars@^4.7.3, handlebars@^4.7.6, handlebars@^4.7.7: version "4.7.7" resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1" integrity sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA== @@ -13412,13 +13701,20 @@ hosted-git-info@^2.1.4, hosted-git-info@^2.7.1: resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg== -hosted-git-info@^3.0.2: +hosted-git-info@^3.0.2, hosted-git-info@^3.0.6: version "3.0.8" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-3.0.8.tgz#6e35d4cc87af2c5f816e4cb9ce350ba87a3f370d" integrity sha512-aXpmwoOhRBrw6X3j0h5RloK4x1OzsxMPyxqIHyNfSe2pypkVTZFpEiRoSipPEPlMrh0HW/XsjkJ5WgnCirpNUw== dependencies: lru-cache "^6.0.0" +hosted-git-info@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.1.0.tgz#827b82867e9ff1c8d0c4d9d53880397d2c86d224" + integrity sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA== + dependencies: + lru-cache "^6.0.0" + hosted-git-info@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.0.2.tgz#5e425507eede4fea846b7262f0838456c4209961" @@ -13521,7 +13817,7 @@ http-cache-semantics@3.8.1, http-cache-semantics@^3.8.1: resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz#39b0e16add9b605bf0a9ef3d9daaf4843b4cacd2" integrity sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w== -http-cache-semantics@^4.0.0: +http-cache-semantics@^4.0.0, http-cache-semantics@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== @@ -13726,16 +14022,23 @@ ignore-walk@3.0.3, ignore-walk@^3.0.1, ignore-walk@^3.0.3: dependencies: minimatch "^3.0.4" -ignore@^3.3.5: - version "3.3.10" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043" - integrity sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug== +ignore-walk@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-5.0.1.tgz#5f199e23e1288f518d90358d461387788a154776" + integrity sha512-yemi4pMf51WKT7khInJqAvsIGzoqYXblnsz0ql8tM+yi1EKYTY1evX4NAbJrLL/Aanr2HyZeluqU+Oi7MGHokw== + dependencies: + minimatch "^5.0.1" ignore@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== +ignore@^5.0.4: + version "5.2.1" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.1.tgz#c2b1f76cb999ede1502f3a226a9310fdfe88d46c" + integrity sha512-d2qQLzTJ9WxQftPAuEQpSPmKqzxePjzVbpAVv62AQ64NTL+wR4JkrVqR/LqFsFEUsHDAiId52mJteHDFuDkElA== + ignore@^5.1.1, ignore@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" @@ -13791,14 +14094,6 @@ import-lazy@^2.1.0: resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43" integrity sha512-m7ZEHgtw69qOGw+jwxXkHlrlIPdTGkyh66zXZ1ajZbxkDBNjSY/LGbmjc7h0s2ELsUDTAhFr55TrPSSqJGPG0A== -import-local@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/import-local/-/import-local-1.0.0.tgz#5e4ffdc03f4fe6c009c6729beb29631c2f8227bc" - integrity sha512-vAaZHieK9qjGo58agRBg+bhHX3hoTZU/Oa3GESWLz7t1U62fk63aHuDJJEteXoDeTCcPmUT+z38gkHPZkkmpmQ== - dependencies: - pkg-dir "^2.0.0" - resolve-cwd "^2.0.0" - import-local@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d" @@ -13827,11 +14122,6 @@ indent-string@^2.1.0: dependencies: repeating "^2.0.0" -indent-string@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289" - integrity sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok= - indent-string@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" @@ -13890,19 +14180,18 @@ ini@^1.3.2, ini@^1.3.4, ini@~1.3.0: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== -init-package-json@^1.10.3: - version "1.10.3" - resolved "https://registry.yarnpkg.com/init-package-json/-/init-package-json-1.10.3.tgz#45ffe2f610a8ca134f2bd1db5637b235070f6cbe" - integrity sha512-zKSiXKhQveNteyhcj1CoOP8tqp1QuxPIPBl8Bid99DGLFqA1p87M6lNgfjJHSBoWJJlidGOv5rWjyYKEB3g2Jw== +init-package-json@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/init-package-json/-/init-package-json-3.0.2.tgz#f5bc9bac93f2bdc005778bc2271be642fecfcd69" + integrity sha512-YhlQPEjNFqlGdzrBfDNRLhvoSgX7iQRgSxgsNknRQ9ITXFT7UMfVMWhBTOh2Y+25lRnGrv5Xz8yZwQ3ACR6T3A== dependencies: - glob "^7.1.1" - npm-package-arg "^4.0.0 || ^5.0.0 || ^6.0.0" + npm-package-arg "^9.0.1" promzard "^0.3.0" - read "~1.0.1" - read-package-json "1 || 2" - semver "2.x || 3.x || 4 || 5" - validate-npm-package-license "^3.0.1" - validate-npm-package-name "^3.0.0" + read "^1.0.7" + read-package-json "^5.0.0" + semver "^7.3.5" + validate-npm-package-license "^3.0.4" + validate-npm-package-name "^4.0.0" injection-js@^2.2.1: version "2.4.0" @@ -13948,7 +14237,7 @@ inquirer@7.3.3, inquirer@^7.0.1: strip-ansi "^6.0.0" through "^2.3.6" -inquirer@^6, inquirer@^6.2.0: +inquirer@^6: version "6.5.2" resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.5.2.tgz#ad50942375d036d327ff528c08bd5fab089928ca" integrity sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ== @@ -13967,6 +14256,27 @@ inquirer@^6, inquirer@^6.2.0: strip-ansi "^5.1.0" through "^2.3.6" +inquirer@^8.2.4: + version "8.2.5" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.5.tgz#d8654a7542c35a9b9e069d27e2df4858784d54f8" + integrity sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ== + dependencies: + ansi-escapes "^4.2.1" + chalk "^4.1.1" + cli-cursor "^3.1.0" + cli-width "^3.0.0" + external-editor "^3.0.3" + figures "^3.0.0" + lodash "^4.17.21" + mute-stream "0.0.8" + ora "^5.4.1" + run-async "^2.4.0" + rxjs "^7.5.5" + string-width "^4.1.0" + strip-ansi "^6.0.0" + through "^2.3.6" + wrap-ansi "^7.0.0" + internal-ip@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-4.3.0.tgz#845452baad9d2ca3b69c635a137acb9a0dad0907" @@ -14004,11 +14314,6 @@ invariant@^2.2.1, invariant@^2.2.2, invariant@^2.2.4: dependencies: loose-envify "^1.0.0" -invert-kv@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02" - integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA== - ip-regex@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" @@ -14019,6 +14324,11 @@ ip@1.1.5, ip@^1.1.0, ip@^1.1.5: resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo= +ip@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.0.tgz#4cf4ab182fee2314c75ede1276f8c80b479936da" + integrity sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ== + ipaddr.js@1.9.1, ipaddr.js@^1.9.0: version "1.9.1" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" @@ -14106,13 +14416,6 @@ is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== -is-ci@^1.0.10: - version "1.2.1" - resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.2.1.tgz#e3779c8ee17fccf428488f6e281187f2e632841c" - integrity sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg== - dependencies: - ci-info "^1.5.0" - is-ci@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" @@ -14132,7 +14435,7 @@ is-color-stop@^1.0.0: rgb-regex "^1.0.1" rgba-regex "^1.0.0" -is-core-module@^2.2.0, is-core-module@^2.9.0: +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" integrity sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw== @@ -14181,7 +14484,7 @@ is-directory@^0.3.1: resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" integrity sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE= -is-docker@^2.0.0: +is-docker@^2.0.0, is-docker@^2.1.1: version "2.2.1" resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== @@ -14267,6 +14570,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-lambda@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5" + integrity sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ== + is-language-code@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/is-language-code/-/is-language-code-3.1.0.tgz#b2386b49227e7010636f16d0c2c681ca40136ab5" @@ -14314,7 +14622,7 @@ is-number@^7.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== -is-obj@^1.0.0, is-obj@^1.0.1: +is-obj@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8= @@ -14353,7 +14661,7 @@ is-path-inside@^3.0.2: resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== -is-plain-obj@2.1.0: +is-plain-obj@2.1.0, is-plain-obj@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== @@ -14427,12 +14735,12 @@ is-shared-array-buffer@^1.0.2: dependencies: call-bind "^1.0.2" -is-ssh@^1.3.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/is-ssh/-/is-ssh-1.3.2.tgz#a4b82ab63d73976fd8263cceee27f99a88bdae2b" - integrity sha512-elEw0/0c2UscLrNG+OAorbP539E3rhliKPg+hDMWN9VwrDXfYK+4PBEykDPfxlYYtQvl84TascnQyobfQLHEhQ== +is-ssh@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/is-ssh/-/is-ssh-1.4.0.tgz#4f8220601d2839d8fa624b3106f8e8884f01b8b2" + integrity sha512-x7+VxdxOdlV3CYpjvRLBv5Lo9OJerlYanjwFrPR9fuGPjCiNiCzFgAWpiLAohSbsnH4ZAys3SBh+hq5rJosxUQ== dependencies: - protocols "^1.1.0" + protocols "^2.0.1" is-stream-ended@^0.1.4: version "0.1.4" @@ -14725,6 +15033,16 @@ isurl@^1.0.0-alpha5: has-to-string-tag-x "^1.2.0" is-object "^1.0.1" +jake@^10.8.5: + version "10.8.5" + resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.5.tgz#f2183d2c59382cb274226034543b9c03b8164c46" + integrity sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw== + dependencies: + async "^3.2.3" + chalk "^4.0.2" + filelist "^1.0.1" + minimatch "^3.0.4" + jest-changed-files@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-27.5.1.tgz#a348aed00ec9bf671cc58a66fcbe7c3dfd6a68f5" @@ -15320,7 +15638,7 @@ js-yaml@3.14.0: argparse "^1.0.7" esprima "^4.0.0" -js-yaml@3.x, 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.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== @@ -15328,7 +15646,7 @@ js-yaml@3.x, js-yaml@^3.13.1, js-yaml@^3.2.5, js-yaml@^3.2.7: argparse "^1.0.7" esprima "^4.0.0" -js-yaml@^4.1.0: +js-yaml@4.1.0, js-yaml@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== @@ -15493,6 +15811,11 @@ json-stable-stringify@^1.0.0, json-stable-stringify@^1.0.1: dependencies: jsonify "~0.0.0" +json-stringify-nice@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/json-stringify-nice/-/json-stringify-nice-1.1.4.tgz#2c937962b80181d3f317dd39aa323e14f5a60a67" + integrity sha512-5Z5RFW63yxReJ7vANgW6eZFGWaQvnPE3WNmZoOJrSkGju2etKA2L5rrOa1sm877TVTFt57A80BH1bArcmlLfPw== + json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" @@ -15525,6 +15848,11 @@ jsonc-parser@2.3.0: resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.3.0.tgz#7c7fc988ee1486d35734faaaa866fadb00fa91ee" integrity sha512-b0EBt8SWFNnixVdvoR2ZtEGa9ZqLhbJnOjezn+WP+8kspFm+PFYDN8Z4Bc7pRlDjvuVcADSUkroIuTWWn/YiIA== +jsonc-parser@3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.0.tgz#31ff3f4c2b9793f89c67212627c51c6394f88e76" + integrity sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w== + jsonfile@^2.1.0: version "2.4.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8" @@ -15553,10 +15881,10 @@ jsonify@~0.0.0: resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM= -jsonparse@^1.2.0: +jsonparse@^1.2.0, jsonparse@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" - integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA= + integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== jsprim@^1.2.2: version "1.4.1" @@ -15576,6 +15904,16 @@ jsprim@^1.2.2: array-includes "^3.1.2" object.assign "^4.1.2" +just-diff-apply@^5.2.0: + version "5.4.1" + resolved "https://registry.yarnpkg.com/just-diff-apply/-/just-diff-apply-5.4.1.tgz#1debed059ad009863b4db0e8d8f333d743cdd83b" + integrity sha512-AAV5Jw7tsniWwih8Ly3fXxEZ06y+6p5TwQMsw0dzZ/wPKilzyDgdAnL0Ug4NNIquPUOh1vfFWEHbmXUqM5+o8g== + +just-diff@^5.0.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/just-diff/-/just-diff-5.1.1.tgz#8da6414342a5ed6d02ccd64f5586cbbed3146202" + integrity sha512-u8HXJ3HlNrTzY7zrYYKjNEfBlyjqhdBkoyTVdjtn7p02RJD5NvR8rIClzeGA7t+UYP1/7eAkWNLU0+P3QrEqKQ== + just-extend@^4.0.2: version "4.1.1" resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.1.1.tgz#158f1fdb01f128c411dc8b286a7b4837b3545282" @@ -15882,13 +16220,6 @@ lazy-cache@^1.0.3: resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" integrity sha1-odePw6UEdMuAhF07O24dpJpEbo4= -lcid@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/lcid/-/lcid-2.0.0.tgz#6ef5d2df60e52f82eb228a4c373e8d1f397253cf" - integrity sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA== - dependencies: - invert-kv "^2.0.0" - leek@0.0.24: version "0.0.24" resolved "https://registry.yarnpkg.com/leek/-/leek-0.0.24.tgz#e400e57f0e60d8ef2bd4d068dc428a54345dbcda" @@ -15898,28 +16229,34 @@ leek@0.0.24: lodash.assign "^3.2.0" rsvp "^3.0.21" -lerna@3.13.4: - version "3.13.4" - resolved "https://registry.yarnpkg.com/lerna/-/lerna-3.13.4.tgz#03026c11c5643f341fda42e4fb1882e2df35e6cb" - integrity sha512-qTp22nlpcgVrJGZuD7oHnFbTk72j2USFimc2Pj4kC0/rXmcU2xPtCiyuxLl8y6/6Lj5g9kwEuvKDZtSXujjX/A== - dependencies: - "@lerna/add" "3.13.3" - "@lerna/bootstrap" "3.13.3" - "@lerna/changed" "3.13.4" - "@lerna/clean" "3.13.3" - "@lerna/cli" "3.13.0" - "@lerna/create" "3.13.3" - "@lerna/diff" "3.13.3" - "@lerna/exec" "3.13.3" - "@lerna/import" "3.13.4" - "@lerna/init" "3.13.3" - "@lerna/link" "3.13.3" - "@lerna/list" "3.13.3" - "@lerna/publish" "3.13.4" - "@lerna/run" "3.13.3" - "@lerna/version" "3.13.4" - import-local "^1.0.0" - npmlog "^4.1.2" +lerna@^6.0.3: + version "6.1.0" + resolved "https://registry.yarnpkg.com/lerna/-/lerna-6.1.0.tgz#693145393ec22fd3ca98d817deab2246c1e2b107" + integrity sha512-3qAjIj8dgBwHtCAiLbq4VU/C1V9D1tvTLm2owZubdGAN72aB5TxuCu2mcw+yeEorOcXuR9YWx7EXIkAf+G0N2w== + dependencies: + "@lerna/add" "6.1.0" + "@lerna/bootstrap" "6.1.0" + "@lerna/changed" "6.1.0" + "@lerna/clean" "6.1.0" + "@lerna/cli" "6.1.0" + "@lerna/command" "6.1.0" + "@lerna/create" "6.1.0" + "@lerna/diff" "6.1.0" + "@lerna/exec" "6.1.0" + "@lerna/import" "6.1.0" + "@lerna/info" "6.1.0" + "@lerna/init" "6.1.0" + "@lerna/link" "6.1.0" + "@lerna/list" "6.1.0" + "@lerna/publish" "6.1.0" + "@lerna/run" "6.1.0" + "@lerna/version" "6.1.0" + "@nrwl/devkit" ">=14.8.6 < 16" + import-local "^3.0.2" + inquirer "^8.2.4" + npmlog "^6.0.2" + nx ">=14.8.6 < 16" + typescript "^3 || ^4" less-loader@6.2.0: version "6.2.0" @@ -15975,30 +16312,26 @@ levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" -libnpmaccess@^3.0.1: - version "3.0.2" - resolved "https://registry.yarnpkg.com/libnpmaccess/-/libnpmaccess-3.0.2.tgz#8b2d72345ba3bef90d3b4f694edd5c0417f58923" - integrity sha512-01512AK7MqByrI2mfC7h5j8N9V4I7MHJuk9buo8Gv+5QgThpOgpjB7sQBDDkeZqRteFb1QM/6YNdHfG7cDvfAQ== +libnpmaccess@^6.0.3: + version "6.0.4" + resolved "https://registry.yarnpkg.com/libnpmaccess/-/libnpmaccess-6.0.4.tgz#2dd158bd8a071817e2207d3b201d37cf1ad6ae6b" + integrity sha512-qZ3wcfIyUoW0+qSFkMBovcTrSGJ3ZeyvpR7d5N9pEYv/kXs8sHP2wiqEIXBKLFrZlmM0kR0RJD7mtfLngtlLag== dependencies: aproba "^2.0.0" - get-stream "^4.0.0" - npm-package-arg "^6.1.0" - npm-registry-fetch "^4.0.0" + minipass "^3.1.1" + npm-package-arg "^9.0.1" + npm-registry-fetch "^13.0.0" -libnpmpublish@^1.1.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/libnpmpublish/-/libnpmpublish-1.1.3.tgz#e3782796722d79eef1a0a22944c117e0c4ca4280" - integrity sha512-/3LsYqVc52cHXBmu26+J8Ed7sLs/hgGVFMH1mwYpL7Qaynb9RenpKqIKu0sJ130FB9PMkpMlWjlbtU8A4m7CQw== +libnpmpublish@^6.0.4: + version "6.0.5" + resolved "https://registry.yarnpkg.com/libnpmpublish/-/libnpmpublish-6.0.5.tgz#5a894f3de2e267d62f86be2a508e362599b5a4b1" + integrity sha512-LUR08JKSviZiqrYTDfywvtnsnxr+tOvBU0BF8H+9frt7HMvc6Qn6F8Ubm72g5hDTHbq8qupKfDvDAln2TVPvFg== dependencies: - aproba "^2.0.0" - figgy-pudding "^3.5.1" - get-stream "^4.0.0" - lodash.clonedeep "^4.5.0" - normalize-package-data "^2.4.0" - npm-package-arg "^6.1.0" - npm-registry-fetch "^4.0.0" - semver "^5.5.1" - ssri "^6.0.1" + normalize-package-data "^4.0.0" + npm-package-arg "^9.0.1" + npm-registry-fetch "^13.0.0" + semver "^7.3.7" + ssri "^9.0.0" license-webpack-plugin@2.3.0: version "2.3.0" @@ -16083,6 +16416,16 @@ load-json-file@^4.0.0: pify "^3.0.0" strip-bom "^3.0.0" +load-json-file@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-6.2.0.tgz#5c7770b42cafa97074ca2848707c61662f4251a1" + integrity sha512-gUD/epcRms75Cw8RT1pUdHugZYM5ce64ucs2GEISABwkRsOQr0q2wm/MV2TKThycIe5e0ytRweW2RZxclogCdQ== + dependencies: + graceful-fs "^4.1.15" + parse-json "^5.0.0" + strip-bom "^4.0.0" + type-fest "^0.6.0" + loader-runner@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357" @@ -16357,7 +16700,7 @@ lodash.sortby@^4.7.0: resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA== -lodash.template@^4.0.2, lodash.template@^4.5.0: +lodash.template@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.5.0.tgz#f976195cf3f347d0d5f52483569fe8031ccce8ab" integrity sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A== @@ -16387,7 +16730,7 @@ lodash.uniqby@^4.7.0: resolved "https://registry.yarnpkg.com/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz#d99c07a669e9e6d24e1362dfe266c67616af1302" integrity sha1-2ZwHpmnp5tJOE2Lf4mbGdhavEwI= -lodash@4.17.21, lodash@^4.17.0, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1, lodash@^4.7.0: +lodash@4.17.21, lodash@^4.17.0, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.7.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -16507,6 +16850,11 @@ lru-cache@^7.10.1, lru-cache@^7.5.1: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.14.0.tgz#21be64954a4680e303a09e9468f880b98a0b3c7f" integrity sha512-EIRtP1GrSJny0dqb50QXRUNBxHJhcpxHC++M5tD7RYbvLLn5KVWKsbyswSSqDuU15UFi3bgTQIY8nhDMeF6aDQ== +lru-cache@^7.4.4, lru-cache@^7.7.1: + version "7.14.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.14.1.tgz#8da8d2f5f59827edb388e63e459ac23d6d408fea" + integrity sha512-ysxwsnTKdAx96aTRdhDOCQfDgbHnt8SK0KY8SEjO0wHinhWOFTESbjVCMPbU1uGXg/ch4lifqx0wfjOawU2+WA== + lru_map@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/lru_map/-/lru_map-0.3.3.tgz#b5c8351b9464cbd750335a79650a0ec0e56118dd" @@ -16522,11 +16870,6 @@ lz-string@^1.4.4: resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26" integrity sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY= -macos-release@^2.2.0: - version "2.4.1" - resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-2.4.1.tgz#64033d0ec6a5e6375155a74b1a1eba8e509820ac" - integrity sha512-H/QHeBIN1fIGJX517pvK8IEK53yQOW7YcEI55oYtgjDdoCQQz7eJS94qt5kNrscReEyuD/JcdFCm2XBEcGOITg== - madge@4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/madge/-/madge-4.0.2.tgz#56a3aff8021a5844f8713e0789f6ee94095f2f41" @@ -16583,13 +16926,6 @@ magic-string@^0.27.0: dependencies: "@jridgewell/sourcemap-codec" "^1.4.13" -make-dir@^1.0.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" - integrity sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ== - dependencies: - pify "^3.0.0" - make-dir@^2.0.0, make-dir@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" @@ -16610,22 +16946,27 @@ make-error@1.x, make-error@^1.1.1: resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== -make-fetch-happen@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-4.0.2.tgz#2d156b11696fb32bffbafe1ac1bc085dd6c78a79" - integrity sha512-YMJrAjHSb/BordlsDEcVcPyTbiJKkzqMf48N8dAJZT9Zjctrkb6Yg4TY9Sq2AwSIQJFn5qBBKVTYt3vP5FMIHA== +make-fetch-happen@^10.0.3, make-fetch-happen@^10.0.6: + version "10.2.1" + resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz#f5e3835c5e9817b617f2770870d9492d28678164" + integrity sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w== dependencies: - agentkeepalive "^3.4.1" - cacache "^11.3.3" - http-cache-semantics "^3.8.1" - http-proxy-agent "^2.1.0" - https-proxy-agent "^2.2.1" - lru-cache "^5.1.1" - mississippi "^3.0.0" - node-fetch-npm "^2.0.2" - promise-retry "^1.1.1" - socks-proxy-agent "^4.0.0" - ssri "^6.0.0" + agentkeepalive "^4.2.1" + cacache "^16.1.0" + http-cache-semantics "^4.1.0" + http-proxy-agent "^5.0.0" + https-proxy-agent "^5.0.0" + is-lambda "^1.0.1" + lru-cache "^7.7.1" + minipass "^3.1.6" + minipass-collect "^1.0.2" + minipass-fetch "^2.0.3" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.4" + negotiator "^0.6.3" + promise-retry "^2.0.1" + socks-proxy-agent "^7.0.0" + ssri "^9.0.0" make-fetch-happen@^5.0.0: version "5.0.2" @@ -16651,13 +16992,6 @@ makeerror@1.0.x: dependencies: tmpl "1.0.x" -map-age-cleaner@^0.1.1: - version "0.1.3" - resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a" - integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w== - dependencies: - p-defer "^1.0.0" - map-cache@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" @@ -16668,11 +17002,6 @@ map-obj@^1.0.0, map-obj@^1.0.1: resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0= -map-obj@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-2.0.0.tgz#a65cd29087a92598b8791257a523e021222ac1f9" - integrity sha1-plzSkIepJZi4eRJXpSPgISIqwfk= - map-obj@^4.0.0: version "4.2.0" resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.2.0.tgz#0e8bc823e2aaca8a0942567d12ed14f389eec153" @@ -16782,15 +17111,6 @@ media-typer@0.3.0: resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= -mem@^4.0.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/mem/-/mem-4.3.0.tgz#461af497bc4ae09608cdb2e60eefb69bff744178" - integrity sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w== - dependencies: - map-age-cleaner "^0.1.1" - mimic-fn "^2.0.0" - p-is-promise "^2.0.0" - memory-fs@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" @@ -16840,21 +17160,6 @@ meow@^3.3.0: redent "^1.0.0" trim-newlines "^1.0.0" -meow@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/meow/-/meow-4.0.1.tgz#d48598f6f4b1472f35bf6317a95945ace347f975" - integrity sha512-xcSBHD5Z86zaOc+781KrupuHAzeGXSLtiAOmBsiLDiPSaYSB6hdew2ng9EBAnZ62jagG9MHAOdxpDi/lWBFJ/A== - dependencies: - camelcase-keys "^4.0.0" - decamelize-keys "^1.0.0" - loud-rejection "^1.0.0" - minimist "^1.1.3" - minimist-options "^3.0.1" - normalize-package-data "^2.3.4" - read-pkg-up "^3.0.0" - redent "^2.0.0" - trim-newlines "^2.0.0" - meow@^8.0.0: version "8.1.2" resolved "https://registry.yarnpkg.com/meow/-/meow-8.1.2.tgz#bcbe45bda0ee1729d350c03cffc8395a36c4e897" @@ -16978,7 +17283,7 @@ mimic-fn@^1.0.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== -mimic-fn@^2.0.0, mimic-fn@^2.1.0: +mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== @@ -17034,6 +17339,13 @@ minimatch@3.0.4: dependencies: brace-expansion "^1.1.7" +minimatch@3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.5.tgz#4da8f1290ee0f0f8e83d60ca69f8f134068604a3" + integrity sha512-tUpxzX0VAzJHjLu0xUfFv1gwVp9ba3IOuRAVH2EGuRW8a5emA2FlACLqiT/lDVtS1W+TGNwqz3sWaNyLgDJWuw== + dependencies: + brace-expansion "^1.1.7" + minimatch@^5.0.1, minimatch@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.0.tgz#1717b464f4971b144f6aabe8f2d0b8e4511e09c7" @@ -17050,14 +17362,6 @@ minimist-options@4.1.0: is-plain-obj "^1.1.0" kind-of "^6.0.3" -minimist-options@^3.0.1: - version "3.0.2" - resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-3.0.2.tgz#fba4c8191339e13ecf4d61beb03f070103f3d954" - integrity sha512-FyBrT/d0d4+uiZRbqznPXqw3IpZZG3gl3wKWiX784FycUKVwBt0uLBFkQrtE4tZOrgo78nZp2jnKz3L65T5LdQ== - dependencies: - arrify "^1.0.1" - is-plain-obj "^1.1.0" - minimist@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" @@ -17080,6 +17384,17 @@ minipass-collect@^1.0.2: dependencies: minipass "^3.0.0" +minipass-fetch@^2.0.3: + version "2.1.2" + resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-2.1.2.tgz#95560b50c472d81a3bc76f20ede80eaed76d8add" + integrity sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA== + dependencies: + minipass "^3.1.6" + minipass-sized "^1.0.3" + minizlib "^2.1.2" + optionalDependencies: + encoding "^0.1.13" + minipass-flush@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/minipass-flush/-/minipass-flush-1.0.5.tgz#82e7135d7e89a50ffe64610a787953c4c4cbb373" @@ -17087,13 +17402,28 @@ minipass-flush@^1.0.5: dependencies: minipass "^3.0.0" -minipass-pipeline@^1.2.2: +minipass-json-stream@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minipass-json-stream/-/minipass-json-stream-1.0.1.tgz#7edbb92588fbfc2ff1db2fc10397acb7b6b44aa7" + integrity sha512-ODqY18UZt/I8k+b7rl2AENgbWE8IDYam+undIJONvigAz8KR5GWblsFTEfQs0WODsjbSXWlm+JHEv8Gr6Tfdbg== + dependencies: + jsonparse "^1.3.1" + minipass "^3.0.0" + +minipass-pipeline@^1.2.2, minipass-pipeline@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz#68472f79711c084657c067c5c6ad93cddea8214c" integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A== dependencies: minipass "^3.0.0" +minipass-sized@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/minipass-sized/-/minipass-sized-1.0.3.tgz#70ee5a7c5052070afacfbc22977ea79def353b70" + integrity sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g== + dependencies: + minipass "^3.0.0" + minipass@^2.2.0, minipass@^2.3.5, minipass@^2.6.0, minipass@^2.9.0: version "2.9.0" resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6" @@ -17109,6 +17439,13 @@ minipass@^3.0.0, minipass@^3.1.1: dependencies: yallist "^4.0.0" +minipass@^3.1.6: + version "3.3.6" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a" + integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== + dependencies: + yallist "^4.0.0" + minizlib@^1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d" @@ -17116,7 +17453,7 @@ minizlib@^1.3.3: dependencies: minipass "^2.9.0" -minizlib@^2.1.1: +minizlib@^2.1.1, minizlib@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== @@ -17166,6 +17503,15 @@ mkdirp-classic@^0.5.2: resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== +mkdirp-infer-owner@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mkdirp-infer-owner/-/mkdirp-infer-owner-2.0.0.tgz#55d3b368e7d89065c38f32fd38e638f0ab61d316" + integrity sha512-sdqtiFt3lkOaYvTXSRIUjkIdPTcxgv5+fgqYE/5qgwdw12cOrAuzzgzvVExIkH/ul1oeHN3bCLOWSG3XOqbKKw== + dependencies: + chownr "^2.0.0" + infer-owner "^1.0.4" + mkdirp "^1.0.3" + mkdirp@0.5.4: version "0.5.4" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.4.tgz#fd01504a6797ec5c9be81ff43d204961ed64a512" @@ -17173,7 +17519,7 @@ mkdirp@0.5.4: dependencies: minimist "^1.2.5" -mkdirp@0.5.x, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.5, mkdirp@^0.5.6, mkdirp@~0.5.0, mkdirp@~0.5.1: +mkdirp@0.5.x, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.5, mkdirp@^0.5.6, mkdirp@~0.5.1: version "0.5.6" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== @@ -17359,15 +17705,16 @@ multicast-dns@^6.0.1: dns-packet "^1.3.1" thunky "^1.0.2" -multimatch@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/multimatch/-/multimatch-2.1.0.tgz#9c7906a22fb4c02919e2f5f75161b4cdbd4b2a2b" - integrity sha1-nHkGoi+0wCkZ4vX3UWG0zb1LKis= +multimatch@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/multimatch/-/multimatch-5.0.0.tgz#932b800963cea7a31a033328fa1e0c3a1874dbe6" + integrity sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA== dependencies: - array-differ "^1.0.0" - array-union "^1.0.1" - arrify "^1.0.0" - minimatch "^3.0.0" + "@types/minimatch" "^3.0.3" + array-differ "^3.0.0" + array-union "^2.1.0" + arrify "^2.0.1" + minimatch "^3.0.4" mustache@^4.2.0: version "4.2.0" @@ -17452,7 +17799,7 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= -negotiator@0.6.3: +negotiator@0.6.3, negotiator@^0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== @@ -17607,6 +17954,11 @@ nock@^13.0.4, nock@^13.0.5, nock@^13.1.0: lodash.set "^4.3.2" propagate "^2.0.0" +node-addon-api@^3.2.1: + version "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-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" @@ -17649,22 +18001,26 @@ node-forge@^0.10.0: resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3" integrity sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA== -node-gyp@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-4.0.0.tgz#972654af4e5dd0cd2a19081b4b46fe0442ba6f45" - integrity sha512-2XiryJ8sICNo6ej8d0idXDEMKfVfFK7kekGCtJAuelGsYHQxhj13KTf95swTCN2dZ/4lTfZ84Fu31jqJEEgjWA== +node-gyp-build@^4.3.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.5.0.tgz#7a64eefa0b21112f89f58379da128ac177f20e40" + integrity sha512-2iGbaQBV+ITgCz76ZEjmhUKAKVf7xfY1sRl4UiKQspfZMH2h06SyhNsnSVy50cwkFQDGLyif6m/6uFXHkOZ6rg== + +node-gyp@^9.0.0: + version "9.3.0" + resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-9.3.0.tgz#f8eefe77f0ad8edb3b3b898409b53e697642b319" + integrity sha512-A6rJWfXFz7TQNjpldJ915WFb1LnhO4lIve3ANPbWreuEoLoKlFT3sxIepPBkLhM27crW8YmN+pjlgbasH6cH/Q== dependencies: - glob "^7.0.3" - graceful-fs "^4.1.2" - mkdirp "^0.5.0" - nopt "2 || 3" - npmlog "0 || 1 || 2 || 3 || 4" - osenv "0" - request "^2.87.0" - rimraf "2" - semver "~5.3.0" - tar "^4.4.8" - which "1" + env-paths "^2.2.0" + glob "^7.1.4" + graceful-fs "^4.2.6" + make-fetch-happen "^10.0.3" + nopt "^6.0.0" + npmlog "^6.0.0" + rimraf "^3.0.2" + semver "^7.3.5" + tar "^6.1.2" + which "^2.0.2" node-html-parser@1.4.9: version "1.4.9" @@ -17769,13 +18125,27 @@ nodemon@^2.0.16: undefsafe "^2.0.5" update-notifier "^5.1.0" -"nopt@2 || 3", nopt@3.x, nopt@^3.0.6: +nopt@3.x, nopt@^3.0.6: version "3.0.6" resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" integrity sha1-xkZdvwirzU2zWTF/eaxopkayj/k= dependencies: abbrev "1" +nopt@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" + integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ== + dependencies: + abbrev "1" + +nopt@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-6.0.0.tgz#245801d8ebf409c6df22ab9d95b65e1309cdb16d" + integrity sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g== + dependencies: + abbrev "^1.0.0" + nopt@~1.0.10: version "1.0.10" resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" @@ -17783,7 +18153,7 @@ nopt@~1.0.10: dependencies: abbrev "1" -normalize-package-data@^2.0.0, normalize-package-data@^2.3.0, normalize-package-data@^2.3.2, normalize-package-data@^2.3.4, normalize-package-data@^2.3.5, normalize-package-data@^2.4.0, normalize-package-data@^2.5.0: +normalize-package-data@^2.0.0, normalize-package-data@^2.3.2, normalize-package-data@^2.3.4, normalize-package-data@^2.4.0, normalize-package-data@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== @@ -17803,6 +18173,16 @@ normalize-package-data@^3.0.0: semver "^7.3.4" validate-npm-package-license "^3.0.1" +normalize-package-data@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-4.0.1.tgz#b46b24e0616d06cadf9d5718b29b6d445a82a62c" + integrity sha512-EBk5QKKuocMJhB3BILuKhmaPjI8vNRSpIfO9woLC6NyHVkKKdVEdAO1mrT0ZfxNR1lKwCcTkuZfmGIFdizZ8Pg== + dependencies: + hosted-git-info "^5.0.0" + is-core-module "^2.8.1" + semver "^7.3.5" + validate-npm-package-license "^3.0.4" + normalize-path@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" @@ -17839,7 +18219,7 @@ normalize-url@2.0.1: query-string "^5.0.1" sort-keys "^2.0.0" -normalize-url@^3.0.0, normalize-url@^3.3.0: +normalize-url@^3.0.0: version "3.3.0" resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-3.3.0.tgz#b2e1c4dc4f7c6d57743df733a4f5978d18650559" integrity sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg== @@ -17856,6 +18236,13 @@ npm-bundled@^1.0.1, npm-bundled@^1.1.1: dependencies: npm-normalize-package-bin "^1.0.1" +npm-bundled@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-2.0.1.tgz#94113f7eb342cd7a67de1e789f896b04d2c600f4" + integrity sha512-gZLxXdjEzE/+mOstGDqR6b0EkhJ+kM6fxM6vUuckuctuVPh80Q6pw/rSZj9s4Gex9GxWtIicO1pc8DB9KZWudw== + dependencies: + npm-normalize-package-bin "^2.0.0" + npm-install-checks@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/npm-install-checks/-/npm-install-checks-4.0.0.tgz#a37facc763a2fde0497ef2c6d0ac7c3fbe00d7b4" @@ -17863,25 +18250,23 @@ npm-install-checks@^4.0.0: dependencies: semver "^7.1.1" -npm-lifecycle@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/npm-lifecycle/-/npm-lifecycle-2.1.1.tgz#0027c09646f0fd346c5c93377bdaba59c6748fdf" - integrity sha512-+Vg6I60Z75V/09pdcH5iUo/99Q/vop35PaI99elvxk56azSVVsdsSsS/sXqKDNwbRRNN1qSxkcO45ZOu0yOWew== +npm-install-checks@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/npm-install-checks/-/npm-install-checks-5.0.0.tgz#5ff27d209a4e3542b8ac6b0c1db6063506248234" + integrity sha512-65lUsMI8ztHCxFz5ckCEC44DRvEGdZX5usQFriauxHEwt7upv1FKaQEmAtU0YnOAdwuNWCmk64xYiQABNrEyLA== dependencies: - byline "^5.0.0" - graceful-fs "^4.1.15" - node-gyp "^4.0.0" - resolve-from "^4.0.0" - slide "^1.1.6" - uid-number "0.0.6" - umask "^1.1.0" - which "^1.3.1" + semver "^7.1.1" npm-normalize-package-bin@^1.0.0, npm-normalize-package-bin@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz#6e79a41f23fd235c0623218228da7d9c23b8f6e2" integrity sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA== +npm-normalize-package-bin@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/npm-normalize-package-bin/-/npm-normalize-package-bin-2.0.0.tgz#9447a1adaaf89d8ad0abe24c6c84ad614a675fff" + integrity sha512-awzfKUO7v0FscrSpRoogyNm0sajikhBWpU0QMrW09AMi9n1PoKU6WaIqUzuJSQnpciZZmJ/jMZ2Egfmb/9LiWQ== + npm-package-arg@8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-8.0.1.tgz#9d76f8d7667b2373ffda60bb801a27ef71e3e270" @@ -17891,7 +18276,16 @@ npm-package-arg@8.0.1: semver "^7.0.0" validate-npm-package-name "^3.0.0" -"npm-package-arg@^4.0.0 || ^5.0.0 || ^6.0.0", npm-package-arg@^6.0.0, npm-package-arg@^6.1.0: +npm-package-arg@8.1.1: + version "8.1.1" + resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-8.1.1.tgz#00ebf16ac395c63318e67ce66780a06db6df1b04" + integrity sha512-CsP95FhWQDwNqiYS+Q0mZ7FAEDytDZAkNxQqea6IaAFJTAY9Lhhqyl0irU/6PMc7BGfUmnsbHcqxJD7XuVM/rg== + dependencies: + hosted-git-info "^3.0.6" + semver "^7.0.0" + validate-npm-package-name "^3.0.0" + +npm-package-arg@^6.0.0, npm-package-arg@^6.1.0: version "6.1.1" resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-6.1.1.tgz#02168cb0a49a2b75bf988a28698de7b529df5cb7" integrity sha512-qBpssaL3IOZWi5vEKUKW0cO7kzLeT+EQO9W8RsLOZf76KF9E/K9+wH0C7t06HXPpaH8WH5xF1MExLuCwbTqRUg== @@ -17910,7 +18304,7 @@ npm-package-arg@^8.0.0: semver "^7.3.4" validate-npm-package-name "^3.0.0" -npm-package-arg@^9.1.0: +npm-package-arg@^9.0.0, npm-package-arg@^9.0.1, npm-package-arg@^9.1.0: version "9.1.2" resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-9.1.2.tgz#fc8acecb00235f42270dda446f36926ddd9ac2bc" integrity sha512-pzd9rLEx4TfNJkovvlBSLGhq31gGu2QDexFPWT19yCDh0JgnRhlBLNo5759N0AJmBk+kQ9Y/hXoLnlgFD+ukmg== @@ -17920,7 +18314,7 @@ npm-package-arg@^9.1.0: semver "^7.3.5" validate-npm-package-name "^4.0.0" -npm-packlist@^1.1.12, npm-packlist@^1.4.1: +npm-packlist@^1.1.12: version "1.4.8" resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.8.tgz#56ee6cc135b9f98ad3d51c1c95da22bbb9b2ef3e" integrity sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A== @@ -17939,6 +18333,16 @@ npm-packlist@^2.1.4: npm-bundled "^1.1.1" npm-normalize-package-bin "^1.0.1" +npm-packlist@^5.1.0, npm-packlist@^5.1.1: + version "5.1.3" + resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-5.1.3.tgz#69d253e6fd664b9058b85005905012e00e69274b" + integrity sha512-263/0NGrn32YFYi4J533qzrQ/krmmrWwhKkzwTuM4f/07ug51odoaNjUexxO4vxlzURHcmYMH1QjvHjsNDKLVg== + dependencies: + glob "^8.0.1" + ignore-walk "^5.0.1" + npm-bundled "^2.0.0" + npm-normalize-package-bin "^2.0.0" + npm-pick-manifest@6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/npm-pick-manifest/-/npm-pick-manifest-6.1.0.tgz#2befed87b0fce956790f62d32afb56d7539c022a" @@ -17957,17 +18361,28 @@ npm-pick-manifest@^3.0.0: npm-package-arg "^6.0.0" semver "^5.4.1" -npm-registry-fetch@^3.9.0: - version "3.9.1" - resolved "https://registry.yarnpkg.com/npm-registry-fetch/-/npm-registry-fetch-3.9.1.tgz#00ff6e4e35d3f75a172b332440b53e93f4cb67de" - integrity sha512-VQCEZlydXw4AwLROAXWUR7QDfe2Y8Id/vpAgp6TI1/H78a4SiQ1kQrKZALm5/zxM5n4HIi+aYb+idUAV/RuY0Q== +npm-pick-manifest@^7.0.0: + version "7.0.2" + resolved "https://registry.yarnpkg.com/npm-pick-manifest/-/npm-pick-manifest-7.0.2.tgz#1d372b4e7ea7c6712316c0e99388a73ed3496e84" + integrity sha512-gk37SyRmlIjvTfcYl6RzDbSmS9Y4TOBXfsPnoYqTHARNgWbyDiCSMLUpmALDj4jjcTZpURiEfsSHJj9k7EV4Rw== dependencies: - JSONStream "^1.3.4" - bluebird "^3.5.1" - figgy-pudding "^3.4.1" - lru-cache "^5.1.1" - make-fetch-happen "^4.0.2" - npm-package-arg "^6.1.0" + npm-install-checks "^5.0.0" + npm-normalize-package-bin "^2.0.0" + npm-package-arg "^9.0.0" + semver "^7.3.5" + +npm-registry-fetch@^13.0.0, npm-registry-fetch@^13.0.1, npm-registry-fetch@^13.3.0: + version "13.3.1" + resolved "https://registry.yarnpkg.com/npm-registry-fetch/-/npm-registry-fetch-13.3.1.tgz#bb078b5fa6c52774116ae501ba1af2a33166af7e" + integrity sha512-eukJPi++DKRTjSBRcDZSDDsGqRK3ehbxfFUcgaRd0Yp6kRwOwh2WVn0r+8rMB4nnuzvAk6rQVzl6K5CkYOmnvw== + dependencies: + make-fetch-happen "^10.0.6" + minipass "^3.1.6" + minipass-fetch "^2.0.3" + minipass-json-stream "^1.0.1" + minizlib "^2.1.2" + npm-package-arg "^9.0.1" + proc-log "^2.0.0" npm-registry-fetch@^4.0.0: version "4.0.7" @@ -18018,7 +18433,7 @@ npm-run-path@^4.0.0, npm-run-path@^4.0.1: dependencies: path-key "^3.0.0" -"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.1.2: +npmlog@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== @@ -18028,7 +18443,7 @@ npm-run-path@^4.0.0, npm-run-path@^4.0.1: gauge "~2.7.3" set-blocking "~2.0.0" -npmlog@^6.0.0, npmlog@^6.0.1: +npmlog@^6.0.0, npmlog@^6.0.1, npmlog@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-6.0.2.tgz#c8166017a42f2dea92d6453168dd865186a70830" integrity sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg== @@ -18072,6 +18487,47 @@ nwsapi@^2.2.0: resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7" integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ== +nx@15.2.1, "nx@>=14.8.6 < 16": + version "15.2.1" + resolved "https://registry.yarnpkg.com/nx/-/nx-15.2.1.tgz#d51962c24180383d9af1880f57f29312c8311da7" + integrity sha512-vVeT5D02cDDiSmS3P2Bos/waYQa3m0yl/rouzsKpusVSmzAQGQbKXhxPb4WnNIj8Iz/8KjFeBM/RZO021BtGNg== + dependencies: + "@nrwl/cli" "15.2.1" + "@nrwl/tao" "15.2.1" + "@parcel/watcher" "2.0.4" + "@yarnpkg/lockfile" "^1.1.0" + "@yarnpkg/parsers" "^3.0.0-rc.18" + "@zkochan/js-yaml" "0.0.6" + axios "^1.0.0" + chalk "4.1.0" + chokidar "^3.5.1" + cli-cursor "3.1.0" + cli-spinners "2.6.1" + cliui "^7.0.2" + dotenv "~10.0.0" + enquirer "~2.3.6" + fast-glob "3.2.7" + figures "3.2.0" + flat "^5.0.2" + fs-extra "^10.1.0" + glob "7.1.4" + ignore "^5.0.4" + js-yaml "4.1.0" + jsonc-parser "3.2.0" + minimatch "3.0.5" + npm-run-path "^4.0.1" + open "^8.4.0" + semver "7.3.4" + string-width "^4.2.3" + strong-log-transformer "^2.1.0" + tar-stream "~2.2.0" + tmp "~0.2.1" + tsconfig-paths "^3.9.0" + tslib "^2.3.0" + v8-compile-cache "2.3.0" + yargs "^17.6.2" + yargs-parser "21.1.1" + oauth-sign@~0.9.0: version "0.9.0" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" @@ -18197,11 +18653,6 @@ obuf@^1.0.0, obuf@^1.1.2: resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== -octokit-pagination-methods@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz#cf472edc9d551055f9ef73f6e42b4dbb4c80bea4" - integrity sha512-fZ4qZdQ2nxJvtcasX7Ghl+WlWS/d9IgnBIwFZXVNNZUmzpno91SX5bc5vuxiuKoCtK78XxGGNuSCrDC7xYB3OQ== - on-finished@2.4.1, on-finished@^2.3.0: version "2.4.1" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" @@ -18258,6 +18709,15 @@ open@^7.4.2: is-docker "^2.0.0" is-wsl "^2.1.1" +open@^8.4.0: + version "8.4.0" + resolved "https://registry.yarnpkg.com/open/-/open-8.4.0.tgz#345321ae18f8138f82565a910fdc6b39e8c244f8" + integrity sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q== + dependencies: + define-lazy-prop "^2.0.0" + is-docker "^2.1.1" + is-wsl "^2.2.0" + opener@^1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" @@ -18335,7 +18795,7 @@ ora@^3.4.0: strip-ansi "^5.2.0" wcwidth "^1.0.1" -ora@^5.1.0, ora@^5.3.0, ora@^5.4.0: +ora@^5.1.0, ora@^5.3.0, ora@^5.4.0, ora@^5.4.1: version "5.4.1" resolved "https://registry.yarnpkg.com/ora/-/ora-5.4.1.tgz#1b2678426af4ac4a509008e5e4ac9e9959db9e18" integrity sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ== @@ -18367,29 +18827,12 @@ os-homedir@^1.0.0, os-homedir@^1.0.1: resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= -os-locale@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a" - integrity sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q== - dependencies: - execa "^1.0.0" - lcid "^2.0.0" - mem "^4.0.0" - -os-name@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/os-name/-/os-name-3.1.0.tgz#dec19d966296e1cd62d701a5a66ee1ddeae70801" - integrity sha512-h8L+8aNjNcMpo/mAIBPn5PXCM16iyPGjHNWo6U1YO8sJTMHtEtyczI6QJnLoplswm6goopQkqc7OAnjhWcugVg== - dependencies: - macos-release "^2.2.0" - windows-release "^3.1.0" - os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.1, os-tmpdir@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= -osenv@0, osenv@^0.1.3, osenv@^0.1.5: +osenv@^0.1.3, osenv@^0.1.5: version "0.1.5" resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g== @@ -18407,11 +18850,6 @@ p-cancelable@^1.0.0: resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw== -p-defer@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" - integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww= - p-defer@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-3.0.0.tgz#d1dceb4ee9b2b604b1d94ffec83760175d4e6f83" @@ -18439,11 +18877,6 @@ p-is-promise@^1.1.0: resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-1.1.0.tgz#9c9456989e9f6588017b0434d56097675c3da05e" integrity sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4= -p-is-promise@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.1.0.tgz#918cebaea248a62cf7ffab8e3bca8c5f882fc42e" - integrity sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg== - p-limit@3.1.0, p-limit@^3.0.1, p-limit@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" @@ -18507,17 +18940,10 @@ p-locate@^6.0.0: dependencies: p-limit "^4.0.0" -p-map-series@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-map-series/-/p-map-series-1.0.0.tgz#bf98fe575705658a9e1351befb85ae4c1f07bdca" - integrity sha1-v5j+V1cFZYqeE1G++4WuTB8Hvco= - dependencies: - p-reduce "^1.0.0" - -p-map@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.2.0.tgz#e4e94f311eabbc8633a1e79908165fca26241b6b" - integrity sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA== +p-map-series@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/p-map-series/-/p-map-series-2.1.0.tgz#7560d4c452d9da0c07e692fdbfe6e2c81a2a91f2" + integrity sha512-RpYIIK1zXSNEOdwxcfe7FdvGcs7+y5n8rifMhMNWvaxRNMPINJHF5GDeuVxWqnfrcHPSCnp7Oo5yNXHId9Av2Q== p-map@^2.0.0: version "2.1.0" @@ -18531,15 +18957,23 @@ p-map@^4.0.0: dependencies: aggregate-error "^3.0.0" -p-pipe@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/p-pipe/-/p-pipe-1.2.0.tgz#4b1a11399a11520a67790ee5a0c1d5881d6befe9" - integrity sha1-SxoROZoRUgpneQ7loMHViB1r7+k= +p-pipe@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-pipe/-/p-pipe-3.1.0.tgz#48b57c922aa2e1af6a6404cb7c6bf0eb9cc8e60e" + integrity sha512-08pj8ATpzMR0Y80x50yJHn37NF6vjrqHutASaX5LiH5npS9XPvrUmscd9MF5R4fuYRHOxQR1FfMIlF7AzwoPqw== -p-reduce@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-reduce/-/p-reduce-1.0.0.tgz#18c2b0dd936a4690a529f8231f58a0fdb6a47dfa" - integrity sha1-GMKw3ZNqRpClKfgjH1ig/bakffo= +p-queue@^6.6.2: + version "6.6.2" + resolved "https://registry.yarnpkg.com/p-queue/-/p-queue-6.6.2.tgz#2068a9dcf8e67dd0ec3e7a2bcb76810faa85e426" + integrity sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ== + dependencies: + eventemitter3 "^4.0.4" + p-timeout "^3.2.0" + +p-reduce@^2.0.0, p-reduce@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/p-reduce/-/p-reduce-2.1.0.tgz#09408da49507c6c274faa31f28df334bc712b64a" + integrity sha512-2USApvnsutq8uoxZBGbbWM0JIYLiEMJ9RlaN7fAzVNb9OZN0SHjjTTfIcb667XynS5Y1VhwDJVDa72TnPzAYWw== p-retry@^3.0.1: version "3.0.1" @@ -18555,7 +18989,7 @@ p-timeout@^2.0.1: dependencies: p-finally "^1.0.0" -p-timeout@^3.1.0: +p-timeout@^3.1.0, p-timeout@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe" integrity sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg== @@ -18572,12 +19006,12 @@ p-try@^2.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== -p-waterfall@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-waterfall/-/p-waterfall-1.0.0.tgz#7ed94b3ceb3332782353af6aae11aa9fc235bb00" - integrity sha1-ftlLPOszMngjU69qrhGqn8I1uwA= +p-waterfall@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/p-waterfall/-/p-waterfall-2.1.1.tgz#63153a774f472ccdc4eb281cdb2967fcf158b2ee" + integrity sha512-RRTnDb2TBG/epPRI2yYXsimO0v3BXC8Yd3ogr1545IaqKK17VGhbWVeGGN+XfCm/08OK8635nH31c8bATkHuSw== dependencies: - p-reduce "^1.0.0" + p-reduce "^2.0.0" package-json@^6.3.0, package-json@^6.5.0: version "6.5.0" @@ -18599,7 +19033,7 @@ packet-reader@1.0.0: resolved "https://registry.yarnpkg.com/packet-reader/-/packet-reader-1.0.0.tgz#9238e5480dedabacfe1fe3f2771063f164157d74" integrity sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ== -pacote@9.5.12, pacote@^9.5.0: +pacote@9.5.12: version "9.5.12" resolved "https://registry.yarnpkg.com/pacote/-/pacote-9.5.12.tgz#1e11dd7a8d736bcc36b375a9804d41bb0377bf66" integrity sha512-BUIj/4kKbwWg4RtnBncXPJd15piFSVNpTzY0rysSr3VnMowTYgkGKcaHrbReepAkjTr8lH2CVWRi58Spg2CicQ== @@ -18635,6 +19069,33 @@ pacote@9.5.12, pacote@^9.5.0: unique-filename "^1.1.1" which "^1.3.1" +pacote@^13.0.3, pacote@^13.6.1: + version "13.6.2" + resolved "https://registry.yarnpkg.com/pacote/-/pacote-13.6.2.tgz#0d444ba3618ab3e5cd330b451c22967bbd0ca48a" + integrity sha512-Gu8fU3GsvOPkak2CkbojR7vjs3k3P9cA6uazKTHdsdV0gpCEQq2opelnEv30KRQWgVzP5Vd/5umjcedma3MKtg== + dependencies: + "@npmcli/git" "^3.0.0" + "@npmcli/installed-package-contents" "^1.0.7" + "@npmcli/promise-spawn" "^3.0.0" + "@npmcli/run-script" "^4.1.0" + cacache "^16.0.0" + chownr "^2.0.0" + fs-minipass "^2.1.0" + infer-owner "^1.0.4" + minipass "^3.1.6" + mkdirp "^1.0.4" + npm-package-arg "^9.0.0" + npm-packlist "^5.1.0" + npm-pick-manifest "^7.0.0" + npm-registry-fetch "^13.0.1" + proc-log "^2.0.0" + promise-retry "^2.0.1" + read-package-json "^5.0.0" + read-package-json-fast "^2.0.3" + rimraf "^3.0.2" + ssri "^9.0.0" + tar "^6.1.11" + pad@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/pad/-/pad-3.2.0.tgz#be7a1d1cb6757049b4ad5b70e71977158fea95d1" @@ -18687,10 +19148,14 @@ parse-asn1@^5.0.0, parse-asn1@^5.1.5: pbkdf2 "^3.0.3" safe-buffer "^5.1.1" -parse-github-repo-url@^1.3.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/parse-github-repo-url/-/parse-github-repo-url-1.4.1.tgz#9e7d8bb252a6cb6ba42595060b7bf6df3dbc1f50" - integrity sha1-nn2LslKmy2ukJZUGC3v23z28H1A= +parse-conflict-json@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/parse-conflict-json/-/parse-conflict-json-2.0.2.tgz#3d05bc8ffe07d39600dc6436c6aefe382033d323" + integrity sha512-jDbRGb00TAPFsKWCpZZOT93SxVP9nONOSgES3AevqRq/CHvavEBvKAjxX9p5Y5F0RZLxH9Ufd9+RwtCsa+lFDA== + dependencies: + json-parse-even-better-errors "^2.3.1" + just-diff "^5.0.1" + just-diff-apply "^5.2.0" parse-json@^2.2.0: version "2.2.0" @@ -18727,30 +19192,24 @@ parse-passwd@^1.0.0: resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY= -parse-path@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/parse-path/-/parse-path-4.0.3.tgz#82d81ec3e071dcc4ab49aa9f2c9c0b8966bb22bf" - integrity sha512-9Cepbp2asKnWTJ9x2kpw6Fe8y9JDbqwahGCTvklzd/cEq5C5JC59x2Xb0Kx+x0QZ8bvNquGO8/BWP0cwBHzSAA== +parse-path@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/parse-path/-/parse-path-7.0.0.tgz#605a2d58d0a749c8594405d8cc3a2bf76d16099b" + integrity sha512-Euf9GG8WT9CdqwuWJGdf3RkUcTBArppHABkO7Lm8IzRQp0e2r/kkFnmhu4TSK30Wcu5rVAZLmfPKSBBi9tWFog== dependencies: - is-ssh "^1.3.0" - protocols "^1.4.0" - qs "^6.9.4" - query-string "^6.13.8" + protocols "^2.0.0" parse-static-imports@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/parse-static-imports/-/parse-static-imports-1.1.0.tgz#ae2f18f18da1a993080ae406a5219455c0bbad5d" integrity sha512-HlxrZcISCblEV0lzXmAHheH/8qEkKgmqkdxyHTPbSqsTUV8GzqmN1L+SSti+VbNPfbBO3bYLPHDiUs2avbAdbA== -parse-url@^5.0.0: - version "5.0.2" - resolved "https://registry.yarnpkg.com/parse-url/-/parse-url-5.0.2.tgz#856a3be1fcdf78dc93fc8b3791f169072d898b59" - integrity sha512-Czj+GIit4cdWtxo3ISZCvLiUjErSo0iI3wJ+q9Oi3QuMYTI6OZu+7cewMWZ+C1YAnKhYTk6/TLuhIgCypLthPA== +parse-url@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/parse-url/-/parse-url-8.1.0.tgz#972e0827ed4b57fc85f0ea6b0d839f0d8a57a57d" + integrity sha512-xDvOoLU5XRrcOZvnI6b8zA6n9O9ejNk/GExuz1yBuWUGn9KA97GI6HTs6u02wKara1CeVmZhH+0TZFdWScR89w== dependencies: - is-ssh "^1.3.0" - normalize-url "^3.3.0" - parse-path "^4.0.0" - protocols "^1.4.0" + parse-path "^7.0.0" parse5-htmlparser2-tree-adapter@6.0.1: version "6.0.1" @@ -19059,6 +19518,11 @@ pify@^4.0.1: resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== +pify@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-5.0.0.tgz#1f5eca3f5e87ebec28cc6d54a0e4aaf00acc127f" + integrity sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA== + pinkie-promise@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" @@ -19711,7 +20175,7 @@ private@^0.1.6, private@^0.1.8: resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" integrity sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg== -proc-log@^2.0.1: +proc-log@^2.0.0, proc-log@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/proc-log/-/proc-log-2.0.1.tgz#8f3f69a1f608de27878f91f5c688b225391cb685" integrity sha512-Kcmo2FhfDTXdcbfDH76N7uBYHINxc/8GW7UAVuVP9I+Va3uHSerrnKV6dLooga/gh7GlgzuCCr/eoldnL1muGw== @@ -19738,6 +20202,16 @@ progress@^2.0.0, progress@^2.0.1, progress@^2.0.3: resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== +promise-all-reject-late@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/promise-all-reject-late/-/promise-all-reject-late-1.0.1.tgz#f8ebf13483e5ca91ad809ccc2fcf25f26f8643c2" + integrity sha512-vuf0Lf0lOxyQREH7GDIOUMLS7kz+gs8i6B+Yi8dC68a2sychGrHTJYghMBD6k7eUcH0H5P73EckCA48xijWqXw== + +promise-call-limit@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/promise-call-limit/-/promise-call-limit-1.0.1.tgz#4bdee03aeb85674385ca934da7114e9bcd3c6e24" + integrity sha512-3+hgaa19jzCGLuSCbieeRsu5C2joKfYn8pY6JAuXFRVfF4IO+L7UPpFWNTeWT9pM7uhskvbPPd/oEOktCn317Q== + promise-inflight@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" @@ -19763,6 +20237,14 @@ promise-retry@^1.1.1: err-code "^1.0.0" retry "^0.10.0" +promise-retry@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-2.0.1.tgz#ff747a13620ab57ba688f5fc67855410c370da22" + integrity sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g== + dependencies: + err-code "^2.0.2" + retry "^0.12.0" + promise.hash.helper@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/promise.hash.helper/-/promise.hash.helper-1.0.8.tgz#8c5fa0570f6f96821f52364fd72292b2c5a114f7" @@ -19846,10 +20328,10 @@ protobufjs@^6.10.2, protobufjs@^6.8.6: "@types/node" ">=13.7.0" long "^4.0.0" -protocols@^1.1.0, protocols@^1.4.0: - version "1.4.8" - resolved "https://registry.yarnpkg.com/protocols/-/protocols-1.4.8.tgz#48eea2d8f58d9644a4a32caae5d5db290a075ce8" - integrity sha512-IgjKyaUSjsROSO8/D49Ab7hP8mJgTYcqApOqdPhLoPxAplXmkp+zRvsrSQjFn5by0rhm4VH0GAUELIPpx7B1yg== +protocols@^2.0.0, protocols@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/protocols/-/protocols-2.0.1.tgz#8f155da3fc0f32644e83c5782c8e8212ccf70a86" + integrity sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q== protoduck@^5.0.1: version "5.0.1" @@ -19980,7 +20462,7 @@ qjobs@^1.2.0: resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.2.0.tgz#c45e9c61800bd087ef88d7e256423bdd49e5d071" integrity sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg== -qs@6.11.0, qs@^6.4.0, qs@^6.9.4: +qs@6.11.0, qs@^6.4.0: version "6.11.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== @@ -20009,16 +20491,6 @@ query-string@^5.0.1: object-assign "^4.1.0" strict-uri-encode "^1.0.0" -query-string@^6.13.8: - version "6.14.1" - resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.14.1.tgz#7ac2dca46da7f309449ba0f86b1fd28255b0c86a" - integrity sha512-XDxAeVmpfu1/6IjyT/gXHOl+S0vQ9owggJ30hhWKdHAsNPOcasn5o9BW0eejZqL2e4vMjhAxoW3jVHcD6mbcYw== - dependencies: - decode-uri-component "^0.2.0" - filter-obj "^1.1.0" - split-on-first "^1.0.0" - strict-uri-encode "^2.0.0" - querystring-es3@0.2.1, querystring-es3@^0.2.0, querystring-es3@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" @@ -20044,11 +20516,6 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== -quick-lru@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-1.1.0.tgz#4360b17c61136ad38078397ff11416e186dcfbb8" - integrity sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g= - quick-lru@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" @@ -20257,14 +20724,20 @@ read-cache@^1.0.0: dependencies: pify "^2.3.0" -read-cmd-shim@^1.0.1: - version "1.0.5" - resolved "https://registry.yarnpkg.com/read-cmd-shim/-/read-cmd-shim-1.0.5.tgz#87e43eba50098ba5a32d0ceb583ab8e43b961c16" - integrity sha512-v5yCqQ/7okKoZZkBQUAfTsQ3sVJtXdNfbPnI5cceppoxEVLYA3k+VtV2omkeo8MS94JCy4fSiUwlRBAwCVRPUA== +read-cmd-shim@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/read-cmd-shim/-/read-cmd-shim-3.0.1.tgz#868c235ec59d1de2db69e11aec885bc095aea087" + integrity sha512-kEmDUoYf/CDy8yZbLTmhB1X9kkjf9Q80PCNsDMb7ufrGd6zZSQA1+UyjrO+pZm5K/S4OXCWJeiIt1JA8kAsa6g== + +read-package-json-fast@^2.0.2, read-package-json-fast@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/read-package-json-fast/-/read-package-json-fast-2.0.3.tgz#323ca529630da82cb34b36cc0b996693c98c2b83" + integrity sha512-W/BKtbL+dUjTuRL2vziuYhp76s5HZ9qQhd/dKfWIZveD0O40453QNyZhC0e63lqZrAQ4jiOapVoeJ7JrszenQQ== dependencies: - graceful-fs "^4.1.2" + json-parse-even-better-errors "^2.3.0" + npm-normalize-package-bin "^1.0.1" -"read-package-json@1 || 2", read-package-json@^2.0.0, read-package-json@^2.0.13: +read-package-json@^2.0.0: version "2.1.2" resolved "https://registry.yarnpkg.com/read-package-json/-/read-package-json-2.1.2.tgz#6992b2b66c7177259feb8eaac73c3acd28b9222a" integrity sha512-D1KmuLQr6ZSJS0tW8hf3WGpRlwszJOXZ3E8Yd/DNRaM5d+1wVRZdHlpGBLAuovjr28LbWvjpWkBHMxpRGGjzNA== @@ -20274,7 +20747,17 @@ read-cmd-shim@^1.0.1: normalize-package-data "^2.0.0" npm-normalize-package-bin "^1.0.0" -read-package-tree@5.3.1, read-package-tree@^5.1.6: +read-package-json@^5.0.0, read-package-json@^5.0.1: + version "5.0.2" + resolved "https://registry.yarnpkg.com/read-package-json/-/read-package-json-5.0.2.tgz#b8779ccfd169f523b67208a89cc912e3f663f3fa" + integrity sha512-BSzugrt4kQ/Z0krro8zhTwV1Kd79ue25IhNN/VtHFy1mG/6Tluyi+msc0UpwaoQzxSHa28mntAjIZY6kEgfR9Q== + dependencies: + glob "^8.0.1" + json-parse-even-better-errors "^2.3.1" + normalize-package-data "^4.0.0" + npm-normalize-package-bin "^2.0.0" + +read-package-tree@5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/read-package-tree/-/read-package-tree-5.3.1.tgz#a32cb64c7f31eb8a6f31ef06f9cedf74068fe636" integrity sha512-mLUDsD5JVtlZxjSlPPx1RETkNjjvQYuweKwNVt1Sn8kP5Jh44pvYuUHCp6xSVDZWbNxVxG5lyZJ921aJH61sTw== @@ -20369,7 +20852,7 @@ read-pkg@^5.0.0, read-pkg@^5.2.0: parse-json "^5.0.0" type-fest "^0.6.0" -read@1, read@~1.0.1: +read@1, read@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/read/-/read-1.0.7.tgz#b3da19bd052431a97671d44a42634adf710b40c4" integrity sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ= @@ -20408,7 +20891,7 @@ readable-stream@~1.0.2: isarray "0.0.1" string_decoder "~0.10.x" -readdir-scoped-modules@^1.0.0: +readdir-scoped-modules@^1.0.0, readdir-scoped-modules@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz#8d45407b4f870a0dcaebc0e28670d18e74514309" integrity sha512-asaikDeqAQg7JifRsZn1NJZXo9E+VwlyCfbkZhwyISinqk5zNS6266HS5kah6P0SaQKGF6SkNnZVHUzHFYxYDw== @@ -20483,14 +20966,6 @@ redent@^1.0.0: indent-string "^2.1.0" strip-indent "^1.0.1" -redent@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/redent/-/redent-2.0.0.tgz#c1b2007b42d57eb1389079b3c8333639d5e1ccaa" - integrity sha1-wbIAe0LVfrE4kHmzyDM2OdXhzKo= - dependencies: - indent-string "^3.0.0" - strip-indent "^2.0.0" - redent@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" @@ -20731,7 +21206,7 @@ replace-in-file@^4.0.0: glob "^7.1.6" yargs "^15.0.2" -request@^2.87.0, request@^2.88.0, request@^2.88.2: +request@^2.88.0, request@^2.88.2: version "2.88.2" resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== @@ -20772,11 +21247,6 @@ require-from-string@^2.0.2: resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== -require-main-filename@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" - integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE= - require-main-filename@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" @@ -21050,13 +21520,6 @@ rgba-regex@^1.0.0: resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3" integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM= -rimraf@2, rimraf@^2.2.8, rimraf@^2.3.4, rimraf@^2.4.3, rimraf@^2.5.3, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3: - version "2.7.1" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" - integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== - dependencies: - glob "^7.1.3" - rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.1, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" @@ -21064,6 +21527,13 @@ rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.1, rimraf@^3.0.2: dependencies: glob "^7.1.3" +rimraf@^2.2.8, rimraf@^2.3.4, rimraf@^2.4.3, rimraf@^2.5.3, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3: + version "2.7.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" + integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== + dependencies: + glob "^7.1.3" + rimraf@~2.5.2: version "2.5.4" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.5.4.tgz#96800093cbf1a0c86bd95b4625467535c29dfa04" @@ -21230,6 +21700,13 @@ rxjs@^6.4.0, rxjs@^6.5.0, rxjs@^6.6.0: dependencies: tslib "^1.9.0" +rxjs@^7.5.5: + version "7.5.7" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.7.tgz#2ec0d57fdc89ece220d2e702730ae8f1e49def39" + integrity sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA== + dependencies: + tslib "^2.1.0" + safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" @@ -21429,7 +21906,7 @@ semver-intersect@1.4.0: dependencies: semver "^5.0.0" -"semver@2 || 3 || 4 || 5", "semver@2.x || 3.x || 4 || 5", semver@^5.0.0, semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1: +"semver@2 || 3 || 4 || 5", semver@^5.0.0, semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== @@ -21458,11 +21935,6 @@ semver@^6.0.0, semver@^6.1.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semve resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== -semver@~5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" - integrity sha1-myzl094C0XxgEq0yaqa00M9U+U8= - send@0.18.0: version "0.18.0" resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" @@ -21577,6 +22049,13 @@ shallow-clone@^0.1.2: lazy-cache "^0.2.3" mixin-object "^2.0.1" +shallow-clone@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" + integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== + dependencies: + kind-of "^6.0.2" + shebang-command@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" @@ -21740,16 +22219,16 @@ slice-ansi@^4.0.0: astral-regex "^2.0.0" is-fullwidth-code-point "^3.0.0" -slide@^1.1.6: - version "1.1.6" - resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707" - integrity sha1-VusCfWW00tzmyy4tMsTUr8nh1wc= - smart-buffer@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.1.0.tgz#91605c25d91652f4661ea69ccf45f1b331ca21ba" integrity sha512-iVICrxOzCynf/SNaBQCw34eM9jROU/s5rzIhpOvzhzuYHfJR/DhZfDkXiZSgKXfgv26HT3Yni3AV/DGw0cGnnw== +smart-buffer@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" + integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== + snake-case@^3.0.3: version "3.0.4" resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c" @@ -21842,6 +22321,23 @@ socks-proxy-agent@^4.0.0: agent-base "~4.2.1" socks "~2.3.2" +socks-proxy-agent@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz#dc069ecf34436621acb41e3efa66ca1b5fed15b6" + integrity sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww== + dependencies: + agent-base "^6.0.2" + debug "^4.3.3" + socks "^2.6.2" + +socks@^2.6.2: + version "2.7.1" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.7.1.tgz#d8e651247178fde79c0663043e07240196857d55" + integrity sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ== + dependencies: + ip "^2.0.0" + smart-buffer "^4.2.0" + socks@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/socks/-/socks-2.3.3.tgz#01129f0a5d534d2b897712ed8aceab7ee65d78e3" @@ -21864,6 +22360,13 @@ sort-keys@^2.0.0: dependencies: is-plain-obj "^1.0.0" +sort-keys@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-4.2.0.tgz#6b7638cee42c506fff8c1cecde7376d21315be18" + integrity sha512-aUYIEU/UviqPgc8mHR6IW1EGxkAXpeRETYcrzg8cLAvUPZcpAlleSXHV2mY7G12GphSH6Gzv+4MMVSSkbdteHg== + dependencies: + is-plain-obj "^2.0.0" + sort-object-keys@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/sort-object-keys/-/sort-object-keys-1.1.3.tgz#bff833fe85cab147b34742e45863453c1e190b45" @@ -22120,11 +22623,6 @@ speed-measure-webpack-plugin@1.3.3: dependencies: chalk "^2.0.1" -split-on-first@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f" - integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw== - split-string@^3.0.1, split-string@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" @@ -22132,13 +22630,6 @@ split-string@^3.0.1, split-string@^3.0.2: dependencies: extend-shallow "^3.0.0" -split2@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/split2/-/split2-2.2.0.tgz#186b2575bcf83e85b7d18465756238ee4ee42493" - integrity sha512-RAb22TG39LhI31MbreBgIuKiIKhVsawfTgEGqKHTK87aG+ul/PB8Sqoi3I7kVdRWiCfrKxK3uo4/YUkpNvhPbw== - dependencies: - through2 "^2.0.2" - split2@^3.0.0: version "3.2.2" resolved "https://registry.yarnpkg.com/split2/-/split2-3.2.2.tgz#bf2cf2a37d838312c249c89206fd7a17dd12365f" @@ -22214,6 +22705,13 @@ ssri@^8.0.0, ssri@^8.0.1: dependencies: minipass "^3.1.1" +ssri@^9.0.0, ssri@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-9.0.1.tgz#544d4c357a8d7b71a19700074b6883fcb4eae057" + integrity sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q== + dependencies: + minipass "^3.1.1" + stable@^0.1.8: version "0.1.8" resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" @@ -22366,11 +22864,6 @@ strict-uri-encode@^1.0.0: resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" integrity sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM= -strict-uri-encode@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546" - integrity sha1-ucczDHBChi9rFC3CdLvMWGbONUY= - string-hash@1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/string-hash/-/string-hash-1.1.3.tgz#e8aafc0ac1855b4666929ed7dd1275df5d6c811b" @@ -22398,7 +22891,7 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1: +"string-width@^1.0.2 || 2", string-width@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== @@ -22562,11 +23055,6 @@ strip-indent@^1.0.1: dependencies: get-stdin "^4.0.1" -strip-indent@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68" - integrity sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g= - strip-indent@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" @@ -22584,7 +23072,7 @@ strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== -strong-log-transformer@^2.0.0: +strong-log-transformer@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/strong-log-transformer/-/strong-log-transformer-2.1.0.tgz#0f5ed78d325e0421ac6f90f7f10e691d6ae3ae10" integrity sha512-B3Hgul+z0L9a236FAUC9iZsL+nVHgoCJnqCbN588DjYxvGXaXaaFbfmQ/JhvKjZwsOukuR72XbHv71Qkug0HxA== @@ -22869,7 +23357,7 @@ tar-fs@^2.0.0: pump "^3.0.0" tar-stream "^2.1.4" -tar-stream@^2.1.4: +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" integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== @@ -22880,7 +23368,7 @@ tar-stream@^2.1.4: inherits "^2.0.3" readable-stream "^3.1.1" -tar@^4.4.10, tar@^4.4.8: +tar@^4.4.10: version "4.4.19" resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.19.tgz#2e4d7263df26f2b914dee10c825ab132123742f3" integrity sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA== @@ -22905,6 +23393,18 @@ tar@^6.0.2: mkdirp "^1.0.3" yallist "^4.0.0" +tar@^6.1.0, tar@^6.1.11, tar@^6.1.2: + version "6.1.12" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.12.tgz#3b742fb05669b55671fb769ab67a7791ea1a62e6" + integrity sha512-jU4TdemS31uABHd+Lt5WEYJuzn+TJTCBLljvIAHZOz6M9Os5pJ4dD+vRFLxPa/n3T0iEFzpi+0x1UfuDZYbRMw== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^3.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + teeny-request@6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/teeny-request/-/teeny-request-6.0.1.tgz#9b1f512cef152945827ba7e34f62523a4ce2c5b0" @@ -22939,18 +23439,6 @@ temp-fs@^0.9.9: dependencies: rimraf "~2.5.2" -temp-write@^3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/temp-write/-/temp-write-3.4.0.tgz#8cff630fb7e9da05f047c74ce4ce4d685457d492" - integrity sha1-jP9jD7fp2gXwR8dM5M5NaFRX1JI= - dependencies: - graceful-fs "^4.1.2" - is-stream "^1.1.0" - make-dir "^1.0.0" - pify "^3.0.0" - temp-dir "^1.0.0" - uuid "^3.0.1" - temp@0.9.4: version "0.9.4" resolved "https://registry.yarnpkg.com/temp/-/temp-0.9.4.tgz#cd20a8580cb63635d0e4e9d4bd989d44286e7620" @@ -23136,7 +23624,7 @@ through2@3.0.0: readable-stream "2 || 3" xtend "~4.0.1" -through2@^2.0.0, through2@^2.0.2: +through2@^2.0.0: version "2.0.5" resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== @@ -23144,7 +23632,7 @@ through2@^2.0.0, through2@^2.0.2: readable-stream "~2.3.6" xtend "~4.0.1" -through2@^3.0.0, through2@^3.0.1: +through2@^3.0.1: version "3.0.2" resolved "https://registry.yarnpkg.com/through2/-/through2-3.0.2.tgz#99f88931cfc761ec7678b41d5d7336b5b6a07bf4" integrity sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ== @@ -23237,7 +23725,7 @@ tmp@^0.1.0: dependencies: rimraf "^2.6.3" -tmp@^0.2.1: +tmp@^0.2.1, tmp@~0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ== @@ -23393,26 +23881,21 @@ tree-sync@^2.0.0, tree-sync@^2.1.0: quick-temp "^0.1.5" walk-sync "^0.3.3" +treeverse@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/treeverse/-/treeverse-2.0.0.tgz#036dcef04bc3fd79a9b79a68d4da03e882d8a9ca" + integrity sha512-N5gJCkLu1aXccpOTtqV6ddSEi6ZmGkh3hjmbu1IjcavJK4qyOVQmi0myQKM7z5jVGmD68SJoliaVrMmVObhj6A== + trim-newlines@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" integrity sha1-WIeWa7WCpFA6QetST301ARgVphM= -trim-newlines@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-2.0.0.tgz#b403d0b91be50c331dfc4b82eeceb22c3de16d20" - integrity sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA= - trim-newlines@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.0.tgz#79726304a6a898aa8373427298d54c2ee8b1cb30" integrity sha512-C4+gOpvmxaSMKuEf9Qc134F1ZuOHVXKRbtEflf4NTtuuJDEIJ9p5PXsalL8SkeRw+qit1Mo+yuvMPAKwWg/1hA== -trim-off-newlines@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/trim-off-newlines/-/trim-off-newlines-1.0.3.tgz#8df24847fcb821b0ab27d58ab6efec9f2fe961a1" - integrity sha512-kh6Tu6GbeSNMGfrrZh6Bb/4ZEHV1QlB4xNDBeog8Y9/QwFlKTRyWvY3Fs9tRDAMZliVUwieMgEdIeL/FtqjkJg== - trim-right@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" @@ -23562,6 +24045,11 @@ type-fest@^0.21.3: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== +type-fest@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.4.1.tgz#8bdf77743385d8a4f13ba95f610f5ccd68c728f8" + integrity sha512-IwzA/LSfD2vC1/YDYMv/zHP4rDF1usCwllsDpbolT3D4fUepIO7f9K70jjmUewU/LmGUKJcwcVtDCpnKk4BPMw== + type-fest@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b" @@ -23650,6 +24138,11 @@ typescript@4.0.2: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.2.tgz#7ea7c88777c723c681e33bf7988be5d008d05ac2" integrity sha512-e4ERvRV2wb+rRZ/IQeb3jm2VxBsirQLpQhdxplZ2MEzGvDkkMmPglecnNDfSUBivMjP93vRbngYYDQqQ/78bcQ== +"typescript@^3 || ^4": + version "4.9.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.3.tgz#3aea307c1746b8c384435d8ac36b8a2e580d85db" + integrity sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA== + typescript@^3.9.5, typescript@^3.9.7: version "3.9.9" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.9.tgz#e69905c54bc0681d0518bd4d587cc6f2d0b1a674" @@ -23680,16 +24173,6 @@ uglify-js@^3.1.4: resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.13.3.tgz#ce72a1ad154348ea2af61f50933c76cc8802276e" integrity sha512-otIc7O9LyxpUcQoXzj2hL4LPWKklO6LJWoJUzNa8A17Xgi4fOeDC8FBDOLHnC/Slo1CQgsZMcM6as0M76BZaig== -uid-number@0.0.6: - version "0.0.6" - resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" - integrity sha1-DqEOgDXo61uOREnwbaHHMGY7qoE= - -umask@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/umask/-/umask-1.1.0.tgz#f29cebf01df517912bb58ff9c4e50fde8e33320d" - integrity sha1-8pzr8B31F5ErtY/5xOUP3o4zMg0= - unbox-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" @@ -23776,6 +24259,13 @@ unique-filename@^1.1.1: dependencies: unique-slug "^2.0.0" +unique-filename@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-2.0.1.tgz#e785f8675a9a7589e0ac77e0b5c34d2eaeac6da2" + integrity sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A== + dependencies: + unique-slug "^3.0.0" + unique-slug@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.2.tgz#baabce91083fc64e945b0f3ad613e264f7cd4e6c" @@ -23783,6 +24273,13 @@ unique-slug@^2.0.0: dependencies: imurmurhash "^0.1.4" +unique-slug@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-3.0.0.tgz#6d347cf57c8a7a7a6044aabd0e2d74e4d76dc7c9" + integrity sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w== + dependencies: + imurmurhash "^0.1.4" + unique-string@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d" @@ -23799,13 +24296,6 @@ universal-analytics@0.4.23: request "^2.88.2" uuid "^3.0.0" -universal-user-agent@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-4.0.1.tgz#fd8d6cb773a679a709e967ef8288a31fcc03e557" - integrity sha512-LnST3ebHwVL2aNe4mejI9IQh2HfZ1RLo8Io2HugSif8ekzD1TlWpHpColOB/eh8JHMLkGH3Akqf040I+4ylNxg== - dependencies: - os-name "^3.1.0" - universal-user-agent@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-6.0.0.tgz#3381f8503b251c0d9cd21bc1de939ec9df5480ee" @@ -24059,7 +24549,7 @@ uuid@8.3.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.0.tgz#ab738085ca22dc9a8c92725e459b1d507df5d6ea" integrity sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ== -uuid@^3.0.0, uuid@^3.0.1, uuid@^3.3.2, uuid@^3.4.0: +uuid@^3.0.0, uuid@^3.3.2, uuid@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== @@ -24074,7 +24564,7 @@ v8-compile-cache-lib@^3.0.1: resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== -v8-compile-cache@^2.0.3, v8-compile-cache@^2.3.0: +v8-compile-cache@2.3.0, v8-compile-cache@^2.0.3, v8-compile-cache@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== @@ -24088,7 +24578,7 @@ v8-to-istanbul@^8.1.0: convert-source-map "^1.6.0" source-map "^0.7.3" -validate-npm-package-license@^3.0.1, validate-npm-package-license@^3.0.3: +validate-npm-package-license@^3.0.1, validate-npm-package-license@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== @@ -24277,6 +24767,11 @@ walk-sync@^3.0.0: matcher-collection "^2.0.1" minimatch "^3.0.4" +walk-up-path@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/walk-up-path/-/walk-up-path-1.0.0.tgz#d4745e893dd5fd0dbb58dd0a4c6a33d9c9fec53e" + integrity sha512-hwj/qMDUEjCU5h0xr90KGCf0tg0/LgJbmOWgrWKYlcJZM7XvquvUJZ0G/HMGr7F7OQMOUuPHWP9JpriinkAlkg== + walkdir@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/walkdir/-/walkdir-0.4.1.tgz#dc119f83f4421df52e3061e514228a2db20afa39" @@ -24709,7 +25204,7 @@ which-typed-array@^1.1.2: has-symbols "^1.0.1" is-typed-array "^1.1.3" -which@1, which@1.3.1, which@^1.1.1, which@^1.2.1, which@^1.2.12, which@^1.2.14, which@^1.2.9, which@^1.3.1: +which@1.3.1, which@^1.1.1, which@^1.2.1, which@^1.2.12, which@^1.2.14, which@^1.2.9, which@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== @@ -24744,13 +25239,6 @@ widest-line@^3.1.0: dependencies: string-width "^4.0.0" -windows-release@^3.1.0: - version "3.3.3" - resolved "https://registry.yarnpkg.com/windows-release/-/windows-release-3.3.3.tgz#1c10027c7225743eec6b89df160d64c2e0293999" - integrity sha512-OSOGH1QYiW5yVor9TtmXKQvt2vjQqbYS+DqmsZw+r7xDwLXEeT3JGW0ZppFmHx4diyXmxt238KFR3N9jzevBRg== - dependencies: - execa "^1.0.0" - word-wrap@^1.2.3, word-wrap@~1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" @@ -24799,14 +25287,6 @@ workerpool@^6.1.5, workerpool@^6.2.1: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== -wrap-ansi@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" - integrity sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU= - dependencies: - string-width "^1.0.1" - strip-ansi "^3.0.1" - wrap-ansi@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" @@ -24848,7 +25328,7 @@ write-file-atomic@2.4.1: imurmurhash "^0.1.4" signal-exit "^3.0.2" -write-file-atomic@^2.0.0, write-file-atomic@^2.3.0: +write-file-atomic@^2.4.2: version "2.4.3" resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.4.3.tgz#1fd2e9ae1df3e75b8d8c367443c692d4ca81f481" integrity sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ== @@ -24867,25 +25347,46 @@ write-file-atomic@^3.0.0: signal-exit "^3.0.2" typedarray-to-buffer "^3.1.5" -write-json-file@^2.2.0, write-json-file@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/write-json-file/-/write-json-file-2.3.0.tgz#2b64c8a33004d54b8698c76d585a77ceb61da32f" - integrity sha1-K2TIozAE1UuGmMdtWFp3zrYdoy8= +write-file-atomic@^4.0.0, write-file-atomic@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-4.0.2.tgz#a9df01ae5b77858a027fd2e80768ee433555fcfd" + integrity sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg== + dependencies: + imurmurhash "^0.1.4" + signal-exit "^3.0.7" + +write-json-file@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/write-json-file/-/write-json-file-3.2.0.tgz#65bbdc9ecd8a1458e15952770ccbadfcff5fe62a" + integrity sha512-3xZqT7Byc2uORAatYiP3DHUUAVEkNOswEWNs9H5KXiicRTvzYzYqKjYc4G7p+8pltvAw641lVByKVtMpf+4sYQ== dependencies: detect-indent "^5.0.0" - graceful-fs "^4.1.2" - make-dir "^1.0.0" - pify "^3.0.0" + graceful-fs "^4.1.15" + make-dir "^2.1.0" + pify "^4.0.1" sort-keys "^2.0.0" - write-file-atomic "^2.0.0" + write-file-atomic "^2.4.2" -write-pkg@^3.1.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/write-pkg/-/write-pkg-3.2.0.tgz#0e178fe97820d389a8928bc79535dbe68c2cff21" - integrity sha512-tX2ifZ0YqEFOF1wjRW2Pk93NLsj02+n1UP5RvO6rCs0K6R2g1padvf006cY74PQJKMGS2r42NK7FD0dG6Y6paw== +write-json-file@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/write-json-file/-/write-json-file-4.3.0.tgz#908493d6fd23225344af324016e4ca8f702dd12d" + integrity sha512-PxiShnxf0IlnQuMYOPPhPkhExoCQuTUNPOa/2JWCYTmBquU9njyyDuwRKN26IZBlp4yn1nt+Agh2HOOBl+55HQ== + dependencies: + detect-indent "^6.0.0" + graceful-fs "^4.1.15" + is-plain-obj "^2.0.0" + make-dir "^3.0.0" + sort-keys "^4.0.0" + write-file-atomic "^3.0.0" + +write-pkg@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/write-pkg/-/write-pkg-4.0.0.tgz#675cc04ef6c11faacbbc7771b24c0abbf2a20039" + integrity sha512-v2UQ+50TNf2rNHJ8NyWttfm/EJUBWMJcx6ZTYZr6Qp52uuegWw/lBkCtCbnYZEmPRNL61m+u67dAmGxo+HTULA== dependencies: sort-keys "^2.0.0" - write-json-file "^2.2.0" + type-fest "^0.4.1" + write-json-file "^3.2.0" ws@^6.2.1: version "6.2.2" @@ -24962,7 +25463,7 @@ xxhashjs@^0.2.1: dependencies: cuint "^0.2.2" -"y18n@^3.2.1 || ^4.0.0", y18n@^4.0.0: +y18n@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.1.tgz#8db2b83c31c5d75099bb890b23f3094891e247d4" integrity sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ== @@ -24995,7 +25496,7 @@ yaml@2.1.1: resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.1.1.tgz#1e06fb4ca46e60d9da07e4f786ea370ed3c3cfec" integrity sha512-o96x3OPo8GjWeSLF+wOAbrPfhFOGY0W00GNaxCDv+9hkcDJEnev1yh8S7pgHF0ik6zc8sQLuL8hjHjJULZp8bw== -yaml@^1.10.2: +yaml@^1.10.0, yaml@^1.10.2: version "1.10.2" resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== @@ -25008,18 +25509,20 @@ yargs-parser@13.1.2, yargs-parser@^13.1.2: camelcase "^5.0.0" decamelize "^1.2.0" +yargs-parser@20.2.4: + version "20.2.4" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" + integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== + yargs-parser@20.x, yargs-parser@^20.2.2, yargs-parser@^20.2.3: version "20.2.9" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== -yargs-parser@^11.1.1: - version "11.1.1" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4" - integrity sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" +yargs-parser@21.1.1, yargs-parser@^21.0.0, yargs-parser@^21.1.1: + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== yargs-parser@^18.1.2: version "18.1.3" @@ -25029,11 +25532,6 @@ yargs-parser@^18.1.2: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^21.0.0: - version "21.1.1" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" - integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== - yargs-unparser@1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-1.6.0.tgz#ef25c2c769ff6bd09e4b0f9d7c605fb27846ea9f" @@ -25059,24 +25557,6 @@ yargs@13.3.2, yargs@^13.3.0, yargs@^13.3.2: y18n "^4.0.0" yargs-parser "^13.1.2" -yargs@^12.0.1: - version "12.0.5" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13" - integrity sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw== - dependencies: - cliui "^4.0.0" - decamelize "^1.2.0" - find-up "^3.0.0" - get-caller-file "^1.0.1" - os-locale "^3.0.0" - require-directory "^2.1.1" - require-main-filename "^1.0.1" - set-blocking "^2.0.0" - string-width "^2.0.0" - which-module "^2.0.0" - y18n "^3.2.1 || ^4.0.0" - yargs-parser "^11.1.1" - yargs@^15.0.2: version "15.4.1" resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" @@ -25120,6 +25600,19 @@ yargs@^17.5.1, yargs@^17.6.0: y18n "^5.0.5" yargs-parser "^21.0.0" +yargs@^17.6.2: + version "17.6.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.6.2.tgz#2e23f2944e976339a1ee00f18c77fedee8332541" + integrity sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw== + dependencies: + cliui "^8.0.1" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.1.1" + yauzl@^2.10.0: version "2.10.0" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" From 277264eacaf3053d95eba27405d66e134d535f2f Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 11 Jan 2023 12:11:51 +0100 Subject: [PATCH 034/113] ref(replay): Remove `clearSession` from replay API (#6704) As this was only used for tests, we instead make it a test util. --- packages/replay/src/replay.ts | 11 ----------- packages/replay/test/unit/flush.test.ts | 3 ++- .../test/unit/index-errorSampleRate.test.ts | 3 ++- .../replay/test/unit/index-noSticky.test.ts | 3 ++- packages/replay/test/unit/index.test.ts | 9 +++++---- .../test/unit/session/deleteSession.test.ts | 19 ------------------- packages/replay/test/unit/stop.test.ts | 3 ++- .../utils/clearSession.ts} | 10 ++++++++-- 8 files changed, 21 insertions(+), 40 deletions(-) delete mode 100644 packages/replay/test/unit/session/deleteSession.test.ts rename packages/replay/{src/session/deleteSession.ts => test/utils/clearSession.ts} (51%) diff --git a/packages/replay/src/replay.ts b/packages/replay/src/replay.ts index a54a5453e689..8a89431204ca 100644 --- a/packages/replay/src/replay.ts +++ b/packages/replay/src/replay.ts @@ -20,7 +20,6 @@ import { handleXhrSpanListener } from './coreHandlers/handleXhr'; import { setupPerformanceObserver } from './coreHandlers/performanceObserver'; import { createPerformanceEntries } from './createPerformanceEntry'; import { createEventBuffer } from './eventBuffer'; -import { deleteSession } from './session/deleteSession'; import { getSession } from './session/getSession'; import { saveSession } from './session/saveSession'; import type { @@ -290,16 +289,6 @@ export class ReplayContainer implements ReplayContainerInterface { } } - /** for tests only */ - clearSession(): void { - try { - deleteSession(); - this.session = undefined; - } catch (err) { - this.handleException(err); - } - } - /** * Loads a session from storage, or creates a new one if it does not exist or * is expired. diff --git a/packages/replay/test/unit/flush.test.ts b/packages/replay/test/unit/flush.test.ts index 3c481c486c73..4397fee40d18 100644 --- a/packages/replay/test/unit/flush.test.ts +++ b/packages/replay/test/unit/flush.test.ts @@ -3,6 +3,7 @@ import * as SentryUtils from '@sentry/utils'; import { DEFAULT_FLUSH_MIN_DELAY, SESSION_IDLE_DURATION, WINDOW } from '../../src/constants'; import * as AddMemoryEntry from '../../src/util/addMemoryEntry'; import { createPerformanceSpans } from '../../src/util/createPerformanceSpans'; +import { clearSession } from '../utils/clearSession'; import { createPerformanceEntries } from './../../src/createPerformanceEntry'; import { ReplayContainer } from './../../src/replay'; import { useFakeTimers } from './../../test/utils/use-fake-timers'; @@ -89,7 +90,7 @@ afterEach(async () => { await new Promise(process.nextTick); jest.setSystemTime(new Date(BASE_TIMESTAMP)); sessionStorage.clear(); - replay.clearSession(); + clearSession(replay); replay.loadSession({ expiry: SESSION_IDLE_DURATION }); mockRecord.takeFullSnapshot.mockClear(); Object.defineProperty(WINDOW, 'location', { diff --git a/packages/replay/test/unit/index-errorSampleRate.test.ts b/packages/replay/test/unit/index-errorSampleRate.test.ts index 02c715f7cb01..a985ff56321d 100644 --- a/packages/replay/test/unit/index-errorSampleRate.test.ts +++ b/packages/replay/test/unit/index-errorSampleRate.test.ts @@ -2,6 +2,7 @@ import { captureException } from '@sentry/core'; import { DEFAULT_FLUSH_MIN_DELAY, REPLAY_SESSION_KEY, VISIBILITY_CHANGE_TIMEOUT, WINDOW } from '../../src/constants'; import { addEvent } from '../../src/util/addEvent'; +import { clearSession } from '../utils/clearSession'; import { ReplayContainer } from './../../src/replay'; import { PerformanceEntryResource } from './../fixtures/performanceEntry/resource'; import { BASE_TIMESTAMP, RecordMock } from './../index'; @@ -34,7 +35,7 @@ describe('Replay (errorSampleRate)', () => { }); afterEach(async () => { - replay.clearSession(); + clearSession(replay); replay.stop(); }); diff --git a/packages/replay/test/unit/index-noSticky.test.ts b/packages/replay/test/unit/index-noSticky.test.ts index f854258a1ec7..56c327871cdc 100644 --- a/packages/replay/test/unit/index-noSticky.test.ts +++ b/packages/replay/test/unit/index-noSticky.test.ts @@ -4,6 +4,7 @@ import * as SentryUtils from '@sentry/utils'; import { DEFAULT_FLUSH_MIN_DELAY, SESSION_IDLE_DURATION, VISIBILITY_CHANGE_TIMEOUT } from '../../src/constants'; import { addEvent } from '../../src/util/addEvent'; +import { clearSession } from '../utils/clearSession'; import { ReplayContainer } from './../../src/replay'; import { BASE_TIMESTAMP, mockRrweb, mockSdk } from './../index'; import { useFakeTimers } from './../utils/use-fake-timers'; @@ -50,7 +51,7 @@ describe('Replay (no sticky)', () => { jest.runAllTimers(); await new Promise(process.nextTick); jest.setSystemTime(new Date(BASE_TIMESTAMP)); - replay.clearSession(); + clearSession(replay); replay.loadSession({ expiry: SESSION_IDLE_DURATION }); }); diff --git a/packages/replay/test/unit/index.test.ts b/packages/replay/test/unit/index.test.ts index 2bd6d61852ac..cdd0567609e8 100644 --- a/packages/replay/test/unit/index.test.ts +++ b/packages/replay/test/unit/index.test.ts @@ -14,6 +14,7 @@ import { ReplayContainer } from '../../src/replay'; import type { RecordingEvent } from '../../src/types'; import { addEvent } from '../../src/util/addEvent'; import { createPerformanceSpans } from '../../src/util/createPerformanceSpans'; +import { clearSession } from '../utils/clearSession'; import { useFakeTimers } from '../utils/use-fake-timers'; import { PerformanceEntryResource } from './../fixtures/performanceEntry/resource'; import { BASE_TIMESTAMP, RecordMock } from './../index'; @@ -128,7 +129,7 @@ describe('Replay', () => { // Create a new session and clear mocks because a segment (from initial // checkout) will have already been uploaded by the time the tests run - replay.clearSession(); + clearSession(replay); replay.loadSession({ expiry: 0 }); mockTransportSend.mockClear(); mockSendReplayRequest = replay.sendReplayRequest as MockSendReplayRequest; @@ -142,7 +143,7 @@ describe('Replay', () => { value: prevLocation, writable: true, }); - replay.clearSession(); + clearSession(replay); jest.clearAllMocks(); mockSendReplayRequest.mockRestore(); mockRecord.takeFullSnapshot.mockClear(); @@ -159,7 +160,7 @@ describe('Replay', () => { }); it('clears session', () => { - replay.clearSession(); + clearSession(replay); expect(WINDOW.sessionStorage.getItem(REPLAY_SESSION_KEY)).toBe(null); expect(replay.session).toBe(undefined); }); @@ -750,7 +751,7 @@ describe('Replay', () => { }); it('has correct timestamps when there events earlier than initial timestamp', async function () { - replay.clearSession(); + clearSession(replay); replay.loadSession({ expiry: 0 }); mockTransportSend.mockClear(); Object.defineProperty(document, 'visibilityState', { diff --git a/packages/replay/test/unit/session/deleteSession.test.ts b/packages/replay/test/unit/session/deleteSession.test.ts deleted file mode 100644 index 3e8354d0c8d4..000000000000 --- a/packages/replay/test/unit/session/deleteSession.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { REPLAY_SESSION_KEY, WINDOW } from '../../../src/constants'; -import { deleteSession } from '../../../src/session/deleteSession'; - -const storageEngine = WINDOW.sessionStorage; - -it('deletes a session', function () { - storageEngine.setItem( - REPLAY_SESSION_KEY, - '{"id":"fd09adfc4117477abc8de643e5a5798a","started":1648827162630,"lastActivity":1648827162658}', - ); - - deleteSession(); - - expect(storageEngine.getItem(REPLAY_SESSION_KEY)).toBe(null); -}); - -it('deletes an empty session', function () { - expect(() => deleteSession()).not.toThrow(); -}); diff --git a/packages/replay/test/unit/stop.test.ts b/packages/replay/test/unit/stop.test.ts index 1adbde5a11d3..7aa5469c1d7d 100644 --- a/packages/replay/test/unit/stop.test.ts +++ b/packages/replay/test/unit/stop.test.ts @@ -3,6 +3,7 @@ import * as SentryUtils from '@sentry/utils'; import { SESSION_IDLE_DURATION, WINDOW } from '../../src/constants'; import { ReplayContainer } from '../../src/replay'; import { addEvent } from '../../src/util/addEvent'; +import { clearSession } from '../utils/clearSession'; import { Replay } from './../../src'; // mock functions need to be imported first import { BASE_TIMESTAMP, mockRrweb, mockSdk } from './../index'; @@ -42,7 +43,7 @@ describe('Replay - stop', () => { await new Promise(process.nextTick); jest.setSystemTime(new Date(BASE_TIMESTAMP)); sessionStorage.clear(); - replay.clearSession(); + clearSession(replay); replay.loadSession({ expiry: SESSION_IDLE_DURATION }); mockRecord.takeFullSnapshot.mockClear(); mockAddInstrumentationHandler.mockClear(); diff --git a/packages/replay/src/session/deleteSession.ts b/packages/replay/test/utils/clearSession.ts similarity index 51% rename from packages/replay/src/session/deleteSession.ts rename to packages/replay/test/utils/clearSession.ts index b1567337ee96..cb9c44b62493 100644 --- a/packages/replay/src/session/deleteSession.ts +++ b/packages/replay/test/utils/clearSession.ts @@ -1,9 +1,15 @@ -import { REPLAY_SESSION_KEY, WINDOW } from '../constants'; +import { REPLAY_SESSION_KEY, WINDOW } from '../../src/constants'; +import { ReplayContainer } from '../../src/types'; + +export function clearSession(replay: ReplayContainer) { + deleteSession(); + replay.session = undefined; +} /** * Deletes a session from storage */ -export function deleteSession(): void { +function deleteSession(): void { const hasSessionStorage = 'sessionStorage' in WINDOW; if (!hasSessionStorage) { From 318b750f88de6058313a2566a06c8ce88d225799 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Wed, 11 Jan 2023 13:09:18 +0100 Subject: [PATCH 035/113] ref(nextjs): Completely deprecate `isBuild()` and `IS_BUILD` (#6727) --- packages/nextjs/src/server/index.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index 2374b33ea519..0bba16aa970c 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -24,13 +24,12 @@ const globalWithInjectedValues = global as typeof global & { const domain = domainModule as typeof domainModule & { active: (domainModule.Domain & Carrier) | null }; -// Exporting this constant means we can compute it without the linter complaining, even if we stop directly using it in -// this file. It's important that it be computed as early as possible, because one of its indicators is seeing 'build' -// (as in the CLI command `next build`) in `process.argv`. Later on in the build process, everything's been spun out -// into child threads and `argv` turns into ['node', 'path/to/childProcess.js'], so the original indicator is lost. We -// thus want to compute it as soon as the SDK is loaded for the first time, which is normally when the user imports -// `withSentryConfig` into `next.config.js`. +// TODO (v8): Remove this +/** + * @deprecated This constant will be removed in the next major update. + */ export const IS_BUILD = isBuild(); + const IS_VERCEL = !!process.env.VERCEL; /** Inits the Sentry NextJS SDK on node. */ @@ -140,7 +139,7 @@ function addServerIntegrations(options: NodeOptions): void { // TODO (v8): Remove this /** - * @deprecated Use the constant `IS_BUILD` instead. + * @deprecated This constant will be removed in the next major update. */ const deprecatedIsBuild = (): boolean => isBuild(); // eslint-disable-next-line deprecation/deprecation From 5a1339ec92eef7e19b149c940c3343b9d8f9090f Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Wed, 11 Jan 2023 14:10:05 +0100 Subject: [PATCH 036/113] chore: Run yarn deduplicate (#6728) --- yarn.lock | 140 +++++++++++------------------------------------------- 1 file changed, 27 insertions(+), 113 deletions(-) diff --git a/yarn.lock b/yarn.lock index 50a3eb10c310..3ced5a52cb6f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4313,20 +4313,20 @@ "@types/estree" "*" "@types/json-schema" "*" -"@types/estree@*", "@types/estree@^0.0.51": - version "0.0.51" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40" - integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== +"@types/estree@*", "@types/estree@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.0.tgz#5fb2e536c1ae9bf35366eed879e827fa59ca41c2" + integrity sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ== "@types/estree@0.0.39": version "0.0.39" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== -"@types/estree@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.0.tgz#5fb2e536c1ae9bf35366eed879e827fa59ca41c2" - integrity sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ== +"@types/estree@^0.0.51": + version "0.0.51" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40" + integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== "@types/express-serve-static-core@4.17.28", "@types/express-serve-static-core@4.17.30", "@types/express-serve-static-core@^4.17.18": version "4.17.30" @@ -4711,16 +4711,11 @@ resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.1.tgz#18845205e86ff0038517aab7a18a62a6b9f71275" integrity sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA== -"@types/semver@^7.3.12": +"@types/semver@^7.3.12", "@types/semver@^7.3.9": version "7.3.13" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.13.tgz#da4bfd73f49bd541d28920ab0e2bf0ee80f71c91" integrity sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw== -"@types/semver@^7.3.9": - version "7.3.9" - resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.9.tgz#152c6c20a7688c30b967ec1841d31ace569863fc" - integrity sha512-L/TMpyURfBkf+o/526Zb6kd/tchUP3iBDEPjqjb+U2MAJhVRxxrmr2fwpe08E7QsV7YLcpq0tUaQ9O9x97ZIxQ== - "@types/serve-static@*": version "1.13.9" resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.9.tgz#aacf28a85a05ee29a11fb7c3ead935ac56f33e4e" @@ -6198,12 +6193,7 @@ async@^2.4.1, async@^2.6.2, async@^2.6.4: dependencies: lodash "^4.17.14" -async@^3.0.1: - version "3.2.0" - resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720" - integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw== - -async@^3.2.3: +async@^3.0.1, async@^3.2.3: version "3.2.4" resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== @@ -12451,12 +12441,7 @@ flush-write-stream@^1.0.0: inherits "^2.0.3" readable-stream "^2.3.6" -follow-redirects@^1.0.0, follow-redirects@^1.14.9: - version "1.15.1" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.1.tgz#0ca6a452306c9b276e4d3127483e29575e207ad5" - integrity sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA== - -follow-redirects@^1.15.0: +follow-redirects@^1.0.0, follow-redirects@^1.14.9, follow-redirects@^1.15.0: version "1.15.2" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== @@ -13708,20 +13693,13 @@ hosted-git-info@^3.0.2, hosted-git-info@^3.0.6: dependencies: lru-cache "^6.0.0" -hosted-git-info@^4.0.0: +hosted-git-info@^4.0.0, hosted-git-info@^4.0.1: version "4.1.0" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.1.0.tgz#827b82867e9ff1c8d0c4d9d53880397d2c86d224" integrity sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA== dependencies: lru-cache "^6.0.0" -hosted-git-info@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.0.2.tgz#5e425507eede4fea846b7262f0838456c4209961" - integrity sha512-c9OGXbZ3guC/xOlCg1Ci/VgWlwsqDv1yMQL1CWqXDL0hDjXuNcq0zuR4xqPSuasI3kqFDhqSyTjREz5gzq0fXg== - dependencies: - lru-cache "^6.0.0" - hosted-git-info@^5.0.0: version "5.1.0" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-5.1.0.tgz#9786123f92ef3627f24abc3f15c20d98ec4a6594" @@ -14034,16 +14012,11 @@ ignore@^4.0.6: resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== -ignore@^5.0.4: +ignore@^5.0.4, ignore@^5.1.1, ignore@^5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.1.tgz#c2b1f76cb999ede1502f3a226a9310fdfe88d46c" integrity sha512-d2qQLzTJ9WxQftPAuEQpSPmKqzxePjzVbpAVv62AQ64NTL+wR4JkrVqR/LqFsFEUsHDAiId52mJteHDFuDkElA== -ignore@^5.1.1, ignore@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" - integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== - image-size@~0.5.0: version "0.5.5" resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c" @@ -16845,12 +16818,7 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" -lru-cache@^7.10.1, lru-cache@^7.5.1: - version "7.14.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.14.0.tgz#21be64954a4680e303a09e9468f880b98a0b3c7f" - integrity sha512-EIRtP1GrSJny0dqb50QXRUNBxHJhcpxHC++M5tD7RYbvLLn5KVWKsbyswSSqDuU15UFi3bgTQIY8nhDMeF6aDQ== - -lru-cache@^7.4.4, lru-cache@^7.7.1: +lru-cache@^7.10.1, lru-cache@^7.4.4, lru-cache@^7.5.1, lru-cache@^7.7.1: version "7.14.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.14.1.tgz#8da8d2f5f59827edb388e63e459ac23d6d408fea" integrity sha512-ysxwsnTKdAx96aTRdhDOCQfDgbHnt8SK0KY8SEjO0wHinhWOFTESbjVCMPbU1uGXg/ch4lifqx0wfjOawU2+WA== @@ -17432,14 +17400,7 @@ minipass@^2.2.0, minipass@^2.3.5, minipass@^2.6.0, minipass@^2.9.0: safe-buffer "^5.1.2" yallist "^3.0.0" -minipass@^3.0.0, minipass@^3.1.1: - version "3.1.6" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.6.tgz#3b8150aa688a711a1521af5e8779c1d3bb4f45ee" - integrity sha512-rty5kpw9/z8SX9dmxblFA6edItUmwJgMeYDZRrwlIVN27i8gysGbznJwUggw2V/FVqFSDdWy040ZPS811DYAqQ== - dependencies: - yallist "^4.0.0" - -minipass@^3.1.6: +minipass@^3.0.0, minipass@^3.1.1, minipass@^3.1.6: version "3.3.6" resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a" integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== @@ -19587,24 +19548,12 @@ platform@1.3.6: resolved "https://registry.yarnpkg.com/platform/-/platform-1.3.6.tgz#48b4ce983164b209c2d45a107adb31f473a6e7a7" integrity sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg== -playwright-core@1.28.1: - version "1.28.1" - resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.28.1.tgz#8400be9f4a8d1c0489abdb9e75a4cc0ffc3c00cb" - integrity sha512-3PixLnGPno0E8rSBJjtwqTwJe3Yw72QwBBBxNoukIj3lEeBNXwbNiKrNuB1oyQgTBw5QHUhNO3SteEtHaMK6ag== - playwright-core@1.29.2: version "1.29.2" resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.29.2.tgz#2e8347e7e8522409f22b244e600e703b64022406" integrity sha512-94QXm4PMgFoHAhlCuoWyaBYKb92yOcGVHdQLoxQ7Wjlc7Flg4aC/jbFW7xMR52OfXMVkWicue4WXE7QEegbIRA== -playwright@^1.27.1: - version "1.28.1" - resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.28.1.tgz#f23247f1de466ff73d7230d94df96271e5da6583" - integrity sha512-92Sz6XBlfHlb9tK5UCDzIFAuIkHHpemA9zwUaqvo+w7sFMSmVMGmvKcbptof/eJObq63PGnMhM75x7qxhTR78Q== - dependencies: - playwright-core "1.28.1" - -playwright@^1.29.2: +playwright@^1.27.1, playwright@^1.29.2: version "1.29.2" resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.29.2.tgz#d6a0a3e8e44f023f7956ed19ffa8af915a042769" integrity sha512-hKBYJUtdmYzcjdhYDkP9WGtORwwZBBKAW8+Lz7sr0ZMxtJr04ASXVzH5eBWtDkdb0c3LLFsehfPBTRfvlfKJOA== @@ -21059,12 +21008,7 @@ regexp.prototype.flags@^1.2.0, regexp.prototype.flags@^1.4.3: define-properties "^1.1.3" functions-have-names "^1.2.2" -regexpp@^3.0.0, regexpp@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2" - integrity sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q== - -regexpp@^3.2.0: +regexpp@^3.0.0, regexpp@^3.1.0, regexpp@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== @@ -22219,12 +22163,7 @@ slice-ansi@^4.0.0: astral-regex "^2.0.0" is-fullwidth-code-point "^3.0.0" -smart-buffer@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.1.0.tgz#91605c25d91652f4661ea69ccf45f1b331ca21ba" - integrity sha512-iVICrxOzCynf/SNaBQCw34eM9jROU/s5rzIhpOvzhzuYHfJR/DhZfDkXiZSgKXfgv26HT3Yni3AV/DGw0cGnnw== - -smart-buffer@^4.2.0: +smart-buffer@^4.1.0, smart-buffer@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== @@ -23381,19 +23320,7 @@ tar@^4.4.10: safe-buffer "^5.2.1" yallist "^3.1.1" -tar@^6.0.2: - version "6.1.11" - resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621" - integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA== - dependencies: - chownr "^2.0.0" - fs-minipass "^2.0.0" - minipass "^3.0.0" - minizlib "^2.1.1" - mkdirp "^1.0.3" - yallist "^4.0.0" - -tar@^6.1.0, tar@^6.1.11, tar@^6.1.2: +tar@^6.0.2, tar@^6.1.0, tar@^6.1.11, tar@^6.1.2: version "6.1.12" resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.12.tgz#3b742fb05669b55671fb769ab67a7791ea1a62e6" integrity sha512-jU4TdemS31uABHd+Lt5WEYJuzn+TJTCBLljvIAHZOz6M9Os5pJ4dD+vRFLxPa/n3T0iEFzpi+0x1UfuDZYbRMw== @@ -24138,7 +24065,7 @@ typescript@4.0.2: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.2.tgz#7ea7c88777c723c681e33bf7988be5d008d05ac2" integrity sha512-e4ERvRV2wb+rRZ/IQeb3jm2VxBsirQLpQhdxplZ2MEzGvDkkMmPglecnNDfSUBivMjP93vRbngYYDQqQ/78bcQ== -"typescript@^3 || ^4": +"typescript@^3 || ^4", typescript@^4.5.2: version "4.9.3" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.3.tgz#3aea307c1746b8c384435d8ac36b8a2e580d85db" integrity sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA== @@ -24148,16 +24075,16 @@ typescript@^3.9.5, typescript@^3.9.7: resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.9.tgz#e69905c54bc0681d0518bd4d587cc6f2d0b1a674" integrity sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w== -typescript@^4.5.2, typescript@~4.5.2: - version "4.5.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.5.tgz#d8c953832d28924a9e3d37c73d729c846c5896f3" - integrity sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA== - typescript@~4.0.2: version "4.0.8" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.8.tgz#5739105541db80a971fdbd0d56511d1a6f17d37f" integrity sha512-oz1765PN+imfz1MlZzSZPtC/tqcwsCyIYA8L47EkRnRW97ztRk83SzMiWLrnChC0vqoYxSU1fcFUDA5gV/ZiPg== +typescript@~4.5.2: + version "4.5.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.5.tgz#d8c953832d28924a9e3d37c73d729c846c5896f3" + integrity sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA== + ua-parser-js@^0.7.18, ua-parser-js@^0.7.30: version "0.7.31" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.31.tgz#649a656b191dffab4f21d5e053e27ca17cbff5c6" @@ -25519,7 +25446,7 @@ yargs-parser@20.x, yargs-parser@^20.2.2, yargs-parser@^20.2.3: resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== -yargs-parser@21.1.1, yargs-parser@^21.0.0, yargs-parser@^21.1.1: +yargs-parser@21.1.1, yargs-parser@^21.1.1: version "21.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== @@ -25587,20 +25514,7 @@ yargs@^16.1.1, yargs@^16.2.0: y18n "^5.0.5" yargs-parser "^20.2.2" -yargs@^17.5.1, yargs@^17.6.0: - version "17.6.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.6.0.tgz#e134900fc1f218bc230192bdec06a0a5f973e46c" - integrity sha512-8H/wTDqlSwoSnScvV2N/JHfLWOKuh5MVla9hqLjK3nsfyy6Y4kDSYSvkU5YCUEPOSnRXfIyx3Sq+B/IWudTo4g== - dependencies: - cliui "^8.0.1" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.3" - y18n "^5.0.5" - yargs-parser "^21.0.0" - -yargs@^17.6.2: +yargs@^17.5.1, yargs@^17.6.0, yargs@^17.6.2: version "17.6.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.6.2.tgz#2e23f2944e976339a1ee00f18c77fedee8332541" integrity sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw== From 1cf0ae014a4fd90e13b09d8dc2c19df32f5126f6 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 11 Jan 2023 14:32:49 +0100 Subject: [PATCH 037/113] test(replay): Streamline replay test naming & modules (#6656) --- packages/replay/src/replay.ts | 2 +- .../createPerformanceEntries.ts} | 24 +- .../test/integration/autoSaveSession.test.ts | 57 ++ .../coreHandlers/handleGlobalEvent.test.ts} | 21 +- .../coreHandlers/handleScope.test.ts | 29 + .../errorSampleRate.test.ts} | 14 +- .../test/integration/eventProcessors.test.ts | 82 ++ .../replay/test/integration/events.test.ts | 208 ++++ .../replay/test/integration/flush.test.ts | 252 +++++ .../integrationSettings.test.ts} | 4 +- .../replay/test/integration/rrweb.test.ts | 31 + .../sampling.test.ts} | 12 +- .../test/integration/sendReplayEvent.test.ts | 435 ++++++++ .../replay/test/integration/session.test.ts | 486 +++++++++ .../test/{unit => integration}/stop.test.ts | 10 +- .../unit/coreHandlers/handleFetch.test.ts | 47 +- .../coreHandlers/handleScope-unit.test.ts | 31 - .../unit/coreHandlers/handleScope.test.ts | 87 +- .../test/unit/createPerformanceEntry.test.ts | 37 - packages/replay/test/unit/eventBuffer.test.ts | 155 +-- packages/replay/test/unit/flush.test.ts | 231 ----- .../replay/test/unit/index-noSticky.test.ts | 281 ------ packages/replay/test/unit/index.test.ts | 951 ------------------ .../test/unit/multiple-instances.test.ts | 8 - .../test/unit/multipleInstances.test.ts | 10 + .../replay/test/unit/session/Session.test.ts | 71 -- .../test/unit/session/createSession.test.ts | 82 +- .../test/unit/session/fetchSession.test.ts | 100 +- .../test/unit/session/getSession.test.ts | 266 ++--- .../test/unit/session/saveSession.test.ts | 34 +- .../test/unit/session/sessionSampling.test.ts | 35 + .../unit/util/createPerformanceEntry.test.ts | 34 + .../unit/util/createReplayEnvelope.test.ts | 2 +- .../replay/test/unit/util/debounce.test.ts | 2 +- .../util/dedupePerformanceEntries.test.ts | 105 +- .../test/unit/util/getReplayEvent.test.ts | 62 ++ .../replay/test/unit/util/isExpired.test.ts | 34 +- .../replay/test/unit/util/isSampled.test.ts | 5 +- .../test/unit/util/isSessionExpired.test.ts | 24 +- .../test/unit/util/prepareReplayEvent.test.ts | 2 +- .../test/unit/worker/Compressor.test.ts | 2 +- 41 files changed, 2246 insertions(+), 2119 deletions(-) rename packages/replay/src/{createPerformanceEntry.ts => util/createPerformanceEntries.ts} (81%) create mode 100644 packages/replay/test/integration/autoSaveSession.test.ts rename packages/replay/test/{unit/index-handleGlobalEvent.test.ts => integration/coreHandlers/handleGlobalEvent.test.ts} (92%) create mode 100644 packages/replay/test/integration/coreHandlers/handleScope.test.ts rename packages/replay/test/{unit/index-errorSampleRate.test.ts => integration/errorSampleRate.test.ts} (97%) create mode 100644 packages/replay/test/integration/eventProcessors.test.ts create mode 100644 packages/replay/test/integration/events.test.ts create mode 100644 packages/replay/test/integration/flush.test.ts rename packages/replay/test/{unit/index-integrationSettings.test.ts => integration/integrationSettings.test.ts} (98%) create mode 100644 packages/replay/test/integration/rrweb.test.ts rename packages/replay/test/{unit/index-sampling.test.ts => integration/sampling.test.ts} (69%) create mode 100644 packages/replay/test/integration/sendReplayEvent.test.ts create mode 100644 packages/replay/test/integration/session.test.ts rename packages/replay/test/{unit => integration}/stop.test.ts (95%) delete mode 100644 packages/replay/test/unit/coreHandlers/handleScope-unit.test.ts delete mode 100644 packages/replay/test/unit/createPerformanceEntry.test.ts delete mode 100644 packages/replay/test/unit/flush.test.ts delete mode 100644 packages/replay/test/unit/index-noSticky.test.ts delete mode 100644 packages/replay/test/unit/index.test.ts delete mode 100644 packages/replay/test/unit/multiple-instances.test.ts create mode 100644 packages/replay/test/unit/multipleInstances.test.ts delete mode 100644 packages/replay/test/unit/session/Session.test.ts create mode 100644 packages/replay/test/unit/session/sessionSampling.test.ts create mode 100644 packages/replay/test/unit/util/createPerformanceEntry.test.ts create mode 100644 packages/replay/test/unit/util/getReplayEvent.test.ts diff --git a/packages/replay/src/replay.ts b/packages/replay/src/replay.ts index 8a89431204ca..612bf5a71c10 100644 --- a/packages/replay/src/replay.ts +++ b/packages/replay/src/replay.ts @@ -18,7 +18,6 @@ import { handleGlobalEventListener } from './coreHandlers/handleGlobalEvent'; import { handleHistorySpanListener } from './coreHandlers/handleHistory'; import { handleXhrSpanListener } from './coreHandlers/handleXhr'; import { setupPerformanceObserver } from './coreHandlers/performanceObserver'; -import { createPerformanceEntries } from './createPerformanceEntry'; import { createEventBuffer } from './eventBuffer'; import { getSession } from './session/getSession'; import { saveSession } from './session/saveSession'; @@ -39,6 +38,7 @@ import type { import { addEvent } from './util/addEvent'; import { addMemoryEntry } from './util/addMemoryEntry'; import { createBreadcrumb } from './util/createBreadcrumb'; +import { createPerformanceEntries } from './util/createPerformanceEntries'; import { createPerformanceSpans } from './util/createPerformanceSpans'; import { createRecordingData } from './util/createRecordingData'; import { createReplayEnvelope } from './util/createReplayEnvelope'; diff --git a/packages/replay/src/createPerformanceEntry.ts b/packages/replay/src/util/createPerformanceEntries.ts similarity index 81% rename from packages/replay/src/createPerformanceEntry.ts rename to packages/replay/src/util/createPerformanceEntries.ts index e502e4d96247..8088ecc55171 100644 --- a/packages/replay/src/createPerformanceEntry.ts +++ b/packages/replay/src/util/createPerformanceEntries.ts @@ -1,13 +1,13 @@ import { browserPerformanceTimeOrigin } from '@sentry/utils'; import { record } from 'rrweb'; -import { WINDOW } from './constants'; +import { WINDOW } from '../constants'; import type { AllPerformanceEntry, PerformanceNavigationTiming, PerformancePaintTiming, ReplayPerformanceEntry, -} from './types'; +} from '../types'; // Map entryType -> function to normalize data for event // @ts-ignore TODO: entry type does not fit the create* functions entry type @@ -18,7 +18,7 @@ const ENTRY_TYPES: Record null | ReplayP // @ts-ignore TODO: entry type does not fit the create* functions entry type navigation: createNavigationEntry, // @ts-ignore TODO: entry type does not fit the create* functions entry type - 'largest-contentful-paint': createLargestContentfulPaint, + ['largest-contentful-paint']: createLargestContentfulPaint, }; /** @@ -42,7 +42,9 @@ function getAbsoluteTime(time: number): number { return ((browserPerformanceTimeOrigin || WINDOW.performance.timeOrigin) + time) / 1000; } -function createPaintEntry(entry: PerformancePaintTiming): ReplayPerformanceEntry { +// TODO: type definition! +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +function createPaintEntry(entry: PerformancePaintTiming) { const { duration, entryType, name, startTime } = entry; const start = getAbsoluteTime(startTime); @@ -54,7 +56,9 @@ function createPaintEntry(entry: PerformancePaintTiming): ReplayPerformanceEntry }; } -function createNavigationEntry(entry: PerformanceNavigationTiming): ReplayPerformanceEntry | null { +// TODO: type definition! +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +function createNavigationEntry(entry: PerformanceNavigationTiming) { // TODO: There looks to be some more interesting bits in here (domComplete, domContentLoaded) const { entryType, name, duration, domComplete, startTime, transferSize, type } = entry; @@ -75,7 +79,9 @@ function createNavigationEntry(entry: PerformanceNavigationTiming): ReplayPerfor }; } -function createResourceEntry(entry: PerformanceResourceTiming): ReplayPerformanceEntry | null { +// TODO: type definition! +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +function createResourceEntry(entry: PerformanceResourceTiming) { const { entryType, initiatorType, name, responseEnd, startTime, encodedBodySize, transferSize } = entry; // Core SDK handles these @@ -95,9 +101,9 @@ function createResourceEntry(entry: PerformanceResourceTiming): ReplayPerformanc }; } -function createLargestContentfulPaint( - entry: PerformanceEntry & { size: number; element: Node }, -): ReplayPerformanceEntry { +// TODO: type definition! +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +function createLargestContentfulPaint(entry: PerformanceEntry & { size: number; element: Node }) { const { duration, entryType, startTime, size } = entry; const start = getAbsoluteTime(startTime); diff --git a/packages/replay/test/integration/autoSaveSession.test.ts b/packages/replay/test/integration/autoSaveSession.test.ts new file mode 100644 index 000000000000..5cdcf3a6525b --- /dev/null +++ b/packages/replay/test/integration/autoSaveSession.test.ts @@ -0,0 +1,57 @@ +import { EventType } from 'rrweb'; + +import type { RecordingEvent } from '../../src/types'; +import { addEvent } from '../../src/util/addEvent'; +import { resetSdkMock } from '../mocks/resetSdkMock'; +import { useFakeTimers } from '../utils/use-fake-timers'; + +useFakeTimers(); + +describe('Integration | autoSaveSession', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test.each([ + ['with stickySession=true', true, 1], + ['with stickySession=false', false, 0], + ])('%s', async (_: string, stickySession: boolean, addSummand: number) => { + let saveSessionSpy; + + jest.mock('../../src/session/saveSession', () => { + saveSessionSpy = jest.fn(); + + return { + saveSession: saveSessionSpy, + }; + }); + + const { replay } = await resetSdkMock({ + replayOptions: { + stickySession, + }, + }); + + // Initially called up to three times: once for start, then once for replay.updateSessionActivity & once for segmentId increase + expect(saveSessionSpy).toHaveBeenCalledTimes(addSummand * 3); + + replay.updateSessionActivity(); + + expect(saveSessionSpy).toHaveBeenCalledTimes(addSummand * 4); + + // In order for runFlush to actually do something, we need to add an event + const event = { + type: EventType.Custom, + data: { + tag: 'test custom', + }, + timestamp: new Date().valueOf(), + } as RecordingEvent; + + addEvent(replay, event); + + await replay.runFlush(); + + expect(saveSessionSpy).toHaveBeenCalledTimes(addSummand * 5); + }); +}); diff --git a/packages/replay/test/unit/index-handleGlobalEvent.test.ts b/packages/replay/test/integration/coreHandlers/handleGlobalEvent.test.ts similarity index 92% rename from packages/replay/test/unit/index-handleGlobalEvent.test.ts rename to packages/replay/test/integration/coreHandlers/handleGlobalEvent.test.ts index 0103c1ab2240..a3d3ca17ad4f 100644 --- a/packages/replay/test/unit/index-handleGlobalEvent.test.ts +++ b/packages/replay/test/integration/coreHandlers/handleGlobalEvent.test.ts @@ -1,19 +1,22 @@ import { getCurrentHub } from '@sentry/core'; import { Event } from '@sentry/types'; -import { REPLAY_EVENT_NAME } from '../../src/constants'; -import { handleGlobalEventListener } from '../../src/coreHandlers/handleGlobalEvent'; -import { overwriteRecordDroppedEvent, restoreRecordDroppedEvent } from '../../src/util/monkeyPatchRecordDroppedEvent'; -import { ReplayContainer } from './../../src/replay'; -import { Error } from './../fixtures/error'; -import { Transaction } from './../fixtures/transaction'; -import { resetSdkMock } from './../mocks/resetSdkMock'; -import { useFakeTimers } from './../utils/use-fake-timers'; +import { REPLAY_EVENT_NAME } from '../../../src/constants'; +import { handleGlobalEventListener } from '../../../src/coreHandlers/handleGlobalEvent'; +import { ReplayContainer } from '../../../src/replay'; +import { + overwriteRecordDroppedEvent, + restoreRecordDroppedEvent, +} from '../../../src/util/monkeyPatchRecordDroppedEvent'; +import { Error } from '../../fixtures/error'; +import { Transaction } from '../../fixtures/transaction'; +import { resetSdkMock } from '../../mocks/resetSdkMock'; +import { useFakeTimers } from '../../utils/use-fake-timers'; useFakeTimers(); let replay: ReplayContainer; -describe('handleGlobalEvent', () => { +describe('Integration | coreHandlers | handleGlobalEvent', () => { beforeEach(async () => { ({ replay } = await resetSdkMock({ replayOptions: { diff --git a/packages/replay/test/integration/coreHandlers/handleScope.test.ts b/packages/replay/test/integration/coreHandlers/handleScope.test.ts new file mode 100644 index 000000000000..016aba2f9917 --- /dev/null +++ b/packages/replay/test/integration/coreHandlers/handleScope.test.ts @@ -0,0 +1,29 @@ +import { getCurrentHub } from '@sentry/core'; + +import * as HandleScope from '../../../src/coreHandlers/handleScope'; +import { mockSdk } from './../../index'; + +jest.useFakeTimers(); + +describe('Integration | coreHandlers | handleScope', () => { + beforeAll(async function () { + await mockSdk(); + jest.runAllTimers(); + }); + + it('returns a breadcrumb only if last breadcrumb has changed', function () { + const mockHandleScope = jest.spyOn(HandleScope, 'handleScope'); + getCurrentHub().getScope()?.addBreadcrumb({ message: 'testing' }); + + expect(mockHandleScope).toHaveBeenCalledTimes(1); + expect(mockHandleScope).toHaveReturnedWith(expect.objectContaining({ message: 'testing' })); + + mockHandleScope.mockClear(); + + // This will trigger breadcrumb/scope listener, but handleScope should return + // null because breadcrumbs has not changed + getCurrentHub().getScope()?.setUser({ email: 'foo@foo.com' }); + expect(mockHandleScope).toHaveBeenCalledTimes(1); + expect(mockHandleScope).toHaveReturnedWith(null); + }); +}); diff --git a/packages/replay/test/unit/index-errorSampleRate.test.ts b/packages/replay/test/integration/errorSampleRate.test.ts similarity index 97% rename from packages/replay/test/unit/index-errorSampleRate.test.ts rename to packages/replay/test/integration/errorSampleRate.test.ts index a985ff56321d..c8a7237a7951 100644 --- a/packages/replay/test/unit/index-errorSampleRate.test.ts +++ b/packages/replay/test/integration/errorSampleRate.test.ts @@ -1,14 +1,14 @@ import { captureException } from '@sentry/core'; import { DEFAULT_FLUSH_MIN_DELAY, REPLAY_SESSION_KEY, VISIBILITY_CHANGE_TIMEOUT, WINDOW } from '../../src/constants'; +import { ReplayContainer } from '../../src/replay'; import { addEvent } from '../../src/util/addEvent'; +import { PerformanceEntryResource } from '../fixtures/performanceEntry/resource'; +import { BASE_TIMESTAMP, RecordMock } from '../index'; +import { resetSdkMock } from '../mocks/resetSdkMock'; +import { DomHandler } from '../types'; import { clearSession } from '../utils/clearSession'; -import { ReplayContainer } from './../../src/replay'; -import { PerformanceEntryResource } from './../fixtures/performanceEntry/resource'; -import { BASE_TIMESTAMP, RecordMock } from './../index'; -import { resetSdkMock } from './../mocks/resetSdkMock'; -import { DomHandler } from './../types'; -import { useFakeTimers } from './../utils/use-fake-timers'; +import { useFakeTimers } from '../utils/use-fake-timers'; useFakeTimers(); @@ -17,7 +17,7 @@ async function advanceTimers(time: number) { await new Promise(process.nextTick); } -describe('Replay (errorSampleRate)', () => { +describe('Integration | errorSampleRate', () => { let replay: ReplayContainer; let mockRecord: RecordMock; let domHandler: DomHandler; diff --git a/packages/replay/test/integration/eventProcessors.test.ts b/packages/replay/test/integration/eventProcessors.test.ts new file mode 100644 index 000000000000..5b4d96077f08 --- /dev/null +++ b/packages/replay/test/integration/eventProcessors.test.ts @@ -0,0 +1,82 @@ +import { getCurrentHub } from '@sentry/core'; +import { Event, Hub, Scope } from '@sentry/types'; + +import { BASE_TIMESTAMP } from '..'; +import { resetSdkMock } from '../mocks/resetSdkMock'; +import { useFakeTimers } from '../utils/use-fake-timers'; + +useFakeTimers(); + +describe('Integration | eventProcessors', () => { + let hub: Hub; + let scope: Scope; + + beforeEach(() => { + hub = getCurrentHub(); + scope = hub.pushScope(); + }); + + afterEach(() => { + hub.popScope(); + jest.resetAllMocks(); + }); + + it('handles event processors properly', async () => { + const MUTATED_TIMESTAMP = BASE_TIMESTAMP + 3000; + + const { mockRecord } = await resetSdkMock({ + replayOptions: { + stickySession: false, + }, + }); + + const client = hub.getClient()!; + + jest.runAllTimers(); + const mockTransportSend = jest.spyOn(client.getTransport()!, 'send'); + mockTransportSend.mockReset(); + + const handler1 = jest.fn((event: Event): Event | null => { + event.timestamp = MUTATED_TIMESTAMP; + + return event; + }); + + const handler2 = jest.fn((): Event | null => { + return null; + }); + + scope.addEventProcessor(handler1); + + const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 3 }; + + mockRecord._emitter(TEST_EVENT); + jest.runAllTimers(); + jest.advanceTimersByTime(1); + await new Promise(process.nextTick); + + expect(mockTransportSend).toHaveBeenCalledTimes(1); + + scope.addEventProcessor(handler2); + + const TEST_EVENT2 = { data: {}, timestamp: BASE_TIMESTAMP, type: 3 }; + + mockRecord._emitter(TEST_EVENT2); + jest.runAllTimers(); + jest.advanceTimersByTime(1); + await new Promise(process.nextTick); + + expect(mockTransportSend).toHaveBeenCalledTimes(1); + + expect(handler1).toHaveBeenCalledTimes(2); + expect(handler2).toHaveBeenCalledTimes(1); + + // This receives an envelope, which is a deeply nested array + // We only care about the fact that the timestamp was mutated + expect(mockTransportSend).toHaveBeenCalledWith( + expect.arrayContaining([ + expect.arrayContaining([expect.arrayContaining([expect.objectContaining({ timestamp: MUTATED_TIMESTAMP })])]), + ]), + ); + }); +}); diff --git a/packages/replay/test/integration/events.test.ts b/packages/replay/test/integration/events.test.ts new file mode 100644 index 000000000000..e2770101d395 --- /dev/null +++ b/packages/replay/test/integration/events.test.ts @@ -0,0 +1,208 @@ +import { getCurrentHub } from '@sentry/core'; + +import { WINDOW } from '../../src/constants'; +import { ReplayContainer } from '../../src/replay'; +import { addEvent } from '../../src/util/addEvent'; +import { PerformanceEntryResource } from '../fixtures/performanceEntry/resource'; +import { BASE_TIMESTAMP, RecordMock } from '../index'; +import { resetSdkMock } from '../mocks/resetSdkMock'; +import { clearSession } from '../utils/clearSession'; +import { useFakeTimers } from '../utils/use-fake-timers'; + +useFakeTimers(); + +async function advanceTimers(time: number) { + jest.advanceTimersByTime(time); + await new Promise(process.nextTick); +} + +describe('Integration | events', () => { + let replay: ReplayContainer; + let mockRecord: RecordMock; + let mockTransportSend: jest.SpyInstance; + const prevLocation = WINDOW.location; + + type MockSendReplayRequest = jest.MockedFunction; + let mockSendReplayRequest: MockSendReplayRequest; + + beforeAll(async () => { + jest.setSystemTime(new Date(BASE_TIMESTAMP)); + jest.runAllTimers(); + }); + + beforeEach(async () => { + ({ mockRecord, replay } = await resetSdkMock({ + replayOptions: { + stickySession: false, + }, + })); + + mockTransportSend = jest.spyOn(getCurrentHub().getClient()!.getTransport()!, 'send'); + + jest.spyOn(replay, 'flush'); + jest.spyOn(replay, 'runFlush'); + jest.spyOn(replay, 'sendReplayRequest'); + + // Create a new session and clear mocks because a segment (from initial + // checkout) will have already been uploaded by the time the tests run + clearSession(replay); + replay.loadSession({ expiry: 0 }); + mockTransportSend.mockClear(); + mockSendReplayRequest = replay.sendReplayRequest as MockSendReplayRequest; + mockSendReplayRequest.mockClear(); + }); + + afterEach(async () => { + jest.runAllTimers(); + await new Promise(process.nextTick); + Object.defineProperty(WINDOW, 'location', { + value: prevLocation, + writable: true, + }); + clearSession(replay); + jest.clearAllMocks(); + mockSendReplayRequest.mockRestore(); + mockRecord.takeFullSnapshot.mockClear(); + replay.stop(); + }); + + it('does not create replay event when there are no events to send', async () => { + Object.defineProperty(document, 'visibilityState', { + configurable: true, + get: function () { + return 'hidden'; + }, + }); + + document.dispatchEvent(new Event('visibilitychange')); + await new Promise(process.nextTick); + expect(replay).not.toHaveLastSentReplay(); + + // Pretend 5 seconds have passed + const ELAPSED = 5000; + await advanceTimers(ELAPSED); + + const TEST_EVENT = { + data: {}, + timestamp: BASE_TIMESTAMP + ELAPSED, + type: 2, + }; + + addEvent(replay, TEST_EVENT); + WINDOW.dispatchEvent(new Event('blur')); + await new Promise(process.nextTick); + + expect(replay).toHaveLastSentReplay({ + replayEventPayload: expect.objectContaining({ + replay_start_timestamp: BASE_TIMESTAMP / 1000, + urls: ['http://localhost/'], // this doesn't truly test if we are capturing the right URL as we don't change URLs, but good enough + }), + }); + }); + + it('has correct timestamps when there are events earlier than initial timestamp', async function () { + clearSession(replay); + replay.loadSession({ expiry: 0 }); + mockTransportSend.mockClear(); + Object.defineProperty(document, 'visibilityState', { + configurable: true, + get: function () { + return 'hidden'; + }, + }); + + document.dispatchEvent(new Event('visibilitychange')); + await new Promise(process.nextTick); + expect(replay).not.toHaveLastSentReplay(); + + // Pretend 5 seconds have passed + const ELAPSED = 5000; + await advanceTimers(ELAPSED); + + const TEST_EVENT = { + data: {}, + timestamp: BASE_TIMESTAMP + ELAPSED, + type: 2, + }; + + addEvent(replay, TEST_EVENT); + + // Add a fake event that started BEFORE + addEvent(replay, { + data: {}, + timestamp: (BASE_TIMESTAMP - 10000) / 1000, + type: 5, + }); + + WINDOW.dispatchEvent(new Event('blur')); + await new Promise(process.nextTick); + expect(replay).toHaveLastSentReplay({ + replayEventPayload: expect.objectContaining({ + replay_start_timestamp: (BASE_TIMESTAMP - 10000) / 1000, + urls: ['http://localhost/'], // this doesn't truly test if we are capturing the right URL as we don't change URLs, but good enough + tags: expect.objectContaining({ + errorSampleRate: 0, + sessionSampleRate: 1, + }), + }), + }); + }); + + it('does not have stale `replay_start_timestamp` due to an old time origin', async function () { + const ELAPSED = 86400000 * 2; // 2 days + // Add a mock performance event that happens 2 days ago. This can happen in the browser + // when a tab has sat idle for a long period and user comes back to it. + // + // We pass a negative start time as it's a bit difficult to mock + // `@sentry/utils/browserPerformanceTimeOrigin`. This would not happen in + // real world. + replay.performanceEvents.push( + PerformanceEntryResource({ + startTime: -ELAPSED, + }), + ); + + // This should be null because `addEvent` has not been called yet + expect(replay.getContext().earliestEvent).toBe(null); + expect(mockTransportSend).toHaveBeenCalledTimes(0); + + // A new checkout occurs (i.e. a new session was started) + const TEST_EVENT = { + data: {}, + timestamp: BASE_TIMESTAMP, + type: 2, + }; + + addEvent(replay, TEST_EVENT); + // This event will trigger a flush + WINDOW.dispatchEvent(new Event('blur')); + jest.runAllTimers(); + await new Promise(process.nextTick); + + expect(mockTransportSend).toHaveBeenCalledTimes(1); + expect(replay).toHaveLastSentReplay({ + replayEventPayload: expect.objectContaining({ + // Make sure the old performance event is thrown out + replay_start_timestamp: BASE_TIMESTAMP / 1000, + }), + events: JSON.stringify([ + TEST_EVENT, + { + type: 5, + timestamp: BASE_TIMESTAMP / 1000, + data: { + tag: 'breadcrumb', + payload: { + timestamp: BASE_TIMESTAMP / 1000, + type: 'default', + category: 'ui.blur', + }, + }, + }, + ]), + }); + + // This gets reset after sending replay + expect(replay.getContext().earliestEvent).toBe(null); + }); +}); diff --git a/packages/replay/test/integration/flush.test.ts b/packages/replay/test/integration/flush.test.ts new file mode 100644 index 000000000000..c43729d79de1 --- /dev/null +++ b/packages/replay/test/integration/flush.test.ts @@ -0,0 +1,252 @@ +import * as SentryUtils from '@sentry/utils'; + +import { DEFAULT_FLUSH_MIN_DELAY, SESSION_IDLE_DURATION, WINDOW } from '../../src/constants'; +import { ReplayContainer } from '../../src/replay'; +import * as AddMemoryEntry from '../../src/util/addMemoryEntry'; +import { createPerformanceEntries } from '../../src/util/createPerformanceEntries'; +import { createPerformanceSpans } from '../../src/util/createPerformanceSpans'; +import { BASE_TIMESTAMP, mockRrweb, mockSdk } from '../index'; +import { clearSession } from '../utils/clearSession'; +import { useFakeTimers } from '../utils/use-fake-timers'; + +useFakeTimers(); + +async function advanceTimers(time: number) { + jest.advanceTimersByTime(time); + await new Promise(process.nextTick); +} + +type MockSendReplay = jest.MockedFunction; +type MockAddPerformanceEntries = jest.MockedFunction; +type MockAddMemoryEntry = jest.SpyInstance; +type MockEventBufferFinish = jest.MockedFunction['finish']>; +type MockFlush = jest.MockedFunction; +type MockRunFlush = jest.MockedFunction; + +const prevLocation = WINDOW.location; + +describe('Integration | flush', () => { + let domHandler: (args: any) => any; + + const { record: mockRecord } = mockRrweb(); + + let replay: ReplayContainer; + let mockSendReplay: MockSendReplay; + let mockFlush: MockFlush; + let mockRunFlush: MockRunFlush; + let mockEventBufferFinish: MockEventBufferFinish; + let mockAddMemoryEntry: MockAddMemoryEntry; + let mockAddPerformanceEntries: MockAddPerformanceEntries; + + beforeAll(async () => { + jest.spyOn(SentryUtils, 'addInstrumentationHandler').mockImplementation((type, handler: (args: any) => any) => { + if (type === 'dom') { + domHandler = handler; + } + }); + + ({ replay } = await mockSdk()); + jest.spyOn(replay, 'sendReplay'); + mockSendReplay = replay.sendReplay as MockSendReplay; + mockSendReplay.mockImplementation( + jest.fn(async () => { + return; + }), + ); + + jest.spyOn(replay, 'flush'); + mockFlush = replay.flush as MockFlush; + + jest.spyOn(replay, 'runFlush'); + mockRunFlush = replay.runFlush as MockRunFlush; + + jest.spyOn(replay, 'addPerformanceEntries'); + mockAddPerformanceEntries = replay.addPerformanceEntries as MockAddPerformanceEntries; + + mockAddPerformanceEntries.mockImplementation(async () => { + return []; + }); + + mockAddMemoryEntry = jest.spyOn(AddMemoryEntry, 'addMemoryEntry'); + }); + + beforeEach(() => { + jest.runAllTimers(); + jest.setSystemTime(new Date(BASE_TIMESTAMP)); + mockSendReplay.mockClear(); + replay.eventBuffer?.destroy(); + mockAddPerformanceEntries.mockClear(); + mockFlush.mockClear(); + mockRunFlush.mockClear(); + mockAddMemoryEntry.mockClear(); + + if (replay.eventBuffer) { + jest.spyOn(replay.eventBuffer, 'finish'); + } + mockEventBufferFinish = replay.eventBuffer?.finish as MockEventBufferFinish; + mockEventBufferFinish.mockClear(); + }); + + afterEach(async () => { + jest.runAllTimers(); + await new Promise(process.nextTick); + jest.setSystemTime(new Date(BASE_TIMESTAMP)); + sessionStorage.clear(); + clearSession(replay); + replay.loadSession({ expiry: SESSION_IDLE_DURATION }); + mockRecord.takeFullSnapshot.mockClear(); + Object.defineProperty(WINDOW, 'location', { + value: prevLocation, + writable: true, + }); + }); + + afterAll(() => { + replay && replay.stop(); + }); + + it('flushes twice after multiple flush() calls)', async () => { + // blur events cause an immediate flush (as well as a flush due to adding a + // breadcrumb) -- this means that the first blur event will be flushed and + // the following blur events will all call a debounced flush function, which + // should end up queueing a second flush + + WINDOW.dispatchEvent(new Event('blur')); + WINDOW.dispatchEvent(new Event('blur')); + WINDOW.dispatchEvent(new Event('blur')); + WINDOW.dispatchEvent(new Event('blur')); + + expect(replay.flush).toHaveBeenCalledTimes(4); + + jest.runAllTimers(); + await new Promise(process.nextTick); + expect(replay.runFlush).toHaveBeenCalledTimes(1); + + jest.runAllTimers(); + await new Promise(process.nextTick); + expect(replay.runFlush).toHaveBeenCalledTimes(2); + + jest.runAllTimers(); + await new Promise(process.nextTick); + expect(replay.runFlush).toHaveBeenCalledTimes(2); + }); + + it('long first flush enqueues following events', async () => { + // Mock this to resolve after 20 seconds so that we can queue up following flushes + mockAddPerformanceEntries.mockImplementationOnce(async () => { + return await new Promise(resolve => setTimeout(resolve, 20000)); + }); + + expect(mockAddPerformanceEntries).not.toHaveBeenCalled(); + + // flush #1 @ t=0s - due to blur + WINDOW.dispatchEvent(new Event('blur')); + expect(replay.flush).toHaveBeenCalledTimes(1); + expect(replay.runFlush).toHaveBeenCalledTimes(1); + + // This will attempt to flush in 5 seconds (flushMinDelay) + domHandler({ + name: 'click', + }); + await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); + // flush #2 @ t=5s - due to click + expect(replay.flush).toHaveBeenCalledTimes(2); + + await advanceTimers(1000); + // flush #3 @ t=6s - due to blur + WINDOW.dispatchEvent(new Event('blur')); + expect(replay.flush).toHaveBeenCalledTimes(3); + + // NOTE: Blur also adds a breadcrumb which calls `addUpdate`, meaning it will + // flush after `flushMinDelay`, but this gets cancelled by the blur + await advanceTimers(8000); + expect(replay.flush).toHaveBeenCalledTimes(3); + + // flush #4 @ t=14s - due to blur + WINDOW.dispatchEvent(new Event('blur')); + expect(replay.flush).toHaveBeenCalledTimes(4); + + expect(replay.runFlush).toHaveBeenCalledTimes(1); + await advanceTimers(6000); + // t=20s + // addPerformanceEntries is finished, `flushLock` promise is resolved, calls + // debouncedFlush, which will call `flush` in 1 second + expect(replay.flush).toHaveBeenCalledTimes(4); + // sendReplay is called with replayId, events, segment + expect(mockSendReplay).toHaveBeenLastCalledWith({ + events: expect.any(String), + replayId: expect.any(String), + includeReplayStartTimestamp: true, + segmentId: 0, + eventContext: expect.anything(), + }); + + // Add this to test that segment ID increases + mockAddPerformanceEntries.mockImplementationOnce(() => { + createPerformanceSpans( + replay, + createPerformanceEntries([ + { + name: 'https://sentry.io/foo.js', + entryType: 'resource', + startTime: 176.59999990463257, + duration: 5.600000023841858, + initiatorType: 'link', + nextHopProtocol: 'h2', + workerStart: 177.5, + redirectStart: 0, + redirectEnd: 0, + fetchStart: 177.69999992847443, + domainLookupStart: 177.69999992847443, + domainLookupEnd: 177.69999992847443, + connectStart: 177.69999992847443, + connectEnd: 177.69999992847443, + secureConnectionStart: 177.69999992847443, + requestStart: 177.5, + responseStart: 181, + responseEnd: 182.19999992847443, + transferSize: 0, + encodedBodySize: 0, + decodedBodySize: 0, + serverTiming: [], + } as unknown as PerformanceResourceTiming, + ]), + ); + }); + // flush #5 @ t=25s - debounced flush calls `flush` + // 20s + `flushMinDelay` which is 5 seconds + await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); + expect(replay.flush).toHaveBeenCalledTimes(5); + expect(replay.runFlush).toHaveBeenCalledTimes(2); + expect(mockSendReplay).toHaveBeenLastCalledWith({ + events: expect.any(String), + replayId: expect.any(String), + includeReplayStartTimestamp: false, + segmentId: 1, + eventContext: expect.anything(), + }); + + // Make sure there's no other calls + jest.runAllTimers(); + await new Promise(process.nextTick); + expect(mockSendReplay).toHaveBeenCalledTimes(2); + }); + + it('has single flush when checkout flush and debounce flush happen near simultaneously', async () => { + // click happens first + domHandler({ + name: 'click', + }); + + // checkout + const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 2 }; + mockRecord._emitter(TEST_EVENT); + + await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); + expect(replay.flush).toHaveBeenCalledTimes(1); + + // Make sure there's nothing queued up after + await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); + expect(replay.flush).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/replay/test/unit/index-integrationSettings.test.ts b/packages/replay/test/integration/integrationSettings.test.ts similarity index 98% rename from packages/replay/test/unit/index-integrationSettings.test.ts rename to packages/replay/test/integration/integrationSettings.test.ts index c78214b8c2ea..6c63839b1c06 100644 --- a/packages/replay/test/unit/index-integrationSettings.test.ts +++ b/packages/replay/test/integration/integrationSettings.test.ts @@ -1,7 +1,7 @@ import { MASK_ALL_TEXT_SELECTOR } from '../../src/constants'; -import { mockSdk } from './../index'; +import { mockSdk } from '../index'; -describe('integration settings', () => { +describe('Integration | integrationSettings', () => { beforeEach(() => { jest.resetModules(); }); diff --git a/packages/replay/test/integration/rrweb.test.ts b/packages/replay/test/integration/rrweb.test.ts new file mode 100644 index 000000000000..15c8cdba432b --- /dev/null +++ b/packages/replay/test/integration/rrweb.test.ts @@ -0,0 +1,31 @@ +import { MASK_ALL_TEXT_SELECTOR } from '../../src/constants'; +import { resetSdkMock } from '../mocks/resetSdkMock'; +import { useFakeTimers } from '../utils/use-fake-timers'; + +useFakeTimers(); + +describe('Integration | rrweb', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('calls rrweb.record with custom options', async () => { + const { mockRecord } = await resetSdkMock({ + replayOptions: { + ignoreClass: 'sentry-test-ignore', + stickySession: false, + }, + }); + expect(mockRecord.mock.calls[0][0]).toMatchInlineSnapshot(` + Object { + "blockClass": "sentry-block", + "blockSelector": "[data-sentry-block],img,image,svg,path,rect,area,video,object,picture,embed,map,audio", + "emit": [Function], + "ignoreClass": "sentry-test-ignore", + "maskAllInputs": true, + "maskTextClass": "sentry-mask", + "maskTextSelector": "${MASK_ALL_TEXT_SELECTOR}", + } + `); + }); +}); diff --git a/packages/replay/test/unit/index-sampling.test.ts b/packages/replay/test/integration/sampling.test.ts similarity index 69% rename from packages/replay/test/unit/index-sampling.test.ts rename to packages/replay/test/integration/sampling.test.ts index 1ce841128895..d17052a4e7f6 100644 --- a/packages/replay/test/unit/index-sampling.test.ts +++ b/packages/replay/test/integration/sampling.test.ts @@ -1,10 +1,9 @@ -// mock functions need to be imported first -import { mockRrweb, mockSdk } from './../index'; -import { useFakeTimers } from './../utils/use-fake-timers'; +import { mockRrweb, mockSdk } from '../index'; +import { useFakeTimers } from '../utils/use-fake-timers'; useFakeTimers(); -describe('Replay (sampling)', () => { +describe('Integration | sampling', () => { it('does nothing if not sampled', async () => { const { record: mockRecord } = mockRrweb(); const { replay } = await mockSdk({ @@ -19,13 +18,10 @@ describe('Replay (sampling)', () => { jest.spyOn(replay, 'loadSession'); jest.spyOn(replay, 'addListeners'); - // @ts-ignore private - expect(replay.initialState).toEqual(undefined); jest.runAllTimers(); expect(replay.session?.sampled).toBe(false); - // @ts-ignore private - expect(replay._context).toEqual( + expect(replay.getContext()).toEqual( expect.objectContaining({ initialTimestamp: expect.any(Number), initialUrl: 'http://localhost/', diff --git a/packages/replay/test/integration/sendReplayEvent.test.ts b/packages/replay/test/integration/sendReplayEvent.test.ts new file mode 100644 index 000000000000..1701a0a48826 --- /dev/null +++ b/packages/replay/test/integration/sendReplayEvent.test.ts @@ -0,0 +1,435 @@ +import { getCurrentHub } from '@sentry/core'; +import { Transport } from '@sentry/types'; +import * as SentryUtils from '@sentry/utils'; + +import { DEFAULT_FLUSH_MIN_DELAY, SESSION_IDLE_DURATION, WINDOW } from '../../src/constants'; +import { ReplayContainer } from '../../src/replay'; +import { addEvent } from '../../src/util/addEvent'; +import { BASE_TIMESTAMP, mockRrweb, mockSdk } from '../index'; +import { clearSession } from '../utils/clearSession'; +import { useFakeTimers } from '../utils/use-fake-timers'; + +useFakeTimers(); + +async function advanceTimers(time: number) { + jest.advanceTimersByTime(time); + await new Promise(process.nextTick); +} + +type MockTransportSend = jest.MockedFunction; +type MockSendReplayRequest = jest.MockedFunction; + +describe('Integration | sendReplayEvent', () => { + let replay: ReplayContainer; + let mockTransportSend: MockTransportSend; + let mockSendReplayRequest: MockSendReplayRequest; + let domHandler: (args: any) => any; + const { record: mockRecord } = mockRrweb(); + + beforeAll(async () => { + jest.setSystemTime(new Date(BASE_TIMESTAMP)); + jest.spyOn(SentryUtils, 'addInstrumentationHandler').mockImplementation((type, handler: (args: any) => any) => { + if (type === 'dom') { + domHandler = handler; + } + }); + + ({ replay } = await mockSdk({ + replayOptions: { + stickySession: false, + }, + })); + + jest.spyOn(replay, 'sendReplayRequest'); + + jest.runAllTimers(); + mockTransportSend = getCurrentHub()?.getClient()?.getTransport()?.send as MockTransportSend; + mockSendReplayRequest = replay.sendReplayRequest as MockSendReplayRequest; + }); + + beforeEach(() => { + jest.setSystemTime(new Date(BASE_TIMESTAMP)); + mockRecord.takeFullSnapshot.mockClear(); + mockTransportSend.mockClear(); + + // Create a new session and clear mocks because a segment (from initial + // checkout) will have already been uploaded by the time the tests run + clearSession(replay); + replay.loadSession({ expiry: 0 }); + + mockSendReplayRequest.mockClear(); + }); + + afterEach(async () => { + jest.runAllTimers(); + await new Promise(process.nextTick); + jest.setSystemTime(new Date(BASE_TIMESTAMP)); + clearSession(replay); + replay.loadSession({ expiry: SESSION_IDLE_DURATION }); + }); + + afterAll(() => { + replay && replay.stop(); + }); + + it('uploads a replay event when document becomes hidden', async () => { + mockRecord.takeFullSnapshot.mockClear(); + Object.defineProperty(document, 'visibilityState', { + configurable: true, + get: function () { + return 'hidden'; + }, + }); + + // Pretend 5 seconds have passed + const ELAPSED = 5000; + jest.advanceTimersByTime(ELAPSED); + + const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 2 }; + addEvent(replay, TEST_EVENT); + + document.dispatchEvent(new Event('visibilitychange')); + + await new Promise(process.nextTick); + + expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); + + expect(replay).toHaveLastSentReplay({ events: JSON.stringify([TEST_EVENT]) }); + + // Session's last activity is not updated because we do not consider + // visibilitystate as user being active + expect(replay.session?.lastActivity).toBe(BASE_TIMESTAMP); + expect(replay.session?.segmentId).toBe(1); + + // events array should be empty + expect(replay.eventBuffer?.length).toBe(0); + }); + + it('update last activity when user clicks mouse', async () => { + expect(replay.session?.lastActivity).toBe(BASE_TIMESTAMP); + + domHandler({ + name: 'click', + }); + + expect(replay.session?.lastActivity).toBe(BASE_TIMESTAMP); + + // Pretend 5 seconds have passed + const ELAPSED = 5000; + jest.advanceTimersByTime(ELAPSED); + + domHandler({ + name: 'click', + }); + + expect(replay.session?.lastActivity).toBe(BASE_TIMESTAMP + ELAPSED); + }); + + it('uploads a replay event if 5 seconds have elapsed since the last replay event occurred', async () => { + const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 3 }; + mockRecord._emitter(TEST_EVENT); + // Pretend 5 seconds have passed + const ELAPSED = 5000; + await advanceTimers(ELAPSED); + + expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); + + expect(replay).toHaveLastSentReplay({ events: JSON.stringify([TEST_EVENT]) }); + + // No user activity to trigger an update + expect(replay.session?.lastActivity).toBe(BASE_TIMESTAMP); + expect(replay.session?.segmentId).toBe(1); + + // events array should be empty + expect(replay.eventBuffer?.length).toBe(0); + }); + + it('uploads a replay event if 15 seconds have elapsed since the last replay upload', async () => { + const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 3 }; + // Fire a new event every 4 seconds, 4 times + [...Array(4)].forEach(() => { + mockRecord._emitter(TEST_EVENT); + jest.advanceTimersByTime(4000); + }); + + // We are at time = +16seconds now (relative to BASE_TIMESTAMP) + // The next event should cause an upload immediately + mockRecord._emitter(TEST_EVENT); + await new Promise(process.nextTick); + + expect(replay).toHaveLastSentReplay({ + events: JSON.stringify([...Array(5)].map(() => TEST_EVENT)), + }); + + // There should also not be another attempt at an upload 5 seconds after the last replay event + mockTransportSend.mockClear(); + await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); + expect(replay).not.toHaveLastSentReplay(); + + expect(replay.session?.lastActivity).toBe(BASE_TIMESTAMP); + expect(replay.session?.segmentId).toBe(1); + // events array should be empty + expect(replay.eventBuffer?.length).toBe(0); + + // Let's make sure it continues to work + mockTransportSend.mockClear(); + mockRecord._emitter(TEST_EVENT); + await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); + expect(replay).toHaveLastSentReplay({ events: JSON.stringify([TEST_EVENT]) }); + }); + + it('uploads a replay event when WINDOW is blurred', async () => { + Object.defineProperty(document, 'visibilityState', { + configurable: true, + get: function () { + return 'hidden'; + }, + }); + + // Pretend 5 seconds have passed + const ELAPSED = 5000; + jest.advanceTimersByTime(ELAPSED); + + const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 2 }; + const hiddenBreadcrumb = { + type: 5, + timestamp: +new Date(BASE_TIMESTAMP + ELAPSED) / 1000, + data: { + tag: 'breadcrumb', + payload: { + timestamp: +new Date(BASE_TIMESTAMP + ELAPSED) / 1000, + type: 'default', + category: 'ui.blur', + }, + }, + }; + + addEvent(replay, TEST_EVENT); + WINDOW.dispatchEvent(new Event('blur')); + await new Promise(process.nextTick); + expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); + expect(replay).toHaveLastSentReplay({ + events: JSON.stringify([TEST_EVENT, hiddenBreadcrumb]), + }); + // Session's last activity should not be updated + expect(replay.session?.lastActivity).toBe(BASE_TIMESTAMP); + // events array should be empty + expect(replay.eventBuffer?.length).toBe(0); + }); + + it('uploads a replay event when document becomes hidden', async () => { + Object.defineProperty(document, 'visibilityState', { + configurable: true, + get: function () { + return 'hidden'; + }, + }); + // Pretend 5 seconds have passed + const ELAPSED = 5000; + jest.advanceTimersByTime(ELAPSED); + + const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 2 }; + + addEvent(replay, TEST_EVENT); + document.dispatchEvent(new Event('visibilitychange')); + jest.runAllTimers(); + await new Promise(process.nextTick); + + expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); + expect(replay).toHaveLastSentReplay({ events: JSON.stringify([TEST_EVENT]) }); + + // Session's last activity is not updated because we do not consider + // visibilitystate as user being active + expect(replay.session?.lastActivity).toBe(BASE_TIMESTAMP); + // events array should be empty + expect(replay.eventBuffer?.length).toBe(0); + }); + + it('uploads a replay event if 5 seconds have elapsed since the last replay event occurred', async () => { + const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 3 }; + mockRecord._emitter(TEST_EVENT); + // Pretend 5 seconds have passed + const ELAPSED = 5000; + await advanceTimers(ELAPSED); + + expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); + expect(mockTransportSend).toHaveBeenCalledTimes(1); + expect(replay).toHaveLastSentReplay({ events: JSON.stringify([TEST_EVENT]) }); + + // No user activity to trigger an update + expect(replay.session?.lastActivity).toBe(BASE_TIMESTAMP); + expect(replay.session?.segmentId).toBe(1); + + // events array should be empty + expect(replay.eventBuffer?.length).toBe(0); + }); + + it('uploads a replay event if 15 seconds have elapsed since the last replay upload', async () => { + const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 3 }; + // Fire a new event every 4 seconds, 4 times + [...Array(4)].forEach(() => { + mockRecord._emitter(TEST_EVENT); + jest.advanceTimersByTime(4000); + }); + + // We are at time = +16seconds now (relative to BASE_TIMESTAMP) + // The next event should cause an upload immediately + mockRecord._emitter(TEST_EVENT); + await new Promise(process.nextTick); + + expect(replay).toHaveLastSentReplay({ + events: JSON.stringify([...Array(5)].map(() => TEST_EVENT)), + }); + + // There should also not be another attempt at an upload 5 seconds after the last replay event + mockTransportSend.mockClear(); + await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); + + expect(replay).not.toHaveLastSentReplay(); + + expect(replay.session?.lastActivity).toBe(BASE_TIMESTAMP); + expect(replay.session?.segmentId).toBe(1); + // events array should be empty + expect(replay.eventBuffer?.length).toBe(0); + + // Let's make sure it continues to work + mockTransportSend.mockClear(); + mockRecord._emitter(TEST_EVENT); + await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); + expect(replay).toHaveLastSentReplay({ events: JSON.stringify([TEST_EVENT]) }); + }); + + it('uploads a dom breadcrumb 5 seconds after listener receives an event', async () => { + domHandler({ + name: 'click', + }); + + // Pretend 5 seconds have passed + const ELAPSED = 5000; + await advanceTimers(ELAPSED); + + expect(replay).toHaveLastSentReplay({ + events: JSON.stringify([ + { + type: 5, + timestamp: BASE_TIMESTAMP, + data: { + tag: 'breadcrumb', + payload: { + timestamp: BASE_TIMESTAMP / 1000, + type: 'default', + category: 'ui.click', + message: '', + data: {}, + }, + }, + }, + ]), + }); + + expect(replay.session?.segmentId).toBe(1); + }); + + it('fails to upload data on first two calls and succeeds on the third', async () => { + expect(replay.session?.segmentId).toBe(0); + const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 3 }; + + // Suppress console.errors + const mockConsole = jest.spyOn(console, 'error').mockImplementation(jest.fn()); + + // fail the first and second requests and pass the third one + mockTransportSend.mockImplementationOnce(() => { + throw new Error('Something bad happened'); + }); + mockRecord._emitter(TEST_EVENT); + await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); + + expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); + mockTransportSend.mockImplementationOnce(() => { + throw new Error('Something bad happened'); + }); + await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); + + // next tick should retry and succeed + mockConsole.mockRestore(); + + await advanceTimers(8000); + await advanceTimers(2000); + + expect(replay).toHaveLastSentReplay({ + replayEventPayload: expect.objectContaining({ + error_ids: [], + replay_id: expect.any(String), + replay_start_timestamp: BASE_TIMESTAMP / 1000, + // 20seconds = Add up all of the previous `advanceTimers()` + timestamp: (BASE_TIMESTAMP + 20000) / 1000 + 0.02, + trace_ids: [], + urls: ['http://localhost/'], + }), + recordingPayloadHeader: { segment_id: 0 }, + events: JSON.stringify([TEST_EVENT]), + }); + + mockTransportSend.mockClear(); + // No activity has occurred, session's last activity should remain the same + expect(replay.session?.lastActivity).toBeGreaterThanOrEqual(BASE_TIMESTAMP); + expect(replay.session?.segmentId).toBe(1); + + // next tick should do nothing + await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); + expect(replay).not.toHaveLastSentReplay(); + }); + + it('fails to upload data and hits retry max and stops', async () => { + const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 3 }; + jest.spyOn(replay, 'sendReplay'); + + // Suppress console.errors + const mockConsole = jest.spyOn(console, 'error').mockImplementation(jest.fn()); + + // Check errors + const spyHandleException = jest.spyOn(replay, 'handleException'); + + expect(replay.session?.segmentId).toBe(0); + + // fail the first and second requests and pass the third one + mockSendReplayRequest.mockReset(); + mockSendReplayRequest.mockImplementation(() => { + throw new Error('Something bad happened'); + }); + mockRecord._emitter(TEST_EVENT); + + await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); + + expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); + expect(replay.sendReplayRequest).toHaveBeenCalledTimes(1); + + await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); + expect(replay.sendReplayRequest).toHaveBeenCalledTimes(2); + + await advanceTimers(10000); + expect(replay.sendReplayRequest).toHaveBeenCalledTimes(3); + + await advanceTimers(30000); + expect(replay.sendReplayRequest).toHaveBeenCalledTimes(4); + expect(replay.sendReplay).toHaveBeenCalledTimes(4); + + mockConsole.mockReset(); + + // Make sure it doesn't retry again + jest.runAllTimers(); + expect(replay.sendReplayRequest).toHaveBeenCalledTimes(4); + expect(replay.sendReplay).toHaveBeenCalledTimes(4); + + // Retries = 3 (total tries = 4 including initial attempt) + // + last exception is max retries exceeded + expect(spyHandleException).toHaveBeenCalledTimes(5); + expect(spyHandleException).toHaveBeenLastCalledWith(new Error('Unable to send Replay - max retries exceeded')); + + // No activity has occurred, session's last activity should remain the same + expect(replay.session?.lastActivity).toBe(BASE_TIMESTAMP); + + // segmentId increases despite error + expect(replay.session?.segmentId).toBe(1); + }); +}); diff --git a/packages/replay/test/integration/session.test.ts b/packages/replay/test/integration/session.test.ts new file mode 100644 index 000000000000..61b08e292582 --- /dev/null +++ b/packages/replay/test/integration/session.test.ts @@ -0,0 +1,486 @@ +import { getCurrentHub } from '@sentry/core'; +import { Transport } from '@sentry/types'; + +import { + DEFAULT_FLUSH_MIN_DELAY, + MAX_SESSION_LIFE, + REPLAY_SESSION_KEY, + VISIBILITY_CHANGE_TIMEOUT, + WINDOW, +} from '../../src/constants'; +import { ReplayContainer } from '../../src/replay'; +import { addEvent } from '../../src/util/addEvent'; +import { createPerformanceSpans } from '../../src/util/createPerformanceSpans'; +import { BASE_TIMESTAMP } from '../index'; +import { RecordMock } from '../mocks/mockRrweb'; +import { resetSdkMock } from '../mocks/resetSdkMock'; +import { clearSession } from '../utils/clearSession'; +import { useFakeTimers } from '../utils/use-fake-timers'; + +useFakeTimers(); + +async function advanceTimers(time: number) { + jest.advanceTimersByTime(time); + await new Promise(process.nextTick); +} + +const prevLocation = WINDOW.location; + +describe('Integration | session', () => { + let replay: ReplayContainer; + let domHandler: (args: any) => any; + let mockRecord: RecordMock; + + beforeEach(async () => { + ({ mockRecord, domHandler, replay } = await resetSdkMock({ + replayOptions: { + stickySession: false, + }, + })); + + const mockTransport = getCurrentHub()?.getClient()?.getTransport()?.send as jest.MockedFunction; + mockTransport?.mockClear(); + }); + + afterEach(async () => { + replay.stop(); + + jest.runAllTimers(); + await new Promise(process.nextTick); + jest.setSystemTime(new Date(BASE_TIMESTAMP)); + + Object.defineProperty(WINDOW, 'location', { + value: prevLocation, + writable: true, + }); + }); + + it('creates a new session and triggers a full dom snapshot when document becomes visible after [VISIBILITY_CHANGE_TIMEOUT]ms', () => { + Object.defineProperty(document, 'visibilityState', { + configurable: true, + get: function () { + return 'visible'; + }, + }); + + const initialSession = replay.session; + + jest.advanceTimersByTime(VISIBILITY_CHANGE_TIMEOUT + 1); + + document.dispatchEvent(new Event('visibilitychange')); + + expect(mockRecord.takeFullSnapshot).toHaveBeenLastCalledWith(true); + + // Should have created a new session + expect(replay).not.toHaveSameSession(initialSession); + }); + + it('does not create a new session if user hides the tab and comes back within [VISIBILITY_CHANGE_TIMEOUT] seconds', () => { + const initialSession = replay.session; + + Object.defineProperty(document, 'visibilityState', { + configurable: true, + get: function () { + return 'hidden'; + }, + }); + document.dispatchEvent(new Event('visibilitychange')); + expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); + expect(replay).toHaveSameSession(initialSession); + + // User comes back before `VISIBILITY_CHANGE_TIMEOUT` elapses + jest.advanceTimersByTime(VISIBILITY_CHANGE_TIMEOUT - 1); + Object.defineProperty(document, 'visibilityState', { + configurable: true, + get: function () { + return 'visible'; + }, + }); + document.dispatchEvent(new Event('visibilitychange')); + + expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); + // Should NOT have created a new session + expect(replay).toHaveSameSession(initialSession); + }); + + it('creates a new session if user has been idle for more than 15 minutes and comes back to move their mouse', async () => { + const initialSession = replay.session; + + expect(initialSession?.id).toBeDefined(); + + // Idle for 15 minutes + const FIFTEEN_MINUTES = 15 * 60000; + jest.advanceTimersByTime(FIFTEEN_MINUTES); + + // TBD: We are currently deciding that this event will get dropped, but + // this could/should change in the future. + const TEST_EVENT = { + data: { name: 'lost event' }, + timestamp: BASE_TIMESTAMP, + type: 3, + }; + mockRecord._emitter(TEST_EVENT); + expect(replay).not.toHaveLastSentReplay(); + + await new Promise(process.nextTick); + + // Instead of recording the above event, a full snapshot will occur. + // + // TODO: We could potentially figure out a way to save the last session, + // and produce a checkout based on a previous checkout + updates, and then + // replay the event on top. Or maybe replay the event on top of a refresh + // snapshot. + expect(mockRecord.takeFullSnapshot).toHaveBeenCalledWith(true); + + // Should be a new session + expect(replay).not.toHaveSameSession(initialSession); + + // Replay does not send immediately because checkout was due to expired session + expect(replay).not.toHaveLastSentReplay(); + + // Now do a click + domHandler({ + name: 'click', + }); + + await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); + + const newTimestamp = BASE_TIMESTAMP + FIFTEEN_MINUTES; + const breadcrumbTimestamp = newTimestamp + 20; // I don't know where this 20ms comes from + + expect(replay).toHaveLastSentReplay({ + events: JSON.stringify([ + { data: { isCheckout: true }, timestamp: newTimestamp, type: 2 }, + { + type: 5, + timestamp: breadcrumbTimestamp, + data: { + tag: 'breadcrumb', + payload: { + timestamp: breadcrumbTimestamp / 1000, + type: 'default', + category: 'ui.click', + message: '', + data: {}, + }, + }, + }, + ]), + }); + }); + + it('should have a session after setup', () => { + expect(replay.session).toMatchObject({ + lastActivity: BASE_TIMESTAMP, + started: BASE_TIMESTAMP, + }); + expect(replay.session?.id).toBeDefined(); + expect(replay.session?.segmentId).toBeDefined(); + }); + + it('clears session', () => { + clearSession(replay); + expect(WINDOW.sessionStorage.getItem(REPLAY_SESSION_KEY)).toBe(null); + expect(replay.session).toBe(undefined); + }); + + it('creates a new session and triggers a full dom snapshot when document becomes visible after [VISIBILITY_CHANGE_TIMEOUT]ms', () => { + Object.defineProperty(document, 'visibilityState', { + configurable: true, + get: function () { + return 'visible'; + }, + }); + + const initialSession = replay.session; + + jest.advanceTimersByTime(VISIBILITY_CHANGE_TIMEOUT + 1); + + document.dispatchEvent(new Event('visibilitychange')); + + expect(mockRecord.takeFullSnapshot).toHaveBeenLastCalledWith(true); + + // Should have created a new session + expect(replay).not.toHaveSameSession(initialSession); + }); + + it('creates a new session and triggers a full dom snapshot when document becomes focused after [VISIBILITY_CHANGE_TIMEOUT]ms', () => { + Object.defineProperty(document, 'visibilityState', { + configurable: true, + get: function () { + return 'visible'; + }, + }); + + const initialSession = replay.session; + + jest.advanceTimersByTime(VISIBILITY_CHANGE_TIMEOUT + 1); + + WINDOW.dispatchEvent(new Event('focus')); + + expect(mockRecord.takeFullSnapshot).toHaveBeenLastCalledWith(true); + + // Should have created a new session + expect(replay).not.toHaveSameSession(initialSession); + }); + + it('does not create a new session if user hides the tab and comes back within [VISIBILITY_CHANGE_TIMEOUT] seconds', () => { + const initialSession = replay.session; + + Object.defineProperty(document, 'visibilityState', { + configurable: true, + get: function () { + return 'hidden'; + }, + }); + document.dispatchEvent(new Event('visibilitychange')); + expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); + expect(replay).toHaveSameSession(initialSession); + + // User comes back before `VISIBILITY_CHANGE_TIMEOUT` elapses + jest.advanceTimersByTime(VISIBILITY_CHANGE_TIMEOUT - 1); + Object.defineProperty(document, 'visibilityState', { + configurable: true, + get: function () { + return 'visible'; + }, + }); + document.dispatchEvent(new Event('visibilitychange')); + + expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); + // Should NOT have created a new session + expect(replay).toHaveSameSession(initialSession); + }); + + it('creates a new session if user has been idle for 15 minutes and comes back to click their mouse', async () => { + const initialSession = replay.session; + + expect(initialSession?.id).toBeDefined(); + expect(replay.getContext()).toEqual( + expect.objectContaining({ + initialUrl: 'http://localhost/', + initialTimestamp: BASE_TIMESTAMP, + }), + ); + + const url = 'http://dummy/'; + Object.defineProperty(WINDOW, 'location', { + value: new URL(url), + }); + + // Idle for 15 minutes + const FIFTEEN_MINUTES = 15 * 60000; + jest.advanceTimersByTime(FIFTEEN_MINUTES); + + // TBD: We are currently deciding that this event will get dropped, but + // this could/should change in the future. + const TEST_EVENT = { + data: { name: 'lost event' }, + timestamp: BASE_TIMESTAMP, + type: 3, + }; + mockRecord._emitter(TEST_EVENT); + expect(replay).not.toHaveLastSentReplay(); + + await new Promise(process.nextTick); + + // Instead of recording the above event, a full snapshot will occur. + // + // TODO: We could potentially figure out a way to save the last session, + // and produce a checkout based on a previous checkout + updates, and then + // replay the event on top. Or maybe replay the event on top of a refresh + // snapshot. + expect(mockRecord.takeFullSnapshot).toHaveBeenCalledWith(true); + + expect(replay).not.toHaveLastSentReplay(); + + // Should be a new session + expect(replay).not.toHaveSameSession(initialSession); + + // Now do a click + domHandler({ + name: 'click', + }); + + await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); + + const newTimestamp = BASE_TIMESTAMP + FIFTEEN_MINUTES; + const breadcrumbTimestamp = newTimestamp + 20; // I don't know where this 20ms comes from + + expect(replay).toHaveLastSentReplay({ + recordingPayloadHeader: { segment_id: 0 }, + events: JSON.stringify([ + { data: { isCheckout: true }, timestamp: newTimestamp, type: 2 }, + { + type: 5, + timestamp: breadcrumbTimestamp, + data: { + tag: 'breadcrumb', + payload: { + timestamp: breadcrumbTimestamp / 1000, + type: 'default', + category: 'ui.click', + message: '', + data: {}, + }, + }, + }, + ]), + }); + + // `_context` should be reset when a new session is created + expect(replay.getContext()).toEqual( + expect.objectContaining({ + initialUrl: 'http://dummy/', + initialTimestamp: newTimestamp, + }), + ); + }); + + it('does not record if user has been idle for more than MAX_SESSION_LIFE and only starts a new session after a user action', async () => { + jest.clearAllMocks(); + + const initialSession = replay.session; + + expect(initialSession?.id).toBeDefined(); + expect(replay.getContext()).toEqual( + expect.objectContaining({ + initialUrl: 'http://localhost/', + initialTimestamp: BASE_TIMESTAMP, + }), + ); + + const url = 'http://dummy/'; + Object.defineProperty(WINDOW, 'location', { + value: new URL(url), + }); + + // Idle for MAX_SESSION_LIFE + jest.advanceTimersByTime(MAX_SESSION_LIFE); + + // These events will not get flushed and will eventually be dropped because user is idle and session is expired + const TEST_EVENT = { + data: { name: 'lost event' }, + timestamp: MAX_SESSION_LIFE, + type: 3, + }; + mockRecord._emitter(TEST_EVENT); + // performance events can still be collected while recording is stopped + // TODO: we may want to prevent `addEvent` from adding to buffer when user is inactive + replay.addUpdate(() => { + createPerformanceSpans(replay, [ + { + type: 'navigation.navigate', + name: 'foo', + start: BASE_TIMESTAMP + MAX_SESSION_LIFE, + end: BASE_TIMESTAMP + MAX_SESSION_LIFE + 100, + }, + ]); + return true; + }); + + WINDOW.dispatchEvent(new Event('blur')); + await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); + + expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); + expect(replay).not.toHaveLastSentReplay(); + // Should be the same session because user has been idle and no events have caused a new session to be created + expect(replay).toHaveSameSession(initialSession); + + // @ts-ignore private + expect(replay._stopRecording).toBeUndefined(); + + // Now do a click + domHandler({ + name: 'click', + }); + // This should still be thrown away + mockRecord._emitter(TEST_EVENT); + + const NEW_TEST_EVENT = { + data: { name: 'test' }, + timestamp: BASE_TIMESTAMP + MAX_SESSION_LIFE + DEFAULT_FLUSH_MIN_DELAY + 20, + type: 3, + }; + + mockRecord._emitter(NEW_TEST_EVENT); + + // new session is created + jest.runAllTimers(); + await new Promise(process.nextTick); + + expect(replay).not.toHaveSameSession(initialSession); + await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); + + const newTimestamp = BASE_TIMESTAMP + MAX_SESSION_LIFE + DEFAULT_FLUSH_MIN_DELAY + 20; // I don't know where this 20ms comes from + const breadcrumbTimestamp = newTimestamp; + + jest.runAllTimers(); + await new Promise(process.nextTick); + + expect(replay).toHaveLastSentReplay({ + recordingPayloadHeader: { segment_id: 0 }, + events: JSON.stringify([ + { data: { isCheckout: true }, timestamp: newTimestamp, type: 2 }, + { + type: 5, + timestamp: breadcrumbTimestamp, + data: { + tag: 'breadcrumb', + payload: { + timestamp: breadcrumbTimestamp / 1000, + type: 'default', + category: 'ui.click', + message: '', + data: {}, + }, + }, + }, + NEW_TEST_EVENT, + ]), + }); + + // `_context` should be reset when a new session is created + expect(replay.getContext()).toEqual( + expect.objectContaining({ + initialUrl: 'http://dummy/', + initialTimestamp: newTimestamp, + }), + ); + }); + + it('increases segment id after each event', async () => { + clearSession(replay); + replay.loadSession({ expiry: 0 }); + + Object.defineProperty(document, 'visibilityState', { + configurable: true, + get: function () { + return 'hidden'; + }, + }); + + // Pretend 5 seconds have passed + const ELAPSED = 5000; + await advanceTimers(ELAPSED); + + const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 2 }; + + addEvent(replay, TEST_EVENT); + WINDOW.dispatchEvent(new Event('blur')); + await new Promise(process.nextTick); + expect(replay).toHaveLastSentReplay({ + recordingPayloadHeader: { segment_id: 0 }, + }); + expect(replay.session?.segmentId).toBe(1); + + addEvent(replay, TEST_EVENT); + WINDOW.dispatchEvent(new Event('blur')); + jest.runAllTimers(); + await new Promise(process.nextTick); + expect(replay.session?.segmentId).toBe(2); + expect(replay).toHaveLastSentReplay({ + recordingPayloadHeader: { segment_id: 1 }, + }); + }); +}); diff --git a/packages/replay/test/unit/stop.test.ts b/packages/replay/test/integration/stop.test.ts similarity index 95% rename from packages/replay/test/unit/stop.test.ts rename to packages/replay/test/integration/stop.test.ts index 7aa5469c1d7d..46c43e8afea5 100644 --- a/packages/replay/test/unit/stop.test.ts +++ b/packages/replay/test/integration/stop.test.ts @@ -1,17 +1,17 @@ import * as SentryUtils from '@sentry/utils'; +import { Replay } from '../../src'; import { SESSION_IDLE_DURATION, WINDOW } from '../../src/constants'; import { ReplayContainer } from '../../src/replay'; import { addEvent } from '../../src/util/addEvent'; -import { clearSession } from '../utils/clearSession'; -import { Replay } from './../../src'; // mock functions need to be imported first -import { BASE_TIMESTAMP, mockRrweb, mockSdk } from './../index'; -import { useFakeTimers } from './../utils/use-fake-timers'; +import { BASE_TIMESTAMP, mockRrweb, mockSdk } from '../index'; +import { clearSession } from '../utils/clearSession'; +import { useFakeTimers } from '../utils/use-fake-timers'; useFakeTimers(); -describe('Replay - stop', () => { +describe('Integration | stop', () => { let replay: ReplayContainer; let integration: Replay; const prevLocation = WINDOW.location; diff --git a/packages/replay/test/unit/coreHandlers/handleFetch.test.ts b/packages/replay/test/unit/coreHandlers/handleFetch.test.ts index 0d3386aee15a..e936b693af56 100644 --- a/packages/replay/test/unit/coreHandlers/handleFetch.test.ts +++ b/packages/replay/test/unit/coreHandlers/handleFetch.test.ts @@ -1,9 +1,4 @@ import { handleFetch } from '../../../src/coreHandlers/handleFetch'; -import { mockSdk } from './../../index'; - -beforeAll(function () { - mockSdk(); -}); const DEFAULT_DATA = { args: ['/api/0/organizations/sentry/', { method: 'GET', headers: {}, credentials: 'include' }] as Parameters< @@ -24,28 +19,30 @@ const DEFAULT_DATA = { startTimestamp: 10000, }; -it('formats fetch calls from core SDK to replay breadcrumbs', function () { - expect(handleFetch(DEFAULT_DATA)).toEqual({ - type: 'resource.fetch', - name: '/api/0/organizations/sentry/', - start: 10, - end: 15, - data: { - method: 'GET', - statusCode: 200, - }, +describe('Unit | coreHandlers | handleFetch', () => { + it('formats fetch calls from core SDK to replay breadcrumbs', function () { + expect(handleFetch(DEFAULT_DATA)).toEqual({ + type: 'resource.fetch', + name: '/api/0/organizations/sentry/', + start: 10, + end: 15, + data: { + method: 'GET', + statusCode: 200, + }, + }); }); -}); -it('ignores fetches that have not completed yet', function () { - const data = { - ...DEFAULT_DATA, - }; + it('ignores fetches that have not completed yet', function () { + const data = { + ...DEFAULT_DATA, + }; - // @ts-ignore: The operand of a 'delete' operator must be optional.ts(2790) - delete data.endTimestamp; - // @ts-ignore: The operand of a 'delete' operator must be optional.ts(2790) - delete data.response; + // @ts-ignore: The operand of a 'delete' operator must be optional.ts(2790) + delete data.endTimestamp; + // @ts-ignore: The operand of a 'delete' operator must be optional.ts(2790) + delete data.response; - expect(handleFetch(data)).toEqual(null); + expect(handleFetch(data)).toEqual(null); + }); }); diff --git a/packages/replay/test/unit/coreHandlers/handleScope-unit.test.ts b/packages/replay/test/unit/coreHandlers/handleScope-unit.test.ts deleted file mode 100644 index cec834472029..000000000000 --- a/packages/replay/test/unit/coreHandlers/handleScope-unit.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { getCurrentHub } from '@sentry/core'; - -import * as HandleScope from '../../../src/coreHandlers/handleScope'; -import { mockSdk } from './../../index'; - -let mockHandleScope: jest.MockedFunction; - -jest.useFakeTimers(); - -beforeAll(async function () { - await mockSdk(); - jest.spyOn(HandleScope, 'handleScope'); - mockHandleScope = HandleScope.handleScope as jest.MockedFunction; - - jest.runAllTimers(); -}); - -it('returns a breadcrumb only if last breadcrumb has changed (integration)', function () { - getCurrentHub().getScope()?.addBreadcrumb({ message: 'testing' }); - - expect(mockHandleScope).toHaveBeenCalledTimes(1); - expect(mockHandleScope).toHaveReturnedWith(expect.objectContaining({ message: 'testing' })); - - mockHandleScope.mockClear(); - - // This will trigger breadcrumb/scope listener, but handleScope should return - // null because breadcrumbs has not changed - getCurrentHub().getScope()?.setUser({ email: 'foo@foo.com' }); - expect(mockHandleScope).toHaveBeenCalledTimes(1); - expect(mockHandleScope).toHaveReturnedWith(null); -}); diff --git a/packages/replay/test/unit/coreHandlers/handleScope.test.ts b/packages/replay/test/unit/coreHandlers/handleScope.test.ts index dd650a685293..db562a7b98aa 100644 --- a/packages/replay/test/unit/coreHandlers/handleScope.test.ts +++ b/packages/replay/test/unit/coreHandlers/handleScope.test.ts @@ -2,48 +2,49 @@ import type { Breadcrumb, Scope } from '@sentry/types'; import * as HandleScope from '../../../src/coreHandlers/handleScope'; -jest.spyOn(HandleScope, 'handleScope'); -const mockHandleScope = HandleScope.handleScope as jest.MockedFunction; - -it('returns a breadcrumb only if last breadcrumb has changed (unit)', function () { - const scope = { - _breadcrumbs: [], - getLastBreadcrumb() { - return this._breadcrumbs[this._breadcrumbs.length - 1]; - }, - } as unknown as Scope; - - function addBreadcrumb(breadcrumb: Breadcrumb) { - // @ts-ignore using private member - scope._breadcrumbs.push(breadcrumb); - } - - const testMsg = { - timestamp: new Date().getTime() / 1000, - message: 'testing', - category: 'console', - }; - - addBreadcrumb(testMsg); - // integration testing here is a bit tricky, because the core SDK can - // interfere with console output from test runner - HandleScope.handleScope(scope); - expect(mockHandleScope).toHaveBeenCalledTimes(1); - expect(mockHandleScope).toHaveReturnedWith(expect.objectContaining({ message: 'testing', category: 'console' })); - - // This will trigger breadcrumb/scope listener, but handleScope should return - // null because breadcrumbs has not changed - mockHandleScope.mockClear(); - HandleScope.handleScope(scope); - expect(mockHandleScope).toHaveBeenCalledTimes(1); - expect(mockHandleScope).toHaveReturnedWith(null); - - mockHandleScope.mockClear(); - addBreadcrumb({ - message: 'f00', - category: 'console', +describe('Unit | coreHandlers | handleScope', () => { + const mockHandleScope = jest.spyOn(HandleScope, 'handleScope'); + + it('returns a breadcrumb only if last breadcrumb has changed (unit)', function () { + const scope = { + _breadcrumbs: [], + getLastBreadcrumb() { + return this._breadcrumbs[this._breadcrumbs.length - 1]; + }, + } as unknown as Scope; + + function addBreadcrumb(breadcrumb: Breadcrumb) { + // @ts-ignore using private member + scope._breadcrumbs.push(breadcrumb); + } + + const testMsg = { + timestamp: new Date().getTime() / 1000, + message: 'testing', + category: 'console', + }; + + addBreadcrumb(testMsg); + // integration testing here is a bit tricky, because the core SDK can + // interfere with console output from test runner + HandleScope.handleScope(scope); + expect(mockHandleScope).toHaveBeenCalledTimes(1); + expect(mockHandleScope).toHaveReturnedWith(expect.objectContaining({ message: 'testing', category: 'console' })); + + // This will trigger breadcrumb/scope listener, but handleScope should return + // null because breadcrumbs has not changed + mockHandleScope.mockClear(); + HandleScope.handleScope(scope); + expect(mockHandleScope).toHaveBeenCalledTimes(1); + expect(mockHandleScope).toHaveReturnedWith(null); + + mockHandleScope.mockClear(); + addBreadcrumb({ + message: 'f00', + category: 'console', + }); + HandleScope.handleScope(scope); + expect(mockHandleScope).toHaveBeenCalledTimes(1); + expect(mockHandleScope).toHaveReturnedWith(expect.objectContaining({ message: 'f00', category: 'console' })); }); - HandleScope.handleScope(scope); - expect(mockHandleScope).toHaveBeenCalledTimes(1); - expect(mockHandleScope).toHaveReturnedWith(expect.objectContaining({ message: 'f00', category: 'console' })); }); diff --git a/packages/replay/test/unit/createPerformanceEntry.test.ts b/packages/replay/test/unit/createPerformanceEntry.test.ts deleted file mode 100644 index ecd0d746806e..000000000000 --- a/packages/replay/test/unit/createPerformanceEntry.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { createPerformanceEntries } from '../../src/createPerformanceEntry'; -import { mockSdk } from './../index'; - -beforeAll(function () { - mockSdk(); -}); - -it('ignores sdks own requests', function () { - const data = { - name: 'https://ingest.f00.f00/api/1/envelope/?sentry_key=dsn&sentry_version=7', - entryType: 'resource', - startTime: 234462.69999998808, - duration: 55.70000001788139, - initiatorType: 'fetch', - nextHopProtocol: '', - workerStart: 0, - redirectStart: 0, - redirectEnd: 0, - fetchStart: 234462.69999998808, - domainLookupStart: 0, - domainLookupEnd: 0, - connectStart: 0, - connectEnd: 0, - secureConnectionStart: 0, - requestStart: 0, - responseStart: 0, - responseEnd: 234518.40000000596, - transferSize: 0, - encodedBodySize: 0, - decodedBodySize: 0, - serverTiming: [], - workerTiming: [], - } as const; - - // @ts-ignore Needs a PerformanceEntry mock - expect(createPerformanceEntries([data])).toEqual([]); -}); diff --git a/packages/replay/test/unit/eventBuffer.test.ts b/packages/replay/test/unit/eventBuffer.test.ts index a34230138fea..ac5a3be70183 100644 --- a/packages/replay/test/unit/eventBuffer.test.ts +++ b/packages/replay/test/unit/eventBuffer.test.ts @@ -6,114 +6,115 @@ import { createEventBuffer, EventBufferCompressionWorker } from './../../src/eve import { BASE_TIMESTAMP } from './../index'; const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 3 }; +describe('Unit | eventBuffer', () => { + it('adds events to normal event buffer', async function () { + const buffer = createEventBuffer({ useCompression: false }); -it('adds events to normal event buffer', async function () { - const buffer = createEventBuffer({ useCompression: false }); + buffer.addEvent(TEST_EVENT); + buffer.addEvent(TEST_EVENT); - buffer.addEvent(TEST_EVENT); - buffer.addEvent(TEST_EVENT); + const result = await buffer.finish(); - const result = await buffer.finish(); + expect(result).toEqual(JSON.stringify([TEST_EVENT, TEST_EVENT])); + }); - expect(result).toEqual(JSON.stringify([TEST_EVENT, TEST_EVENT])); -}); + it('adds checkout event to normal event buffer', async function () { + const buffer = createEventBuffer({ useCompression: false }); -it('adds checkout event to normal event buffer', async function () { - const buffer = createEventBuffer({ useCompression: false }); + buffer.addEvent(TEST_EVENT); + buffer.addEvent(TEST_EVENT); + buffer.addEvent(TEST_EVENT); - buffer.addEvent(TEST_EVENT); - buffer.addEvent(TEST_EVENT); - buffer.addEvent(TEST_EVENT); + buffer.addEvent(TEST_EVENT, true); + const result = await buffer.finish(); - buffer.addEvent(TEST_EVENT, true); - const result = await buffer.finish(); + expect(result).toEqual(JSON.stringify([TEST_EVENT])); + }); - expect(result).toEqual(JSON.stringify([TEST_EVENT])); -}); + it('calling `finish()` multiple times does not result in duplicated events', async function () { + const buffer = createEventBuffer({ useCompression: false }); -it('calling `finish()` multiple times does not result in duplicated events', async function () { - const buffer = createEventBuffer({ useCompression: false }); + buffer.addEvent(TEST_EVENT); - buffer.addEvent(TEST_EVENT); + const promise1 = buffer.finish(); + const promise2 = buffer.finish(); - const promise1 = buffer.finish(); - const promise2 = buffer.finish(); + const result1 = (await promise1) as Uint8Array; + const result2 = (await promise2) as Uint8Array; - const result1 = (await promise1) as Uint8Array; - const result2 = (await promise2) as Uint8Array; + expect(result1).toEqual(JSON.stringify([TEST_EVENT])); + expect(result2).toEqual(JSON.stringify([])); + }); - expect(result1).toEqual(JSON.stringify([TEST_EVENT])); - expect(result2).toEqual(JSON.stringify([])); -}); + it('adds events to event buffer with compression worker', async function () { + const buffer = createEventBuffer({ + useCompression: true, + }) as EventBufferCompressionWorker; -it('adds events to event buffer with compression worker', async function () { - const buffer = createEventBuffer({ - useCompression: true, - }) as EventBufferCompressionWorker; + buffer.addEvent(TEST_EVENT); + buffer.addEvent(TEST_EVENT); - buffer.addEvent(TEST_EVENT); - buffer.addEvent(TEST_EVENT); + const result = await buffer.finish(); + const restored = pako.inflate(result, { to: 'string' }); - const result = await buffer.finish(); - const restored = pako.inflate(result, { to: 'string' }); + expect(restored).toEqual(JSON.stringify([TEST_EVENT, TEST_EVENT])); + }); - expect(restored).toEqual(JSON.stringify([TEST_EVENT, TEST_EVENT])); -}); + it('adds checkout events to event buffer with compression worker', async function () { + const buffer = createEventBuffer({ + useCompression: true, + }) as EventBufferCompressionWorker; -it('adds checkout events to event buffer with compression worker', async function () { - const buffer = createEventBuffer({ - useCompression: true, - }) as EventBufferCompressionWorker; + await buffer.addEvent(TEST_EVENT); + await buffer.addEvent(TEST_EVENT); - await buffer.addEvent(TEST_EVENT); - await buffer.addEvent(TEST_EVENT); + // This should clear previous buffer + await buffer.addEvent({ ...TEST_EVENT, type: 2 }, true); - // This should clear previous buffer - await buffer.addEvent({ ...TEST_EVENT, type: 2 }, true); + const result = await buffer.finish(); + const restored = pako.inflate(result, { to: 'string' }); - const result = await buffer.finish(); - const restored = pako.inflate(result, { to: 'string' }); + expect(restored).toEqual(JSON.stringify([{ ...TEST_EVENT, type: 2 }])); + }); - expect(restored).toEqual(JSON.stringify([{ ...TEST_EVENT, type: 2 }])); -}); - -it('calling `finish()` multiple times does not result in duplicated events', async function () { - const buffer = createEventBuffer({ - useCompression: true, - }) as EventBufferCompressionWorker; + it('calling `finish()` multiple times does not result in duplicated events', async function () { + const buffer = createEventBuffer({ + useCompression: true, + }) as EventBufferCompressionWorker; - buffer.addEvent(TEST_EVENT); + buffer.addEvent(TEST_EVENT); - const promise1 = buffer.finish(); - const promise2 = buffer.finish(); + const promise1 = buffer.finish(); + const promise2 = buffer.finish(); - const result1 = (await promise1) as Uint8Array; - const result2 = (await promise2) as Uint8Array; - const restored1 = pako.inflate(result1, { to: 'string' }); - const restored2 = pako.inflate(result2, { to: 'string' }); + const result1 = (await promise1) as Uint8Array; + const result2 = (await promise2) as Uint8Array; + const restored1 = pako.inflate(result1, { to: 'string' }); + const restored2 = pako.inflate(result2, { to: 'string' }); - expect(restored1).toEqual(JSON.stringify([TEST_EVENT])); - expect(restored2).toEqual(JSON.stringify([])); -}); + expect(restored1).toEqual(JSON.stringify([TEST_EVENT])); + expect(restored2).toEqual(JSON.stringify([])); + }); -it('calling `finish()` multiple times, with events in between, does not result in duplicated or dropped events', async function () { - const buffer = createEventBuffer({ - useCompression: true, - }) as EventBufferCompressionWorker; + it('calling `finish()` multiple times, with events in between, does not result in duplicated or dropped events', async function () { + const buffer = createEventBuffer({ + useCompression: true, + }) as EventBufferCompressionWorker; - buffer.addEvent(TEST_EVENT); + buffer.addEvent(TEST_EVENT); - const promise1 = buffer.finish(); + const promise1 = buffer.finish(); - buffer.addEvent({ ...TEST_EVENT, type: 5 }); - const promise2 = buffer.finish(); + buffer.addEvent({ ...TEST_EVENT, type: 5 }); + const promise2 = buffer.finish(); - const result1 = (await promise1) as Uint8Array; - const result2 = (await promise2) as Uint8Array; + const result1 = (await promise1) as Uint8Array; + const result2 = (await promise2) as Uint8Array; - const restored1 = pako.inflate(result1, { to: 'string' }); - const restored2 = pako.inflate(result2, { to: 'string' }); + const restored1 = pako.inflate(result1, { to: 'string' }); + const restored2 = pako.inflate(result2, { to: 'string' }); - expect(restored1).toEqual(JSON.stringify([TEST_EVENT])); - expect(restored2).toEqual(JSON.stringify([{ ...TEST_EVENT, type: 5 }])); + expect(restored1).toEqual(JSON.stringify([TEST_EVENT])); + expect(restored2).toEqual(JSON.stringify([{ ...TEST_EVENT, type: 5 }])); + }); }); diff --git a/packages/replay/test/unit/flush.test.ts b/packages/replay/test/unit/flush.test.ts deleted file mode 100644 index 4397fee40d18..000000000000 --- a/packages/replay/test/unit/flush.test.ts +++ /dev/null @@ -1,231 +0,0 @@ -import * as SentryUtils from '@sentry/utils'; - -import { DEFAULT_FLUSH_MIN_DELAY, SESSION_IDLE_DURATION, WINDOW } from '../../src/constants'; -import * as AddMemoryEntry from '../../src/util/addMemoryEntry'; -import { createPerformanceSpans } from '../../src/util/createPerformanceSpans'; -import { clearSession } from '../utils/clearSession'; -import { createPerformanceEntries } from './../../src/createPerformanceEntry'; -import { ReplayContainer } from './../../src/replay'; -import { useFakeTimers } from './../../test/utils/use-fake-timers'; -import { BASE_TIMESTAMP, mockRrweb, mockSdk } from './../index'; - -useFakeTimers(); - -async function advanceTimers(time: number) { - jest.advanceTimersByTime(time); - await new Promise(process.nextTick); -} - -type MockSendReplay = jest.MockedFunction; -type MockAddPerformanceEntries = jest.MockedFunction; -type MockAddMemoryEntry = jest.SpyInstance; -type MockEventBufferFinish = jest.MockedFunction['finish']>; -type MockFlush = jest.MockedFunction; -type MockRunFlush = jest.MockedFunction; - -const prevLocation = WINDOW.location; -let domHandler: (args: any) => any; - -const { record: mockRecord } = mockRrweb(); - -let replay: ReplayContainer; -let mockSendReplay: MockSendReplay; -let mockFlush: MockFlush; -let mockRunFlush: MockRunFlush; -let mockEventBufferFinish: MockEventBufferFinish; -let mockAddMemoryEntry: MockAddMemoryEntry; -let mockAddPerformanceEntries: MockAddPerformanceEntries; - -beforeAll(async () => { - jest.spyOn(SentryUtils, 'addInstrumentationHandler').mockImplementation((type, handler: (args: any) => any) => { - if (type === 'dom') { - domHandler = handler; - } - }); - - ({ replay } = await mockSdk()); - jest.spyOn(replay, 'sendReplay'); - mockSendReplay = replay.sendReplay as MockSendReplay; - mockSendReplay.mockImplementation( - jest.fn(async () => { - return; - }), - ); - - jest.spyOn(replay, 'flush'); - mockFlush = replay.flush as MockFlush; - - jest.spyOn(replay, 'runFlush'); - mockRunFlush = replay.runFlush as MockRunFlush; - - jest.spyOn(replay, 'addPerformanceEntries'); - mockAddPerformanceEntries = replay.addPerformanceEntries as MockAddPerformanceEntries; - - mockAddPerformanceEntries.mockImplementation(async () => { - return []; - }); - - mockAddMemoryEntry = jest.spyOn(AddMemoryEntry, 'addMemoryEntry'); -}); - -beforeEach(() => { - jest.runAllTimers(); - jest.setSystemTime(new Date(BASE_TIMESTAMP)); - mockSendReplay.mockClear(); - replay.eventBuffer?.destroy(); - mockAddPerformanceEntries.mockClear(); - mockFlush.mockClear(); - mockRunFlush.mockClear(); - mockAddMemoryEntry.mockClear(); - - if (replay.eventBuffer) { - jest.spyOn(replay.eventBuffer, 'finish'); - } - mockEventBufferFinish = replay.eventBuffer?.finish as MockEventBufferFinish; - mockEventBufferFinish.mockClear(); -}); - -afterEach(async () => { - jest.runAllTimers(); - await new Promise(process.nextTick); - jest.setSystemTime(new Date(BASE_TIMESTAMP)); - sessionStorage.clear(); - clearSession(replay); - replay.loadSession({ expiry: SESSION_IDLE_DURATION }); - mockRecord.takeFullSnapshot.mockClear(); - Object.defineProperty(WINDOW, 'location', { - value: prevLocation, - writable: true, - }); -}); - -afterAll(() => { - replay && replay.stop(); -}); - -it('flushes twice after multiple flush() calls)', async () => { - // blur events cause an immediate flush (as well as a flush due to adding a - // breadcrumb) -- this means that the first blur event will be flushed and - // the following blur events will all call a debounced flush function, which - // should end up queueing a second flush - - WINDOW.dispatchEvent(new Event('blur')); - WINDOW.dispatchEvent(new Event('blur')); - WINDOW.dispatchEvent(new Event('blur')); - WINDOW.dispatchEvent(new Event('blur')); - - expect(replay.flush).toHaveBeenCalledTimes(4); - - jest.runAllTimers(); - await new Promise(process.nextTick); - expect(replay.runFlush).toHaveBeenCalledTimes(1); - - jest.runAllTimers(); - await new Promise(process.nextTick); - expect(replay.runFlush).toHaveBeenCalledTimes(2); - - jest.runAllTimers(); - await new Promise(process.nextTick); - expect(replay.runFlush).toHaveBeenCalledTimes(2); -}); - -it('long first flush enqueues following events', async () => { - // Mock this to resolve after 20 seconds so that we can queue up following flushes - mockAddPerformanceEntries.mockImplementationOnce(async () => { - return await new Promise(resolve => setTimeout(resolve, 20000)); - }); - - expect(mockAddPerformanceEntries).not.toHaveBeenCalled(); - - // flush #1 @ t=0s - due to blur - WINDOW.dispatchEvent(new Event('blur')); - expect(replay.flush).toHaveBeenCalledTimes(1); - expect(replay.runFlush).toHaveBeenCalledTimes(1); - - // This will attempt to flush in 5 seconds (flushMinDelay) - domHandler({ - name: 'click', - }); - await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); - // flush #2 @ t=5s - due to click - expect(replay.flush).toHaveBeenCalledTimes(2); - - await advanceTimers(1000); - // flush #3 @ t=6s - due to blur - WINDOW.dispatchEvent(new Event('blur')); - expect(replay.flush).toHaveBeenCalledTimes(3); - - // NOTE: Blur also adds a breadcrumb which calls `addUpdate`, meaning it will - // flush after `flushMinDelay`, but this gets cancelled by the blur - await advanceTimers(8000); - expect(replay.flush).toHaveBeenCalledTimes(3); - - // flush #4 @ t=14s - due to blur - WINDOW.dispatchEvent(new Event('blur')); - expect(replay.flush).toHaveBeenCalledTimes(4); - - expect(replay.runFlush).toHaveBeenCalledTimes(1); - await advanceTimers(6000); - // t=20s - // addPerformanceEntries is finished, `flushLock` promise is resolved, calls - // debouncedFlush, which will call `flush` in 1 second - expect(replay.flush).toHaveBeenCalledTimes(4); - // sendReplay is called with replayId, events, segment - expect(mockSendReplay).toHaveBeenLastCalledWith({ - events: expect.any(String), - replayId: expect.any(String), - includeReplayStartTimestamp: true, - segmentId: 0, - eventContext: expect.anything(), - }); - - // Add this to test that segment ID increases - mockAddPerformanceEntries.mockImplementationOnce(() => { - createPerformanceSpans( - replay, - createPerformanceEntries([ - { - name: 'https://sentry.io/foo.js', - entryType: 'resource', - startTime: 176.59999990463257, - duration: 5.600000023841858, - initiatorType: 'link', - nextHopProtocol: 'h2', - workerStart: 177.5, - redirectStart: 0, - redirectEnd: 0, - fetchStart: 177.69999992847443, - domainLookupStart: 177.69999992847443, - domainLookupEnd: 177.69999992847443, - connectStart: 177.69999992847443, - connectEnd: 177.69999992847443, - secureConnectionStart: 177.69999992847443, - requestStart: 177.5, - responseStart: 181, - responseEnd: 182.19999992847443, - transferSize: 0, - encodedBodySize: 0, - decodedBodySize: 0, - serverTiming: [], - } as unknown as PerformanceResourceTiming, - ]), - ); - }); - // flush #5 @ t=25s - debounced flush calls `flush` - // 20s + `flushMinDelay` which is 5 seconds - await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); - expect(replay.flush).toHaveBeenCalledTimes(5); - expect(replay.runFlush).toHaveBeenCalledTimes(2); - expect(mockSendReplay).toHaveBeenLastCalledWith({ - events: expect.any(String), - replayId: expect.any(String), - includeReplayStartTimestamp: false, - segmentId: 1, - eventContext: expect.anything(), - }); - - // Make sure there's no other calls - jest.runAllTimers(); - await new Promise(process.nextTick); - expect(mockSendReplay).toHaveBeenCalledTimes(2); -}); diff --git a/packages/replay/test/unit/index-noSticky.test.ts b/packages/replay/test/unit/index-noSticky.test.ts deleted file mode 100644 index 56c327871cdc..000000000000 --- a/packages/replay/test/unit/index-noSticky.test.ts +++ /dev/null @@ -1,281 +0,0 @@ -import { getCurrentHub } from '@sentry/core'; -import { Transport } from '@sentry/types'; -import * as SentryUtils from '@sentry/utils'; - -import { DEFAULT_FLUSH_MIN_DELAY, SESSION_IDLE_DURATION, VISIBILITY_CHANGE_TIMEOUT } from '../../src/constants'; -import { addEvent } from '../../src/util/addEvent'; -import { clearSession } from '../utils/clearSession'; -import { ReplayContainer } from './../../src/replay'; -import { BASE_TIMESTAMP, mockRrweb, mockSdk } from './../index'; -import { useFakeTimers } from './../utils/use-fake-timers'; - -useFakeTimers(); - -async function advanceTimers(time: number) { - jest.advanceTimersByTime(time); - await new Promise(process.nextTick); -} - -type MockTransport = jest.MockedFunction; - -describe('Replay (no sticky)', () => { - let replay: ReplayContainer; - let mockTransport: MockTransport; - let domHandler: (args: any) => any; - const { record: mockRecord } = mockRrweb(); - - beforeAll(async () => { - jest.setSystemTime(new Date(BASE_TIMESTAMP)); - jest.spyOn(SentryUtils, 'addInstrumentationHandler').mockImplementation((type, handler: (args: any) => any) => { - if (type === 'dom') { - domHandler = handler; - } - }); - - ({ replay } = await mockSdk({ - replayOptions: { - stickySession: false, - }, - })); - jest.runAllTimers(); - mockTransport = getCurrentHub()?.getClient()?.getTransport()?.send as MockTransport; - }); - - beforeEach(() => { - jest.setSystemTime(new Date(BASE_TIMESTAMP)); - mockRecord.takeFullSnapshot.mockClear(); - mockTransport.mockClear(); - }); - - afterEach(async () => { - jest.runAllTimers(); - await new Promise(process.nextTick); - jest.setSystemTime(new Date(BASE_TIMESTAMP)); - clearSession(replay); - replay.loadSession({ expiry: SESSION_IDLE_DURATION }); - }); - - afterAll(() => { - replay && replay.stop(); - }); - - it('creates a new session and triggers a full dom snapshot when document becomes visible after [VISIBILITY_CHANGE_TIMEOUT]ms', () => { - Object.defineProperty(document, 'visibilityState', { - configurable: true, - get: function () { - return 'visible'; - }, - }); - - const initialSession = replay.session; - - jest.advanceTimersByTime(VISIBILITY_CHANGE_TIMEOUT + 1); - - document.dispatchEvent(new Event('visibilitychange')); - - expect(mockRecord.takeFullSnapshot).toHaveBeenLastCalledWith(true); - - // Should have created a new session - expect(replay).not.toHaveSameSession(initialSession); - }); - - it('does not create a new session if user hides the tab and comes back within [VISIBILITY_CHANGE_TIMEOUT] seconds', () => { - const initialSession = replay.session; - - Object.defineProperty(document, 'visibilityState', { - configurable: true, - get: function () { - return 'hidden'; - }, - }); - document.dispatchEvent(new Event('visibilitychange')); - expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); - expect(replay).toHaveSameSession(initialSession); - - // User comes back before `VISIBILITY_CHANGE_TIMEOUT` elapses - jest.advanceTimersByTime(VISIBILITY_CHANGE_TIMEOUT - 1); - Object.defineProperty(document, 'visibilityState', { - configurable: true, - get: function () { - return 'visible'; - }, - }); - document.dispatchEvent(new Event('visibilitychange')); - - expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); - // Should NOT have created a new session - expect(replay).toHaveSameSession(initialSession); - }); - - it('uploads a replay event when document becomes hidden', async () => { - mockRecord.takeFullSnapshot.mockClear(); - Object.defineProperty(document, 'visibilityState', { - configurable: true, - get: function () { - return 'hidden'; - }, - }); - - // Pretend 5 seconds have passed - const ELAPSED = 5000; - jest.advanceTimersByTime(ELAPSED); - - const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 2 }; - addEvent(replay, TEST_EVENT); - - document.dispatchEvent(new Event('visibilitychange')); - - await new Promise(process.nextTick); - - expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); - - expect(replay).toHaveLastSentReplay({ events: JSON.stringify([TEST_EVENT]) }); - - // Session's last activity is not updated because we do not consider - // visibilitystate as user being active - expect(replay.session?.lastActivity).toBe(BASE_TIMESTAMP); - expect(replay.session?.segmentId).toBe(1); - - // events array should be empty - expect(replay.eventBuffer?.length).toBe(0); - }); - - it('update last activity when user clicks mouse', async () => { - expect(replay.session?.lastActivity).toBe(BASE_TIMESTAMP); - - domHandler({ - name: 'click', - }); - - expect(replay.session?.lastActivity).toBe(BASE_TIMESTAMP); - - // Pretend 5 seconds have passed - const ELAPSED = 5000; - jest.advanceTimersByTime(ELAPSED); - - domHandler({ - name: 'click', - }); - - expect(replay.session?.lastActivity).toBe(BASE_TIMESTAMP + ELAPSED); - }); - - it('uploads a replay event if 5 seconds have elapsed since the last replay event occurred', async () => { - const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 3 }; - mockRecord._emitter(TEST_EVENT); - // Pretend 5 seconds have passed - const ELAPSED = 5000; - await advanceTimers(ELAPSED); - - expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); - - expect(replay).toHaveLastSentReplay({ events: JSON.stringify([TEST_EVENT]) }); - - // No user activity to trigger an update - expect(replay.session?.lastActivity).toBe(BASE_TIMESTAMP); - expect(replay.session?.segmentId).toBe(1); - - // events array should be empty - expect(replay.eventBuffer?.length).toBe(0); - }); - - it('uploads a replay event if 15 seconds have elapsed since the last replay upload', async () => { - const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 3 }; - // Fire a new event every 4 seconds, 4 times - [...Array(4)].forEach(() => { - mockRecord._emitter(TEST_EVENT); - jest.advanceTimersByTime(4000); - }); - - // We are at time = +16seconds now (relative to BASE_TIMESTAMP) - // The next event should cause an upload immediately - mockRecord._emitter(TEST_EVENT); - await new Promise(process.nextTick); - - expect(replay).toHaveLastSentReplay({ - events: JSON.stringify([...Array(5)].map(() => TEST_EVENT)), - }); - - // There should also not be another attempt at an upload 5 seconds after the last replay event - mockTransport.mockClear(); - await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); - expect(replay).not.toHaveLastSentReplay(); - - expect(replay.session?.lastActivity).toBe(BASE_TIMESTAMP); - expect(replay.session?.segmentId).toBe(1); - // events array should be empty - expect(replay.eventBuffer?.length).toBe(0); - - // Let's make sure it continues to work - mockTransport.mockClear(); - mockRecord._emitter(TEST_EVENT); - await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); - expect(replay).toHaveLastSentReplay({ events: JSON.stringify([TEST_EVENT]) }); - }); - - it('creates a new session if user has been idle for more than 15 minutes and comes back to move their mouse', async () => { - const initialSession = replay.session; - - expect(initialSession?.id).toBeDefined(); - - // Idle for 15 minutes - const FIFTEEN_MINUTES = 15 * 60000; - jest.advanceTimersByTime(FIFTEEN_MINUTES); - - // TBD: We are currently deciding that this event will get dropped, but - // this could/should change in the future. - const TEST_EVENT = { - data: { name: 'lost event' }, - timestamp: BASE_TIMESTAMP, - type: 3, - }; - mockRecord._emitter(TEST_EVENT); - expect(replay).not.toHaveLastSentReplay(); - - await new Promise(process.nextTick); - - // Instead of recording the above event, a full snapshot will occur. - // - // TODO: We could potentially figure out a way to save the last session, - // and produce a checkout based on a previous checkout + updates, and then - // replay the event on top. Or maybe replay the event on top of a refresh - // snapshot. - expect(mockRecord.takeFullSnapshot).toHaveBeenCalledWith(true); - - // Should be a new session - expect(replay).not.toHaveSameSession(initialSession); - - // Replay does not send immediately because checkout was due to expired session - expect(replay).not.toHaveLastSentReplay(); - - // Now do a click - domHandler({ - name: 'click', - }); - - await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); - - const newTimestamp = BASE_TIMESTAMP + FIFTEEN_MINUTES; - const breadcrumbTimestamp = newTimestamp + 20; // I don't know where this 20ms comes from - - expect(replay).toHaveLastSentReplay({ - events: JSON.stringify([ - { data: { isCheckout: true }, timestamp: newTimestamp, type: 2 }, - { - type: 5, - timestamp: breadcrumbTimestamp, - data: { - tag: 'breadcrumb', - payload: { - timestamp: breadcrumbTimestamp / 1000, - type: 'default', - category: 'ui.click', - message: '', - data: {}, - }, - }, - }, - ]), - }); - }); -}); diff --git a/packages/replay/test/unit/index.test.ts b/packages/replay/test/unit/index.test.ts deleted file mode 100644 index cdd0567609e8..000000000000 --- a/packages/replay/test/unit/index.test.ts +++ /dev/null @@ -1,951 +0,0 @@ -import { getCurrentHub, Hub } from '@sentry/core'; -import { Event, Scope } from '@sentry/types'; -import { EventType } from 'rrweb'; - -import { - DEFAULT_FLUSH_MIN_DELAY, - MASK_ALL_TEXT_SELECTOR, - MAX_SESSION_LIFE, - REPLAY_SESSION_KEY, - VISIBILITY_CHANGE_TIMEOUT, - WINDOW, -} from '../../src/constants'; -import { ReplayContainer } from '../../src/replay'; -import type { RecordingEvent } from '../../src/types'; -import { addEvent } from '../../src/util/addEvent'; -import { createPerformanceSpans } from '../../src/util/createPerformanceSpans'; -import { clearSession } from '../utils/clearSession'; -import { useFakeTimers } from '../utils/use-fake-timers'; -import { PerformanceEntryResource } from './../fixtures/performanceEntry/resource'; -import { BASE_TIMESTAMP, RecordMock } from './../index'; -import { resetSdkMock } from './../mocks/resetSdkMock'; -import { DomHandler } from './../types'; - -useFakeTimers(); - -async function advanceTimers(time: number) { - jest.advanceTimersByTime(time); - await new Promise(process.nextTick); -} - -describe('Replay with custom mock', () => { - afterEach(() => { - jest.clearAllMocks(); - }); - - it('calls rrweb.record with custom options', async () => { - const { mockRecord } = await resetSdkMock({ - replayOptions: { - ignoreClass: 'sentry-test-ignore', - stickySession: false, - }, - }); - expect(mockRecord.mock.calls[0][0]).toMatchInlineSnapshot(` - Object { - "blockClass": "sentry-block", - "blockSelector": "[data-sentry-block],img,image,svg,path,rect,area,video,object,picture,embed,map,audio", - "emit": [Function], - "ignoreClass": "sentry-test-ignore", - "maskAllInputs": true, - "maskTextClass": "sentry-mask", - "maskTextSelector": "${MASK_ALL_TEXT_SELECTOR}", - } - `); - }); - - describe('auto save session', () => { - test.each([ - ['with stickySession=true', true, 1], - ['with stickySession=false', false, 0], - ])('%s', async (_: string, stickySession: boolean, addSummand: number) => { - let saveSessionSpy; - - jest.mock('../../src/session/saveSession', () => { - saveSessionSpy = jest.fn(); - - return { - saveSession: saveSessionSpy, - }; - }); - - const { replay } = await resetSdkMock({ - replayOptions: { - stickySession, - }, - }); - - // Initially called up to three times: once for start, then once for replay.updateSessionActivity & once for segmentId increase - expect(saveSessionSpy).toHaveBeenCalledTimes(addSummand * 3); - - replay.updateSessionActivity(); - - expect(saveSessionSpy).toHaveBeenCalledTimes(addSummand * 4); - - // In order for runFlush to actually do something, we need to add an event - const event = { - type: EventType.Custom, - data: { - tag: 'test custom', - }, - timestamp: new Date().valueOf(), - } as RecordingEvent; - - addEvent(replay, event); - - await replay.runFlush(); - - expect(saveSessionSpy).toHaveBeenCalledTimes(addSummand * 5); - }); - }); -}); - -describe('Replay', () => { - let replay: ReplayContainer; - let mockRecord: RecordMock; - let mockTransportSend: jest.SpyInstance; - let domHandler: DomHandler; - const prevLocation = WINDOW.location; - - type MockSendReplayRequest = jest.MockedFunction; - let mockSendReplayRequest: MockSendReplayRequest; - - beforeAll(async () => { - jest.setSystemTime(new Date(BASE_TIMESTAMP)); - jest.runAllTimers(); - }); - - beforeEach(async () => { - ({ mockRecord, domHandler, replay } = await resetSdkMock({ - replayOptions: { - stickySession: false, - }, - })); - - mockTransportSend = jest.spyOn(getCurrentHub().getClient()!.getTransport()!, 'send'); - - jest.spyOn(replay, 'flush'); - jest.spyOn(replay, 'runFlush'); - jest.spyOn(replay, 'sendReplayRequest'); - - // Create a new session and clear mocks because a segment (from initial - // checkout) will have already been uploaded by the time the tests run - clearSession(replay); - replay.loadSession({ expiry: 0 }); - mockTransportSend.mockClear(); - mockSendReplayRequest = replay.sendReplayRequest as MockSendReplayRequest; - mockSendReplayRequest.mockClear(); - }); - - afterEach(async () => { - jest.runAllTimers(); - await new Promise(process.nextTick); - Object.defineProperty(WINDOW, 'location', { - value: prevLocation, - writable: true, - }); - clearSession(replay); - jest.clearAllMocks(); - mockSendReplayRequest.mockRestore(); - mockRecord.takeFullSnapshot.mockClear(); - replay.stop(); - }); - - it('should have a session after setup', () => { - expect(replay.session).toMatchObject({ - lastActivity: BASE_TIMESTAMP, - started: BASE_TIMESTAMP, - }); - expect(replay.session?.id).toBeDefined(); - expect(replay.session?.segmentId).toBeDefined(); - }); - - it('clears session', () => { - clearSession(replay); - expect(WINDOW.sessionStorage.getItem(REPLAY_SESSION_KEY)).toBe(null); - expect(replay.session).toBe(undefined); - }); - - it('creates a new session and triggers a full dom snapshot when document becomes visible after [VISIBILITY_CHANGE_TIMEOUT]ms', () => { - Object.defineProperty(document, 'visibilityState', { - configurable: true, - get: function () { - return 'visible'; - }, - }); - - const initialSession = replay.session; - - jest.advanceTimersByTime(VISIBILITY_CHANGE_TIMEOUT + 1); - - document.dispatchEvent(new Event('visibilitychange')); - - expect(mockRecord.takeFullSnapshot).toHaveBeenLastCalledWith(true); - - // Should have created a new session - expect(replay).not.toHaveSameSession(initialSession); - }); - - it('creates a new session and triggers a full dom snapshot when document becomes focused after [VISIBILITY_CHANGE_TIMEOUT]ms', () => { - Object.defineProperty(document, 'visibilityState', { - configurable: true, - get: function () { - return 'visible'; - }, - }); - - const initialSession = replay.session; - - jest.advanceTimersByTime(VISIBILITY_CHANGE_TIMEOUT + 1); - - WINDOW.dispatchEvent(new Event('focus')); - - expect(mockRecord.takeFullSnapshot).toHaveBeenLastCalledWith(true); - - // Should have created a new session - expect(replay).not.toHaveSameSession(initialSession); - }); - - it('does not create a new session if user hides the tab and comes back within [VISIBILITY_CHANGE_TIMEOUT] seconds', () => { - const initialSession = replay.session; - - Object.defineProperty(document, 'visibilityState', { - configurable: true, - get: function () { - return 'hidden'; - }, - }); - document.dispatchEvent(new Event('visibilitychange')); - expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); - expect(replay).toHaveSameSession(initialSession); - - // User comes back before `VISIBILITY_CHANGE_TIMEOUT` elapses - jest.advanceTimersByTime(VISIBILITY_CHANGE_TIMEOUT - 1); - Object.defineProperty(document, 'visibilityState', { - configurable: true, - get: function () { - return 'visible'; - }, - }); - document.dispatchEvent(new Event('visibilitychange')); - - expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); - // Should NOT have created a new session - expect(replay).toHaveSameSession(initialSession); - }); - - it('uploads a replay event when WINDOW is blurred', async () => { - Object.defineProperty(document, 'visibilityState', { - configurable: true, - get: function () { - return 'hidden'; - }, - }); - - // Pretend 5 seconds have passed - const ELAPSED = 5000; - jest.advanceTimersByTime(ELAPSED); - - const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 2 }; - const hiddenBreadcrumb = { - type: 5, - timestamp: +new Date(BASE_TIMESTAMP + ELAPSED) / 1000, - data: { - tag: 'breadcrumb', - payload: { - timestamp: +new Date(BASE_TIMESTAMP + ELAPSED) / 1000, - type: 'default', - category: 'ui.blur', - }, - }, - }; - - addEvent(replay, TEST_EVENT); - WINDOW.dispatchEvent(new Event('blur')); - await new Promise(process.nextTick); - expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); - expect(replay).toHaveLastSentReplay({ - events: JSON.stringify([TEST_EVENT, hiddenBreadcrumb]), - }); - // Session's last activity should not be updated - expect(replay.session?.lastActivity).toBe(BASE_TIMESTAMP); - // events array should be empty - expect(replay.eventBuffer?.length).toBe(0); - }); - - it('uploads a replay event when document becomes hidden', async () => { - Object.defineProperty(document, 'visibilityState', { - configurable: true, - get: function () { - return 'hidden'; - }, - }); - // Pretend 5 seconds have passed - const ELAPSED = 5000; - jest.advanceTimersByTime(ELAPSED); - - const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 2 }; - - addEvent(replay, TEST_EVENT); - document.dispatchEvent(new Event('visibilitychange')); - jest.runAllTimers(); - await new Promise(process.nextTick); - - expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); - expect(replay).toHaveLastSentReplay({ events: JSON.stringify([TEST_EVENT]) }); - - // Session's last activity is not updated because we do not consider - // visibilitystate as user being active - expect(replay.session?.lastActivity).toBe(BASE_TIMESTAMP); - // events array should be empty - expect(replay.eventBuffer?.length).toBe(0); - }); - - it('uploads a replay event if 5 seconds have elapsed since the last replay event occurred', async () => { - const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 3 }; - mockRecord._emitter(TEST_EVENT); - // Pretend 5 seconds have passed - const ELAPSED = 5000; - await advanceTimers(ELAPSED); - - expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); - expect(mockTransportSend).toHaveBeenCalledTimes(1); - expect(replay).toHaveLastSentReplay({ events: JSON.stringify([TEST_EVENT]) }); - - // No user activity to trigger an update - expect(replay.session?.lastActivity).toBe(BASE_TIMESTAMP); - expect(replay.session?.segmentId).toBe(1); - - // events array should be empty - expect(replay.eventBuffer?.length).toBe(0); - }); - - it('uploads a replay event if 15 seconds have elapsed since the last replay upload', async () => { - const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 3 }; - // Fire a new event every 4 seconds, 4 times - [...Array(4)].forEach(() => { - mockRecord._emitter(TEST_EVENT); - jest.advanceTimersByTime(4000); - }); - - // We are at time = +16seconds now (relative to BASE_TIMESTAMP) - // The next event should cause an upload immediately - mockRecord._emitter(TEST_EVENT); - await new Promise(process.nextTick); - - expect(replay).toHaveLastSentReplay({ - events: JSON.stringify([...Array(5)].map(() => TEST_EVENT)), - }); - - // There should also not be another attempt at an upload 5 seconds after the last replay event - mockTransportSend.mockClear(); - await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); - - expect(replay).not.toHaveLastSentReplay(); - - expect(replay.session?.lastActivity).toBe(BASE_TIMESTAMP); - expect(replay.session?.segmentId).toBe(1); - // events array should be empty - expect(replay.eventBuffer?.length).toBe(0); - - // Let's make sure it continues to work - mockTransportSend.mockClear(); - mockRecord._emitter(TEST_EVENT); - await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); - expect(replay).toHaveLastSentReplay({ events: JSON.stringify([TEST_EVENT]) }); - }); - - it('creates a new session if user has been idle for 15 minutes and comes back to click their mouse', async () => { - const initialSession = replay.session; - - expect(initialSession?.id).toBeDefined(); - expect(replay.getContext()).toEqual( - expect.objectContaining({ - initialUrl: 'http://localhost/', - initialTimestamp: BASE_TIMESTAMP, - }), - ); - - const url = 'http://dummy/'; - Object.defineProperty(WINDOW, 'location', { - value: new URL(url), - }); - - // Idle for 15 minutes - const FIFTEEN_MINUTES = 15 * 60000; - jest.advanceTimersByTime(FIFTEEN_MINUTES); - - // TBD: We are currently deciding that this event will get dropped, but - // this could/should change in the future. - const TEST_EVENT = { - data: { name: 'lost event' }, - timestamp: BASE_TIMESTAMP, - type: 3, - }; - mockRecord._emitter(TEST_EVENT); - expect(replay).not.toHaveLastSentReplay(); - - await new Promise(process.nextTick); - - // Instead of recording the above event, a full snapshot will occur. - // - // TODO: We could potentially figure out a way to save the last session, - // and produce a checkout based on a previous checkout + updates, and then - // replay the event on top. Or maybe replay the event on top of a refresh - // snapshot. - expect(mockRecord.takeFullSnapshot).toHaveBeenCalledWith(true); - - expect(replay).not.toHaveLastSentReplay(); - - // Should be a new session - expect(replay).not.toHaveSameSession(initialSession); - - // Now do a click - domHandler({ - name: 'click', - }); - - await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); - - const newTimestamp = BASE_TIMESTAMP + FIFTEEN_MINUTES; - const breadcrumbTimestamp = newTimestamp + 20; // I don't know where this 20ms comes from - - expect(replay).toHaveLastSentReplay({ - recordingPayloadHeader: { segment_id: 0 }, - events: JSON.stringify([ - { data: { isCheckout: true }, timestamp: newTimestamp, type: 2 }, - { - type: 5, - timestamp: breadcrumbTimestamp, - data: { - tag: 'breadcrumb', - payload: { - timestamp: breadcrumbTimestamp / 1000, - type: 'default', - category: 'ui.click', - message: '', - data: {}, - }, - }, - }, - ]), - }); - - // `_context` should be reset when a new session is created - expect(replay.getContext()).toEqual( - expect.objectContaining({ - initialUrl: 'http://dummy/', - initialTimestamp: newTimestamp, - }), - ); - }); - - it('does not record if user has been idle for more than MAX_SESSION_LIFE and only starts a new session after a user action', async () => { - jest.clearAllMocks(); - const initialSession = replay.session; - - expect(initialSession?.id).toBeDefined(); - expect(replay.getContext()).toEqual( - expect.objectContaining({ - initialUrl: 'http://localhost/', - initialTimestamp: BASE_TIMESTAMP, - }), - ); - - const url = 'http://dummy/'; - Object.defineProperty(WINDOW, 'location', { - value: new URL(url), - }); - - // Idle for MAX_SESSION_LIFE - jest.advanceTimersByTime(MAX_SESSION_LIFE); - - // These events will not get flushed and will eventually be dropped because user is idle and session is expired - const TEST_EVENT = { - data: { name: 'lost event' }, - timestamp: MAX_SESSION_LIFE, - type: 3, - }; - mockRecord._emitter(TEST_EVENT); - // performance events can still be collected while recording is stopped - // TODO: we may want to prevent `addEvent` from adding to buffer when user is inactive - replay.addUpdate(() => { - createPerformanceSpans(replay, [ - { - type: 'navigation.navigate', - name: 'foo', - start: BASE_TIMESTAMP + MAX_SESSION_LIFE, - end: BASE_TIMESTAMP + MAX_SESSION_LIFE + 100, - }, - ]); - return true; - }); - - WINDOW.dispatchEvent(new Event('blur')); - await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); - - expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); - expect(replay).not.toHaveLastSentReplay(); - // Should be the same session because user has been idle and no events have caused a new session to be created - expect(replay).toHaveSameSession(initialSession); - - // @ts-ignore private - expect(replay._stopRecording).toBeUndefined(); - - // Now do a click - domHandler({ - name: 'click', - }); - // This should still be thrown away - mockRecord._emitter(TEST_EVENT); - - const NEW_TEST_EVENT = { - data: { name: 'test' }, - timestamp: BASE_TIMESTAMP + MAX_SESSION_LIFE + DEFAULT_FLUSH_MIN_DELAY + 20, - type: 3, - }; - - mockRecord._emitter(NEW_TEST_EVENT); - - // new session is created - jest.runAllTimers(); - await new Promise(process.nextTick); - - expect(replay).not.toHaveSameSession(initialSession); - await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); - - const newTimestamp = BASE_TIMESTAMP + MAX_SESSION_LIFE + DEFAULT_FLUSH_MIN_DELAY + 20; // I don't know where this 20ms comes from - const breadcrumbTimestamp = newTimestamp; - - jest.runAllTimers(); - await new Promise(process.nextTick); - - expect(replay).toHaveLastSentReplay({ - recordingPayloadHeader: { segment_id: 0 }, - events: JSON.stringify([ - { data: { isCheckout: true }, timestamp: newTimestamp, type: 2 }, - { - type: 5, - timestamp: breadcrumbTimestamp, - data: { - tag: 'breadcrumb', - payload: { - timestamp: breadcrumbTimestamp / 1000, - type: 'default', - category: 'ui.click', - message: '', - data: {}, - }, - }, - }, - NEW_TEST_EVENT, - ]), - }); - - // `_context` should be reset when a new session is created - expect(replay.getContext()).toEqual( - expect.objectContaining({ - initialUrl: 'http://dummy/', - initialTimestamp: newTimestamp, - }), - ); - }); - - it('uploads a dom breadcrumb 5 seconds after listener receives an event', async () => { - domHandler({ - name: 'click', - }); - - // Pretend 5 seconds have passed - const ELAPSED = 5000; - await advanceTimers(ELAPSED); - - expect(replay).toHaveLastSentReplay({ - events: JSON.stringify([ - { - type: 5, - timestamp: BASE_TIMESTAMP, - data: { - tag: 'breadcrumb', - payload: { - timestamp: BASE_TIMESTAMP / 1000, - type: 'default', - category: 'ui.click', - message: '', - data: {}, - }, - }, - }, - ]), - }); - - expect(replay.session?.segmentId).toBe(1); - }); - - it('fails to upload data on first two calls and succeeds on the third', async () => { - expect(replay.session?.segmentId).toBe(0); - const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 3 }; - - // Suppress console.errors - const mockConsole = jest.spyOn(console, 'error').mockImplementation(jest.fn()); - - // fail the first and second requests and pass the third one - mockTransportSend.mockImplementationOnce(() => { - throw new Error('Something bad happened'); - }); - mockRecord._emitter(TEST_EVENT); - await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); - - expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); - mockTransportSend.mockImplementationOnce(() => { - throw new Error('Something bad happened'); - }); - await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); - - // next tick should retry and succeed - mockConsole.mockRestore(); - - await advanceTimers(8000); - await advanceTimers(2000); - - expect(replay).toHaveLastSentReplay({ - replayEventPayload: expect.objectContaining({ - error_ids: [], - replay_id: expect.any(String), - replay_start_timestamp: BASE_TIMESTAMP / 1000, - // 20seconds = Add up all of the previous `advanceTimers()` - timestamp: (BASE_TIMESTAMP + 20000) / 1000 + 0.02, - trace_ids: [], - urls: ['http://localhost/'], - }), - recordingPayloadHeader: { segment_id: 0 }, - events: JSON.stringify([TEST_EVENT]), - }); - - mockTransportSend.mockClear(); - // No activity has occurred, session's last activity should remain the same - expect(replay.session?.lastActivity).toBeGreaterThanOrEqual(BASE_TIMESTAMP); - expect(replay.session?.segmentId).toBe(1); - - // next tick should do nothing - await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); - expect(replay).not.toHaveLastSentReplay(); - }); - - it('fails to upload data and hits retry max and stops', async () => { - const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 3 }; - jest.spyOn(replay, 'sendReplay'); - - // Suppress console.errors - const mockConsole = jest.spyOn(console, 'error').mockImplementation(jest.fn()); - - // Check errors - const spyHandleException = jest.spyOn(replay, 'handleException'); - - expect(replay.session?.segmentId).toBe(0); - - // fail the first and second requests and pass the third one - mockSendReplayRequest.mockReset(); - mockSendReplayRequest.mockImplementation(() => { - throw new Error('Something bad happened'); - }); - mockRecord._emitter(TEST_EVENT); - - await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); - - expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); - expect(replay.sendReplayRequest).toHaveBeenCalledTimes(1); - - await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); - expect(replay.sendReplayRequest).toHaveBeenCalledTimes(2); - - await advanceTimers(10000); - expect(replay.sendReplayRequest).toHaveBeenCalledTimes(3); - - await advanceTimers(30000); - expect(replay.sendReplayRequest).toHaveBeenCalledTimes(4); - expect(replay.sendReplay).toHaveBeenCalledTimes(4); - - mockConsole.mockReset(); - - // Make sure it doesn't retry again - jest.runAllTimers(); - expect(replay.sendReplayRequest).toHaveBeenCalledTimes(4); - expect(replay.sendReplay).toHaveBeenCalledTimes(4); - - // Retries = 3 (total tries = 4 including initial attempt) - // + last exception is max retries exceeded - expect(spyHandleException).toHaveBeenCalledTimes(5); - expect(spyHandleException).toHaveBeenLastCalledWith(new Error('Unable to send Replay - max retries exceeded')); - - // No activity has occurred, session's last activity should remain the same - expect(replay.session?.lastActivity).toBe(BASE_TIMESTAMP); - - // segmentId increases despite error - expect(replay.session?.segmentId).toBe(1); - }); - - it('increases segment id after each event', async () => { - Object.defineProperty(document, 'visibilityState', { - configurable: true, - get: function () { - return 'hidden'; - }, - }); - - // Pretend 5 seconds have passed - const ELAPSED = 5000; - await advanceTimers(ELAPSED); - - const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 2 }; - - addEvent(replay, TEST_EVENT); - WINDOW.dispatchEvent(new Event('blur')); - await new Promise(process.nextTick); - expect(replay).toHaveLastSentReplay({ - recordingPayloadHeader: { segment_id: 0 }, - }); - expect(replay.session?.segmentId).toBe(1); - - addEvent(replay, TEST_EVENT); - WINDOW.dispatchEvent(new Event('blur')); - jest.runAllTimers(); - await new Promise(process.nextTick); - expect(replay.session?.segmentId).toBe(2); - expect(replay).toHaveLastSentReplay({ - recordingPayloadHeader: { segment_id: 1 }, - }); - }); - - it('does not create replay event when there are no events to send', async () => { - Object.defineProperty(document, 'visibilityState', { - configurable: true, - get: function () { - return 'hidden'; - }, - }); - - document.dispatchEvent(new Event('visibilitychange')); - await new Promise(process.nextTick); - expect(replay).not.toHaveLastSentReplay(); - - // Pretend 5 seconds have passed - const ELAPSED = 5000; - await advanceTimers(ELAPSED); - - const TEST_EVENT = { - data: {}, - timestamp: BASE_TIMESTAMP + ELAPSED, - type: 2, - }; - - addEvent(replay, TEST_EVENT); - WINDOW.dispatchEvent(new Event('blur')); - await new Promise(process.nextTick); - - expect(replay).toHaveLastSentReplay({ - replayEventPayload: expect.objectContaining({ - replay_start_timestamp: BASE_TIMESTAMP / 1000, - urls: ['http://localhost/'], // this doesn't truly test if we are capturing the right URL as we don't change URLs, but good enough - }), - }); - }); - - it('has correct timestamps when there events earlier than initial timestamp', async function () { - clearSession(replay); - replay.loadSession({ expiry: 0 }); - mockTransportSend.mockClear(); - Object.defineProperty(document, 'visibilityState', { - configurable: true, - get: function () { - return 'hidden'; - }, - }); - - document.dispatchEvent(new Event('visibilitychange')); - await new Promise(process.nextTick); - expect(replay).not.toHaveLastSentReplay(); - - // Pretend 5 seconds have passed - const ELAPSED = 5000; - await advanceTimers(ELAPSED); - - const TEST_EVENT = { - data: {}, - timestamp: BASE_TIMESTAMP + ELAPSED, - type: 2, - }; - - addEvent(replay, TEST_EVENT); - - // Add a fake event that started BEFORE - addEvent(replay, { - data: {}, - timestamp: (BASE_TIMESTAMP - 10000) / 1000, - type: 5, - }); - - WINDOW.dispatchEvent(new Event('blur')); - await new Promise(process.nextTick); - expect(replay).toHaveLastSentReplay({ - replayEventPayload: expect.objectContaining({ - replay_start_timestamp: (BASE_TIMESTAMP - 10000) / 1000, - urls: ['http://localhost/'], // this doesn't truly test if we are capturing the right URL as we don't change URLs, but good enough - replay_type: 'session', - tags: expect.objectContaining({ - errorSampleRate: 0, - sessionSampleRate: 1, - }), - }), - }); - }); - - it('does not have stale `replay_start_timestamp` due to an old time origin', async function () { - const ELAPSED = 86400000 * 2; // 2 days - // Add a mock performance event that happens 2 days ago. This can happen in the browser - // when a tab has sat idle for a long period and user comes back to it. - // - // We pass a negative start time as it's a bit difficult to mock - // `@sentry/utils/browserPerformanceTimeOrigin`. This would not happen in - // real world. - replay.performanceEvents.push( - PerformanceEntryResource({ - startTime: -ELAPSED, - }), - ); - - // This should be null because `addEvent` has not been called yet - expect(replay.getContext().earliestEvent).toBe(null); - expect(mockTransportSend).toHaveBeenCalledTimes(0); - - // A new checkout occurs (i.e. a new session was started) - const TEST_EVENT = { - data: {}, - timestamp: BASE_TIMESTAMP, - type: 2, - }; - - addEvent(replay, TEST_EVENT); - // This event will trigger a flush - WINDOW.dispatchEvent(new Event('blur')); - jest.runAllTimers(); - await new Promise(process.nextTick); - - expect(mockTransportSend).toHaveBeenCalledTimes(1); - expect(replay).toHaveLastSentReplay({ - replayEventPayload: expect.objectContaining({ - // Make sure the old performance event is thrown out - replay_start_timestamp: BASE_TIMESTAMP / 1000, - }), - events: JSON.stringify([ - TEST_EVENT, - { - type: 5, - timestamp: BASE_TIMESTAMP / 1000, - data: { - tag: 'breadcrumb', - payload: { - timestamp: BASE_TIMESTAMP / 1000, - type: 'default', - category: 'ui.blur', - }, - }, - }, - ]), - }); - - // This gets reset after sending replay - expect(replay.getContext().earliestEvent).toBe(null); - }); - - it('has single flush when checkout flush and debounce flush happen near simultaneously', async () => { - // click happens first - domHandler({ - name: 'click', - }); - - // checkout - const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 2 }; - mockRecord._emitter(TEST_EVENT); - - await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); - expect(replay.flush).toHaveBeenCalledTimes(1); - - // Make sure there's nothing queued up after - await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); - expect(replay.flush).toHaveBeenCalledTimes(1); - }); -}); - -describe('eventProcessors', () => { - let hub: Hub; - let scope: Scope; - - beforeEach(() => { - hub = getCurrentHub(); - scope = hub.pushScope(); - }); - - afterEach(() => { - hub.popScope(); - jest.resetAllMocks(); - }); - - it('handles event processors properly', async () => { - const MUTATED_TIMESTAMP = BASE_TIMESTAMP + 3000; - - const { mockRecord } = await resetSdkMock({ - replayOptions: { - stickySession: false, - }, - }); - - const client = hub.getClient()!; - - jest.runAllTimers(); - const mockTransportSend = jest.spyOn(client.getTransport()!, 'send'); - mockTransportSend.mockReset(); - - const handler1 = jest.fn((event: Event): Event | null => { - event.timestamp = MUTATED_TIMESTAMP; - - return event; - }); - - const handler2 = jest.fn((): Event | null => { - return null; - }); - - scope.addEventProcessor(handler1); - - const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 3 }; - - mockRecord._emitter(TEST_EVENT); - jest.runAllTimers(); - jest.advanceTimersByTime(1); - await new Promise(process.nextTick); - - expect(mockTransportSend).toHaveBeenCalledTimes(1); - - scope.addEventProcessor(handler2); - - const TEST_EVENT2 = { data: {}, timestamp: BASE_TIMESTAMP, type: 3 }; - - mockRecord._emitter(TEST_EVENT2); - jest.runAllTimers(); - jest.advanceTimersByTime(1); - await new Promise(process.nextTick); - - expect(mockTransportSend).toHaveBeenCalledTimes(1); - - expect(handler1).toHaveBeenCalledTimes(2); - expect(handler2).toHaveBeenCalledTimes(1); - - // This receives an envelope, which is a deeply nested array - // We only care about the fact that the timestamp was mutated - expect(mockTransportSend).toHaveBeenCalledWith( - expect.arrayContaining([ - expect.arrayContaining([expect.arrayContaining([expect.objectContaining({ timestamp: MUTATED_TIMESTAMP })])]), - ]), - ); - }); -}); diff --git a/packages/replay/test/unit/multiple-instances.test.ts b/packages/replay/test/unit/multiple-instances.test.ts deleted file mode 100644 index 9ae622605590..000000000000 --- a/packages/replay/test/unit/multiple-instances.test.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Replay } from './../../src'; - -it('throws on creating multiple instances', function () { - expect(() => { - new Replay(); - new Replay(); - }).toThrow(); -}); diff --git a/packages/replay/test/unit/multipleInstances.test.ts b/packages/replay/test/unit/multipleInstances.test.ts new file mode 100644 index 000000000000..a271801936ff --- /dev/null +++ b/packages/replay/test/unit/multipleInstances.test.ts @@ -0,0 +1,10 @@ +import { Replay } from '../../src'; + +describe('Unit | multipleInstances', () => { + it('throws on creating multiple instances', function () { + expect(() => { + new Replay(); + new Replay(); + }).toThrow(); + }); +}); diff --git a/packages/replay/test/unit/session/Session.test.ts b/packages/replay/test/unit/session/Session.test.ts deleted file mode 100644 index ca34e72b4172..000000000000 --- a/packages/replay/test/unit/session/Session.test.ts +++ /dev/null @@ -1,71 +0,0 @@ -jest.mock('./../../../src/session/saveSession'); - -jest.mock('@sentry/browser', () => { - const originalModule = jest.requireActual('@sentry/browser'); - - return { - ...originalModule, - getCurrentHub: jest.fn(() => { - return { - captureEvent: jest.fn(), - getClient: jest.fn(() => ({ getDsn: jest.fn() })), - }; - }), - }; -}); - -jest.mock('@sentry/utils', () => { - const originalModule = jest.requireActual('@sentry/utils'); - - return { - ...originalModule, - uuid4: jest.fn(() => 'test_session_id'), - }; -}); - -import * as Sentry from '@sentry/browser'; - -import { WINDOW } from '../../../src/constants'; -import { getSessionSampleType, makeSession } from '../../../src/session/Session'; - -type CaptureEventMockType = jest.MockedFunction; - -beforeEach(() => { - WINDOW.sessionStorage.clear(); -}); - -afterEach(() => { - (Sentry.getCurrentHub().captureEvent as CaptureEventMockType).mockReset(); -}); - -it('does not sample', function () { - const newSession = makeSession({ - sampled: getSessionSampleType(0, 0), - }); - - expect(newSession.sampled).toBe(false); -}); - -it('samples using `sessionSampleRate`', function () { - const newSession = makeSession({ - sampled: getSessionSampleType(1.0, 0), - }); - - expect(newSession.sampled).toBe('session'); -}); - -it('samples using `errorSampleRate`', function () { - const newSession = makeSession({ - sampled: getSessionSampleType(0, 1), - }); - - expect(newSession.sampled).toBe('error'); -}); - -it('does not run sampling function if existing session was sampled', function () { - const newSession = makeSession({ - sampled: 'session', - }); - - expect(newSession.sampled).toBe('session'); -}); diff --git a/packages/replay/test/unit/session/createSession.test.ts b/packages/replay/test/unit/session/createSession.test.ts index f6942f1f6252..afe2ce3df374 100644 --- a/packages/replay/test/unit/session/createSession.test.ts +++ b/packages/replay/test/unit/session/createSession.test.ts @@ -15,53 +15,55 @@ jest.mock('@sentry/utils', () => { type CaptureEventMockType = jest.MockedFunction; -const captureEventMock: CaptureEventMockType = jest.fn(); +describe('Unit | session | createSession', () => { + const captureEventMock: CaptureEventMockType = jest.fn(); -beforeAll(() => { - WINDOW.sessionStorage.clear(); - jest.spyOn(Sentry, 'getCurrentHub'); - (Sentry.getCurrentHub as jest.Mock).mockImplementation(() => ({ - captureEvent: captureEventMock, - })); -}); - -afterEach(() => { - captureEventMock.mockReset(); -}); + beforeAll(() => { + WINDOW.sessionStorage.clear(); + jest.spyOn(Sentry, 'getCurrentHub'); + (Sentry.getCurrentHub as jest.Mock).mockImplementation(() => ({ + captureEvent: captureEventMock, + })); + }); -it('creates a new session with no sticky sessions', function () { - const newSession = createSession({ - stickySession: false, - sessionSampleRate: 1.0, - errorSampleRate: 0, + afterEach(() => { + captureEventMock.mockReset(); }); - expect(captureEventMock).not.toHaveBeenCalled(); - expect(saveSession).not.toHaveBeenCalled(); + it('creates a new session with no sticky sessions', function () { + const newSession = createSession({ + stickySession: false, + sessionSampleRate: 1.0, + errorSampleRate: 0, + }); + expect(captureEventMock).not.toHaveBeenCalled(); - expect(newSession.id).toBe('test_session_id'); - expect(newSession.started).toBeGreaterThan(0); - expect(newSession.lastActivity).toEqual(newSession.started); -}); + expect(saveSession).not.toHaveBeenCalled(); -it('creates a new session with sticky sessions', function () { - const newSession = createSession({ - stickySession: true, - sessionSampleRate: 1.0, - errorSampleRate: 0, + expect(newSession.id).toBe('test_session_id'); + expect(newSession.started).toBeGreaterThan(0); + expect(newSession.lastActivity).toEqual(newSession.started); }); - expect(captureEventMock).not.toHaveBeenCalled(); - expect(saveSession).toHaveBeenCalledWith( - expect.objectContaining({ - id: 'test_session_id', - segmentId: 0, - started: expect.any(Number), - lastActivity: expect.any(Number), - }), - ); + it('creates a new session with sticky sessions', function () { + const newSession = createSession({ + stickySession: true, + sessionSampleRate: 1.0, + errorSampleRate: 0, + }); + expect(captureEventMock).not.toHaveBeenCalled(); + + expect(saveSession).toHaveBeenCalledWith( + expect.objectContaining({ + id: 'test_session_id', + segmentId: 0, + started: expect.any(Number), + lastActivity: expect.any(Number), + }), + ); - expect(newSession.id).toBe('test_session_id'); - expect(newSession.started).toBeGreaterThan(0); - expect(newSession.lastActivity).toEqual(newSession.started); + expect(newSession.id).toBe('test_session_id'); + expect(newSession.started).toBeGreaterThan(0); + expect(newSession.lastActivity).toEqual(newSession.started); + }); }); diff --git a/packages/replay/test/unit/session/fetchSession.test.ts b/packages/replay/test/unit/session/fetchSession.test.ts index 9f7e7694a5e0..526c9c7969d1 100644 --- a/packages/replay/test/unit/session/fetchSession.test.ts +++ b/packages/replay/test/unit/session/fetchSession.test.ts @@ -3,67 +3,69 @@ import { fetchSession } from '../../../src/session/fetchSession'; const oldSessionStorage = WINDOW.sessionStorage; -beforeAll(() => { - WINDOW.sessionStorage.clear(); -}); +describe('Unit | session | fetchSession', () => { + beforeAll(() => { + WINDOW.sessionStorage.clear(); + }); -afterEach(() => { - Object.defineProperty(WINDOW, 'sessionStorage', { - writable: true, - value: oldSessionStorage, + afterEach(() => { + Object.defineProperty(WINDOW, 'sessionStorage', { + writable: true, + value: oldSessionStorage, + }); + WINDOW.sessionStorage.clear(); }); - WINDOW.sessionStorage.clear(); -}); -it('fetches a valid and sampled session', function () { - WINDOW.sessionStorage.setItem( - REPLAY_SESSION_KEY, - '{"id":"fd09adfc4117477abc8de643e5a5798a","sampled": "session","started":1648827162630,"lastActivity":1648827162658}', - ); + it('fetches a valid and sampled session', function () { + WINDOW.sessionStorage.setItem( + REPLAY_SESSION_KEY, + '{"id":"fd09adfc4117477abc8de643e5a5798a","sampled": "session","started":1648827162630,"lastActivity":1648827162658}', + ); - expect(fetchSession()).toEqual({ - id: 'fd09adfc4117477abc8de643e5a5798a', - lastActivity: 1648827162658, - segmentId: 0, - sampled: 'session', - started: 1648827162630, + expect(fetchSession()).toEqual({ + id: 'fd09adfc4117477abc8de643e5a5798a', + lastActivity: 1648827162658, + segmentId: 0, + sampled: 'session', + started: 1648827162630, + }); }); -}); -it('fetches an unsampled session', function () { - WINDOW.sessionStorage.setItem( - REPLAY_SESSION_KEY, - '{"id":"fd09adfc4117477abc8de643e5a5798a","sampled": false,"started":1648827162630,"lastActivity":1648827162658}', - ); + it('fetches an unsampled session', function () { + WINDOW.sessionStorage.setItem( + REPLAY_SESSION_KEY, + '{"id":"fd09adfc4117477abc8de643e5a5798a","sampled": false,"started":1648827162630,"lastActivity":1648827162658}', + ); - expect(fetchSession()).toEqual({ - id: 'fd09adfc4117477abc8de643e5a5798a', - lastActivity: 1648827162658, - segmentId: 0, - sampled: false, - started: 1648827162630, + expect(fetchSession()).toEqual({ + id: 'fd09adfc4117477abc8de643e5a5798a', + lastActivity: 1648827162658, + segmentId: 0, + sampled: false, + started: 1648827162630, + }); }); -}); -it('fetches a session that does not exist', function () { - expect(fetchSession()).toBe(null); -}); + it('fetches a session that does not exist', function () { + expect(fetchSession()).toBe(null); + }); -it('fetches an invalid session', function () { - WINDOW.sessionStorage.setItem(REPLAY_SESSION_KEY, '{"id":"fd09adfc4117477abc8de643e5a5798a",'); + it('fetches an invalid session', function () { + WINDOW.sessionStorage.setItem(REPLAY_SESSION_KEY, '{"id":"fd09adfc4117477abc8de643e5a5798a",'); - expect(fetchSession()).toBe(null); -}); + expect(fetchSession()).toBe(null); + }); -it('safely attempts to fetch session when Session Storage is disabled', function () { - Object.defineProperty(WINDOW, 'sessionStorage', { - writable: true, - value: { - getItem: () => { - throw new Error('No Session Storage for you'); + it('safely attempts to fetch session when Session Storage is disabled', function () { + Object.defineProperty(WINDOW, 'sessionStorage', { + writable: true, + value: { + getItem: () => { + throw new Error('No Session Storage for you'); + }, }, - }, - }); + }); - expect(fetchSession()).toEqual(null); + expect(fetchSession()).toEqual(null); + }); }); diff --git a/packages/replay/test/unit/session/getSession.test.ts b/packages/replay/test/unit/session/getSession.test.ts index fd1606820c99..54c259fb7772 100644 --- a/packages/replay/test/unit/session/getSession.test.ts +++ b/packages/replay/test/unit/session/getSession.test.ts @@ -8,7 +8,7 @@ import { makeSession } from '../../../src/session/Session'; jest.mock('@sentry/utils', () => { return { ...(jest.requireActual('@sentry/utils') as { string: unknown }), - uuid4: jest.fn(() => 'test_session_id'), + uuid4: jest.fn(() => 'test_session_uuid'), }; }); @@ -27,166 +27,168 @@ function createMockSession(when: number = new Date().getTime()) { }); } -beforeAll(() => { - jest.spyOn(CreateSession, 'createSession'); - jest.spyOn(FetchSession, 'fetchSession'); - WINDOW.sessionStorage.clear(); -}); - -afterEach(() => { - WINDOW.sessionStorage.clear(); - (CreateSession.createSession as jest.MockedFunction).mockClear(); - (FetchSession.fetchSession as jest.MockedFunction).mockClear(); -}); - -it('creates a non-sticky session when one does not exist', function () { - const { session } = getSession({ - expiry: 900000, - stickySession: false, - ...SAMPLE_RATES, +describe('Unit | session | getSession', () => { + beforeAll(() => { + jest.spyOn(CreateSession, 'createSession'); + jest.spyOn(FetchSession, 'fetchSession'); + WINDOW.sessionStorage.clear(); }); - expect(FetchSession.fetchSession).not.toHaveBeenCalled(); - expect(CreateSession.createSession).toHaveBeenCalled(); - - expect(session).toEqual({ - id: 'test_session_id', - segmentId: 0, - lastActivity: expect.any(Number), - sampled: 'session', - started: expect.any(Number), + afterEach(() => { + WINDOW.sessionStorage.clear(); + (CreateSession.createSession as jest.MockedFunction).mockClear(); + (FetchSession.fetchSession as jest.MockedFunction).mockClear(); }); - // Should not have anything in storage - expect(FetchSession.fetchSession()).toBe(null); -}); + it('creates a non-sticky session when one does not exist', function () { + const { session } = getSession({ + expiry: 900000, + stickySession: false, + ...SAMPLE_RATES, + }); -it('creates a non-sticky session, regardless of session existing in sessionStorage', function () { - saveSession(createMockSession(new Date().getTime() - 10000)); - - const { session } = getSession({ - expiry: 1000, - stickySession: false, - ...SAMPLE_RATES, - }); + expect(FetchSession.fetchSession).not.toHaveBeenCalled(); + expect(CreateSession.createSession).toHaveBeenCalled(); - expect(FetchSession.fetchSession).not.toHaveBeenCalled(); - expect(CreateSession.createSession).toHaveBeenCalled(); - - expect(session).toBeDefined(); -}); - -it('creates a non-sticky session, when one is expired', function () { - const { session } = getSession({ - expiry: 1000, - stickySession: false, - ...SAMPLE_RATES, - currentSession: makeSession({ - id: 'old_session_id', - lastActivity: new Date().getTime() - 1001, - started: new Date().getTime() - 1001, + expect(session).toEqual({ + id: 'test_session_uuid', segmentId: 0, + lastActivity: expect.any(Number), sampled: 'session', - }), + started: expect.any(Number), + }); + + // Should not have anything in storage + expect(FetchSession.fetchSession()).toBe(null); }); - expect(FetchSession.fetchSession).not.toHaveBeenCalled(); - expect(CreateSession.createSession).toHaveBeenCalled(); + it('creates a non-sticky session, regardless of session existing in sessionStorage', function () { + saveSession(createMockSession(new Date().getTime() - 10000)); - expect(session).toBeDefined(); - expect(session.id).not.toBe('old_session_id'); -}); + const { session } = getSession({ + expiry: 1000, + stickySession: false, + ...SAMPLE_RATES, + }); -it('creates a sticky session when one does not exist', function () { - expect(FetchSession.fetchSession()).toBe(null); + expect(FetchSession.fetchSession).not.toHaveBeenCalled(); + expect(CreateSession.createSession).toHaveBeenCalled(); - const { session } = getSession({ - expiry: 900000, - stickySession: true, - sessionSampleRate: 1.0, - errorSampleRate: 0.0, + expect(session).toBeDefined(); }); - expect(FetchSession.fetchSession).toHaveBeenCalled(); - expect(CreateSession.createSession).toHaveBeenCalled(); - - expect(session).toEqual({ - id: 'test_session_id', - segmentId: 0, - lastActivity: expect.any(Number), - sampled: 'session', - started: expect.any(Number), + it('creates a non-sticky session, when one is expired', function () { + const { session } = getSession({ + expiry: 1000, + stickySession: false, + ...SAMPLE_RATES, + currentSession: makeSession({ + id: 'old_session_id', + lastActivity: new Date().getTime() - 1001, + started: new Date().getTime() - 1001, + segmentId: 0, + sampled: 'session', + }), + }); + + expect(FetchSession.fetchSession).not.toHaveBeenCalled(); + expect(CreateSession.createSession).toHaveBeenCalled(); + + expect(session).toBeDefined(); + expect(session.id).not.toBe('old_session_id'); }); - // Should not have anything in storage - expect(FetchSession.fetchSession()).toEqual({ - id: 'test_session_id', - segmentId: 0, - lastActivity: expect.any(Number), - sampled: 'session', - started: expect.any(Number), - }); -}); + it('creates a sticky session when one does not exist', function () { + expect(FetchSession.fetchSession()).toBe(null); -it('fetches an existing sticky session', function () { - const now = new Date().getTime(); - saveSession(createMockSession(now)); + const { session } = getSession({ + expiry: 900000, + stickySession: true, + sessionSampleRate: 1.0, + errorSampleRate: 0.0, + }); - const { session } = getSession({ - expiry: 1000, - stickySession: true, - sessionSampleRate: 1.0, - errorSampleRate: 0.0, - }); + expect(FetchSession.fetchSession).toHaveBeenCalled(); + expect(CreateSession.createSession).toHaveBeenCalled(); - expect(FetchSession.fetchSession).toHaveBeenCalled(); - expect(CreateSession.createSession).not.toHaveBeenCalled(); + expect(session).toEqual({ + id: 'test_session_uuid', + segmentId: 0, + lastActivity: expect.any(Number), + sampled: 'session', + started: expect.any(Number), + }); - expect(session).toEqual({ - id: 'test_session_id', - segmentId: 0, - lastActivity: now, - sampled: 'session', - started: now, + // Should not have anything in storage + expect(FetchSession.fetchSession()).toEqual({ + id: 'test_session_uuid', + segmentId: 0, + lastActivity: expect.any(Number), + sampled: 'session', + started: expect.any(Number), + }); }); -}); -it('fetches an expired sticky session', function () { - const now = new Date().getTime(); - saveSession(createMockSession(new Date().getTime() - 2000)); + it('fetches an existing sticky session', function () { + const now = new Date().getTime(); + saveSession(createMockSession(now)); - const { session } = getSession({ - expiry: 1000, - stickySession: true, - ...SAMPLE_RATES, - }); + const { session } = getSession({ + expiry: 1000, + stickySession: true, + sessionSampleRate: 1.0, + errorSampleRate: 0.0, + }); - expect(FetchSession.fetchSession).toHaveBeenCalled(); - expect(CreateSession.createSession).toHaveBeenCalled(); - - expect(session.id).toBe('test_session_id'); - expect(session.lastActivity).toBeGreaterThanOrEqual(now); - expect(session.started).toBeGreaterThanOrEqual(now); - expect(session.segmentId).toBe(0); -}); + expect(FetchSession.fetchSession).toHaveBeenCalled(); + expect(CreateSession.createSession).not.toHaveBeenCalled(); -it('fetches a non-expired non-sticky session', function () { - const { session } = getSession({ - expiry: 1000, - stickySession: false, - ...SAMPLE_RATES, - currentSession: makeSession({ - id: 'test_session_id_2', - lastActivity: +new Date() - 500, - started: +new Date() - 500, + expect(session).toEqual({ + id: 'test_session_id', segmentId: 0, + lastActivity: now, sampled: 'session', - }), + started: now, + }); }); - expect(FetchSession.fetchSession).not.toHaveBeenCalled(); - expect(CreateSession.createSession).not.toHaveBeenCalled(); + it('fetches an expired sticky session', function () { + const now = new Date().getTime(); + saveSession(createMockSession(new Date().getTime() - 2000)); - expect(session.id).toBe('test_session_id_2'); - expect(session.segmentId).toBe(0); + const { session } = getSession({ + expiry: 1000, + stickySession: true, + ...SAMPLE_RATES, + }); + + expect(FetchSession.fetchSession).toHaveBeenCalled(); + expect(CreateSession.createSession).toHaveBeenCalled(); + + expect(session.id).toBe('test_session_uuid'); + expect(session.lastActivity).toBeGreaterThanOrEqual(now); + expect(session.started).toBeGreaterThanOrEqual(now); + expect(session.segmentId).toBe(0); + }); + + it('fetches a non-expired non-sticky session', function () { + const { session } = getSession({ + expiry: 1000, + stickySession: false, + ...SAMPLE_RATES, + currentSession: makeSession({ + id: 'test_session_uuid_2', + lastActivity: +new Date() - 500, + started: +new Date() - 500, + segmentId: 0, + sampled: 'session', + }), + }); + + expect(FetchSession.fetchSession).not.toHaveBeenCalled(); + expect(CreateSession.createSession).not.toHaveBeenCalled(); + + expect(session.id).toBe('test_session_uuid_2'); + expect(session.segmentId).toBe(0); + }); }); diff --git a/packages/replay/test/unit/session/saveSession.test.ts b/packages/replay/test/unit/session/saveSession.test.ts index c1a884bca84f..ff8b8e15f204 100644 --- a/packages/replay/test/unit/session/saveSession.test.ts +++ b/packages/replay/test/unit/session/saveSession.test.ts @@ -2,23 +2,25 @@ import { REPLAY_SESSION_KEY, WINDOW } from '../../../src/constants'; import { saveSession } from '../../../src/session/saveSession'; import { makeSession } from '../../../src/session/Session'; -beforeAll(() => { - WINDOW.sessionStorage.clear(); -}); - -afterEach(() => { - WINDOW.sessionStorage.clear(); -}); +describe('Unit | session | saveSession', () => { + beforeAll(() => { + WINDOW.sessionStorage.clear(); + }); -it('saves a valid session', function () { - const session = makeSession({ - id: 'fd09adfc4117477abc8de643e5a5798a', - segmentId: 0, - started: 1648827162630, - lastActivity: 1648827162658, - sampled: 'session', + afterEach(() => { + WINDOW.sessionStorage.clear(); }); - saveSession(session); - expect(WINDOW.sessionStorage.getItem(REPLAY_SESSION_KEY)).toEqual(JSON.stringify(session)); + it('saves a valid session', function () { + const session = makeSession({ + id: 'fd09adfc4117477abc8de643e5a5798a', + segmentId: 0, + started: 1648827162630, + lastActivity: 1648827162658, + sampled: 'session', + }); + saveSession(session); + + expect(WINDOW.sessionStorage.getItem(REPLAY_SESSION_KEY)).toEqual(JSON.stringify(session)); + }); }); diff --git a/packages/replay/test/unit/session/sessionSampling.test.ts b/packages/replay/test/unit/session/sessionSampling.test.ts new file mode 100644 index 000000000000..cb730006d572 --- /dev/null +++ b/packages/replay/test/unit/session/sessionSampling.test.ts @@ -0,0 +1,35 @@ +import { getSessionSampleType, makeSession } from '../../../src/session/Session'; + +describe('Unit | session | sessionSampling', () => { + it('does not sample', function () { + const newSession = makeSession({ + sampled: getSessionSampleType(0, 0), + }); + + expect(newSession.sampled).toBe(false); + }); + + it('samples using `sessionSampleRate`', function () { + const newSession = makeSession({ + sampled: getSessionSampleType(1.0, 0), + }); + + expect(newSession.sampled).toBe('session'); + }); + + it('samples using `errorSampleRate`', function () { + const newSession = makeSession({ + sampled: getSessionSampleType(0, 1), + }); + + expect(newSession.sampled).toBe('error'); + }); + + it('does not run sampling function if existing session was sampled', function () { + const newSession = makeSession({ + sampled: 'session', + }); + + expect(newSession.sampled).toBe('session'); + }); +}); diff --git a/packages/replay/test/unit/util/createPerformanceEntry.test.ts b/packages/replay/test/unit/util/createPerformanceEntry.test.ts new file mode 100644 index 000000000000..cd250f7db64c --- /dev/null +++ b/packages/replay/test/unit/util/createPerformanceEntry.test.ts @@ -0,0 +1,34 @@ +import { createPerformanceEntries } from '../../../src/util/createPerformanceEntries'; + +describe('Unit | util | createPerformanceEntries', () => { + it('ignores sdks own requests', function () { + const data = { + name: 'https://ingest.f00.f00/api/1/envelope/?sentry_key=dsn&sentry_version=7', + entryType: 'resource', + startTime: 234462.69999998808, + duration: 55.70000001788139, + initiatorType: 'fetch', + nextHopProtocol: '', + workerStart: 0, + redirectStart: 0, + redirectEnd: 0, + fetchStart: 234462.69999998808, + domainLookupStart: 0, + domainLookupEnd: 0, + connectStart: 0, + connectEnd: 0, + secureConnectionStart: 0, + requestStart: 0, + responseStart: 0, + responseEnd: 234518.40000000596, + transferSize: 0, + encodedBodySize: 0, + decodedBodySize: 0, + serverTiming: [], + workerTiming: [], + } as const; + + // @ts-ignore Needs a PerformanceEntry mock + expect(createPerformanceEntries([data])).toEqual([]); + }); +}); diff --git a/packages/replay/test/unit/util/createReplayEnvelope.test.ts b/packages/replay/test/unit/util/createReplayEnvelope.test.ts index 1e5ec5334784..d5c5f2e5b75f 100644 --- a/packages/replay/test/unit/util/createReplayEnvelope.test.ts +++ b/packages/replay/test/unit/util/createReplayEnvelope.test.ts @@ -3,7 +3,7 @@ import { makeDsn } from '@sentry/utils'; import { createReplayEnvelope } from '../../../src/util/createReplayEnvelope'; -describe('createReplayEnvelope', () => { +describe('Unit | util | createReplayEnvelope', () => { const REPLAY_ID = 'MY_REPLAY_ID'; const replayEvent: ReplayEvent = { diff --git a/packages/replay/test/unit/util/debounce.test.ts b/packages/replay/test/unit/util/debounce.test.ts index 27f95c96a4c6..b75adc6bf3bd 100644 --- a/packages/replay/test/unit/util/debounce.test.ts +++ b/packages/replay/test/unit/util/debounce.test.ts @@ -1,6 +1,6 @@ import { debounce } from '../../../src/util/debounce'; -describe('debounce', () => { +describe('Unit | util | debounce', () => { jest.useFakeTimers(); it('delay the execution of the passed callback function by the passed minDelay', () => { const callback = jest.fn(); diff --git a/packages/replay/test/unit/util/dedupePerformanceEntries.test.ts b/packages/replay/test/unit/util/dedupePerformanceEntries.test.ts index 15e66421829b..08884a19407a 100644 --- a/packages/replay/test/unit/util/dedupePerformanceEntries.test.ts +++ b/packages/replay/test/unit/util/dedupePerformanceEntries.test.ts @@ -1,66 +1,67 @@ -// eslint-disable-next-line import/no-unresolved import { dedupePerformanceEntries } from '../../../src/util/dedupePerformanceEntries'; import { PerformanceEntryLcp } from './../../fixtures/performanceEntry/lcp'; import { PerformanceEntryNavigation } from './../../fixtures/performanceEntry/navigation'; import { PerformanceEntryResource } from './../../fixtures/performanceEntry/resource'; -it('does nothing with a single entry', function () { - const entries = [PerformanceEntryNavigation()]; - expect(dedupePerformanceEntries([], entries)).toEqual(entries); -}); +describe('Unit | util | dedupePerformanceEntries', () => { + it('does nothing with a single entry', function () { + const entries = [PerformanceEntryNavigation()]; + expect(dedupePerformanceEntries([], entries)).toEqual(entries); + }); -it('dedupes 2 duplicate entries correctly', function () { - const entries = [PerformanceEntryNavigation(), PerformanceEntryNavigation()]; - expect(dedupePerformanceEntries([], entries)).toEqual([entries[0]]); -}); + it('dedupes 2 duplicate entries correctly', function () { + const entries = [PerformanceEntryNavigation(), PerformanceEntryNavigation()]; + expect(dedupePerformanceEntries([], entries)).toEqual([entries[0]]); + }); -it('dedupes multiple+mixed entries from new list', function () { - const a = PerformanceEntryNavigation({ startTime: 0 }); - const b = PerformanceEntryNavigation({ - startTime: 1, - name: 'https://foo.bar/', + it('dedupes multiple+mixed entries from new list', function () { + const a = PerformanceEntryNavigation({ startTime: 0 }); + const b = PerformanceEntryNavigation({ + startTime: 1, + name: 'https://foo.bar/', + }); + const c = PerformanceEntryNavigation({ startTime: 2, type: 'reload' }); + const d = PerformanceEntryResource({ startTime: 1.5 }); + const entries = [a, a, b, d, b, c]; + expect(dedupePerformanceEntries([], entries)).toEqual([a, b, d, c]); }); - const c = PerformanceEntryNavigation({ startTime: 2, type: 'reload' }); - const d = PerformanceEntryResource({ startTime: 1.5 }); - const entries = [a, a, b, d, b, c]; - expect(dedupePerformanceEntries([], entries)).toEqual([a, b, d, c]); -}); -it('dedupes from initial list and new list', function () { - const a = PerformanceEntryNavigation({ startTime: 0 }); - const b = PerformanceEntryNavigation({ - startTime: 1, - name: 'https://foo.bar/', + it('dedupes from initial list and new list', function () { + const a = PerformanceEntryNavigation({ startTime: 0 }); + const b = PerformanceEntryNavigation({ + startTime: 1, + name: 'https://foo.bar/', + }); + const c = PerformanceEntryNavigation({ startTime: 2, type: 'reload' }); + const d = PerformanceEntryNavigation({ startTime: 1000 }); + const entries = [a, a, b, b, c]; + expect(dedupePerformanceEntries([a, d], entries)).toEqual([a, b, c, d]); }); - const c = PerformanceEntryNavigation({ startTime: 2, type: 'reload' }); - const d = PerformanceEntryNavigation({ startTime: 1000 }); - const entries = [a, a, b, b, c]; - expect(dedupePerformanceEntries([a, d], entries)).toEqual([a, b, c, d]); -}); -it('selects the latest lcp value given multiple lcps in new list', function () { - const a = PerformanceEntryResource({ startTime: 0 }); - const b = PerformanceEntryLcp({ startTime: 100, name: 'b' }); - const c = PerformanceEntryLcp({ startTime: 200, name: 'c' }); - const d = PerformanceEntryLcp({ startTime: 5, name: 'd' }); // don't assume they are ordered - const entries = [a, b, c, d]; - expect(dedupePerformanceEntries([], entries)).toEqual([a, c]); -}); + it('selects the latest lcp value given multiple lcps in new list', function () { + const a = PerformanceEntryResource({ startTime: 0 }); + const b = PerformanceEntryLcp({ startTime: 100, name: 'b' }); + const c = PerformanceEntryLcp({ startTime: 200, name: 'c' }); + const d = PerformanceEntryLcp({ startTime: 5, name: 'd' }); // don't assume they are ordered + const entries = [a, b, c, d]; + expect(dedupePerformanceEntries([], entries)).toEqual([a, c]); + }); -it('selects the latest lcp value from new list, given multiple lcps in new list with an existing lcp', function () { - const a = PerformanceEntryResource({ startTime: 0 }); - const b = PerformanceEntryLcp({ startTime: 100, name: 'b' }); - const c = PerformanceEntryLcp({ startTime: 200, name: 'c' }); - const d = PerformanceEntryLcp({ startTime: 5, name: 'd' }); // don't assume they are ordered - const entries = [b, c, d]; - expect(dedupePerformanceEntries([a, d], entries)).toEqual([a, c]); -}); + it('selects the latest lcp value from new list, given multiple lcps in new list with an existing lcp', function () { + const a = PerformanceEntryResource({ startTime: 0 }); + const b = PerformanceEntryLcp({ startTime: 100, name: 'b' }); + const c = PerformanceEntryLcp({ startTime: 200, name: 'c' }); + const d = PerformanceEntryLcp({ startTime: 5, name: 'd' }); // don't assume they are ordered + const entries = [b, c, d]; + expect(dedupePerformanceEntries([a, d], entries)).toEqual([a, c]); + }); -it('selects the existing lcp value given multiple lcps in new list with an existing lcp having the latest startTime', function () { - const a = PerformanceEntryResource({ startTime: 0 }); - const b = PerformanceEntryLcp({ startTime: 100, name: 'b' }); - const c = PerformanceEntryLcp({ startTime: 200, name: 'c' }); - const d = PerformanceEntryLcp({ startTime: 5, name: 'd' }); // don't assume they are ordered - const entries = [b, d]; - expect(dedupePerformanceEntries([a, c], entries)).toEqual([a, c]); + it('selects the existing lcp value given multiple lcps in new list with an existing lcp having the latest startTime', function () { + const a = PerformanceEntryResource({ startTime: 0 }); + const b = PerformanceEntryLcp({ startTime: 100, name: 'b' }); + const c = PerformanceEntryLcp({ startTime: 200, name: 'c' }); + const d = PerformanceEntryLcp({ startTime: 5, name: 'd' }); // don't assume they are ordered + const entries = [b, d]; + expect(dedupePerformanceEntries([a, c], entries)).toEqual([a, c]); + }); }); diff --git a/packages/replay/test/unit/util/getReplayEvent.test.ts b/packages/replay/test/unit/util/getReplayEvent.test.ts new file mode 100644 index 000000000000..f9f6dcdfc79e --- /dev/null +++ b/packages/replay/test/unit/util/getReplayEvent.test.ts @@ -0,0 +1,62 @@ +import { BrowserClient } from '@sentry/browser'; +import { getCurrentHub, Hub, Scope } from '@sentry/core'; +import { Client, ReplayEvent } from '@sentry/types'; + +import { REPLAY_EVENT_NAME } from '../../../src/constants'; +import { prepareReplayEvent } from '../../../src/util/prepareReplayEvent'; +import { getDefaultBrowserClientOptions } from '../../utils/getDefaultBrowserClientOptions'; + +describe('Unit | util | getReplayEvent', () => { + let hub: Hub; + let client: Client; + let scope: Scope; + + beforeEach(() => { + hub = getCurrentHub(); + client = new BrowserClient(getDefaultBrowserClientOptions()); + hub.bindClient(client); + + client = hub.getClient()!; + scope = hub.getScope()!; + }); + + it('works', async () => { + expect(client).toBeDefined(); + expect(scope).toBeDefined(); + + const replayId = 'replay-ID'; + const event: ReplayEvent = { + // @ts-ignore private api + type: REPLAY_EVENT_NAME, + timestamp: 1670837008.634, + error_ids: ['error-ID'], + trace_ids: ['trace-ID'], + urls: ['https://sentry.io/'], + replay_id: replayId, + replay_type: 'session', + segment_id: 3, + }; + + const replayEvent = await prepareReplayEvent({ scope, client, replayId, event }); + + expect(replayEvent).toEqual({ + type: 'replay_event', + timestamp: 1670837008.634, + error_ids: ['error-ID'], + trace_ids: ['trace-ID'], + urls: ['https://sentry.io/'], + replay_id: 'replay-ID', + replay_type: 'session', + segment_id: 3, + platform: 'javascript', + event_id: 'replay-ID', + environment: 'production', + sdk: { + name: 'sentry.javascript.browser', + version: 'version:Test', + }, + sdkProcessingMetadata: {}, + breadcrumbs: undefined, + }); + }); +}); diff --git a/packages/replay/test/unit/util/isExpired.test.ts b/packages/replay/test/unit/util/isExpired.test.ts index a83677cf856e..2914cf1198d1 100644 --- a/packages/replay/test/unit/util/isExpired.test.ts +++ b/packages/replay/test/unit/util/isExpired.test.ts @@ -1,23 +1,25 @@ import { isExpired } from '../../../src/util/isExpired'; -it('is expired', function () { - expect(isExpired(0, 150, 200)).toBe(true); // expired at ts = 150 -}); +describe('Unit | util | isExpired', () => { + it('is expired', function () { + expect(isExpired(0, 150, 200)).toBe(true); // expired at ts = 150 + }); -it('is not expired', function () { - expect(isExpired(100, 150, 200)).toBe(false); // expires at ts >= 250 -}); + it('is not expired', function () { + expect(isExpired(100, 150, 200)).toBe(false); // expires at ts >= 250 + }); -it('is expired when target time reaches exactly the expiry time', function () { - expect(isExpired(100, 150, 250)).toBe(true); // expires at ts >= 250 -}); + it('is expired when target time reaches exactly the expiry time', function () { + expect(isExpired(100, 150, 250)).toBe(true); // expires at ts >= 250 + }); -it('never expires if expiry is 0', function () { - expect(isExpired(300, 0, 200)).toBe(false); - expect(isExpired(0, 0, 200)).toBe(false); -}); + it('never expires if expiry is 0', function () { + expect(isExpired(300, 0, 200)).toBe(false); + expect(isExpired(0, 0, 200)).toBe(false); + }); -it('always expires if expiry is < 0', function () { - expect(isExpired(300, -1, 200)).toBe(true); - expect(isExpired(0, -1, 200)).toBe(true); + it('always expires if expiry is < 0', function () { + expect(isExpired(300, -1, 200)).toBe(true); + expect(isExpired(0, -1, 200)).toBe(true); + }); }); diff --git a/packages/replay/test/unit/util/isSampled.test.ts b/packages/replay/test/unit/util/isSampled.test.ts index 6b26f1d11046..97a824cb0e9d 100644 --- a/packages/replay/test/unit/util/isSampled.test.ts +++ b/packages/replay/test/unit/util/isSampled.test.ts @@ -13,10 +13,9 @@ const cases: [number, number, boolean][] = [ [0.5, 0.0, true], ]; -jest.spyOn(Math, 'random'); -const mockRandom = Math.random as jest.MockedFunction; +describe('Unit | util | isSampled', () => { + const mockRandom = jest.spyOn(Math, 'random'); -describe('isSampled', () => { test.each(cases)( 'given sample rate of %p and RNG returns %p, result should be %p', (sampleRate: number, mockRandomValue: number, expectedResult: boolean) => { diff --git a/packages/replay/test/unit/util/isSessionExpired.test.ts b/packages/replay/test/unit/util/isSessionExpired.test.ts index 0db371e8e4ef..381b8ffe6428 100644 --- a/packages/replay/test/unit/util/isSessionExpired.test.ts +++ b/packages/replay/test/unit/util/isSessionExpired.test.ts @@ -12,18 +12,20 @@ function createSession(extra?: Record) { }); } -it('session last activity is older than expiry time', function () { - expect(isSessionExpired(createSession(), 100, 200)).toBe(true); // Session expired at ts = 100 -}); +describe('Unit | util | isSessionExpired', () => { + it('session last activity is older than expiry time', function () { + expect(isSessionExpired(createSession(), 100, 200)).toBe(true); // Session expired at ts = 100 + }); -it('session last activity is not older than expiry time', function () { - expect(isSessionExpired(createSession({ lastActivity: 100 }), 150, 200)).toBe(false); // Session expires at ts >= 250 -}); + it('session last activity is not older than expiry time', function () { + expect(isSessionExpired(createSession({ lastActivity: 100 }), 150, 200)).toBe(false); // Session expires at ts >= 250 + }); -it('session age is not older than max session life', function () { - expect(isSessionExpired(createSession(), 1_800_000, 50_000)).toBe(false); -}); + it('session age is not older than max session life', function () { + expect(isSessionExpired(createSession(), 1_800_000, 50_000)).toBe(false); + }); -it('session age is older than max session life', function () { - expect(isSessionExpired(createSession(), 1_800_000, 1_800_001)).toBe(true); // Session expires at ts >= 1_800_000 + it('session age is older than max session life', function () { + expect(isSessionExpired(createSession(), 1_800_000, 1_800_001)).toBe(true); // Session expires at ts >= 1_800_000 + }); }); diff --git a/packages/replay/test/unit/util/prepareReplayEvent.test.ts b/packages/replay/test/unit/util/prepareReplayEvent.test.ts index 4c7785bf3735..1213548cddb2 100644 --- a/packages/replay/test/unit/util/prepareReplayEvent.test.ts +++ b/packages/replay/test/unit/util/prepareReplayEvent.test.ts @@ -6,7 +6,7 @@ import { REPLAY_EVENT_NAME } from '../../../src/constants'; import { prepareReplayEvent } from '../../../src/util/prepareReplayEvent'; import { getDefaultBrowserClientOptions } from '../../utils/getDefaultBrowserClientOptions'; -describe('prepareReplayEvent', () => { +describe('Unit | util | prepareReplayEvent', () => { let hub: Hub; let client: Client; let scope: Scope; diff --git a/packages/replay/test/unit/worker/Compressor.test.ts b/packages/replay/test/unit/worker/Compressor.test.ts index 77b95a07439b..afafa52da8cb 100644 --- a/packages/replay/test/unit/worker/Compressor.test.ts +++ b/packages/replay/test/unit/worker/Compressor.test.ts @@ -2,7 +2,7 @@ import pako from 'pako'; import { Compressor } from '../../../worker/src/Compressor'; -describe('Compressor', () => { +describe('Unit | worker | Compressor', () => { it('compresses multiple events', () => { const compressor = new Compressor(); From 6fe893d71cc244344da3c6c4096a50378cbfbda3 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Wed, 11 Jan 2023 14:50:25 +0100 Subject: [PATCH 038/113] fix(otel): Prevent baggage from being overwritten (#6709) --- packages/opentelemetry-node/src/propagator.ts | 25 +++++++++++----- .../test/propagator.test.ts | 30 ++++++++++++++++++- 2 files changed, 46 insertions(+), 9 deletions(-) diff --git a/packages/opentelemetry-node/src/propagator.ts b/packages/opentelemetry-node/src/propagator.ts index 4e0df90f912a..039dcc04a30d 100644 --- a/packages/opentelemetry-node/src/propagator.ts +++ b/packages/opentelemetry-node/src/propagator.ts @@ -1,17 +1,18 @@ import { + Baggage, Context, isSpanContextValid, + propagation, TextMapGetter, - TextMapPropagator, TextMapSetter, trace, TraceFlags, } from '@opentelemetry/api'; -import { isTracingSuppressed } from '@opentelemetry/core'; +import { isTracingSuppressed, W3CBaggagePropagator } from '@opentelemetry/core'; import { baggageHeaderToDynamicSamplingContext, - dynamicSamplingContextToSentryBaggageHeader, extractTraceparentData, + SENTRY_BAGGAGE_KEY_PREFIX, } from '@sentry/utils'; import { @@ -25,7 +26,7 @@ import { SENTRY_SPAN_PROCESSOR_MAP } from './spanprocessor'; /** * Injects and extracts `sentry-trace` and `baggage` headers from carriers. */ -export class SentryPropagator implements TextMapPropagator { +export class SentryPropagator extends W3CBaggagePropagator { /** * @inheritDoc */ @@ -41,10 +42,18 @@ export class SentryPropagator implements TextMapPropagator { if (span.transaction) { const dynamicSamplingContext = span.transaction.getDynamicSamplingContext(); - const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); - if (sentryBaggageHeader) { - setter.set(carrier, SENTRY_BAGGAGE_HEADER, sentryBaggageHeader); - } + + const baggage = propagation.getBaggage(context) || propagation.createBaggage({}); + const baggageWithSentryInfo = Object.entries(dynamicSamplingContext).reduce( + (b, [dscKey, dscValue]) => { + if (dscValue) { + return b.setEntry(`${SENTRY_BAGGAGE_KEY_PREFIX}${dscKey}`, { value: dscValue }); + } + return b; + }, + baggage, + ); + super.inject(propagation.setBaggage(context, baggageWithSentryInfo), carrier, setter); } } } diff --git a/packages/opentelemetry-node/test/propagator.test.ts b/packages/opentelemetry-node/test/propagator.test.ts index 49e2243fe8ee..ec7a047d7735 100644 --- a/packages/opentelemetry-node/test/propagator.test.ts +++ b/packages/opentelemetry-node/test/propagator.test.ts @@ -1,4 +1,11 @@ -import { defaultTextMapGetter, defaultTextMapSetter, ROOT_CONTEXT, trace, TraceFlags } from '@opentelemetry/api'; +import { + defaultTextMapGetter, + defaultTextMapSetter, + propagation, + ROOT_CONTEXT, + trace, + TraceFlags, +} from '@opentelemetry/api'; import { suppressTracing } from '@opentelemetry/core'; import { Hub, makeMain } from '@sentry/core'; import { addExtensionMethods, Transaction } from '@sentry/tracing'; @@ -138,6 +145,27 @@ describe('SentryPropagator', () => { expect(carrier[SENTRY_TRACE_HEADER]).toBe(sentryTrace); }); + it('should include existing baggage', () => { + const transactionContext = { + name: 'sampled-transaction', + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: '6e0c63257de34c92', + sampled: true, + }; + const spanContext = { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: '6e0c63257de34c92', + traceFlags: TraceFlags.SAMPLED, + }; + createTransactionAndMaybeSpan(type, transactionContext); + 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,sentry-environment=production,sentry-release=1.0.0,sentry-transaction=sampled-transaction,sentry-public_key=abc,sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b', + ); + }); + it('should NOT set baggage and sentry-trace header if instrumentation is supressed', () => { const spanContext = { traceId: 'd4cda95b652f4a1592b449d5929fda1b', From 8f299b62beaf16491d066b8bc31855394da91f2d Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 11 Jan 2023 14:57:39 +0100 Subject: [PATCH 039/113] build: Improve nx caching strategy (#6731) 1. `build:tarball` is actually not cacheable 2. Make sure that we do not mark a package as needing to rebuild when tests change 3. Add missing dependencies for serverless build --- nx.json | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/nx.json b/nx.json index 3b2b7778c325..2a801035b45c 100644 --- a/nx.json +++ b/nx.json @@ -6,12 +6,24 @@ "cacheableOperations": [ "build:bundle", "build:transpile", - "build:tarball", "build:types" ] } } }, + "namedInputs": { + "sharedGlobals": [ + "{workspaceRoot}/*.js", + "{workspaceRoot}/*.json", + "{workspaceRoot}/yarn.lock" + ], + "default": [ + "{projectRoot}/**/*", + "sharedGlobals", + "!{projectRoot}/test/**/*", + "!{projectRoot}/**/*.md" + ] + }, "targetDefaults": { "build:bundle": { "dependsOn": [ @@ -58,7 +70,12 @@ "targets": { "@sentry/serverless": { "build:bundle": { - "dependsOn": [], + "dependsOn": [ + "^build:transpile", + "build:transpile", + "^build:types", + "build:types" + ], "outputs": [ "{projectRoot}/build/aws" ] From 23dbd45b72c5c8d74d9486b25b80acce05877a4d Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 11 Jan 2023 15:51:13 +0100 Subject: [PATCH 040/113] ci: Use nx cache from master by default (#6732) Currently, we do not store any cache for the master branch. When we run a PR, it will try to restore the cache from itself, then from the current branch, then just the newest cache it finds at all. This means that on a fresh branch, it will restore the cache from the last action that ran - which could be any branch, which means the cache may incorporate changed we don't have, leading to us having to rebuild the cache. Instead, this makes sure we restore from the master branch first, if possible. This makes it necessary to ensure we actually also _store_ the cache when running on master - we only pass a restore key in that case that will never match anything. --- .github/workflows/build.yml | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0228771337ac..e2468933eeef 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -40,6 +40,15 @@ env: BUILD_CACHE_KEY: ${{ github.event.inputs.commit || github.sha }} + # GH will use the first restore-key it finds that matches + # So it will start by looking for one from the same branch, else take the newest one it can find elsewhere + # We want to prefer the cache from the current master branch, if we don't find any on the current branch + NX_CACHE_RESTORE_KEYS: | + nx-Linux-${{ github.ref }}-${{ github.event.inputs.commit || github.sha }} + nx-Linux-${{ github.ref }} + nx-Linux-refs/heads/master + nx-Linux + jobs: job_get_metadata: name: Get Metadata @@ -194,18 +203,14 @@ jobs: # - on release branches # - when PR has `ci-skip-cache` label if: | - needs.job_get_metadata.outputs.is_master == 'false' && needs.job_get_metadata.outputs.is_release == 'false' && needs.job_get_metadata.outputs.force_skip_cache == 'false' with: path: node_modules/.cache/nx - key: nx-${{ runner.os }}-${{ github.ref }}-${{ env.HEAD_COMMIT }} - # GH will use the first restore-key it finds that matches - # So it will start by looking for one from the same branch, else take the newest one it can find elsewhere - restore-keys: | - nx-${{ runner.os }}-${{ github.ref }}-${{ env.HEAD_COMMIT }} - nx-${{ runner.os }}-${{ github.ref }} - nx-${{ runner.os }} + key: nx-Linux-${{ github.ref }}-${{ env.HEAD_COMMIT }} + # On master branch, we want to _store_ the cache (so it can be used by other branches), but never _restore_ from it + restore-keys: + ${{needs.job_get_metadata.outputs.is_master == 'false' && env.NX_CACHE_RESTORE_KEYS || 'nx-never-restore'}} - name: Build packages # Under normal circumstances, using the git SHA as a cache key, there shouldn't ever be a cache hit on the built From b250f9fbbe3616a36250c062981684f13f8e161b Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Wed, 11 Jan 2023 16:01:47 +0100 Subject: [PATCH 041/113] fix(replay): Fix `checkoutEveryNms` (#6722) This was inadvertantly changed and was causing replays to have very long durations since it was waiting for 60k events before resetting buffer. Fixes #6720 --- packages/replay/src/replay.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/replay/src/replay.ts b/packages/replay/src/replay.ts index 612bf5a71c10..c90b18de2d42 100644 --- a/packages/replay/src/replay.ts +++ b/packages/replay/src/replay.ts @@ -178,10 +178,8 @@ export class ReplayContainer implements ReplayContainerInterface { return; } - // Modify recording options to checkoutEveryNthSecond if - // sampling for error replay. This is because we don't know - // when an error will occur, so we need to keep a buffer of - // replay events. + // If session is sampled for errors, then we need to set the recordingMode + // to 'error', which will configure recording with different options. if (this.session.sampled === 'error') { this.recordingMode = 'error'; } @@ -211,10 +209,10 @@ export class ReplayContainer implements ReplayContainerInterface { try { this._stopRecording = record({ ...this._recordingOptions, - // When running in error sampling mode, we need to overwrite `checkoutEveryNth` + // When running in error sampling mode, we need to overwrite `checkoutEveryNms` // Without this, it would record forever, until an error happens, which we don't want // instead, we'll always keep the last 60 seconds of replay before an error happened - ...(this.recordingMode === 'error' && { checkoutEveryNth: 60000 }), + ...(this.recordingMode === 'error' && { checkoutEveryNms: 60000 }), emit: this.handleRecordingEmit, }); } catch (err) { From 9ec436b875249372d8d4783c5e87c8f2e0558453 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Wed, 11 Jan 2023 16:19:47 +0100 Subject: [PATCH 042/113] ref(replay): Allow `timestamp` to be passed as an argument to `sendReplayRequest` (#6696) This will be needed when attempting to send a replay request at a different time (e.g. after a reload). --- packages/replay/src/replay.ts | 5 ++--- packages/replay/src/types.ts | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/replay/src/replay.ts b/packages/replay/src/replay.ts index c90b18de2d42..561d3f10b58b 100644 --- a/packages/replay/src/replay.ts +++ b/packages/replay/src/replay.ts @@ -893,6 +893,7 @@ export class ReplayContainer implements ReplayContainerInterface { segmentId: segment_id, includeReplayStartTimestamp, eventContext, + timestamp = new Date().getTime(), }: SendReplay): Promise { const recordingData = createRecordingData({ events, @@ -903,8 +904,6 @@ export class ReplayContainer implements ReplayContainerInterface { const { urls, errorIds, traceIds, initialTimestamp } = eventContext; - const currentTimestamp = new Date().getTime(); - const hub = getCurrentHub(); const client = hub.getClient(); const scope = hub.getScope(); @@ -919,7 +918,7 @@ export class ReplayContainer implements ReplayContainerInterface { // @ts-ignore private api type: REPLAY_EVENT_NAME, ...(includeReplayStartTimestamp ? { replay_start_timestamp: initialTimestamp / 1000 } : {}), - timestamp: currentTimestamp / 1000, + timestamp: timestamp / 1000, error_ids: errorIds, trace_ids: traceIds, urls, diff --git a/packages/replay/src/types.ts b/packages/replay/src/types.ts index 029bd109c31f..0c05bdeaabcf 100644 --- a/packages/replay/src/types.ts +++ b/packages/replay/src/types.ts @@ -15,6 +15,7 @@ export interface SendReplay { segmentId: number; includeReplayStartTimestamp: boolean; eventContext: PopEventContext; + timestamp?: number; } export type InstrumentationTypeBreadcrumb = 'dom' | 'scope'; From 2aa4e94b036675245290596884959e06dcced044 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 11 Jan 2023 16:39:34 +0100 Subject: [PATCH 043/113] build: Add `@typescript-eslint/consistent-type-imports` rule (#6662) This ensures we use a `type` import when we only need type stuff. --- packages/angular/src/errorhandler.ts | 3 +- packages/angular/src/sdk.ts | 3 +- packages/angular/src/tracing.ts | 11 ++++--- packages/angular/test/tracing.test.ts | 4 +-- packages/angular/test/utils/index.ts | 8 +++-- packages/browser/src/client.ts | 18 ++++++++--- packages/browser/src/eventbuilder.ts | 2 +- packages/browser/src/helpers.ts | 2 +- .../browser/src/integrations/breadcrumbs.ts | 2 +- packages/browser/src/integrations/dedupe.ts | 2 +- .../src/integrations/globalhandlers.ts | 4 +-- .../browser/src/integrations/httpcontext.ts | 2 +- .../browser/src/integrations/linkederrors.ts | 4 +-- packages/browser/src/integrations/trycatch.ts | 2 +- packages/browser/src/sdk.ts | 8 +++-- packages/browser/src/stack-parsers.ts | 2 +- packages/browser/src/transports/fetch.ts | 7 ++-- packages/browser/src/transports/types.ts | 2 +- packages/browser/src/transports/xhr.ts | 4 +-- .../browser/test/unit/eventbuilder.test.ts | 2 +- .../unit/helper/browser-client-options.ts | 2 +- packages/browser/test/unit/index.test.ts | 2 +- .../test/unit/integrations/helpers.test.ts | 2 +- .../unit/integrations/linkederrors.test.ts | 2 +- packages/browser/test/unit/sdk.test.ts | 4 +-- .../test/unit/transports/fetch.test.ts | 6 ++-- .../browser/test/unit/transports/xhr.test.ts | 4 +-- packages/core/src/api.ts | 2 +- packages/core/src/baseclient.ts | 7 ++-- packages/core/src/envelope.ts | 2 +- packages/core/src/exports.ts | 7 ++-- packages/core/src/hub.ts | 2 +- packages/core/src/integration.ts | 2 +- .../core/src/integrations/functiontostring.ts | 2 +- .../core/src/integrations/inboundfilters.ts | 2 +- packages/core/src/scope.ts | 2 +- packages/core/src/sdk.ts | 2 +- packages/core/src/session.ts | 2 +- packages/core/src/sessionflusher.ts | 8 ++++- packages/core/src/transports/base.ts | 5 ++- packages/core/src/utils/prepareEvent.ts | 2 +- packages/core/test/lib/api.test.ts | 2 +- packages/core/test/lib/base.test.ts | 2 +- packages/core/test/lib/envelope.test.ts | 2 +- packages/core/test/lib/integration.test.ts | 2 +- .../lib/integrations/inboundfilters.test.ts | 5 +-- packages/core/test/lib/sdk.test.ts | 2 +- .../core/test/lib/transports/base.test.ts | 5 +-- packages/core/test/mocks/client.ts | 11 ++++++- packages/core/test/mocks/integration.ts | 2 +- packages/core/test/mocks/transport.ts | 2 +- packages/eslint-config-sdk/src/index.js | 2 ++ packages/gatsby/src/sdk.ts | 2 +- packages/gatsby/src/utils/integrations.ts | 4 +-- packages/gatsby/src/utils/types.ts | 2 +- packages/gatsby/test/sdk.test.ts | 6 ++-- packages/hub/test/exports.test.ts | 2 +- packages/hub/test/hub.test.ts | 2 +- packages/hub/test/scope.test.ts | 2 +- packages/hub/test/session.test.ts | 2 +- packages/hub/test/sessionflusher.test.ts | 2 +- packages/integration-tests/package.json | 3 ++ .../integration-tests/playwright.config.ts | 2 +- .../integrations/httpclient/fetch/test.ts | 2 +- .../integrations/httpclient/xhr/test.ts | 2 +- .../addBreadcrumb/empty_obj/test.ts | 2 +- .../multiple_breadcrumbs/test.ts | 2 +- .../addBreadcrumb/simple_breadcrumb/test.ts | 2 +- .../addBreadcrumb/undefined_arg/test.ts | 2 +- .../captureException/empty_obj/test.ts | 2 +- .../captureException/simple_error/test.ts | 2 +- .../captureException/undefined_arg/test.ts | 2 +- .../captureMessage/simple_message/test.ts | 2 +- .../captureMessage/with_level/test.ts | 2 +- .../configureScope/clear_scope/test.ts | 2 +- .../configureScope/set_properties/test.ts | 2 +- .../suites/public-api/init/console/test.ts | 3 +- .../instrumentation/eventListener/test.ts | 2 +- .../setContext/multiple_contexts/test.ts | 2 +- .../non_serializable_context/test.ts | 2 +- .../setContext/simple_context/test.ts | 2 +- .../setExtra/multiple_extras/test.ts | 2 +- .../setExtra/non_serializable_extra/test.ts | 2 +- .../public-api/setExtra/simple_extra/test.ts | 2 +- .../setExtras/consecutive_calls/test.ts | 2 +- .../setExtras/multiple_extras/test.ts | 2 +- .../setTag/with_non_primitives/test.ts | 2 +- .../public-api/setTag/with_primitives/test.ts | 2 +- .../setTags/with_non_primitives/test.ts | 2 +- .../setTags/with_primitives/test.ts | 2 +- .../public-api/setUser/unset_user/test.ts | 2 +- .../public-api/setUser/update_user/test.ts | 2 +- .../startTransaction/basic_usage/test.ts | 2 +- .../startTransaction/circular_data/test.ts | 2 +- .../startTransaction/setMeasurement/test.ts | 2 +- .../withScope/nested_scopes/test.ts | 2 +- .../suites/sessions/start-session/test.ts | 5 +-- .../suites/sessions/update-session/test.ts | 2 +- .../test.ts | 2 +- .../protocol_fn_identifiers/test.ts | 2 +- .../regular_fn_identifiers/test.ts | 2 +- .../backgroundtab-custom/test.ts | 5 +-- .../backgroundtab-pageload/test.ts | 2 +- .../browsertracing/interactions/test.ts | 5 +-- .../long-tasks-disabled/test.ts | 5 +-- .../browsertracing/long-tasks-enabled/test.ts | 5 +-- .../tracing/browsertracing/meta/test.ts | 2 +- .../tracing/browsertracing/navigation/test.ts | 2 +- .../tracing/browsertracing/pageload/test.ts | 2 +- .../customTargets/test.ts | 3 +- .../customTargetsAndOrigins/test.ts | 3 +- .../customTracingOrigins/test.ts | 3 +- .../defaultTargetsMatch/test.ts | 3 +- .../defaultTargetsNoMatch/test.ts | 3 +- .../envelope-header-transaction-name/test.ts | 2 +- .../suites/tracing/envelope-header/test.ts | 2 +- .../tracing/metrics/connection-rtt/test.ts | 5 +-- .../metrics/pageload-browser-spans/test.ts | 2 +- .../metrics/pageload-resource-spans/test.ts | 5 +-- .../tracing/metrics/web-vitals-cls/test.ts | 2 +- .../tracing/metrics/web-vitals-fid/test.ts | 2 +- .../tracing/metrics/web-vitals-fp-fcp/test.ts | 2 +- .../tracing/metrics/web-vitals-lcp/test.ts | 5 +-- .../tracing/metrics/web-vitals-ttfb/test.ts | 2 +- .../suites/tracing/request/fetch/test.ts | 5 +-- .../suites/tracing/request/xhr/test.ts | 5 +-- .../integration-tests/utils/generatePlugin.ts | 4 +-- packages/integration-tests/utils/helpers.ts | 4 +-- packages/integration-tests/webpack.config.ts | 2 +- packages/integrations/src/captureconsole.ts | 2 +- packages/integrations/src/debug.ts | 2 +- packages/integrations/src/dedupe.ts | 2 +- packages/integrations/src/extraerrordata.ts | 2 +- packages/integrations/src/httpclient.ts | 2 +- packages/integrations/src/offline.ts | 2 +- .../integrations/src/reportingobserver.ts | 2 +- packages/integrations/src/rewriteframes.ts | 2 +- packages/integrations/src/sessiontiming.ts | 2 +- packages/integrations/src/transaction.ts | 2 +- .../integrations/test/captureconsole.test.ts | 2 +- packages/integrations/test/debug.test.ts | 2 +- packages/integrations/test/dedupe.test.ts | 2 +- .../integrations/test/extraerrordata.test.ts | 2 +- packages/integrations/test/offline.test.ts | 5 +-- .../test/reportingobserver.test.ts | 2 +- .../integrations/test/rewriteframes.test.ts | 2 +- packages/nextjs/src/client/index.ts | 5 +-- packages/nextjs/src/client/performance.ts | 2 +- packages/nextjs/src/client/tunnelRoute.ts | 2 +- packages/nextjs/src/common/_error.ts | 2 +- packages/nextjs/src/common/metadata.ts | 2 +- .../nextjs/src/common/userIntegrations.ts | 2 +- .../nextjs/src/config/loaders/prefixLoader.ts | 2 +- .../config/loaders/valueInjectionLoader.ts | 2 +- packages/nextjs/src/config/types.ts | 6 ++-- packages/nextjs/src/server/index.ts | 11 ++++--- .../nextjs/src/server/utils/responseEnd.ts | 6 ++-- .../nextjs/src/server/utils/wrapperUtils.ts | 4 +-- packages/nextjs/src/server/withSentryAPI.ts | 2 +- .../server/withSentryGetServerSideProps.ts | 2 +- .../src/server/withSentryGetStaticProps.ts | 2 +- .../withSentryServerSideAppGetInitialProps.ts | 2 +- ...SentryServerSideDocumentGetInitialProps.ts | 2 +- ...ithSentryServerSideErrorGetInitialProps.ts | 4 +-- .../withSentryServerSideGetInitialProps.ts | 2 +- packages/nextjs/test/clientSdk.test.ts | 4 +-- packages/nextjs/test/config/fixtures.ts | 2 +- packages/nextjs/test/config/testUtils.ts | 7 ++-- .../webpack/sentryWebpackPlugin.test.ts | 2 +- .../nextjs/test/config/withSentry.test.ts | 4 +-- packages/nextjs/test/config/wrappers.test.ts | 2 +- .../nextjs/test/performance/client.test.ts | 4 +-- packages/nextjs/test/serverSdk.test.ts | 2 +- packages/nextjs/test/types/next.config.ts | 2 +- .../nextjs/test/utils/tunnelRoute.test.ts | 2 +- .../test/utils/userIntegrations.test.ts | 7 ++-- .../baggage-header-assign/test.ts | 2 +- .../sentry-trace/baggage-header-out/test.ts | 2 +- .../test.ts | 2 +- .../baggage-other-vendors/test.ts | 2 +- .../baggage-transaction-name/test.ts | 2 +- .../sentry-trace/trace-header-assign/test.ts | 2 +- .../sentry-trace/trace-header-out/test.ts | 2 +- .../suites/public-api/LocalVariables/test.ts | 2 +- .../configureScope/clear_scope/test.ts | 2 +- .../setContext/multiple-contexts/test.ts | 2 +- .../non-serializable-context/test.ts | 2 +- .../setContext/simple-context/test.ts | 2 +- .../public-api/setUser/unset_user/test.ts | 2 +- .../withScope/nested-scopes/test.ts | 2 +- .../node-integration-tests/utils/index.ts | 11 ++++--- packages/node/src/client.ts | 7 ++-- packages/node/src/eventbuilder.ts | 2 +- packages/node/src/handlers.ts | 8 ++--- packages/node/src/integrations/console.ts | 2 +- packages/node/src/integrations/context.ts | 2 +- .../node/src/integrations/contextlines.ts | 2 +- packages/node/src/integrations/http.ts | 21 +++++------- .../node/src/integrations/linkederrors.ts | 4 +-- .../node/src/integrations/localvariables.ts | 5 +-- packages/node/src/integrations/modules.ts | 2 +- .../src/integrations/onuncaughtexception.ts | 7 ++-- .../src/integrations/onunhandledrejection.ts | 5 +-- packages/node/src/integrations/requestdata.ts | 5 +-- .../src/integrations/utils/errorhandling.ts | 2 +- packages/node/src/integrations/utils/http.ts | 4 +-- packages/node/src/requestDataDeprecated.ts | 9 ++---- packages/node/src/requestdata.ts | 8 ++++- packages/node/src/sdk.ts | 4 +-- packages/node/src/transports/http-module.ts | 8 ++--- packages/node/src/transports/http.ts | 4 +-- packages/node/src/types.ts | 4 +-- packages/node/test/client.test.ts | 2 +- packages/node/test/context-lines.test.ts | 2 +- packages/node/test/eventbuilders.test.ts | 2 +- packages/node/test/handlers.test.ts | 2 +- .../node/test/helper/node-client-options.ts | 2 +- packages/node/test/index.test.ts | 7 ++-- packages/node/test/integrations/http.test.ts | 9 +++--- .../test/integrations/linkederrors.test.ts | 5 +-- .../test/integrations/localvariables.test.ts | 9 +++--- .../test/integrations/requestdata.test.ts | 5 +-- packages/node/test/requestdata.test.ts | 13 +++----- packages/node/test/sdk.test.ts | 2 +- packages/node/test/transports/http.test.ts | 2 +- packages/node/test/transports/https.test.ts | 4 +-- packages/opentelemetry-node/src/propagator.ts | 12 ++----- .../opentelemetry-node/src/spanprocessor.ts | 7 ++-- .../src/utils/is-sentry-request.ts | 2 +- .../src/utils/map-otel-status.ts | 4 +-- .../src/utils/parse-otel-span-description.ts | 7 ++-- .../test/propagator.test.ts | 2 +- .../test/spanprocessor.test.ts | 7 ++-- packages/react/src/errorboundary.tsx | 3 +- packages/react/src/profiler.tsx | 5 +-- packages/react/src/reactrouter.tsx | 4 +-- packages/react/src/reactrouterv3.ts | 4 +-- packages/react/src/reactrouterv6.tsx | 4 +-- packages/react/src/redux.ts | 2 +- packages/react/src/sdk.ts | 3 +- packages/react/src/types.ts | 2 +- packages/react/test/errorboundary.test.tsx | 3 +- packages/react/test/profiler.test.tsx | 2 +- packages/react/test/reactrouterv3.test.tsx | 3 +- packages/react/test/reactrouterv4.test.tsx | 2 +- packages/react/test/reactrouterv5.test.tsx | 2 +- packages/react/test/reactrouterv6.4.test.tsx | 2 +- packages/react/test/redux.test.ts | 2 +- packages/remix/src/index.client.tsx | 2 +- packages/remix/src/index.server.ts | 2 +- packages/remix/src/index.types.ts | 2 +- packages/remix/src/performance/client.tsx | 5 +-- packages/remix/src/utils/instrumentServer.ts | 7 ++-- packages/remix/src/utils/metadata.ts | 2 +- packages/remix/src/utils/remixOptions.ts | 6 ++-- .../remix/src/utils/serverAdapters/express.ts | 4 +-- packages/remix/src/utils/types.ts | 2 +- packages/remix/src/utils/web-fetch.ts | 2 +- packages/replay/jest.setup.ts | 2 +- .../src/coreHandlers/breadcrumbHandler.ts | 5 +-- packages/replay/src/coreHandlers/handleDom.ts | 2 +- .../src/coreHandlers/handleGlobalEvent.ts | 2 +- .../replay/src/coreHandlers/handleScope.ts | 2 +- packages/replay/src/eventBuffer.ts | 2 +- packages/replay/src/integration.ts | 3 +- .../replay/src/util/createRecordingData.ts | 2 +- .../replay/src/util/createReplayEnvelope.ts | 2 +- packages/replay/src/util/isRrwebError.ts | 2 +- .../src/util/monkeyPatchRecordDroppedEvent.ts | 2 +- .../replay/src/util/prepareReplayEvent.ts | 5 +-- packages/replay/test/fixtures/error.ts | 4 +-- packages/replay/test/fixtures/transaction.ts | 2 +- .../coreHandlers/handleGlobalEvent.test.ts | 4 +-- .../test/integration/errorSampleRate.test.ts | 7 ++-- .../test/integration/eventProcessors.test.ts | 2 +- .../replay/test/integration/events.test.ts | 5 +-- .../replay/test/integration/flush.test.ts | 2 +- .../test/integration/sendReplayEvent.test.ts | 4 +-- .../replay/test/integration/session.test.ts | 6 ++-- packages/replay/test/integration/stop.test.ts | 4 +-- packages/replay/test/mocks/mockSdk.ts | 9 +++--- packages/replay/test/mocks/resetSdkMock.ts | 6 ++-- packages/replay/test/unit/eventBuffer.test.ts | 3 +- .../unit/util/createReplayEnvelope.test.ts | 2 +- .../test/unit/util/getReplayEvent.test.ts | 5 +-- .../test/unit/util/prepareReplayEvent.test.ts | 5 +-- packages/replay/test/utils/clearSession.ts | 2 +- .../utils/getDefaultBrowserClientOptions.ts | 2 +- packages/serverless/src/awslambda.ts | 7 ++-- packages/serverless/src/awsservices.ts | 4 +-- .../src/gcpfunction/cloud_events.ts | 2 +- packages/serverless/src/gcpfunction/events.ts | 2 +- packages/serverless/src/gcpfunction/http.ts | 5 +-- packages/serverless/src/gcpfunction/index.ts | 2 +- packages/serverless/src/google-cloud-grpc.ts | 4 +-- packages/serverless/src/google-cloud-http.ts | 4 +-- packages/serverless/src/utils.ts | 2 +- packages/serverless/test/awslambda.test.ts | 2 +- packages/serverless/test/gcpfunction.test.ts | 2 +- packages/svelte/src/config.ts | 4 +-- packages/svelte/src/performance.ts | 4 +-- packages/svelte/src/preprocessors.ts | 4 +-- packages/svelte/src/sdk.ts | 3 +- packages/svelte/src/types.ts | 4 +-- packages/svelte/test/config.test.ts | 2 +- packages/svelte/test/performance.test.ts | 2 +- packages/svelte/test/preprocessors.test.ts | 2 +- packages/svelte/test/sdk.test.ts | 2 +- packages/tracing/src/browser/backgroundtab.ts | 4 +-- .../tracing/src/browser/browsertracing.ts | 19 ++++------- packages/tracing/src/browser/metrics/index.ts | 8 ++--- packages/tracing/src/browser/metrics/utils.ts | 4 +-- packages/tracing/src/browser/router.ts | 2 +- .../tracing/src/browser/web-vitals/getCLS.ts | 2 +- .../tracing/src/browser/web-vitals/getFID.ts | 2 +- .../tracing/src/browser/web-vitals/getLCP.ts | 2 +- .../browser/web-vitals/lib/bindReporter.ts | 2 +- .../web-vitals/lib/getNavigationEntry.ts | 2 +- .../src/browser/web-vitals/lib/initMetric.ts | 2 +- .../src/browser/web-vitals/lib/observe.ts | 2 +- .../tracing/src/browser/web-vitals/types.ts | 2 +- .../src/browser/web-vitals/types/base.ts | 2 +- .../src/browser/web-vitals/types/cls.ts | 2 +- .../src/browser/web-vitals/types/fid.ts | 4 +-- .../src/browser/web-vitals/types/lcp.ts | 4 +-- packages/tracing/src/errors.ts | 2 +- packages/tracing/src/hubextensions.ts | 5 +-- packages/tracing/src/idletransaction.ts | 7 ++-- .../tracing/src/integrations/node/apollo.ts | 4 +-- .../tracing/src/integrations/node/express.ts | 2 +- .../tracing/src/integrations/node/graphql.ts | 4 +-- .../tracing/src/integrations/node/mongo.ts | 4 +-- .../tracing/src/integrations/node/mysql.ts | 4 +-- .../tracing/src/integrations/node/postgres.ts | 4 +-- .../tracing/src/integrations/node/prisma.ts | 4 +-- .../src/integrations/node/utils/node-utils.ts | 2 +- packages/tracing/src/span.ts | 9 +++++- packages/tracing/src/transaction.ts | 5 +-- packages/tracing/src/utils.ts | 5 +-- .../test/browser/browsertracing.test.ts | 13 +++----- .../test/browser/metrics/index.test.ts | 3 +- packages/tracing/test/browser/request.test.ts | 13 +++----- packages/tracing/test/browser/router.test.ts | 2 +- packages/tracing/test/errors.test.ts | 2 +- packages/tracing/test/span.test.ts | 2 +- packages/tracing/test/testutils.ts | 2 +- packages/types/src/breadcrumb.ts | 2 +- packages/types/src/client.ts | 22 ++++++------- packages/types/src/clientreport.ts | 2 +- packages/types/src/context.ts | 2 +- packages/types/src/envelope.ts | 16 +++++----- packages/types/src/event.ts | 32 +++++++++---------- packages/types/src/eventprocessor.ts | 2 +- packages/types/src/exception.ts | 4 +-- packages/types/src/hub.ts | 22 ++++++------- packages/types/src/integration.ts | 4 +-- packages/types/src/misc.ts | 2 +- packages/types/src/options.ts | 18 +++++------ packages/types/src/replay.ts | 2 +- packages/types/src/scope.ts | 22 ++++++------- packages/types/src/sdkinfo.ts | 2 +- packages/types/src/sdkmetadata.ts | 2 +- packages/types/src/session.ts | 2 +- packages/types/src/span.ts | 6 ++-- packages/types/src/stacktrace.ts | 2 +- packages/types/src/thread.ts | 2 +- packages/types/src/transaction.ts | 14 ++++---- packages/types/src/transport.ts | 6 ++-- packages/utils/src/baggage.ts | 2 +- .../src/buildPolyfills/_asyncOptionalChain.ts | 2 +- .../buildPolyfills/_createNamedExportFrom.ts | 2 +- .../src/buildPolyfills/_createStarExport.ts | 2 +- .../src/buildPolyfills/_interopDefault.ts | 2 +- .../src/buildPolyfills/_interopNamespace.ts | 2 +- .../_interopNamespaceDefaultOnly.ts | 2 +- .../buildPolyfills/_interopRequireDefault.ts | 2 +- .../buildPolyfills/_interopRequireWildcard.ts | 2 +- .../src/buildPolyfills/_optionalChain.ts | 2 +- packages/utils/src/buildPolyfills/types.ts | 2 +- packages/utils/src/clientreport.ts | 2 +- packages/utils/src/dsn.ts | 2 +- packages/utils/src/envelope.ts | 2 +- packages/utils/src/instrument.ts | 2 +- packages/utils/src/is.ts | 2 +- packages/utils/src/logger.ts | 2 +- packages/utils/src/misc.ts | 2 +- packages/utils/src/normalize.ts | 5 +-- packages/utils/src/object.ts | 2 +- packages/utils/src/ratelimit.ts | 2 +- packages/utils/src/requestdata.ts | 8 ++++- packages/utils/src/severity.ts | 2 +- packages/utils/src/stacktrace.ts | 2 +- packages/utils/src/tracing.ts | 2 +- packages/utils/src/worldwide.ts | 2 +- .../utils/test/buildPolyfills/interop.test.ts | 2 +- .../buildPolyfills/nullishCoalesce.test.ts | 2 +- .../test/buildPolyfills/optionalChain.test.ts | 2 +- packages/utils/test/clientreport.test.ts | 2 +- packages/utils/test/envelope.test.ts | 2 +- packages/utils/test/misc.test.ts | 2 +- packages/utils/test/ratelimit.test.ts | 2 +- packages/vue/src/components.ts | 2 +- packages/vue/src/constants.ts | 2 +- packages/vue/src/errorhandler.ts | 2 +- packages/vue/src/router.ts | 2 +- packages/vue/src/sdk.ts | 2 +- packages/vue/src/tracing.ts | 4 +-- packages/vue/src/types.ts | 2 +- packages/vue/test/errorHandler.test.ts | 2 +- packages/vue/test/router.test.ts | 4 +-- packages/wasm/src/index.ts | 2 +- packages/wasm/src/registry.ts | 2 +- 412 files changed, 791 insertions(+), 703 deletions(-) diff --git a/packages/angular/src/errorhandler.ts b/packages/angular/src/errorhandler.ts index cf89b0e69eec..941029c2ff9f 100644 --- a/packages/angular/src/errorhandler.ts +++ b/packages/angular/src/errorhandler.ts @@ -1,5 +1,6 @@ import { HttpErrorResponse } from '@angular/common/http'; -import { ErrorHandler as AngularErrorHandler, Inject, Injectable } from '@angular/core'; +import type { ErrorHandler as AngularErrorHandler } from '@angular/core'; +import { Inject, Injectable } from '@angular/core'; import * as Sentry from '@sentry/browser'; import { captureException } from '@sentry/browser'; import { addExceptionMechanism, isString } from '@sentry/utils'; diff --git a/packages/angular/src/sdk.ts b/packages/angular/src/sdk.ts index 93b6148f2943..bd0cbd99747b 100644 --- a/packages/angular/src/sdk.ts +++ b/packages/angular/src/sdk.ts @@ -1,5 +1,6 @@ import { VERSION } from '@angular/core'; -import { BrowserOptions, init as browserInit, SDK_VERSION, setContext } from '@sentry/browser'; +import type { BrowserOptions } from '@sentry/browser'; +import { init as browserInit, SDK_VERSION, setContext } from '@sentry/browser'; import { logger } from '@sentry/utils'; import { ANGULAR_MINIMUM_VERSION } from './constants'; diff --git a/packages/angular/src/tracing.ts b/packages/angular/src/tracing.ts index d2bf67900f3f..56c14b36f409 100644 --- a/packages/angular/src/tracing.ts +++ b/packages/angular/src/tracing.ts @@ -1,10 +1,13 @@ /* eslint-disable max-lines */ -import { AfterViewInit, Directive, Injectable, Input, NgModule, OnDestroy, OnInit } from '@angular/core'; -import { ActivatedRouteSnapshot, Event, NavigationEnd, NavigationStart, ResolveEnd, Router } from '@angular/router'; +import type { AfterViewInit, OnDestroy, OnInit } from '@angular/core'; +import { Directive, Injectable, Input, NgModule } from '@angular/core'; +import type { ActivatedRouteSnapshot, Event, Router } from '@angular/router'; +import { NavigationEnd, NavigationStart, ResolveEnd } from '@angular/router'; import { getCurrentHub, WINDOW } from '@sentry/browser'; -import { Span, Transaction, TransactionContext } from '@sentry/types'; +import type { Span, Transaction, TransactionContext } from '@sentry/types'; import { logger, stripUrlQueryAndFragment, timestampWithMs } from '@sentry/utils'; -import { Observable, Subscription } from 'rxjs'; +import type { Observable } from 'rxjs'; +import { Subscription } from 'rxjs'; import { filter, tap } from 'rxjs/operators'; import { ANGULAR_INIT_OP, ANGULAR_OP, ANGULAR_ROUTING_OP } from './constants'; diff --git a/packages/angular/test/tracing.test.ts b/packages/angular/test/tracing.test.ts index d5a88c9bb082..18bc1324f1eb 100644 --- a/packages/angular/test/tracing.test.ts +++ b/packages/angular/test/tracing.test.ts @@ -1,6 +1,6 @@ import { Component } from '@angular/core'; -import { ActivatedRouteSnapshot } from '@angular/router'; -import { Hub } from '@sentry/types'; +import type { ActivatedRouteSnapshot } from '@angular/router'; +import type { Hub } from '@sentry/types'; import { instrumentAngularRouting, TraceClassDecorator, TraceDirective, TraceMethodDecorator } from '../src'; import { getParameterizedRouteFromSnapshot } from '../src/tracing'; diff --git a/packages/angular/test/utils/index.ts b/packages/angular/test/utils/index.ts index 0f4ff55b0b23..b15ad2028560 100644 --- a/packages/angular/test/utils/index.ts +++ b/packages/angular/test/utils/index.ts @@ -1,8 +1,10 @@ import { Component, NgModule } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { Router, Routes } from '@angular/router'; +import type { ComponentFixture } from '@angular/core/testing'; +import { TestBed } from '@angular/core/testing'; +import type { Routes } from '@angular/router'; +import { Router } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; -import { Transaction } from '@sentry/types'; +import type { Transaction } from '@sentry/types'; import { instrumentAngularRouting, TraceService } from '../../src'; diff --git a/packages/browser/src/client.ts b/packages/browser/src/client.ts index dbec43cd19cd..fe0265cfd98b 100644 --- a/packages/browser/src/client.ts +++ b/packages/browser/src/client.ts @@ -1,13 +1,21 @@ -import { BaseClient, getEnvelopeEndpointWithUrlEncodedAuth, Scope, SDK_VERSION } from '@sentry/core'; -import type { BrowserClientReplayOptions } from '@sentry/types'; -import { ClientOptions, Event, EventHint, Options, Severity, SeverityLevel } from '@sentry/types'; +import type { Scope } from '@sentry/core'; +import { BaseClient, getEnvelopeEndpointWithUrlEncodedAuth, SDK_VERSION } from '@sentry/core'; +import type { + BrowserClientReplayOptions, + ClientOptions, + Event, + EventHint, + Options, + Severity, + SeverityLevel, +} from '@sentry/types'; import { createClientReportEnvelope, dsnToString, logger, serializeEnvelope } from '@sentry/utils'; import { eventFromException, eventFromMessage } from './eventbuilder'; import { WINDOW } from './helpers'; -import { Breadcrumbs } from './integrations'; +import type { Breadcrumbs } from './integrations'; import { BREADCRUMB_INTEGRATION_ID } from './integrations/breadcrumbs'; -import { BrowserTransportOptions } from './transports/types'; +import type { BrowserTransportOptions } from './transports/types'; /** * Configuration options for the Sentry Browser SDK. * @see @sentry/types Options for more information. diff --git a/packages/browser/src/eventbuilder.ts b/packages/browser/src/eventbuilder.ts index d089a5b670e0..4db58b9b7b43 100644 --- a/packages/browser/src/eventbuilder.ts +++ b/packages/browser/src/eventbuilder.ts @@ -1,5 +1,5 @@ import { getCurrentHub } from '@sentry/core'; -import { Event, EventHint, Exception, Severity, SeverityLevel, StackFrame, StackParser } from '@sentry/types'; +import type { Event, EventHint, Exception, Severity, SeverityLevel, StackFrame, StackParser } from '@sentry/types'; import { addExceptionMechanism, addExceptionTypeValue, diff --git a/packages/browser/src/helpers.ts b/packages/browser/src/helpers.ts index 7b56e30fa829..5f7b6cee7df3 100644 --- a/packages/browser/src/helpers.ts +++ b/packages/browser/src/helpers.ts @@ -1,5 +1,5 @@ import { captureException, withScope } from '@sentry/core'; -import { DsnLike, Event as SentryEvent, Mechanism, Scope, WrappedFunction } from '@sentry/types'; +import type { DsnLike, Event as SentryEvent, Mechanism, Scope, WrappedFunction } from '@sentry/types'; import { addExceptionMechanism, addExceptionTypeValue, diff --git a/packages/browser/src/integrations/breadcrumbs.ts b/packages/browser/src/integrations/breadcrumbs.ts index 8ffeb5a9f99d..b54aad03e9b2 100644 --- a/packages/browser/src/integrations/breadcrumbs.ts +++ b/packages/browser/src/integrations/breadcrumbs.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable max-lines */ import { getCurrentHub } from '@sentry/core'; -import { Event, Integration } from '@sentry/types'; +import type { Event, Integration } from '@sentry/types'; import { addInstrumentationHandler, getEventDescription, diff --git a/packages/browser/src/integrations/dedupe.ts b/packages/browser/src/integrations/dedupe.ts index aaaef31d7257..84d6d983812b 100644 --- a/packages/browser/src/integrations/dedupe.ts +++ b/packages/browser/src/integrations/dedupe.ts @@ -1,4 +1,4 @@ -import { Event, EventProcessor, Exception, Hub, Integration, StackFrame } from '@sentry/types'; +import type { Event, EventProcessor, Exception, Hub, Integration, StackFrame } from '@sentry/types'; import { logger } from '@sentry/utils'; /** Deduplication filter */ diff --git a/packages/browser/src/integrations/globalhandlers.ts b/packages/browser/src/integrations/globalhandlers.ts index 79d5d3d6a444..5bfdc126a712 100644 --- a/packages/browser/src/integrations/globalhandlers.ts +++ b/packages/browser/src/integrations/globalhandlers.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { getCurrentHub } from '@sentry/core'; -import { Event, EventHint, Hub, Integration, Primitive, StackParser } from '@sentry/types'; +import type { Event, EventHint, Hub, Integration, Primitive, StackParser } from '@sentry/types'; import { addExceptionMechanism, addInstrumentationHandler, @@ -11,7 +11,7 @@ import { logger, } from '@sentry/utils'; -import { BrowserClient } from '../client'; +import type { BrowserClient } from '../client'; import { eventFromUnknownInput } from '../eventbuilder'; import { shouldIgnoreOnError } from '../helpers'; diff --git a/packages/browser/src/integrations/httpcontext.ts b/packages/browser/src/integrations/httpcontext.ts index 88f2873882d4..24b6df24312a 100644 --- a/packages/browser/src/integrations/httpcontext.ts +++ b/packages/browser/src/integrations/httpcontext.ts @@ -1,5 +1,5 @@ import { addGlobalEventProcessor, getCurrentHub } from '@sentry/core'; -import { Event, Integration } from '@sentry/types'; +import type { Event, Integration } from '@sentry/types'; import { WINDOW } from '../helpers'; diff --git a/packages/browser/src/integrations/linkederrors.ts b/packages/browser/src/integrations/linkederrors.ts index 8fa4413f5a83..bb53d0210f96 100644 --- a/packages/browser/src/integrations/linkederrors.ts +++ b/packages/browser/src/integrations/linkederrors.ts @@ -1,8 +1,8 @@ import { addGlobalEventProcessor, getCurrentHub } from '@sentry/core'; -import { Event, EventHint, Exception, ExtendedError, Integration, StackParser } from '@sentry/types'; +import type { Event, EventHint, Exception, ExtendedError, Integration, StackParser } from '@sentry/types'; import { isInstanceOf } from '@sentry/utils'; -import { BrowserClient } from '../client'; +import type { BrowserClient } from '../client'; import { exceptionFromError } from '../eventbuilder'; const DEFAULT_KEY = 'cause'; diff --git a/packages/browser/src/integrations/trycatch.ts b/packages/browser/src/integrations/trycatch.ts index 2690b977ce80..d29be09a6a0b 100644 --- a/packages/browser/src/integrations/trycatch.ts +++ b/packages/browser/src/integrations/trycatch.ts @@ -1,4 +1,4 @@ -import { Integration, WrappedFunction } from '@sentry/types'; +import type { Integration, WrappedFunction } from '@sentry/types'; import { fill, getFunctionName, getOriginalFunction } from '@sentry/utils'; import { WINDOW, wrap } from '../helpers'; diff --git a/packages/browser/src/sdk.ts b/packages/browser/src/sdk.ts index 9b10074f8b51..c7f2ed74194a 100644 --- a/packages/browser/src/sdk.ts +++ b/packages/browser/src/sdk.ts @@ -1,8 +1,8 @@ +import type { Hub } from '@sentry/core'; import { getCurrentHub, getIntegrationsToSetup, getReportDialogEndpoint, - Hub, initAndBind, Integrations as CoreIntegrations, } from '@sentry/core'; @@ -14,8 +14,10 @@ import { supportsFetch, } from '@sentry/utils'; -import { BrowserClient, BrowserClientOptions, BrowserOptions } from './client'; -import { ReportDialogOptions, WINDOW, wrap as internalWrap } from './helpers'; +import type { BrowserClientOptions, BrowserOptions } from './client'; +import { BrowserClient } from './client'; +import type { ReportDialogOptions } from './helpers'; +import { WINDOW, wrap as internalWrap } from './helpers'; import { Breadcrumbs, Dedupe, GlobalHandlers, HttpContext, LinkedErrors, TryCatch } from './integrations'; import { defaultStackParser } from './stack-parsers'; import { makeFetchTransport, makeXHRTransport } from './transports'; diff --git a/packages/browser/src/stack-parsers.ts b/packages/browser/src/stack-parsers.ts index eeb08c882d8c..7e77a6c0d8e3 100644 --- a/packages/browser/src/stack-parsers.ts +++ b/packages/browser/src/stack-parsers.ts @@ -1,4 +1,4 @@ -import { StackFrame, StackLineParser, StackLineParserFn } from '@sentry/types'; +import type { StackFrame, StackLineParser, StackLineParserFn } from '@sentry/types'; import { createStackParser } from '@sentry/utils'; // global reference to slice diff --git a/packages/browser/src/transports/fetch.ts b/packages/browser/src/transports/fetch.ts index 5f64bcdde841..83b75c82ba39 100644 --- a/packages/browser/src/transports/fetch.ts +++ b/packages/browser/src/transports/fetch.ts @@ -1,9 +1,10 @@ import { createTransport } from '@sentry/core'; -import { Transport, TransportMakeRequestResponse, TransportRequest } from '@sentry/types'; +import type { Transport, TransportMakeRequestResponse, TransportRequest } from '@sentry/types'; import { rejectedSyncPromise } from '@sentry/utils'; -import { BrowserTransportOptions } from './types'; -import { clearCachedFetchImplementation, FetchImpl, getNativeFetchImplementation } from './utils'; +import type { BrowserTransportOptions } from './types'; +import type { FetchImpl } from './utils'; +import { clearCachedFetchImplementation, getNativeFetchImplementation } from './utils'; /** * Creates a Transport that uses the Fetch API to send events to Sentry. diff --git a/packages/browser/src/transports/types.ts b/packages/browser/src/transports/types.ts index 4f4b7ef2c98a..e0ed666cc787 100644 --- a/packages/browser/src/transports/types.ts +++ b/packages/browser/src/transports/types.ts @@ -1,4 +1,4 @@ -import { BaseTransportOptions } from '@sentry/types'; +import type { BaseTransportOptions } from '@sentry/types'; export interface BrowserTransportOptions extends BaseTransportOptions { /** Fetch API init parameters. Used by the FetchTransport */ diff --git a/packages/browser/src/transports/xhr.ts b/packages/browser/src/transports/xhr.ts index b10ca4e2b7ca..8fae5dc8067e 100644 --- a/packages/browser/src/transports/xhr.ts +++ b/packages/browser/src/transports/xhr.ts @@ -1,8 +1,8 @@ import { createTransport } from '@sentry/core'; -import { Transport, TransportMakeRequestResponse, TransportRequest } from '@sentry/types'; +import type { Transport, TransportMakeRequestResponse, TransportRequest } from '@sentry/types'; import { SyncPromise } from '@sentry/utils'; -import { BrowserTransportOptions } from './types'; +import type { BrowserTransportOptions } from './types'; /** * The DONE ready state for XmlHttpRequest diff --git a/packages/browser/test/unit/eventbuilder.test.ts b/packages/browser/test/unit/eventbuilder.test.ts index 62a70a4b2740..ac9b564e99e0 100644 --- a/packages/browser/test/unit/eventbuilder.test.ts +++ b/packages/browser/test/unit/eventbuilder.test.ts @@ -1,4 +1,4 @@ -import { Client } from '@sentry/types'; +import type { Client } from '@sentry/types'; import { defaultStackParser } from '../../src'; import { eventFromPlainObject } from '../../src/eventbuilder'; diff --git a/packages/browser/test/unit/helper/browser-client-options.ts b/packages/browser/test/unit/helper/browser-client-options.ts index 9bdaf2518d40..8ca73faa2d23 100644 --- a/packages/browser/test/unit/helper/browser-client-options.ts +++ b/packages/browser/test/unit/helper/browser-client-options.ts @@ -1,7 +1,7 @@ import { createTransport } from '@sentry/core'; import { resolvedSyncPromise } from '@sentry/utils'; -import { BrowserClientOptions } from '../../../src/client'; +import type { BrowserClientOptions } from '../../../src/client'; export function getDefaultBrowserClientOptions(options: Partial = {}): BrowserClientOptions { return { diff --git a/packages/browser/test/unit/index.test.ts b/packages/browser/test/unit/index.test.ts index 8a59db3bdd46..8bc3ffab36e1 100644 --- a/packages/browser/test/unit/index.test.ts +++ b/packages/browser/test/unit/index.test.ts @@ -1,5 +1,6 @@ import { getReportDialogEndpoint, SDK_VERSION } from '@sentry/core'; +import type { Event } from '../../src'; import { addBreadcrumb, BrowserClient, @@ -7,7 +8,6 @@ import { captureException, captureMessage, configureScope, - Event, flush, getCurrentHub, init, diff --git a/packages/browser/test/unit/integrations/helpers.test.ts b/packages/browser/test/unit/integrations/helpers.test.ts index e17ef73d4e1c..a3fe734d79d4 100644 --- a/packages/browser/test/unit/integrations/helpers.test.ts +++ b/packages/browser/test/unit/integrations/helpers.test.ts @@ -1,4 +1,4 @@ -import { WrappedFunction } from '@sentry/types'; +import type { WrappedFunction } from '@sentry/types'; import { spy } from 'sinon'; import { wrap } from '../../../src/helpers'; diff --git a/packages/browser/test/unit/integrations/linkederrors.test.ts b/packages/browser/test/unit/integrations/linkederrors.test.ts index 1a80a86093b2..40738ca2af09 100644 --- a/packages/browser/test/unit/integrations/linkederrors.test.ts +++ b/packages/browser/test/unit/integrations/linkederrors.test.ts @@ -1,4 +1,4 @@ -import { Event as SentryEvent, Exception, ExtendedError } from '@sentry/types'; +import type { Event as SentryEvent, Exception, ExtendedError } from '@sentry/types'; import { BrowserClient } from '../../../src/client'; import * as LinkedErrorsModule from '../../../src/integrations/linkederrors'; diff --git a/packages/browser/test/unit/sdk.test.ts b/packages/browser/test/unit/sdk.test.ts index facf8f9f4aae..4afac502c562 100644 --- a/packages/browser/test/unit/sdk.test.ts +++ b/packages/browser/test/unit/sdk.test.ts @@ -1,10 +1,10 @@ /* eslint-disable @typescript-eslint/unbound-method */ import { createTransport, Scope } from '@sentry/core'; import { MockIntegration } from '@sentry/core/test/lib/sdk.test'; -import { Client, Integration } from '@sentry/types'; +import type { Client, Integration } from '@sentry/types'; import { resolvedSyncPromise } from '@sentry/utils'; -import { BrowserOptions } from '../../src'; +import type { BrowserOptions } from '../../src'; import { init } from '../../src/sdk'; const PUBLIC_DSN = 'https://username@domain/123'; diff --git a/packages/browser/test/unit/transports/fetch.test.ts b/packages/browser/test/unit/transports/fetch.test.ts index 624a3d1d007a..53ed5c34e21b 100644 --- a/packages/browser/test/unit/transports/fetch.test.ts +++ b/packages/browser/test/unit/transports/fetch.test.ts @@ -1,10 +1,10 @@ -import { EventEnvelope, EventItem } from '@sentry/types'; +import type { EventEnvelope, EventItem } from '@sentry/types'; import { createEnvelope, serializeEnvelope } from '@sentry/utils'; import { TextEncoder } from 'util'; import { makeFetchTransport } from '../../../src/transports/fetch'; -import { BrowserTransportOptions } from '../../../src/transports/types'; -import { FetchImpl } from '../../../src/transports/utils'; +import type { BrowserTransportOptions } from '../../../src/transports/types'; +import type { FetchImpl } from '../../../src/transports/utils'; const DEFAULT_FETCH_TRANSPORT_OPTIONS: BrowserTransportOptions = { url: 'https://sentry.io/api/42/store/?sentry_key=123&sentry_version=7', diff --git a/packages/browser/test/unit/transports/xhr.test.ts b/packages/browser/test/unit/transports/xhr.test.ts index 117edce8d2ea..07ca7f4e07dc 100644 --- a/packages/browser/test/unit/transports/xhr.test.ts +++ b/packages/browser/test/unit/transports/xhr.test.ts @@ -1,8 +1,8 @@ -import { EventEnvelope, EventItem } from '@sentry/types'; +import type { EventEnvelope, EventItem } from '@sentry/types'; import { createEnvelope, serializeEnvelope } from '@sentry/utils'; import { TextEncoder } from 'util'; -import { BrowserTransportOptions } from '../../../src/transports/types'; +import type { BrowserTransportOptions } from '../../../src/transports/types'; import { makeXHRTransport } from '../../../src/transports/xhr'; const DEFAULT_XHR_TRANSPORT_OPTIONS: BrowserTransportOptions = { diff --git a/packages/core/src/api.ts b/packages/core/src/api.ts index f454e148cc6c..7191871d9ec4 100644 --- a/packages/core/src/api.ts +++ b/packages/core/src/api.ts @@ -1,4 +1,4 @@ -import { ClientOptions, DsnComponents, DsnLike, SdkInfo } from '@sentry/types'; +import type { ClientOptions, DsnComponents, DsnLike, SdkInfo } from '@sentry/types'; import { dsnToString, makeDsn, urlEncode } from '@sentry/utils'; const SENTRY_API_VERSION = '7'; diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index eb674a46ab99..957e7f6321d2 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -1,5 +1,5 @@ /* eslint-disable max-lines */ -import { +import type { Client, ClientOptions, DataCategory, @@ -37,8 +37,9 @@ import { import { getEnvelopeEndpointWithUrlEncodedAuth } from './api'; import { createEventEnvelope, createSessionEnvelope } from './envelope'; -import { IntegrationIndex, setupIntegration, setupIntegrations } from './integration'; -import { Scope } from './scope'; +import type { IntegrationIndex } from './integration'; +import { setupIntegration, setupIntegrations } from './integration'; +import type { Scope } from './scope'; import { updateSession } from './session'; import { prepareEvent } from './utils/prepareEvent'; diff --git a/packages/core/src/envelope.ts b/packages/core/src/envelope.ts index 2ae474dd99b0..0a4cd6a87870 100644 --- a/packages/core/src/envelope.ts +++ b/packages/core/src/envelope.ts @@ -1,4 +1,4 @@ -import { +import type { DsnComponents, Event, EventEnvelope, diff --git a/packages/core/src/exports.ts b/packages/core/src/exports.ts index 97a70d327b8a..72a5f8587f94 100644 --- a/packages/core/src/exports.ts +++ b/packages/core/src/exports.ts @@ -1,4 +1,4 @@ -import { +import type { Breadcrumb, CaptureContext, CustomSamplingContext, @@ -13,8 +13,9 @@ import { User, } from '@sentry/types'; -import { getCurrentHub, Hub } from './hub'; -import { Scope } from './scope'; +import type { Hub } from './hub'; +import { getCurrentHub } from './hub'; +import type { Scope } from './scope'; // Note: All functions in this file are typed with a return value of `ReturnType`, // where HUB_FUNCTION is some method on the Hub class. diff --git a/packages/core/src/hub.ts b/packages/core/src/hub.ts index aa3a8015c0a0..dde906bffde5 100644 --- a/packages/core/src/hub.ts +++ b/packages/core/src/hub.ts @@ -1,5 +1,5 @@ /* eslint-disable max-lines */ -import { +import type { Breadcrumb, BreadcrumbHint, Client, diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts index b8c9ef16da6d..77d232b00987 100644 --- a/packages/core/src/integration.ts +++ b/packages/core/src/integration.ts @@ -1,4 +1,4 @@ -import { Integration, Options } from '@sentry/types'; +import type { Integration, Options } from '@sentry/types'; import { arrayify, logger } from '@sentry/utils'; import { getCurrentHub } from './hub'; diff --git a/packages/core/src/integrations/functiontostring.ts b/packages/core/src/integrations/functiontostring.ts index 6eb1cdbda123..d26a49b99780 100644 --- a/packages/core/src/integrations/functiontostring.ts +++ b/packages/core/src/integrations/functiontostring.ts @@ -1,4 +1,4 @@ -import { Integration, WrappedFunction } from '@sentry/types'; +import type { Integration, WrappedFunction } from '@sentry/types'; import { getOriginalFunction } from '@sentry/utils'; let originalFunctionToString: () => void; diff --git a/packages/core/src/integrations/inboundfilters.ts b/packages/core/src/integrations/inboundfilters.ts index ca71d13873ce..047060ae4961 100644 --- a/packages/core/src/integrations/inboundfilters.ts +++ b/packages/core/src/integrations/inboundfilters.ts @@ -1,4 +1,4 @@ -import { Event, EventProcessor, Hub, Integration, StackFrame } from '@sentry/types'; +import type { Event, EventProcessor, Hub, Integration, StackFrame } from '@sentry/types'; import { getEventDescription, logger, stringMatchesSomePattern } from '@sentry/utils'; // "Script error." is hard coded into browsers for errors that it can't read. diff --git a/packages/core/src/scope.ts b/packages/core/src/scope.ts index 44d2d6d61a18..7143275baa8b 100644 --- a/packages/core/src/scope.ts +++ b/packages/core/src/scope.ts @@ -1,5 +1,5 @@ /* eslint-disable max-lines */ -import { +import type { Attachment, Breadcrumb, CaptureContext, diff --git a/packages/core/src/sdk.ts b/packages/core/src/sdk.ts index 16a64520ff48..7d2df2115ddd 100644 --- a/packages/core/src/sdk.ts +++ b/packages/core/src/sdk.ts @@ -1,4 +1,4 @@ -import { Client, ClientOptions } from '@sentry/types'; +import type { Client, ClientOptions } from '@sentry/types'; import { logger } from '@sentry/utils'; import { getCurrentHub } from './hub'; diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index c8cba3e30a04..2987f09addb5 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -1,4 +1,4 @@ -import { SerializedSession, Session, SessionContext, SessionStatus } from '@sentry/types'; +import type { SerializedSession, Session, SessionContext, SessionStatus } from '@sentry/types'; import { dropUndefinedKeys, timestampInSeconds, uuid4 } from '@sentry/utils'; /** diff --git a/packages/core/src/sessionflusher.ts b/packages/core/src/sessionflusher.ts index 7b2bda98c4da..9b4579ade486 100644 --- a/packages/core/src/sessionflusher.ts +++ b/packages/core/src/sessionflusher.ts @@ -1,4 +1,10 @@ -import { AggregationCounts, Client, RequestSessionStatus, SessionAggregates, SessionFlusherLike } from '@sentry/types'; +import type { + AggregationCounts, + Client, + RequestSessionStatus, + SessionAggregates, + SessionFlusherLike, +} from '@sentry/types'; import { dropUndefinedKeys } from '@sentry/utils'; import { getCurrentHub } from './hub'; diff --git a/packages/core/src/transports/base.ts b/packages/core/src/transports/base.ts index 3c7603b024ff..4a3be77e8531 100644 --- a/packages/core/src/transports/base.ts +++ b/packages/core/src/transports/base.ts @@ -1,4 +1,4 @@ -import { +import type { Envelope, EnvelopeItem, EnvelopeItemType, @@ -10,6 +10,7 @@ import { TransportMakeRequestResponse, TransportRequestExecutor, } from '@sentry/types'; +import type { PromiseBuffer, RateLimits } from '@sentry/utils'; import { createEnvelope, envelopeItemTypeToDataCategory, @@ -17,8 +18,6 @@ import { isRateLimited, logger, makePromiseBuffer, - PromiseBuffer, - RateLimits, resolvedSyncPromise, SentryError, serializeEnvelope, diff --git a/packages/core/src/utils/prepareEvent.ts b/packages/core/src/utils/prepareEvent.ts index 1d23d4d89572..3aa0d86df3f6 100644 --- a/packages/core/src/utils/prepareEvent.ts +++ b/packages/core/src/utils/prepareEvent.ts @@ -1,4 +1,4 @@ -import { ClientOptions, Event, EventHint } from '@sentry/types'; +import type { ClientOptions, Event, EventHint } from '@sentry/types'; import { dateTimestampInSeconds, normalize, resolvedSyncPromise, truncate, uuid4 } from '@sentry/utils'; import { Scope } from '../scope'; diff --git a/packages/core/test/lib/api.test.ts b/packages/core/test/lib/api.test.ts index 752f23844aad..141301878a5d 100644 --- a/packages/core/test/lib/api.test.ts +++ b/packages/core/test/lib/api.test.ts @@ -1,5 +1,5 @@ /* eslint-disable deprecation/deprecation */ -import { ClientOptions, DsnComponents } from '@sentry/types'; +import type { ClientOptions, DsnComponents } from '@sentry/types'; import { makeDsn } from '@sentry/utils'; import { getEnvelopeEndpointWithUrlEncodedAuth, getReportDialogEndpoint } from '../../src/api'; diff --git a/packages/core/test/lib/base.test.ts b/packages/core/test/lib/base.test.ts index 5b11e59fd7f3..c82014515067 100644 --- a/packages/core/test/lib/base.test.ts +++ b/packages/core/test/lib/base.test.ts @@ -1,4 +1,4 @@ -import { Event, Span } from '@sentry/types'; +import type { Event, Span } from '@sentry/types'; import { dsnToString, logger, SentryError, SyncPromise } from '@sentry/utils'; import { Hub, makeSession, Scope } from '../../src'; diff --git a/packages/core/test/lib/envelope.test.ts b/packages/core/test/lib/envelope.test.ts index ecd409b23041..9d24e1eef19e 100644 --- a/packages/core/test/lib/envelope.test.ts +++ b/packages/core/test/lib/envelope.test.ts @@ -1,4 +1,4 @@ -import { DsnComponents, DynamicSamplingContext, Event } from '@sentry/types'; +import type { DsnComponents, DynamicSamplingContext, Event } from '@sentry/types'; import { createEventEnvelope } from '../../src/envelope'; diff --git a/packages/core/test/lib/integration.test.ts b/packages/core/test/lib/integration.test.ts index 1b5b161b3d86..14b1697b9054 100644 --- a/packages/core/test/lib/integration.test.ts +++ b/packages/core/test/lib/integration.test.ts @@ -1,4 +1,4 @@ -import { Integration, Options } from '@sentry/types'; +import type { Integration, Options } from '@sentry/types'; import { getIntegrationsToSetup } from '../../src/integration'; diff --git a/packages/core/test/lib/integrations/inboundfilters.test.ts b/packages/core/test/lib/integrations/inboundfilters.test.ts index 8170e68d9cd5..ff9aca20270a 100644 --- a/packages/core/test/lib/integrations/inboundfilters.test.ts +++ b/packages/core/test/lib/integrations/inboundfilters.test.ts @@ -1,6 +1,7 @@ -import { Event, EventProcessor } from '@sentry/types'; +import type { Event, EventProcessor } from '@sentry/types'; -import { InboundFilters, InboundFiltersOptions } from '../../../src/integrations/inboundfilters'; +import type { InboundFiltersOptions } from '../../../src/integrations/inboundfilters'; +import { InboundFilters } from '../../../src/integrations/inboundfilters'; /** * Creates an instance of the InboundFilters integration and returns diff --git a/packages/core/test/lib/sdk.test.ts b/packages/core/test/lib/sdk.test.ts index 84dc75fbc3a2..c2838d7ed115 100644 --- a/packages/core/test/lib/sdk.test.ts +++ b/packages/core/test/lib/sdk.test.ts @@ -1,5 +1,5 @@ import { Scope } from '@sentry/core'; -import { Client, Integration } from '@sentry/types'; +import type { Client, Integration } from '@sentry/types'; import { installedIntegrations } from '../../src/integration'; import { initAndBind } from '../../src/sdk'; diff --git a/packages/core/test/lib/transports/base.test.ts b/packages/core/test/lib/transports/base.test.ts index 27f21b9391b1..4f4072803f7b 100644 --- a/packages/core/test/lib/transports/base.test.ts +++ b/packages/core/test/lib/transports/base.test.ts @@ -1,5 +1,6 @@ -import { AttachmentItem, EventEnvelope, EventItem, TransportMakeRequestResponse } from '@sentry/types'; -import { createEnvelope, PromiseBuffer, resolvedSyncPromise, serializeEnvelope } from '@sentry/utils'; +import type { AttachmentItem, EventEnvelope, EventItem, TransportMakeRequestResponse } from '@sentry/types'; +import type { PromiseBuffer } from '@sentry/utils'; +import { createEnvelope, resolvedSyncPromise, serializeEnvelope } from '@sentry/utils'; import { TextEncoder } from 'util'; import { createTransport } from '../../../src/transports/base'; diff --git a/packages/core/test/mocks/client.ts b/packages/core/test/mocks/client.ts index aec42264ff58..7ca980189e19 100644 --- a/packages/core/test/mocks/client.ts +++ b/packages/core/test/mocks/client.ts @@ -1,4 +1,13 @@ -import { ClientOptions, Event, EventHint, Integration, Outcome, Session, Severity, SeverityLevel } from '@sentry/types'; +import type { + ClientOptions, + Event, + EventHint, + Integration, + Outcome, + Session, + Severity, + SeverityLevel, +} from '@sentry/types'; import { resolvedSyncPromise } from '@sentry/utils'; import { TextEncoder } from 'util'; diff --git a/packages/core/test/mocks/integration.ts b/packages/core/test/mocks/integration.ts index ff15be3f2c4d..ff01158b0632 100644 --- a/packages/core/test/mocks/integration.ts +++ b/packages/core/test/mocks/integration.ts @@ -1,4 +1,4 @@ -import { Event, EventProcessor, Integration } from '@sentry/types'; +import type { Event, EventProcessor, Integration } from '@sentry/types'; import { configureScope, getCurrentHub } from '../../src'; diff --git a/packages/core/test/mocks/transport.ts b/packages/core/test/mocks/transport.ts index 0541dab4a77d..2559e6e2cf2e 100644 --- a/packages/core/test/mocks/transport.ts +++ b/packages/core/test/mocks/transport.ts @@ -1,4 +1,4 @@ -import { Transport } from '@sentry/types'; +import type { Transport } from '@sentry/types'; import { SyncPromise } from '@sentry/utils'; import { TextEncoder } from 'util'; diff --git a/packages/eslint-config-sdk/src/index.js b/packages/eslint-config-sdk/src/index.js index a405a12fc4bb..4076f507c4b3 100644 --- a/packages/eslint-config-sdk/src/index.js +++ b/packages/eslint-config-sdk/src/index.js @@ -55,6 +55,8 @@ module.exports = { // in SDKs, we should make sure that we are correctly preserving class scope. '@typescript-eslint/unbound-method': 'error', + '@typescript-eslint/consistent-type-imports': 'error', + // Private and protected members of a class should be prefixed with a leading underscore. // typeLike declarations (class, interface, typeAlias, enum, typeParameter) should be // PascalCase. diff --git a/packages/gatsby/src/sdk.ts b/packages/gatsby/src/sdk.ts index d3a7cac50a1f..6f4f8a110e3f 100644 --- a/packages/gatsby/src/sdk.ts +++ b/packages/gatsby/src/sdk.ts @@ -1,7 +1,7 @@ import { init as reactInit, SDK_VERSION } from '@sentry/react'; import { getIntegrationsFromOptions } from './utils/integrations'; -import { GatsbyOptions } from './utils/types'; +import type { GatsbyOptions } from './utils/types'; /** * Inits the Sentry Gatsby SDK. diff --git a/packages/gatsby/src/utils/integrations.ts b/packages/gatsby/src/utils/integrations.ts index 3d6f725e3666..96cabb33bb68 100644 --- a/packages/gatsby/src/utils/integrations.ts +++ b/packages/gatsby/src/utils/integrations.ts @@ -1,7 +1,7 @@ import * as Tracing from '@sentry/tracing'; -import { Integration } from '@sentry/types'; +import type { Integration } from '@sentry/types'; -import { GatsbyOptions } from './types'; +import type { GatsbyOptions } from './types'; type UserFnIntegrations = (integrations: Integration[]) => Integration[]; export type UserIntegrations = Integration[] | UserFnIntegrations; diff --git a/packages/gatsby/src/utils/types.ts b/packages/gatsby/src/utils/types.ts index 936a5b3c6ae6..87ba3f7caf46 100644 --- a/packages/gatsby/src/utils/types.ts +++ b/packages/gatsby/src/utils/types.ts @@ -1,3 +1,3 @@ -import { BrowserOptions } from '@sentry/react'; +import type { BrowserOptions } from '@sentry/react'; export type GatsbyOptions = BrowserOptions; diff --git a/packages/gatsby/test/sdk.test.ts b/packages/gatsby/test/sdk.test.ts index a1e60c59fd06..1c4342a13a4b 100644 --- a/packages/gatsby/test/sdk.test.ts +++ b/packages/gatsby/test/sdk.test.ts @@ -1,10 +1,10 @@ import { init, SDK_VERSION } from '@sentry/react'; import { Integrations } from '@sentry/tracing'; -import { Integration } from '@sentry/types'; +import type { Integration } from '@sentry/types'; import { init as gatsbyInit } from '../src/sdk'; -import { UserIntegrations } from '../src/utils/integrations'; -import { GatsbyOptions } from '../src/utils/types'; +import type { UserIntegrations } from '../src/utils/integrations'; +import type { GatsbyOptions } from '../src/utils/types'; jest.mock('@sentry/react', () => { const actual = jest.requireActual('@sentry/react'); diff --git a/packages/hub/test/exports.test.ts b/packages/hub/test/exports.test.ts index a0fabf8061f6..967448a05d4a 100644 --- a/packages/hub/test/exports.test.ts +++ b/packages/hub/test/exports.test.ts @@ -1,5 +1,6 @@ /* eslint-disable deprecation/deprecation */ +import type { Scope } from '../src'; import { captureEvent, captureException, @@ -7,7 +8,6 @@ import { configureScope, getCurrentHub, getHubFromCarrier, - Scope, setContext, setExtra, setExtras, diff --git a/packages/hub/test/hub.test.ts b/packages/hub/test/hub.test.ts index c97670c0e7cb..08b876174eb7 100644 --- a/packages/hub/test/hub.test.ts +++ b/packages/hub/test/hub.test.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/unbound-method */ /* eslint-disable deprecation/deprecation */ -import { Client, Event, EventType } from '@sentry/types'; +import type { Client, Event, EventType } from '@sentry/types'; import { getCurrentHub, Hub, Scope } from '../src'; diff --git a/packages/hub/test/scope.test.ts b/packages/hub/test/scope.test.ts index 468d29dee030..d2686afb6477 100644 --- a/packages/hub/test/scope.test.ts +++ b/packages/hub/test/scope.test.ts @@ -1,6 +1,6 @@ /* eslint-disable deprecation/deprecation */ -import { Event, EventHint, RequestSessionStatus } from '@sentry/types'; +import type { Event, EventHint, RequestSessionStatus } from '@sentry/types'; import { GLOBAL_OBJ } from '@sentry/utils'; import { addGlobalEventProcessor, Scope } from '../src'; diff --git a/packages/hub/test/session.test.ts b/packages/hub/test/session.test.ts index 9de03fa34940..fd8a1a58c359 100644 --- a/packages/hub/test/session.test.ts +++ b/packages/hub/test/session.test.ts @@ -1,6 +1,6 @@ /* eslint-disable deprecation/deprecation */ -import { SessionContext } from '@sentry/types'; +import type { SessionContext } from '@sentry/types'; import { timestampInSeconds } from '@sentry/utils'; import { closeSession, makeSession, updateSession } from '../src'; diff --git a/packages/hub/test/sessionflusher.test.ts b/packages/hub/test/sessionflusher.test.ts index b743eeca1e70..0079e56087b4 100644 --- a/packages/hub/test/sessionflusher.test.ts +++ b/packages/hub/test/sessionflusher.test.ts @@ -1,6 +1,6 @@ /* eslint-disable deprecation/deprecation */ -import { Client } from '@sentry/types'; +import type { Client } from '@sentry/types'; import { SessionFlusher } from '../src'; diff --git a/packages/integration-tests/package.json b/packages/integration-tests/package.json index 9117874b84a4..d9377403dbf4 100644 --- a/packages/integration-tests/package.json +++ b/packages/integration-tests/package.json @@ -13,6 +13,9 @@ "lint": "run-s lint:prettier lint:eslint", "lint:eslint": "eslint . --cache --cache-location '../../eslintcache/' --format stylish", "lint:prettier": "prettier --check \"{suites,utils}/**/*.ts\"", + "fix": "run-s fix:eslint fix:prettier", + "fix:eslint": "eslint . --format stylish --fix", + "fix:prettier": "prettier --write \"{suites,utils}/**/*.ts\"", "type-check": "tsc", "pretest": "yarn clean && yarn type-check", "test": "playwright test ./suites", diff --git a/packages/integration-tests/playwright.config.ts b/packages/integration-tests/playwright.config.ts index 4ae274b13f22..39c4dd6a213e 100644 --- a/packages/integration-tests/playwright.config.ts +++ b/packages/integration-tests/playwright.config.ts @@ -1,4 +1,4 @@ -import { PlaywrightTestConfig } from '@playwright/test'; +import type { PlaywrightTestConfig } from '@playwright/test'; const config: PlaywrightTestConfig = { retries: 2, diff --git a/packages/integration-tests/suites/integrations/httpclient/fetch/test.ts b/packages/integration-tests/suites/integrations/httpclient/fetch/test.ts index d399dd209a84..3b845c8a8029 100644 --- a/packages/integration-tests/suites/integrations/httpclient/fetch/test.ts +++ b/packages/integration-tests/suites/integrations/httpclient/fetch/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { Event } from '@sentry/types'; +import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; diff --git a/packages/integration-tests/suites/integrations/httpclient/xhr/test.ts b/packages/integration-tests/suites/integrations/httpclient/xhr/test.ts index 72167f73ca2b..a6dfcc755ae0 100644 --- a/packages/integration-tests/suites/integrations/httpclient/xhr/test.ts +++ b/packages/integration-tests/suites/integrations/httpclient/xhr/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { Event } from '@sentry/types'; +import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; diff --git a/packages/integration-tests/suites/public-api/addBreadcrumb/empty_obj/test.ts b/packages/integration-tests/suites/public-api/addBreadcrumb/empty_obj/test.ts index 3fea4283b71e..47435f3d57be 100644 --- a/packages/integration-tests/suites/public-api/addBreadcrumb/empty_obj/test.ts +++ b/packages/integration-tests/suites/public-api/addBreadcrumb/empty_obj/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { Event } from '@sentry/types'; +import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; diff --git a/packages/integration-tests/suites/public-api/addBreadcrumb/multiple_breadcrumbs/test.ts b/packages/integration-tests/suites/public-api/addBreadcrumb/multiple_breadcrumbs/test.ts index d864be4f9073..bbaafa997fda 100644 --- a/packages/integration-tests/suites/public-api/addBreadcrumb/multiple_breadcrumbs/test.ts +++ b/packages/integration-tests/suites/public-api/addBreadcrumb/multiple_breadcrumbs/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { Event } from '@sentry/types'; +import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; diff --git a/packages/integration-tests/suites/public-api/addBreadcrumb/simple_breadcrumb/test.ts b/packages/integration-tests/suites/public-api/addBreadcrumb/simple_breadcrumb/test.ts index 224d4dba0932..d6a24b8cc167 100644 --- a/packages/integration-tests/suites/public-api/addBreadcrumb/simple_breadcrumb/test.ts +++ b/packages/integration-tests/suites/public-api/addBreadcrumb/simple_breadcrumb/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { Event } from '@sentry/types'; +import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; diff --git a/packages/integration-tests/suites/public-api/addBreadcrumb/undefined_arg/test.ts b/packages/integration-tests/suites/public-api/addBreadcrumb/undefined_arg/test.ts index 5e5ec669a7dc..ec04b9027783 100644 --- a/packages/integration-tests/suites/public-api/addBreadcrumb/undefined_arg/test.ts +++ b/packages/integration-tests/suites/public-api/addBreadcrumb/undefined_arg/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { Event } from '@sentry/types'; +import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; diff --git a/packages/integration-tests/suites/public-api/captureException/empty_obj/test.ts b/packages/integration-tests/suites/public-api/captureException/empty_obj/test.ts index a41fdcc6a6e1..6ce86bfe7aeb 100644 --- a/packages/integration-tests/suites/public-api/captureException/empty_obj/test.ts +++ b/packages/integration-tests/suites/public-api/captureException/empty_obj/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { Event } from '@sentry/types'; +import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; diff --git a/packages/integration-tests/suites/public-api/captureException/simple_error/test.ts b/packages/integration-tests/suites/public-api/captureException/simple_error/test.ts index 49627e826726..7e884c6eb6dc 100644 --- a/packages/integration-tests/suites/public-api/captureException/simple_error/test.ts +++ b/packages/integration-tests/suites/public-api/captureException/simple_error/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { Event } from '@sentry/types'; +import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; diff --git a/packages/integration-tests/suites/public-api/captureException/undefined_arg/test.ts b/packages/integration-tests/suites/public-api/captureException/undefined_arg/test.ts index 52e2ef5c21f8..5bf560e93707 100644 --- a/packages/integration-tests/suites/public-api/captureException/undefined_arg/test.ts +++ b/packages/integration-tests/suites/public-api/captureException/undefined_arg/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { Event } from '@sentry/types'; +import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; diff --git a/packages/integration-tests/suites/public-api/captureMessage/simple_message/test.ts b/packages/integration-tests/suites/public-api/captureMessage/simple_message/test.ts index cfd5580653ac..60cc4a19f089 100644 --- a/packages/integration-tests/suites/public-api/captureMessage/simple_message/test.ts +++ b/packages/integration-tests/suites/public-api/captureMessage/simple_message/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { Event } from '@sentry/types'; +import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; diff --git a/packages/integration-tests/suites/public-api/captureMessage/with_level/test.ts b/packages/integration-tests/suites/public-api/captureMessage/with_level/test.ts index da17ff07a77e..1422ed64bf31 100644 --- a/packages/integration-tests/suites/public-api/captureMessage/with_level/test.ts +++ b/packages/integration-tests/suites/public-api/captureMessage/with_level/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { Event } from '@sentry/types'; +import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getMultipleSentryEnvelopeRequests } from '../../../../utils/helpers'; diff --git a/packages/integration-tests/suites/public-api/configureScope/clear_scope/test.ts b/packages/integration-tests/suites/public-api/configureScope/clear_scope/test.ts index e86d5e11aef6..02a82ffef26d 100644 --- a/packages/integration-tests/suites/public-api/configureScope/clear_scope/test.ts +++ b/packages/integration-tests/suites/public-api/configureScope/clear_scope/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { Event } from '@sentry/types'; +import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; diff --git a/packages/integration-tests/suites/public-api/configureScope/set_properties/test.ts b/packages/integration-tests/suites/public-api/configureScope/set_properties/test.ts index 992dd7c31043..ba31c0ca18e7 100644 --- a/packages/integration-tests/suites/public-api/configureScope/set_properties/test.ts +++ b/packages/integration-tests/suites/public-api/configureScope/set_properties/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { Event } from '@sentry/types'; +import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; diff --git a/packages/integration-tests/suites/public-api/init/console/test.ts b/packages/integration-tests/suites/public-api/init/console/test.ts index 1f71332533c8..5fc32e430caf 100644 --- a/packages/integration-tests/suites/public-api/init/console/test.ts +++ b/packages/integration-tests/suites/public-api/init/console/test.ts @@ -1,5 +1,6 @@ /* eslint-disable no-console */ -import { ConsoleMessage, expect } from '@playwright/test'; +import type { ConsoleMessage } from '@playwright/test'; +import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; diff --git a/packages/integration-tests/suites/public-api/instrumentation/eventListener/test.ts b/packages/integration-tests/suites/public-api/instrumentation/eventListener/test.ts index 71eb3e7023b3..8d821a14906e 100644 --- a/packages/integration-tests/suites/public-api/instrumentation/eventListener/test.ts +++ b/packages/integration-tests/suites/public-api/instrumentation/eventListener/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { Event } from '@sentry/types'; +import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; diff --git a/packages/integration-tests/suites/public-api/setContext/multiple_contexts/test.ts b/packages/integration-tests/suites/public-api/setContext/multiple_contexts/test.ts index 6d00519cbad4..d1e85f49cf27 100644 --- a/packages/integration-tests/suites/public-api/setContext/multiple_contexts/test.ts +++ b/packages/integration-tests/suites/public-api/setContext/multiple_contexts/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { Event } from '@sentry/types'; +import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; diff --git a/packages/integration-tests/suites/public-api/setContext/non_serializable_context/test.ts b/packages/integration-tests/suites/public-api/setContext/non_serializable_context/test.ts index 54fea2c68908..3c6d17dbdb03 100644 --- a/packages/integration-tests/suites/public-api/setContext/non_serializable_context/test.ts +++ b/packages/integration-tests/suites/public-api/setContext/non_serializable_context/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { Event } from '@sentry/types'; +import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; diff --git a/packages/integration-tests/suites/public-api/setContext/simple_context/test.ts b/packages/integration-tests/suites/public-api/setContext/simple_context/test.ts index a39f838f5b18..37e91dbf314d 100644 --- a/packages/integration-tests/suites/public-api/setContext/simple_context/test.ts +++ b/packages/integration-tests/suites/public-api/setContext/simple_context/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { Event } from '@sentry/types'; +import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; diff --git a/packages/integration-tests/suites/public-api/setExtra/multiple_extras/test.ts b/packages/integration-tests/suites/public-api/setExtra/multiple_extras/test.ts index 82a2b4ce21e5..17e5fa9790c7 100644 --- a/packages/integration-tests/suites/public-api/setExtra/multiple_extras/test.ts +++ b/packages/integration-tests/suites/public-api/setExtra/multiple_extras/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { Event } from '@sentry/types'; +import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; diff --git a/packages/integration-tests/suites/public-api/setExtra/non_serializable_extra/test.ts b/packages/integration-tests/suites/public-api/setExtra/non_serializable_extra/test.ts index 168bfc88e2c5..67b2e9cd162c 100644 --- a/packages/integration-tests/suites/public-api/setExtra/non_serializable_extra/test.ts +++ b/packages/integration-tests/suites/public-api/setExtra/non_serializable_extra/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { Event } from '@sentry/types'; +import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; diff --git a/packages/integration-tests/suites/public-api/setExtra/simple_extra/test.ts b/packages/integration-tests/suites/public-api/setExtra/simple_extra/test.ts index 95a6184e95a9..3f77998cd758 100644 --- a/packages/integration-tests/suites/public-api/setExtra/simple_extra/test.ts +++ b/packages/integration-tests/suites/public-api/setExtra/simple_extra/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { Event } from '@sentry/types'; +import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; diff --git a/packages/integration-tests/suites/public-api/setExtras/consecutive_calls/test.ts b/packages/integration-tests/suites/public-api/setExtras/consecutive_calls/test.ts index 641325affa34..9caae5b0bc7c 100644 --- a/packages/integration-tests/suites/public-api/setExtras/consecutive_calls/test.ts +++ b/packages/integration-tests/suites/public-api/setExtras/consecutive_calls/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { Event } from '@sentry/types'; +import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; diff --git a/packages/integration-tests/suites/public-api/setExtras/multiple_extras/test.ts b/packages/integration-tests/suites/public-api/setExtras/multiple_extras/test.ts index 1e238739d8a1..fdea76a5344a 100644 --- a/packages/integration-tests/suites/public-api/setExtras/multiple_extras/test.ts +++ b/packages/integration-tests/suites/public-api/setExtras/multiple_extras/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { Event } from '@sentry/types'; +import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; diff --git a/packages/integration-tests/suites/public-api/setTag/with_non_primitives/test.ts b/packages/integration-tests/suites/public-api/setTag/with_non_primitives/test.ts index e4a1f9b19bd4..3eff6ec07858 100644 --- a/packages/integration-tests/suites/public-api/setTag/with_non_primitives/test.ts +++ b/packages/integration-tests/suites/public-api/setTag/with_non_primitives/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { Event } from '@sentry/types'; +import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; diff --git a/packages/integration-tests/suites/public-api/setTag/with_primitives/test.ts b/packages/integration-tests/suites/public-api/setTag/with_primitives/test.ts index ba2b648ad913..915c39a51596 100644 --- a/packages/integration-tests/suites/public-api/setTag/with_primitives/test.ts +++ b/packages/integration-tests/suites/public-api/setTag/with_primitives/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { Event } from '@sentry/types'; +import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; diff --git a/packages/integration-tests/suites/public-api/setTags/with_non_primitives/test.ts b/packages/integration-tests/suites/public-api/setTags/with_non_primitives/test.ts index e4a1f9b19bd4..3eff6ec07858 100644 --- a/packages/integration-tests/suites/public-api/setTags/with_non_primitives/test.ts +++ b/packages/integration-tests/suites/public-api/setTags/with_non_primitives/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { Event } from '@sentry/types'; +import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; diff --git a/packages/integration-tests/suites/public-api/setTags/with_primitives/test.ts b/packages/integration-tests/suites/public-api/setTags/with_primitives/test.ts index ba2b648ad913..915c39a51596 100644 --- a/packages/integration-tests/suites/public-api/setTags/with_primitives/test.ts +++ b/packages/integration-tests/suites/public-api/setTags/with_primitives/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { Event } from '@sentry/types'; +import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; diff --git a/packages/integration-tests/suites/public-api/setUser/unset_user/test.ts b/packages/integration-tests/suites/public-api/setUser/unset_user/test.ts index 2aed7beb60aa..193a10b8677d 100644 --- a/packages/integration-tests/suites/public-api/setUser/unset_user/test.ts +++ b/packages/integration-tests/suites/public-api/setUser/unset_user/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { Event } from '@sentry/types'; +import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getMultipleSentryEnvelopeRequests } from '../../../../utils/helpers'; diff --git a/packages/integration-tests/suites/public-api/setUser/update_user/test.ts b/packages/integration-tests/suites/public-api/setUser/update_user/test.ts index fa846f0221c2..f673280d5c0b 100644 --- a/packages/integration-tests/suites/public-api/setUser/update_user/test.ts +++ b/packages/integration-tests/suites/public-api/setUser/update_user/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { Event } from '@sentry/types'; +import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getMultipleSentryEnvelopeRequests } from '../../../../utils/helpers'; diff --git a/packages/integration-tests/suites/public-api/startTransaction/basic_usage/test.ts b/packages/integration-tests/suites/public-api/startTransaction/basic_usage/test.ts index 144f0ae29211..7b42d280248d 100644 --- a/packages/integration-tests/suites/public-api/startTransaction/basic_usage/test.ts +++ b/packages/integration-tests/suites/public-api/startTransaction/basic_usage/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { Event } from '@sentry/types'; +import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; diff --git a/packages/integration-tests/suites/public-api/startTransaction/circular_data/test.ts b/packages/integration-tests/suites/public-api/startTransaction/circular_data/test.ts index 7ad0ec532fb7..88ed63b08864 100644 --- a/packages/integration-tests/suites/public-api/startTransaction/circular_data/test.ts +++ b/packages/integration-tests/suites/public-api/startTransaction/circular_data/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { Event } from '@sentry/types'; +import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; diff --git a/packages/integration-tests/suites/public-api/startTransaction/setMeasurement/test.ts b/packages/integration-tests/suites/public-api/startTransaction/setMeasurement/test.ts index e91231093bf3..fecda098bf43 100644 --- a/packages/integration-tests/suites/public-api/startTransaction/setMeasurement/test.ts +++ b/packages/integration-tests/suites/public-api/startTransaction/setMeasurement/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { Event } from '@sentry/types'; +import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; diff --git a/packages/integration-tests/suites/public-api/withScope/nested_scopes/test.ts b/packages/integration-tests/suites/public-api/withScope/nested_scopes/test.ts index 1cc024e799fc..415a173bc64f 100644 --- a/packages/integration-tests/suites/public-api/withScope/nested_scopes/test.ts +++ b/packages/integration-tests/suites/public-api/withScope/nested_scopes/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { Event } from '@sentry/types'; +import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getMultipleSentryEnvelopeRequests } from '../../../../utils/helpers'; diff --git a/packages/integration-tests/suites/sessions/start-session/test.ts b/packages/integration-tests/suites/sessions/start-session/test.ts index e4469bd369c1..8a48f161c93b 100644 --- a/packages/integration-tests/suites/sessions/start-session/test.ts +++ b/packages/integration-tests/suites/sessions/start-session/test.ts @@ -1,5 +1,6 @@ -import { expect, Route } from '@playwright/test'; -import { SessionContext } from '@sentry/types'; +import type { Route } from '@playwright/test'; +import { expect } from '@playwright/test'; +import type { SessionContext } from '@sentry/types'; import { sentryTest } from '../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../utils/helpers'; diff --git a/packages/integration-tests/suites/sessions/update-session/test.ts b/packages/integration-tests/suites/sessions/update-session/test.ts index 9634e66c360e..5ce88e4bdc0e 100644 --- a/packages/integration-tests/suites/sessions/update-session/test.ts +++ b/packages/integration-tests/suites/sessions/update-session/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { SessionContext } from '@sentry/types'; +import type { SessionContext } from '@sentry/types'; import { sentryTest } from '../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../utils/helpers'; diff --git a/packages/integration-tests/suites/stacktraces/protocol_containing_fn_identifiers/test.ts b/packages/integration-tests/suites/stacktraces/protocol_containing_fn_identifiers/test.ts index 3f1e01f88070..cd79799df328 100644 --- a/packages/integration-tests/suites/stacktraces/protocol_containing_fn_identifiers/test.ts +++ b/packages/integration-tests/suites/stacktraces/protocol_containing_fn_identifiers/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { Event } from '@sentry/types'; +import type { Event } from '@sentry/types'; import { sentryTest } from '../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../utils/helpers'; diff --git a/packages/integration-tests/suites/stacktraces/protocol_fn_identifiers/test.ts b/packages/integration-tests/suites/stacktraces/protocol_fn_identifiers/test.ts index 4c3e7cfe067e..ce2142169bdc 100644 --- a/packages/integration-tests/suites/stacktraces/protocol_fn_identifiers/test.ts +++ b/packages/integration-tests/suites/stacktraces/protocol_fn_identifiers/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { Event } from '@sentry/types'; +import type { Event } from '@sentry/types'; import { sentryTest } from '../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../utils/helpers'; diff --git a/packages/integration-tests/suites/stacktraces/regular_fn_identifiers/test.ts b/packages/integration-tests/suites/stacktraces/regular_fn_identifiers/test.ts index 1c30a04cd4fc..e7a9f452fe6a 100644 --- a/packages/integration-tests/suites/stacktraces/regular_fn_identifiers/test.ts +++ b/packages/integration-tests/suites/stacktraces/regular_fn_identifiers/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { Event } from '@sentry/types'; +import type { Event } from '@sentry/types'; import { sentryTest } from '../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../utils/helpers'; diff --git a/packages/integration-tests/suites/tracing/browsertracing/backgroundtab-custom/test.ts b/packages/integration-tests/suites/tracing/browsertracing/backgroundtab-custom/test.ts index 8a50d643e0c9..8b201b7d7bbf 100644 --- a/packages/integration-tests/suites/tracing/browsertracing/backgroundtab-custom/test.ts +++ b/packages/integration-tests/suites/tracing/browsertracing/backgroundtab-custom/test.ts @@ -1,5 +1,6 @@ -import { expect, JSHandle } from '@playwright/test'; -import { Event } from '@sentry/types'; +import type { JSHandle } from '@playwright/test'; +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; diff --git a/packages/integration-tests/suites/tracing/browsertracing/backgroundtab-pageload/test.ts b/packages/integration-tests/suites/tracing/browsertracing/backgroundtab-pageload/test.ts index 80e99b0ede13..690e6e857409 100644 --- a/packages/integration-tests/suites/tracing/browsertracing/backgroundtab-pageload/test.ts +++ b/packages/integration-tests/suites/tracing/browsertracing/backgroundtab-pageload/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { Event } from '@sentry/types'; +import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; diff --git a/packages/integration-tests/suites/tracing/browsertracing/interactions/test.ts b/packages/integration-tests/suites/tracing/browsertracing/interactions/test.ts index 9f12b2e76e6d..b9a70ebda3ec 100644 --- a/packages/integration-tests/suites/tracing/browsertracing/interactions/test.ts +++ b/packages/integration-tests/suites/tracing/browsertracing/interactions/test.ts @@ -1,5 +1,6 @@ -import { expect, Route } from '@playwright/test'; -import { Event } from '@sentry/types'; +import type { Route } from '@playwright/test'; +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest, getMultipleSentryEnvelopeRequests } from '../../../../utils/helpers'; diff --git a/packages/integration-tests/suites/tracing/browsertracing/long-tasks-disabled/test.ts b/packages/integration-tests/suites/tracing/browsertracing/long-tasks-disabled/test.ts index a8c8c2b16798..c392258570d9 100644 --- a/packages/integration-tests/suites/tracing/browsertracing/long-tasks-disabled/test.ts +++ b/packages/integration-tests/suites/tracing/browsertracing/long-tasks-disabled/test.ts @@ -1,5 +1,6 @@ -import { expect, Route } from '@playwright/test'; -import { Event } from '@sentry/types'; +import type { Route } from '@playwright/test'; +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; diff --git a/packages/integration-tests/suites/tracing/browsertracing/long-tasks-enabled/test.ts b/packages/integration-tests/suites/tracing/browsertracing/long-tasks-enabled/test.ts index 83bb5911586a..a91e68c8d1bf 100644 --- a/packages/integration-tests/suites/tracing/browsertracing/long-tasks-enabled/test.ts +++ b/packages/integration-tests/suites/tracing/browsertracing/long-tasks-enabled/test.ts @@ -1,5 +1,6 @@ -import { expect, Route } from '@playwright/test'; -import { Event } from '@sentry/types'; +import type { Route } from '@playwright/test'; +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; diff --git a/packages/integration-tests/suites/tracing/browsertracing/meta/test.ts b/packages/integration-tests/suites/tracing/browsertracing/meta/test.ts index 88ae1cd331d4..c447f41c8660 100644 --- a/packages/integration-tests/suites/tracing/browsertracing/meta/test.ts +++ b/packages/integration-tests/suites/tracing/browsertracing/meta/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { Event, EventEnvelopeHeaders } from '@sentry/types'; +import type { Event, EventEnvelopeHeaders } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { envelopeHeaderRequestParser, getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; diff --git a/packages/integration-tests/suites/tracing/browsertracing/navigation/test.ts b/packages/integration-tests/suites/tracing/browsertracing/navigation/test.ts index e8e6cbd3fc78..77157951f494 100644 --- a/packages/integration-tests/suites/tracing/browsertracing/navigation/test.ts +++ b/packages/integration-tests/suites/tracing/browsertracing/navigation/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { Event } from '@sentry/types'; +import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; diff --git a/packages/integration-tests/suites/tracing/browsertracing/pageload/test.ts b/packages/integration-tests/suites/tracing/browsertracing/pageload/test.ts index 6c5877c62e57..fc7de0b067b7 100644 --- a/packages/integration-tests/suites/tracing/browsertracing/pageload/test.ts +++ b/packages/integration-tests/suites/tracing/browsertracing/pageload/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { Event } from '@sentry/types'; +import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; diff --git a/packages/integration-tests/suites/tracing/browsertracing/tracePropagationTargets/customTargets/test.ts b/packages/integration-tests/suites/tracing/browsertracing/tracePropagationTargets/customTargets/test.ts index 5281b549454c..ca1deada91d0 100644 --- a/packages/integration-tests/suites/tracing/browsertracing/tracePropagationTargets/customTargets/test.ts +++ b/packages/integration-tests/suites/tracing/browsertracing/tracePropagationTargets/customTargets/test.ts @@ -1,4 +1,5 @@ -import { expect, Request } from '@playwright/test'; +import type { Request } from '@playwright/test'; +import { expect } from '@playwright/test'; import { sentryTest } from '../../../../../utils/fixtures'; diff --git a/packages/integration-tests/suites/tracing/browsertracing/tracePropagationTargets/customTargetsAndOrigins/test.ts b/packages/integration-tests/suites/tracing/browsertracing/tracePropagationTargets/customTargetsAndOrigins/test.ts index e39093012d9e..6e3fea23e09b 100644 --- a/packages/integration-tests/suites/tracing/browsertracing/tracePropagationTargets/customTargetsAndOrigins/test.ts +++ b/packages/integration-tests/suites/tracing/browsertracing/tracePropagationTargets/customTargetsAndOrigins/test.ts @@ -1,4 +1,5 @@ -import { expect, Request } from '@playwright/test'; +import type { Request } from '@playwright/test'; +import { expect } from '@playwright/test'; import { sentryTest } from '../../../../../utils/fixtures'; diff --git a/packages/integration-tests/suites/tracing/browsertracing/tracePropagationTargets/customTracingOrigins/test.ts b/packages/integration-tests/suites/tracing/browsertracing/tracePropagationTargets/customTracingOrigins/test.ts index bc54b6bb0687..0983c53a5622 100644 --- a/packages/integration-tests/suites/tracing/browsertracing/tracePropagationTargets/customTracingOrigins/test.ts +++ b/packages/integration-tests/suites/tracing/browsertracing/tracePropagationTargets/customTracingOrigins/test.ts @@ -1,4 +1,5 @@ -import { expect, Request } from '@playwright/test'; +import type { Request } from '@playwright/test'; +import { expect } from '@playwright/test'; import { sentryTest } from '../../../../../utils/fixtures'; diff --git a/packages/integration-tests/suites/tracing/browsertracing/tracePropagationTargets/defaultTargetsMatch/test.ts b/packages/integration-tests/suites/tracing/browsertracing/tracePropagationTargets/defaultTargetsMatch/test.ts index 3fe23f53e37c..c046912e6621 100644 --- a/packages/integration-tests/suites/tracing/browsertracing/tracePropagationTargets/defaultTargetsMatch/test.ts +++ b/packages/integration-tests/suites/tracing/browsertracing/tracePropagationTargets/defaultTargetsMatch/test.ts @@ -1,4 +1,5 @@ -import { expect, Request } from '@playwright/test'; +import type { Request } from '@playwright/test'; +import { expect } from '@playwright/test'; import { sentryTest } from '../../../../../utils/fixtures'; diff --git a/packages/integration-tests/suites/tracing/browsertracing/tracePropagationTargets/defaultTargetsNoMatch/test.ts b/packages/integration-tests/suites/tracing/browsertracing/tracePropagationTargets/defaultTargetsNoMatch/test.ts index 50bce1fdc8fa..0767ced38bb5 100644 --- a/packages/integration-tests/suites/tracing/browsertracing/tracePropagationTargets/defaultTargetsNoMatch/test.ts +++ b/packages/integration-tests/suites/tracing/browsertracing/tracePropagationTargets/defaultTargetsNoMatch/test.ts @@ -1,4 +1,5 @@ -import { expect, Request } from '@playwright/test'; +import type { Request } from '@playwright/test'; +import { expect } from '@playwright/test'; import { sentryTest } from '../../../../../utils/fixtures'; diff --git a/packages/integration-tests/suites/tracing/envelope-header-transaction-name/test.ts b/packages/integration-tests/suites/tracing/envelope-header-transaction-name/test.ts index 7ef6bd4304e1..8d5b75e4907c 100644 --- a/packages/integration-tests/suites/tracing/envelope-header-transaction-name/test.ts +++ b/packages/integration-tests/suites/tracing/envelope-header-transaction-name/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { EventEnvelopeHeaders } from '@sentry/types'; +import type { EventEnvelopeHeaders } from '@sentry/types'; import { sentryTest } from '../../../utils/fixtures'; import { envelopeHeaderRequestParser, getFirstSentryEnvelopeRequest } from '../../../utils/helpers'; diff --git a/packages/integration-tests/suites/tracing/envelope-header/test.ts b/packages/integration-tests/suites/tracing/envelope-header/test.ts index fef9ae14a8fb..b70ae62b4903 100644 --- a/packages/integration-tests/suites/tracing/envelope-header/test.ts +++ b/packages/integration-tests/suites/tracing/envelope-header/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { EventEnvelopeHeaders } from '@sentry/types'; +import type { EventEnvelopeHeaders } from '@sentry/types'; import { sentryTest } from '../../../utils/fixtures'; import { envelopeHeaderRequestParser, getFirstSentryEnvelopeRequest } from '../../../utils/helpers'; diff --git a/packages/integration-tests/suites/tracing/metrics/connection-rtt/test.ts b/packages/integration-tests/suites/tracing/metrics/connection-rtt/test.ts index e64fd704682d..273609e97d15 100644 --- a/packages/integration-tests/suites/tracing/metrics/connection-rtt/test.ts +++ b/packages/integration-tests/suites/tracing/metrics/connection-rtt/test.ts @@ -1,5 +1,6 @@ -import { expect, Page } from '@playwright/test'; -import { Event } from '@sentry/types'; +import type { Page } from '@playwright/test'; +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; diff --git a/packages/integration-tests/suites/tracing/metrics/pageload-browser-spans/test.ts b/packages/integration-tests/suites/tracing/metrics/pageload-browser-spans/test.ts index 475ea7fa4840..8b8ca6b0f7b8 100644 --- a/packages/integration-tests/suites/tracing/metrics/pageload-browser-spans/test.ts +++ b/packages/integration-tests/suites/tracing/metrics/pageload-browser-spans/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { Event } from '@sentry/types'; +import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; diff --git a/packages/integration-tests/suites/tracing/metrics/pageload-resource-spans/test.ts b/packages/integration-tests/suites/tracing/metrics/pageload-resource-spans/test.ts index abf9c3c06d16..83ae9580d84d 100644 --- a/packages/integration-tests/suites/tracing/metrics/pageload-resource-spans/test.ts +++ b/packages/integration-tests/suites/tracing/metrics/pageload-resource-spans/test.ts @@ -1,5 +1,6 @@ -import { expect, Route } from '@playwright/test'; -import { Event } from '@sentry/types'; +import type { Route } from '@playwright/test'; +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; diff --git a/packages/integration-tests/suites/tracing/metrics/web-vitals-cls/test.ts b/packages/integration-tests/suites/tracing/metrics/web-vitals-cls/test.ts index f8848891a39c..41bf941f10d3 100644 --- a/packages/integration-tests/suites/tracing/metrics/web-vitals-cls/test.ts +++ b/packages/integration-tests/suites/tracing/metrics/web-vitals-cls/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { Event } from '@sentry/types'; +import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; diff --git a/packages/integration-tests/suites/tracing/metrics/web-vitals-fid/test.ts b/packages/integration-tests/suites/tracing/metrics/web-vitals-fid/test.ts index ff3f125fcf10..966760096add 100644 --- a/packages/integration-tests/suites/tracing/metrics/web-vitals-fid/test.ts +++ b/packages/integration-tests/suites/tracing/metrics/web-vitals-fid/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { Event } from '@sentry/types'; +import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; diff --git a/packages/integration-tests/suites/tracing/metrics/web-vitals-fp-fcp/test.ts b/packages/integration-tests/suites/tracing/metrics/web-vitals-fp-fcp/test.ts index 8162523542ba..b120e580a55c 100644 --- a/packages/integration-tests/suites/tracing/metrics/web-vitals-fp-fcp/test.ts +++ b/packages/integration-tests/suites/tracing/metrics/web-vitals-fp-fcp/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { Event } from '@sentry/types'; +import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; diff --git a/packages/integration-tests/suites/tracing/metrics/web-vitals-lcp/test.ts b/packages/integration-tests/suites/tracing/metrics/web-vitals-lcp/test.ts index 01a8b5c2e268..28b85d518e80 100644 --- a/packages/integration-tests/suites/tracing/metrics/web-vitals-lcp/test.ts +++ b/packages/integration-tests/suites/tracing/metrics/web-vitals-lcp/test.ts @@ -1,5 +1,6 @@ -import { expect, Route } from '@playwright/test'; -import { Event } from '@sentry/types'; +import type { Route } from '@playwright/test'; +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; diff --git a/packages/integration-tests/suites/tracing/metrics/web-vitals-ttfb/test.ts b/packages/integration-tests/suites/tracing/metrics/web-vitals-ttfb/test.ts index 4259c4a034be..81d5f1f7430e 100644 --- a/packages/integration-tests/suites/tracing/metrics/web-vitals-ttfb/test.ts +++ b/packages/integration-tests/suites/tracing/metrics/web-vitals-ttfb/test.ts @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { Event } from '@sentry/types'; +import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; diff --git a/packages/integration-tests/suites/tracing/request/fetch/test.ts b/packages/integration-tests/suites/tracing/request/fetch/test.ts index 2ccf9633507a..a5b8185e20f1 100644 --- a/packages/integration-tests/suites/tracing/request/fetch/test.ts +++ b/packages/integration-tests/suites/tracing/request/fetch/test.ts @@ -1,5 +1,6 @@ -import { expect, Request } from '@playwright/test'; -import { Event } from '@sentry/types'; +import type { Request } from '@playwright/test'; +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getMultipleSentryEnvelopeRequests } from '../../../../utils/helpers'; diff --git a/packages/integration-tests/suites/tracing/request/xhr/test.ts b/packages/integration-tests/suites/tracing/request/xhr/test.ts index a16c0893b0e7..39b2a37749b8 100644 --- a/packages/integration-tests/suites/tracing/request/xhr/test.ts +++ b/packages/integration-tests/suites/tracing/request/xhr/test.ts @@ -1,5 +1,6 @@ -import { expect, Request } from '@playwright/test'; -import { Event } from '@sentry/types'; +import type { Request } from '@playwright/test'; +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; diff --git a/packages/integration-tests/utils/generatePlugin.ts b/packages/integration-tests/utils/generatePlugin.ts index ebd57bf2cfb6..3e00d97f7803 100644 --- a/packages/integration-tests/utils/generatePlugin.ts +++ b/packages/integration-tests/utils/generatePlugin.ts @@ -1,8 +1,8 @@ -import { Package } from '@sentry/types'; +import type { Package } from '@sentry/types'; import { readdirSync, readFileSync } from 'fs'; import HtmlWebpackPlugin, { createHtmlTagObject } from 'html-webpack-plugin'; import path from 'path'; -import { Compiler } from 'webpack'; +import type { Compiler } from 'webpack'; const PACKAGES_DIR = '../../packages'; diff --git a/packages/integration-tests/utils/helpers.ts b/packages/integration-tests/utils/helpers.ts index 9bacf6e9fbde..12078c09039e 100644 --- a/packages/integration-tests/utils/helpers.ts +++ b/packages/integration-tests/utils/helpers.ts @@ -1,5 +1,5 @@ -import { Page, Request } from '@playwright/test'; -import { Event, EventEnvelopeHeaders } from '@sentry/types'; +import type { Page, Request } from '@playwright/test'; +import type { Event, EventEnvelopeHeaders } from '@sentry/types'; const envelopeUrlRegex = /\.sentry\.io\/api\/\d+\/envelope\//; diff --git a/packages/integration-tests/webpack.config.ts b/packages/integration-tests/webpack.config.ts index ff065a044208..ddf31bc897c4 100644 --- a/packages/integration-tests/webpack.config.ts +++ b/packages/integration-tests/webpack.config.ts @@ -1,4 +1,4 @@ -import { Configuration } from 'webpack'; +import type { Configuration } from 'webpack'; const config = function (userConfig: Record): Configuration { return { diff --git a/packages/integrations/src/captureconsole.ts b/packages/integrations/src/captureconsole.ts index ea9f2df40837..980bc74dcad6 100644 --- a/packages/integrations/src/captureconsole.ts +++ b/packages/integrations/src/captureconsole.ts @@ -1,4 +1,4 @@ -import { EventProcessor, Hub, Integration } from '@sentry/types'; +import type { EventProcessor, Hub, Integration } from '@sentry/types'; import { CONSOLE_LEVELS, fill, GLOBAL_OBJ, safeJoin, severityLevelFromString } from '@sentry/utils'; /** Send Console API calls as Sentry Events */ diff --git a/packages/integrations/src/debug.ts b/packages/integrations/src/debug.ts index efa1beba35c9..950610e531a0 100644 --- a/packages/integrations/src/debug.ts +++ b/packages/integrations/src/debug.ts @@ -1,4 +1,4 @@ -import { Event, EventHint, EventProcessor, Hub, Integration } from '@sentry/types'; +import type { Event, EventHint, EventProcessor, Hub, Integration } from '@sentry/types'; import { consoleSandbox } from '@sentry/utils'; interface DebugOptions { diff --git a/packages/integrations/src/dedupe.ts b/packages/integrations/src/dedupe.ts index bd615fc084d2..625cc88f504a 100644 --- a/packages/integrations/src/dedupe.ts +++ b/packages/integrations/src/dedupe.ts @@ -1,4 +1,4 @@ -import { Event, EventProcessor, Exception, Hub, Integration, StackFrame } from '@sentry/types'; +import type { Event, EventProcessor, Exception, Hub, Integration, StackFrame } from '@sentry/types'; import { logger } from '@sentry/utils'; /** Deduplication filter */ diff --git a/packages/integrations/src/extraerrordata.ts b/packages/integrations/src/extraerrordata.ts index ef4e6d49393f..8e694b20f2c1 100644 --- a/packages/integrations/src/extraerrordata.ts +++ b/packages/integrations/src/extraerrordata.ts @@ -1,4 +1,4 @@ -import { Contexts, Event, EventHint, EventProcessor, ExtendedError, Hub, Integration } from '@sentry/types'; +import type { Contexts, Event, EventHint, EventProcessor, ExtendedError, Hub, Integration } from '@sentry/types'; import { addNonEnumerableProperty, isError, isPlainObject, logger, normalize } from '@sentry/utils'; /** JSDoc */ diff --git a/packages/integrations/src/httpclient.ts b/packages/integrations/src/httpclient.ts index 8792ca5901fc..8fab1825fa03 100644 --- a/packages/integrations/src/httpclient.ts +++ b/packages/integrations/src/httpclient.ts @@ -1,4 +1,4 @@ -import { Event as SentryEvent, EventProcessor, Hub, Integration } from '@sentry/types'; +import type { Event as SentryEvent, EventProcessor, Hub, Integration } from '@sentry/types'; import { addExceptionMechanism, fill, GLOBAL_OBJ, logger, supportsNativeFetch } from '@sentry/utils'; export type HttpStatusCodeRange = [number, number] | number; diff --git a/packages/integrations/src/offline.ts b/packages/integrations/src/offline.ts index 2f2029b67336..e20ea96d8e14 100644 --- a/packages/integrations/src/offline.ts +++ b/packages/integrations/src/offline.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ -import { Event, EventProcessor, Hub, Integration } from '@sentry/types'; +import type { Event, EventProcessor, Hub, Integration } from '@sentry/types'; import { GLOBAL_OBJ, logger, normalize, uuid4 } from '@sentry/utils'; import localForage from 'localforage'; diff --git a/packages/integrations/src/reportingobserver.ts b/packages/integrations/src/reportingobserver.ts index 4b8a7d92881c..87f8763cb28a 100644 --- a/packages/integrations/src/reportingobserver.ts +++ b/packages/integrations/src/reportingobserver.ts @@ -1,4 +1,4 @@ -import { EventProcessor, Hub, Integration } from '@sentry/types'; +import type { EventProcessor, Hub, Integration } from '@sentry/types'; import { GLOBAL_OBJ, supportsReportingObserver } from '@sentry/utils'; const WINDOW = GLOBAL_OBJ as typeof GLOBAL_OBJ & Window; diff --git a/packages/integrations/src/rewriteframes.ts b/packages/integrations/src/rewriteframes.ts index b5b830eb557e..afe084f50411 100644 --- a/packages/integrations/src/rewriteframes.ts +++ b/packages/integrations/src/rewriteframes.ts @@ -1,4 +1,4 @@ -import { Event, EventProcessor, Hub, Integration, StackFrame, Stacktrace } from '@sentry/types'; +import type { Event, EventProcessor, Hub, Integration, StackFrame, Stacktrace } from '@sentry/types'; import { basename, relative } from '@sentry/utils'; type StackFrameIteratee = (frame: StackFrame) => StackFrame; diff --git a/packages/integrations/src/sessiontiming.ts b/packages/integrations/src/sessiontiming.ts index 0e45588e0f31..269ca4148ac2 100644 --- a/packages/integrations/src/sessiontiming.ts +++ b/packages/integrations/src/sessiontiming.ts @@ -1,4 +1,4 @@ -import { Event, EventProcessor, Hub, Integration } from '@sentry/types'; +import type { Event, EventProcessor, Hub, Integration } from '@sentry/types'; /** This function adds duration since Sentry was initialized till the time event was sent */ export class SessionTiming implements Integration { diff --git a/packages/integrations/src/transaction.ts b/packages/integrations/src/transaction.ts index e6d0ac24f770..8fbd52a8a871 100644 --- a/packages/integrations/src/transaction.ts +++ b/packages/integrations/src/transaction.ts @@ -1,4 +1,4 @@ -import { Event, EventProcessor, Hub, Integration, StackFrame } from '@sentry/types'; +import type { Event, EventProcessor, Hub, Integration, StackFrame } from '@sentry/types'; /** Add node transaction to the event */ export class Transaction implements Integration { diff --git a/packages/integrations/test/captureconsole.test.ts b/packages/integrations/test/captureconsole.test.ts index 34b715cfc7a1..6ff08df2e46f 100644 --- a/packages/integrations/test/captureconsole.test.ts +++ b/packages/integrations/test/captureconsole.test.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/unbound-method */ -import { Event, Hub, Integration } from '@sentry/types'; +import type { Event, Hub, Integration } from '@sentry/types'; import { CaptureConsole } from '../src/captureconsole'; diff --git a/packages/integrations/test/debug.test.ts b/packages/integrations/test/debug.test.ts index eed7e52d509e..268c03dcc4d5 100644 --- a/packages/integrations/test/debug.test.ts +++ b/packages/integrations/test/debug.test.ts @@ -1,4 +1,4 @@ -import { EventProcessor, Integration } from '@sentry/types'; +import type { EventProcessor, Integration } from '@sentry/types'; import { Debug } from '../src/debug'; diff --git a/packages/integrations/test/dedupe.test.ts b/packages/integrations/test/dedupe.test.ts index 8bc354ffa620..33704907dc83 100644 --- a/packages/integrations/test/dedupe.test.ts +++ b/packages/integrations/test/dedupe.test.ts @@ -1,4 +1,4 @@ -import { Event as SentryEvent, Exception, StackFrame, Stacktrace } from '@sentry/types'; +import type { Event as SentryEvent, Exception, StackFrame, Stacktrace } from '@sentry/types'; import { _shouldDropEvent } from '../src/dedupe'; diff --git a/packages/integrations/test/extraerrordata.test.ts b/packages/integrations/test/extraerrordata.test.ts index 68e38720f761..2ecee1faae19 100644 --- a/packages/integrations/test/extraerrordata.test.ts +++ b/packages/integrations/test/extraerrordata.test.ts @@ -1,4 +1,4 @@ -import { Event as SentryEvent, ExtendedError } from '@sentry/types'; +import type { Event as SentryEvent, ExtendedError } from '@sentry/types'; import { ExtraErrorData } from '../src/extraerrordata'; diff --git a/packages/integrations/test/offline.test.ts b/packages/integrations/test/offline.test.ts index 02e9ccf4457c..d7ca8099be31 100644 --- a/packages/integrations/test/offline.test.ts +++ b/packages/integrations/test/offline.test.ts @@ -1,7 +1,8 @@ import { WINDOW } from '@sentry/browser'; -import { Event, EventProcessor, Hub, Integration, IntegrationClass } from '@sentry/types'; +import type { Event, EventProcessor, Hub, Integration, IntegrationClass } from '@sentry/types'; -import { Item, Offline } from '../src/offline'; +import type { Item } from '../src/offline'; +import { Offline } from '../src/offline'; // mock localforage methods jest.mock('localforage', () => ({ diff --git a/packages/integrations/test/reportingobserver.test.ts b/packages/integrations/test/reportingobserver.test.ts index 91547d5572f8..1298fb617e56 100644 --- a/packages/integrations/test/reportingobserver.test.ts +++ b/packages/integrations/test/reportingobserver.test.ts @@ -1,4 +1,4 @@ -import { Hub, Integration } from '@sentry/types'; +import type { Hub, Integration } from '@sentry/types'; import { ReportingObserver } from '../src/reportingobserver'; diff --git a/packages/integrations/test/rewriteframes.test.ts b/packages/integrations/test/rewriteframes.test.ts index 78aeec7949d4..b19e70143273 100644 --- a/packages/integrations/test/rewriteframes.test.ts +++ b/packages/integrations/test/rewriteframes.test.ts @@ -1,4 +1,4 @@ -import { Event, StackFrame } from '@sentry/types'; +import type { Event, StackFrame } from '@sentry/types'; import { RewriteFrames } from '../src/rewriteframes'; diff --git a/packages/nextjs/src/client/index.ts b/packages/nextjs/src/client/index.ts index 08f5b113ba74..3ea78374290d 100644 --- a/packages/nextjs/src/client/index.ts +++ b/packages/nextjs/src/client/index.ts @@ -1,7 +1,8 @@ import { RewriteFrames } from '@sentry/integrations'; -import { BrowserOptions, configureScope, init as reactInit, Integrations } from '@sentry/react'; +import type { BrowserOptions } from '@sentry/react'; +import { configureScope, init as reactInit, Integrations } from '@sentry/react'; import { BrowserTracing, defaultRequestInstrumentationOptions, hasTracingEnabled } from '@sentry/tracing'; -import { EventProcessor } from '@sentry/types'; +import type { EventProcessor } from '@sentry/types'; import { buildMetadata } from '../common/metadata'; import { addOrUpdateIntegration } from '../common/userIntegrations'; diff --git a/packages/nextjs/src/client/performance.ts b/packages/nextjs/src/client/performance.ts index 39c52f15356f..2212092d5045 100644 --- a/packages/nextjs/src/client/performance.ts +++ b/packages/nextjs/src/client/performance.ts @@ -1,6 +1,6 @@ import { getCurrentHub } from '@sentry/core'; import { WINDOW } from '@sentry/react'; -import { Primitive, TraceparentData, Transaction, TransactionContext, TransactionSource } from '@sentry/types'; +import type { Primitive, TraceparentData, Transaction, TransactionContext, TransactionSource } from '@sentry/types'; import { baggageHeaderToDynamicSamplingContext, extractTraceparentData, diff --git a/packages/nextjs/src/client/tunnelRoute.ts b/packages/nextjs/src/client/tunnelRoute.ts index 29ad48325bb9..81f9f936cc82 100644 --- a/packages/nextjs/src/client/tunnelRoute.ts +++ b/packages/nextjs/src/client/tunnelRoute.ts @@ -1,4 +1,4 @@ -import { BrowserOptions } from '@sentry/react'; +import type { BrowserOptions } from '@sentry/react'; import { dsnFromString, logger } from '@sentry/utils'; const globalWithInjectedValues = global as typeof global & { diff --git a/packages/nextjs/src/common/_error.ts b/packages/nextjs/src/common/_error.ts index 6508eb39ba6e..624aabdb3b98 100644 --- a/packages/nextjs/src/common/_error.ts +++ b/packages/nextjs/src/common/_error.ts @@ -1,6 +1,6 @@ import { captureException, getCurrentHub, withScope } from '@sentry/core'; import { addExceptionMechanism } from '@sentry/utils'; -import { NextPageContext } from 'next'; +import type { NextPageContext } from 'next'; type ContextOrProps = { req?: NextPageContext['req']; diff --git a/packages/nextjs/src/common/metadata.ts b/packages/nextjs/src/common/metadata.ts index 7985ad6aa9cb..9216c6eaec07 100644 --- a/packages/nextjs/src/common/metadata.ts +++ b/packages/nextjs/src/common/metadata.ts @@ -1,5 +1,5 @@ import { SDK_VERSION } from '@sentry/core'; -import { Options, SdkInfo } from '@sentry/types'; +import type { Options, SdkInfo } from '@sentry/types'; const PACKAGE_NAME_PREFIX = 'npm:@sentry/'; diff --git a/packages/nextjs/src/common/userIntegrations.ts b/packages/nextjs/src/common/userIntegrations.ts index 3f84a58fa41b..119d5db500dd 100644 --- a/packages/nextjs/src/common/userIntegrations.ts +++ b/packages/nextjs/src/common/userIntegrations.ts @@ -1,4 +1,4 @@ -import { Integration } from '@sentry/types'; +import type { Integration } from '@sentry/types'; export type UserIntegrationsFunction = (integrations: Integration[]) => Integration[]; export type UserIntegrations = Integration[] | UserIntegrationsFunction; diff --git a/packages/nextjs/src/config/loaders/prefixLoader.ts b/packages/nextjs/src/config/loaders/prefixLoader.ts index f5aa562b5290..19e728f3b6b3 100644 --- a/packages/nextjs/src/config/loaders/prefixLoader.ts +++ b/packages/nextjs/src/config/loaders/prefixLoader.ts @@ -2,7 +2,7 @@ import { escapeStringForRegex } from '@sentry/utils'; import * as fs from 'fs'; import * as path from 'path'; -import { LoaderThis } from './types'; +import type { LoaderThis } from './types'; type LoaderOptions = { templatePrefix: string; diff --git a/packages/nextjs/src/config/loaders/valueInjectionLoader.ts b/packages/nextjs/src/config/loaders/valueInjectionLoader.ts index be8340dccdf3..6a645b0e798d 100644 --- a/packages/nextjs/src/config/loaders/valueInjectionLoader.ts +++ b/packages/nextjs/src/config/loaders/valueInjectionLoader.ts @@ -1,4 +1,4 @@ -import { LoaderThis } from './types'; +import type { LoaderThis } from './types'; type LoaderOptions = { values: Record; diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index 6a815664e527..b79243627432 100644 --- a/packages/nextjs/src/config/types.ts +++ b/packages/nextjs/src/config/types.ts @@ -1,6 +1,6 @@ -import { GLOBAL_OBJ } from '@sentry/utils'; -import { SentryCliPluginOptions } from '@sentry/webpack-plugin'; -import { WebpackPluginInstance } from 'webpack'; +import type { GLOBAL_OBJ } from '@sentry/utils'; +import type { SentryCliPluginOptions } from '@sentry/webpack-plugin'; +import type { WebpackPluginInstance } from 'webpack'; export type SentryWebpackPluginOptions = SentryCliPluginOptions; export type SentryWebpackPlugin = WebpackPluginInstance & { options: SentryWebpackPluginOptions }; diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index 0bba16aa970c..340f4100d611 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -1,14 +1,17 @@ -import { Carrier, getHubFromCarrier, getMainCarrier } from '@sentry/core'; +import type { Carrier } from '@sentry/core'; +import { getHubFromCarrier, getMainCarrier } from '@sentry/core'; import { RewriteFrames } from '@sentry/integrations'; -import { configureScope, getCurrentHub, init as nodeInit, Integrations, NodeOptions } from '@sentry/node'; +import type { NodeOptions } from '@sentry/node'; +import { configureScope, getCurrentHub, init as nodeInit, Integrations } from '@sentry/node'; import { hasTracingEnabled } from '@sentry/tracing'; -import { EventProcessor } from '@sentry/types'; +import type { EventProcessor } from '@sentry/types'; import { escapeStringForRegex, logger } from '@sentry/utils'; import * as domainModule from 'domain'; import * as path from 'path'; import { buildMetadata } from '../common/metadata'; -import { addOrUpdateIntegration, IntegrationWithExclusionOption } from '../common/userIntegrations'; +import type { IntegrationWithExclusionOption } from '../common/userIntegrations'; +import { addOrUpdateIntegration } from '../common/userIntegrations'; import { isBuild } from './utils/isBuild'; export * from '@sentry/node'; diff --git a/packages/nextjs/src/server/utils/responseEnd.ts b/packages/nextjs/src/server/utils/responseEnd.ts index 82b6fb69ea28..ee2d9f803d3b 100644 --- a/packages/nextjs/src/server/utils/responseEnd.ts +++ b/packages/nextjs/src/server/utils/responseEnd.ts @@ -1,9 +1,9 @@ import { flush } from '@sentry/node'; -import { Transaction } from '@sentry/types'; +import type { Transaction } from '@sentry/types'; import { fill, logger } from '@sentry/utils'; -import { ServerResponse } from 'http'; +import type { ServerResponse } from 'http'; -import { ResponseEndMethod, WrappedResponseEndMethod } from '../types'; +import type { ResponseEndMethod, WrappedResponseEndMethod } from '../types'; /** * Wrap `res.end()` so that it closes the transaction and flushes events before letting the request finish. diff --git a/packages/nextjs/src/server/utils/wrapperUtils.ts b/packages/nextjs/src/server/utils/wrapperUtils.ts index 8883f1163f57..ae05b3f16b8d 100644 --- a/packages/nextjs/src/server/utils/wrapperUtils.ts +++ b/packages/nextjs/src/server/utils/wrapperUtils.ts @@ -1,9 +1,9 @@ import { captureException, getCurrentHub, startTransaction } from '@sentry/core'; import { getActiveTransaction } from '@sentry/tracing'; -import { Transaction } from '@sentry/types'; +import type { Transaction } from '@sentry/types'; import { baggageHeaderToDynamicSamplingContext, extractTraceparentData } from '@sentry/utils'; import * as domain from 'domain'; -import { IncomingMessage, ServerResponse } from 'http'; +import type { IncomingMessage, ServerResponse } from 'http'; import { platformSupportsStreaming } from './platformSupportsStreaming'; import { autoEndTransactionOnResponseEnd, flushQueue } from './responseEnd'; diff --git a/packages/nextjs/src/server/withSentryAPI.ts b/packages/nextjs/src/server/withSentryAPI.ts index acb15a4c5514..5d5180dd741e 100644 --- a/packages/nextjs/src/server/withSentryAPI.ts +++ b/packages/nextjs/src/server/withSentryAPI.ts @@ -1,6 +1,6 @@ import { captureException, getCurrentHub, startTransaction } from '@sentry/node'; import { extractTraceparentData, hasTracingEnabled } from '@sentry/tracing'; -import { Transaction } from '@sentry/types'; +import type { Transaction } from '@sentry/types'; import { addExceptionMechanism, baggageHeaderToDynamicSamplingContext, diff --git a/packages/nextjs/src/server/withSentryGetServerSideProps.ts b/packages/nextjs/src/server/withSentryGetServerSideProps.ts index 76c8b848712a..1f41e91ab3e9 100644 --- a/packages/nextjs/src/server/withSentryGetServerSideProps.ts +++ b/packages/nextjs/src/server/withSentryGetServerSideProps.ts @@ -1,6 +1,6 @@ import { hasTracingEnabled } from '@sentry/tracing'; import { dynamicSamplingContextToSentryBaggageHeader } from '@sentry/utils'; -import { GetServerSideProps } from 'next'; +import type { GetServerSideProps } from 'next'; import { isBuild } from './utils/isBuild'; import { diff --git a/packages/nextjs/src/server/withSentryGetStaticProps.ts b/packages/nextjs/src/server/withSentryGetStaticProps.ts index 7220480b4117..96e6f2b81db9 100644 --- a/packages/nextjs/src/server/withSentryGetStaticProps.ts +++ b/packages/nextjs/src/server/withSentryGetStaticProps.ts @@ -1,4 +1,4 @@ -import { GetStaticProps } from 'next'; +import type { GetStaticProps } from 'next'; import { isBuild } from './utils/isBuild'; import { callDataFetcherTraced, withErrorInstrumentation } from './utils/wrapperUtils'; diff --git a/packages/nextjs/src/server/withSentryServerSideAppGetInitialProps.ts b/packages/nextjs/src/server/withSentryServerSideAppGetInitialProps.ts index 89ed3af12dc7..1508661db73e 100644 --- a/packages/nextjs/src/server/withSentryServerSideAppGetInitialProps.ts +++ b/packages/nextjs/src/server/withSentryServerSideAppGetInitialProps.ts @@ -1,6 +1,6 @@ import { hasTracingEnabled } from '@sentry/tracing'; import { dynamicSamplingContextToSentryBaggageHeader } from '@sentry/utils'; -import App from 'next/app'; +import type App from 'next/app'; import { isBuild } from './utils/isBuild'; import { diff --git a/packages/nextjs/src/server/withSentryServerSideDocumentGetInitialProps.ts b/packages/nextjs/src/server/withSentryServerSideDocumentGetInitialProps.ts index d08222b6064c..95c45da095aa 100644 --- a/packages/nextjs/src/server/withSentryServerSideDocumentGetInitialProps.ts +++ b/packages/nextjs/src/server/withSentryServerSideDocumentGetInitialProps.ts @@ -1,5 +1,5 @@ import { hasTracingEnabled } from '@sentry/tracing'; -import Document from 'next/document'; +import type Document from 'next/document'; import { isBuild } from './utils/isBuild'; import { withErrorInstrumentation, withTracedServerSideDataFetcher } from './utils/wrapperUtils'; diff --git a/packages/nextjs/src/server/withSentryServerSideErrorGetInitialProps.ts b/packages/nextjs/src/server/withSentryServerSideErrorGetInitialProps.ts index a0cadad23fa3..8208c46762a9 100644 --- a/packages/nextjs/src/server/withSentryServerSideErrorGetInitialProps.ts +++ b/packages/nextjs/src/server/withSentryServerSideErrorGetInitialProps.ts @@ -1,7 +1,7 @@ import { hasTracingEnabled } from '@sentry/tracing'; import { dynamicSamplingContextToSentryBaggageHeader } from '@sentry/utils'; -import { NextPageContext } from 'next'; -import { ErrorProps } from 'next/error'; +import type { NextPageContext } from 'next'; +import type { ErrorProps } from 'next/error'; import { isBuild } from './utils/isBuild'; import { diff --git a/packages/nextjs/src/server/withSentryServerSideGetInitialProps.ts b/packages/nextjs/src/server/withSentryServerSideGetInitialProps.ts index d3760d792b9d..9cf7cef3db03 100644 --- a/packages/nextjs/src/server/withSentryServerSideGetInitialProps.ts +++ b/packages/nextjs/src/server/withSentryServerSideGetInitialProps.ts @@ -1,6 +1,6 @@ import { hasTracingEnabled } from '@sentry/tracing'; import { dynamicSamplingContextToSentryBaggageHeader } from '@sentry/utils'; -import { NextPage } from 'next'; +import type { NextPage } from 'next'; import { isBuild } from './utils/isBuild'; import { diff --git a/packages/nextjs/test/clientSdk.test.ts b/packages/nextjs/test/clientSdk.test.ts index 4a7577c21a33..044da248dc41 100644 --- a/packages/nextjs/test/clientSdk.test.ts +++ b/packages/nextjs/test/clientSdk.test.ts @@ -2,12 +2,12 @@ import { BaseClient, getCurrentHub } from '@sentry/core'; import * as SentryReact from '@sentry/react'; import { WINDOW } from '@sentry/react'; import { Integrations as TracingIntegrations } from '@sentry/tracing'; -import { Integration } from '@sentry/types'; +import type { Integration } from '@sentry/types'; import { logger } from '@sentry/utils'; import { JSDOM } from 'jsdom'; import { init, Integrations, nextRouterInstrumentation } from '../src/client'; -import { UserIntegrationsFunction } from '../src/common/userIntegrations'; +import type { UserIntegrationsFunction } from '../src/common/userIntegrations'; const { BrowserTracing } = TracingIntegrations; diff --git a/packages/nextjs/test/config/fixtures.ts b/packages/nextjs/test/config/fixtures.ts index 9accd4a0ba64..8edf88a8caf9 100644 --- a/packages/nextjs/test/config/fixtures.ts +++ b/packages/nextjs/test/config/fixtures.ts @@ -1,4 +1,4 @@ -import { +import type { BuildContext, EntryPropertyFunction, ExportedNextConfig, diff --git a/packages/nextjs/test/config/testUtils.ts b/packages/nextjs/test/config/testUtils.ts index 889ac4bb54da..74f854fa08b8 100644 --- a/packages/nextjs/test/config/testUtils.ts +++ b/packages/nextjs/test/config/testUtils.ts @@ -1,6 +1,6 @@ -import { WebpackPluginInstance } from 'webpack'; +import type { WebpackPluginInstance } from 'webpack'; -import { +import type { BuildContext, EntryPropertyFunction, ExportedNextConfig, @@ -9,7 +9,8 @@ import { WebpackConfigObject, WebpackConfigObjectWithModuleRules, } from '../../src/config/types'; -import { constructWebpackConfigFunction, SentryWebpackPlugin } from '../../src/config/webpack'; +import type { SentryWebpackPlugin } from '../../src/config/webpack'; +import { constructWebpackConfigFunction } from '../../src/config/webpack'; import { withSentryConfig } from '../../src/config/withSentryConfig'; import { defaultRuntimePhase, defaultsObject } from './fixtures'; diff --git a/packages/nextjs/test/config/webpack/sentryWebpackPlugin.test.ts b/packages/nextjs/test/config/webpack/sentryWebpackPlugin.test.ts index bd8314663bb7..220e041c3fda 100644 --- a/packages/nextjs/test/config/webpack/sentryWebpackPlugin.test.ts +++ b/packages/nextjs/test/config/webpack/sentryWebpackPlugin.test.ts @@ -2,7 +2,7 @@ import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; -import { BuildContext, ExportedNextConfig } from '../../../src/config/types'; +import type { BuildContext, ExportedNextConfig } from '../../../src/config/types'; import { getUserConfigFile, getWebpackPluginOptions, SentryWebpackPlugin } from '../../../src/config/webpack'; import { clientBuildContext, diff --git a/packages/nextjs/test/config/withSentry.test.ts b/packages/nextjs/test/config/withSentry.test.ts index 0ff2dca6018f..6cbf4ac02c09 100644 --- a/packages/nextjs/test/config/withSentry.test.ts +++ b/packages/nextjs/test/config/withSentry.test.ts @@ -1,7 +1,7 @@ import * as hub from '@sentry/core'; import * as Sentry from '@sentry/node'; -import { Client, ClientOptions } from '@sentry/types'; -import { NextApiHandler, NextApiRequest, NextApiResponse } from 'next'; +import type { Client, ClientOptions } from '@sentry/types'; +import type { NextApiHandler, NextApiRequest, NextApiResponse } from 'next'; import { withSentry } from '../../src/server'; import type { AugmentedNextApiResponse, WrappedNextApiHandler } from '../../src/server/types'; diff --git a/packages/nextjs/test/config/wrappers.test.ts b/packages/nextjs/test/config/wrappers.test.ts index ba742869ac49..8edf2de57567 100644 --- a/packages/nextjs/test/config/wrappers.test.ts +++ b/packages/nextjs/test/config/wrappers.test.ts @@ -1,6 +1,6 @@ import * as SentryCore from '@sentry/core'; import * as SentryTracing from '@sentry/tracing'; -import { IncomingMessage, ServerResponse } from 'http'; +import type { IncomingMessage, ServerResponse } from 'http'; import { withSentryGetServerSideProps, withSentryServerSideGetInitialProps } from '../../src/server'; diff --git a/packages/nextjs/test/performance/client.test.ts b/packages/nextjs/test/performance/client.test.ts index 1cd7d0e787a7..0b3ae0e7437e 100644 --- a/packages/nextjs/test/performance/client.test.ts +++ b/packages/nextjs/test/performance/client.test.ts @@ -1,7 +1,7 @@ import { WINDOW } from '@sentry/react'; -import { Transaction } from '@sentry/types'; +import type { Transaction } from '@sentry/types'; import { JSDOM } from 'jsdom'; -import { NEXT_DATA as NextData } from 'next/dist/next-server/lib/utils'; +import type { NEXT_DATA as NextData } from 'next/dist/next-server/lib/utils'; import { default as Router } from 'next/router'; import { nextRouterInstrumentation } from '../../src/client/performance'; diff --git a/packages/nextjs/test/serverSdk.test.ts b/packages/nextjs/test/serverSdk.test.ts index 56ef03377df6..1a32551317e5 100644 --- a/packages/nextjs/test/serverSdk.test.ts +++ b/packages/nextjs/test/serverSdk.test.ts @@ -1,6 +1,6 @@ import * as SentryNode from '@sentry/node'; import { getCurrentHub, NodeClient } from '@sentry/node'; -import { Integration } from '@sentry/types'; +import type { Integration } from '@sentry/types'; import { GLOBAL_OBJ, logger } from '@sentry/utils'; import * as domain from 'domain'; diff --git a/packages/nextjs/test/types/next.config.ts b/packages/nextjs/test/types/next.config.ts index a447cb167249..36e50a276125 100644 --- a/packages/nextjs/test/types/next.config.ts +++ b/packages/nextjs/test/types/next.config.ts @@ -1,4 +1,4 @@ -import { NextConfig } from 'next'; +import type { NextConfig } from 'next'; import { withSentryConfig } from '../../src/config/withSentryConfig'; diff --git a/packages/nextjs/test/utils/tunnelRoute.test.ts b/packages/nextjs/test/utils/tunnelRoute.test.ts index ae9b085da7ca..0e0a6d48f47c 100644 --- a/packages/nextjs/test/utils/tunnelRoute.test.ts +++ b/packages/nextjs/test/utils/tunnelRoute.test.ts @@ -1,4 +1,4 @@ -import { BrowserOptions } from '@sentry/react'; +import type { BrowserOptions } from '@sentry/react'; import { applyTunnelRouteOption } from '../../src/client/tunnelRoute'; diff --git a/packages/nextjs/test/utils/userIntegrations.test.ts b/packages/nextjs/test/utils/userIntegrations.test.ts index 7a5d8d879fa1..431caa4071a3 100644 --- a/packages/nextjs/test/utils/userIntegrations.test.ts +++ b/packages/nextjs/test/utils/userIntegrations.test.ts @@ -1,5 +1,8 @@ -import type { IntegrationWithExclusionOption as Integration } from '../../src/common/userIntegrations'; -import { addOrUpdateIntegration, UserIntegrations } from '../../src/common/userIntegrations'; +import type { + IntegrationWithExclusionOption as Integration, + UserIntegrations, +} from '../../src/common/userIntegrations'; +import { addOrUpdateIntegration } from '../../src/common/userIntegrations'; type MockIntegrationOptions = { name: string; diff --git a/packages/node-integration-tests/suites/express/sentry-trace/baggage-header-assign/test.ts b/packages/node-integration-tests/suites/express/sentry-trace/baggage-header-assign/test.ts index 2d51f934b7f4..65aea16c60a1 100644 --- a/packages/node-integration-tests/suites/express/sentry-trace/baggage-header-assign/test.ts +++ b/packages/node-integration-tests/suites/express/sentry-trace/baggage-header-assign/test.ts @@ -1,7 +1,7 @@ import * as path from 'path'; import { TestEnv } from '../../../../utils/index'; -import { TestAPIResponse } from '../server'; +import type { TestAPIResponse } from '../server'; test('Should not overwrite baggage if the incoming request already has Sentry baggage data.', async () => { const env = await TestEnv.init(__dirname, `${path.resolve(__dirname, '..')}/server.ts`); diff --git a/packages/node-integration-tests/suites/express/sentry-trace/baggage-header-out/test.ts b/packages/node-integration-tests/suites/express/sentry-trace/baggage-header-out/test.ts index 2650f1d7afc2..71defa704bba 100644 --- a/packages/node-integration-tests/suites/express/sentry-trace/baggage-header-out/test.ts +++ b/packages/node-integration-tests/suites/express/sentry-trace/baggage-header-out/test.ts @@ -1,7 +1,7 @@ import * as path from 'path'; import { TestEnv } from '../../../../utils/index'; -import { TestAPIResponse } from '../server'; +import type { TestAPIResponse } from '../server'; test('should attach a `baggage` header to an outgoing request.', async () => { const env = await TestEnv.init(__dirname, `${path.resolve(__dirname, '.')}/server.ts`); diff --git a/packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors-with-sentry-entries/test.ts b/packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors-with-sentry-entries/test.ts index 3de98d14bf06..0c2c2d39c606 100644 --- a/packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors-with-sentry-entries/test.ts +++ b/packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors-with-sentry-entries/test.ts @@ -1,7 +1,7 @@ import * as path from 'path'; import { TestEnv } from '../../../../utils/index'; -import { TestAPIResponse } from '../server'; +import type { TestAPIResponse } from '../server'; test('should ignore sentry-values in `baggage` header of a third party vendor and overwrite them with incoming DSC', async () => { const env = await TestEnv.init(__dirname, `${path.resolve(__dirname, '.')}/server.ts`); diff --git a/packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors/test.ts b/packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors/test.ts index 2f88610509c5..65d1d3d64c10 100644 --- a/packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors/test.ts +++ b/packages/node-integration-tests/suites/express/sentry-trace/baggage-other-vendors/test.ts @@ -1,7 +1,7 @@ import * as path from 'path'; import { TestEnv } from '../../../../utils/index'; -import { TestAPIResponse } from '../server'; +import type { TestAPIResponse } from '../server'; test('should merge `baggage` header of a third party vendor with the Sentry DSC baggage items', async () => { const env = await TestEnv.init(__dirname, `${path.resolve(__dirname, '.')}/server.ts`); diff --git a/packages/node-integration-tests/suites/express/sentry-trace/baggage-transaction-name/test.ts b/packages/node-integration-tests/suites/express/sentry-trace/baggage-transaction-name/test.ts index d698fd2b7aa6..28e72d92ebe1 100644 --- a/packages/node-integration-tests/suites/express/sentry-trace/baggage-transaction-name/test.ts +++ b/packages/node-integration-tests/suites/express/sentry-trace/baggage-transaction-name/test.ts @@ -1,7 +1,7 @@ import * as path from 'path'; import { TestEnv } from '../../../../utils/index'; -import { TestAPIResponse } from '../server'; +import type { TestAPIResponse } from '../server'; test('Includes transaction in baggage if the transaction name is parameterized', async () => { const env = await TestEnv.init(__dirname, `${path.resolve(__dirname, '.')}/server.ts`); diff --git a/packages/node-integration-tests/suites/express/sentry-trace/trace-header-assign/test.ts b/packages/node-integration-tests/suites/express/sentry-trace/trace-header-assign/test.ts index 366051b9cac8..cb90133b043f 100644 --- a/packages/node-integration-tests/suites/express/sentry-trace/trace-header-assign/test.ts +++ b/packages/node-integration-tests/suites/express/sentry-trace/trace-header-assign/test.ts @@ -2,7 +2,7 @@ import { TRACEPARENT_REGEXP } from '@sentry/utils'; import * as path from 'path'; import { TestEnv } from '../../../../utils/index'; -import { TestAPIResponse } from '../server'; +import type { TestAPIResponse } from '../server'; test('Should assign `sentry-trace` header which sets parent trace id of an outgoing request.', async () => { const env = await TestEnv.init(__dirname, `${path.resolve(__dirname, '..')}/server.ts`); diff --git a/packages/node-integration-tests/suites/express/sentry-trace/trace-header-out/test.ts b/packages/node-integration-tests/suites/express/sentry-trace/trace-header-out/test.ts index bd4e29e7a807..aeccfd23a2e5 100644 --- a/packages/node-integration-tests/suites/express/sentry-trace/trace-header-out/test.ts +++ b/packages/node-integration-tests/suites/express/sentry-trace/trace-header-out/test.ts @@ -2,7 +2,7 @@ import { TRACEPARENT_REGEXP } from '@sentry/utils'; import * as path from 'path'; import { TestEnv } from '../../../../utils/index'; -import { TestAPIResponse } from '../server'; +import type { TestAPIResponse } from '../server'; test('should attach a `sentry-trace` header to an outgoing request.', async () => { const env = await TestEnv.init(__dirname, `${path.resolve(__dirname, '..')}/server.ts`); diff --git a/packages/node-integration-tests/suites/public-api/LocalVariables/test.ts b/packages/node-integration-tests/suites/public-api/LocalVariables/test.ts index 37f155e534a6..6603202a3013 100644 --- a/packages/node-integration-tests/suites/public-api/LocalVariables/test.ts +++ b/packages/node-integration-tests/suites/public-api/LocalVariables/test.ts @@ -1,4 +1,4 @@ -import { Event } from '@sentry/node'; +import type { Event } from '@sentry/node'; import * as childProcess from 'child_process'; import * as path from 'path'; diff --git a/packages/node-integration-tests/suites/public-api/configureScope/clear_scope/test.ts b/packages/node-integration-tests/suites/public-api/configureScope/clear_scope/test.ts index 63c82f7b4054..4891187a10d4 100644 --- a/packages/node-integration-tests/suites/public-api/configureScope/clear_scope/test.ts +++ b/packages/node-integration-tests/suites/public-api/configureScope/clear_scope/test.ts @@ -1,4 +1,4 @@ -import { Event } from '@sentry/node'; +import type { Event } from '@sentry/node'; import { assertSentryEvent, TestEnv } from '../../../../utils'; diff --git a/packages/node-integration-tests/suites/public-api/setContext/multiple-contexts/test.ts b/packages/node-integration-tests/suites/public-api/setContext/multiple-contexts/test.ts index aaf53e26d83f..51ad8a30af17 100644 --- a/packages/node-integration-tests/suites/public-api/setContext/multiple-contexts/test.ts +++ b/packages/node-integration-tests/suites/public-api/setContext/multiple-contexts/test.ts @@ -1,4 +1,4 @@ -import { Event } from '@sentry/node'; +import type { Event } from '@sentry/node'; import { assertSentryEvent, TestEnv } from '../../../../utils'; diff --git a/packages/node-integration-tests/suites/public-api/setContext/non-serializable-context/test.ts b/packages/node-integration-tests/suites/public-api/setContext/non-serializable-context/test.ts index 03c8727d3b7e..85f29fdab4dc 100644 --- a/packages/node-integration-tests/suites/public-api/setContext/non-serializable-context/test.ts +++ b/packages/node-integration-tests/suites/public-api/setContext/non-serializable-context/test.ts @@ -1,4 +1,4 @@ -import { Event } from '@sentry/node'; +import type { Event } from '@sentry/node'; import { assertSentryEvent, TestEnv } from '../../../../utils'; diff --git a/packages/node-integration-tests/suites/public-api/setContext/simple-context/test.ts b/packages/node-integration-tests/suites/public-api/setContext/simple-context/test.ts index 94758652bbfb..89c1128fefb0 100644 --- a/packages/node-integration-tests/suites/public-api/setContext/simple-context/test.ts +++ b/packages/node-integration-tests/suites/public-api/setContext/simple-context/test.ts @@ -1,4 +1,4 @@ -import { Event } from '@sentry/node'; +import type { Event } from '@sentry/node'; import { assertSentryEvent, TestEnv } from '../../../../utils'; diff --git a/packages/node-integration-tests/suites/public-api/setUser/unset_user/test.ts b/packages/node-integration-tests/suites/public-api/setUser/unset_user/test.ts index 1945608623f2..46d4c5180526 100644 --- a/packages/node-integration-tests/suites/public-api/setUser/unset_user/test.ts +++ b/packages/node-integration-tests/suites/public-api/setUser/unset_user/test.ts @@ -1,4 +1,4 @@ -import { Event } from '@sentry/node'; +import type { Event } from '@sentry/node'; import { assertSentryEvent, TestEnv } from '../../../../utils'; diff --git a/packages/node-integration-tests/suites/public-api/withScope/nested-scopes/test.ts b/packages/node-integration-tests/suites/public-api/withScope/nested-scopes/test.ts index a9dcdcf7bd9d..21c842e6a6f7 100644 --- a/packages/node-integration-tests/suites/public-api/withScope/nested-scopes/test.ts +++ b/packages/node-integration-tests/suites/public-api/withScope/nested-scopes/test.ts @@ -1,4 +1,4 @@ -import { Event } from '@sentry/node'; +import type { Event } from '@sentry/node'; import { assertSentryEvent, TestEnv } from '../../../../utils'; diff --git a/packages/node-integration-tests/utils/index.ts b/packages/node-integration-tests/utils/index.ts index 0be096fc65c5..a46e76621e07 100644 --- a/packages/node-integration-tests/utils/index.ts +++ b/packages/node-integration-tests/utils/index.ts @@ -1,11 +1,12 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ import * as Sentry from '@sentry/node'; -import { EnvelopeItemType } from '@sentry/types'; +import type { EnvelopeItemType } from '@sentry/types'; import { logger, parseSemver } from '@sentry/utils'; -import axios, { AxiosRequestConfig } from 'axios'; -import { Express } from 'express'; -import * as http from 'http'; -import { AddressInfo } from 'net'; +import type { AxiosRequestConfig } from 'axios'; +import axios from 'axios'; +import type { Express } from 'express'; +import type * as http from 'http'; +import type { AddressInfo } from 'net'; import nock from 'nock'; import * as path from 'path'; diff --git a/packages/node/src/client.ts b/packages/node/src/client.ts index 66dc8fb4ce27..0b3a925d775e 100644 --- a/packages/node/src/client.ts +++ b/packages/node/src/client.ts @@ -1,11 +1,12 @@ -import { BaseClient, Scope, SDK_VERSION, SessionFlusher } from '@sentry/core'; -import { Event, EventHint, Severity, SeverityLevel } from '@sentry/types'; +import type { Scope } from '@sentry/core'; +import { BaseClient, SDK_VERSION, SessionFlusher } from '@sentry/core'; +import type { Event, EventHint, Severity, SeverityLevel } from '@sentry/types'; import { logger, resolvedSyncPromise } from '@sentry/utils'; import * as os from 'os'; import { TextEncoder } from 'util'; import { eventFromMessage, eventFromUnknownInput } from './eventbuilder'; -import { NodeClientOptions } from './types'; +import type { NodeClientOptions } from './types'; /** * The Sentry Node SDK Client. diff --git a/packages/node/src/eventbuilder.ts b/packages/node/src/eventbuilder.ts index 3d97d2a602ab..f2bb1443a40f 100644 --- a/packages/node/src/eventbuilder.ts +++ b/packages/node/src/eventbuilder.ts @@ -1,5 +1,5 @@ import { getCurrentHub } from '@sentry/core'; -import { +import type { Event, EventHint, Exception, diff --git a/packages/node/src/handlers.ts b/packages/node/src/handlers.ts index a5e5ff3b6f11..1dfd8e4e27c9 100644 --- a/packages/node/src/handlers.ts +++ b/packages/node/src/handlers.ts @@ -1,8 +1,8 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { captureException, getCurrentHub, startTransaction, withScope } from '@sentry/core'; -import { Span } from '@sentry/types'; +import type { Span } from '@sentry/types'; +import type { AddRequestDataToEventOptions } from '@sentry/utils'; import { - AddRequestDataToEventOptions, addRequestDataToTransaction, baggageHeaderToDynamicSamplingContext, dropUndefinedKeys, @@ -12,9 +12,9 @@ import { logger, } from '@sentry/utils'; import * as domain from 'domain'; -import * as http from 'http'; +import type * as http from 'http'; -import { NodeClient } from './client'; +import type { NodeClient } from './client'; import { extractRequestData } from './requestdata'; // TODO (v8 / XXX) Remove this import import type { ParseRequestOptions } from './requestDataDeprecated'; diff --git a/packages/node/src/integrations/console.ts b/packages/node/src/integrations/console.ts index 9b032c75cced..da0b684c4992 100644 --- a/packages/node/src/integrations/console.ts +++ b/packages/node/src/integrations/console.ts @@ -1,5 +1,5 @@ import { getCurrentHub } from '@sentry/core'; -import { Integration } from '@sentry/types'; +import type { Integration } from '@sentry/types'; import { fill, severityLevelFromString } from '@sentry/utils'; import * as util from 'util'; diff --git a/packages/node/src/integrations/context.ts b/packages/node/src/integrations/context.ts index c0991720d76e..33282d992716 100644 --- a/packages/node/src/integrations/context.ts +++ b/packages/node/src/integrations/context.ts @@ -1,5 +1,5 @@ /* eslint-disable max-lines */ -import { +import type { AppContext, Contexts, CultureContext, diff --git a/packages/node/src/integrations/contextlines.ts b/packages/node/src/integrations/contextlines.ts index cad196342f28..aa41ae68cdd8 100644 --- a/packages/node/src/integrations/contextlines.ts +++ b/packages/node/src/integrations/contextlines.ts @@ -1,4 +1,4 @@ -import { Event, EventProcessor, Integration, StackFrame } from '@sentry/types'; +import type { Event, EventProcessor, Integration, StackFrame } from '@sentry/types'; import { addContextToFrame } from '@sentry/utils'; import { readFile } from 'fs'; import { LRUMap } from 'lru_map'; diff --git a/packages/node/src/integrations/http.ts b/packages/node/src/integrations/http.ts index 9fbc79c335b4..867c6ad9df24 100644 --- a/packages/node/src/integrations/http.ts +++ b/packages/node/src/integrations/http.ts @@ -1,5 +1,6 @@ -import { getCurrentHub, Hub } from '@sentry/core'; -import { EventProcessor, Integration, Span, TracePropagationTargets } from '@sentry/types'; +import type { Hub } from '@sentry/core'; +import { getCurrentHub } from '@sentry/core'; +import type { EventProcessor, Integration, Span, TracePropagationTargets } from '@sentry/types'; import { dynamicSamplingContextToSentryBaggageHeader, fill, @@ -7,18 +8,12 @@ import { parseSemver, stringMatchesSomePattern, } from '@sentry/utils'; -import * as http from 'http'; -import * as https from 'https'; +import type * as http from 'http'; +import type * as https from 'https'; -import { NodeClient } from '../client'; -import { - cleanSpanDescription, - extractUrl, - isSentryRequest, - normalizeRequestArgs, - RequestMethod, - RequestMethodArgs, -} from './utils/http'; +import type { NodeClient } from '../client'; +import type { RequestMethod, RequestMethodArgs } from './utils/http'; +import { cleanSpanDescription, extractUrl, isSentryRequest, normalizeRequestArgs } from './utils/http'; const NODE_VERSION = parseSemver(process.versions.node); diff --git a/packages/node/src/integrations/linkederrors.ts b/packages/node/src/integrations/linkederrors.ts index 1b0a6bf12565..e2894a128be5 100644 --- a/packages/node/src/integrations/linkederrors.ts +++ b/packages/node/src/integrations/linkederrors.ts @@ -1,8 +1,8 @@ import { addGlobalEventProcessor, getCurrentHub } from '@sentry/core'; -import { Event, EventHint, Exception, ExtendedError, Integration, StackParser } from '@sentry/types'; +import type { Event, EventHint, Exception, ExtendedError, Integration, StackParser } from '@sentry/types'; import { isInstanceOf, resolvedSyncPromise, SyncPromise } from '@sentry/utils'; -import { NodeClient } from '../client'; +import type { NodeClient } from '../client'; import { exceptionFromError } from '../eventbuilder'; import { ContextLines } from './contextlines'; diff --git a/packages/node/src/integrations/localvariables.ts b/packages/node/src/integrations/localvariables.ts index fc82e1a611f0..785c0fc74713 100644 --- a/packages/node/src/integrations/localvariables.ts +++ b/packages/node/src/integrations/localvariables.ts @@ -1,4 +1,4 @@ -import { +import type { ClientOptions, Event, EventProcessor, @@ -8,7 +8,8 @@ import { StackFrame, StackParser, } from '@sentry/types'; -import { Debugger, InspectorNotification, Runtime, Session } from 'inspector'; +import type { Debugger, InspectorNotification, Runtime } from 'inspector'; +import { Session } from 'inspector'; import { LRUMap } from 'lru_map'; export interface DebugSession { diff --git a/packages/node/src/integrations/modules.ts b/packages/node/src/integrations/modules.ts index 5140c5ee253a..376c0b0144f6 100644 --- a/packages/node/src/integrations/modules.ts +++ b/packages/node/src/integrations/modules.ts @@ -1,4 +1,4 @@ -import { EventProcessor, Hub, Integration } from '@sentry/types'; +import type { EventProcessor, Hub, Integration } from '@sentry/types'; import { existsSync, readFileSync } from 'fs'; import { dirname, join } from 'path'; diff --git a/packages/node/src/integrations/onuncaughtexception.ts b/packages/node/src/integrations/onuncaughtexception.ts index e97796a3a33c..2d10ae61d696 100644 --- a/packages/node/src/integrations/onuncaughtexception.ts +++ b/packages/node/src/integrations/onuncaughtexception.ts @@ -1,8 +1,9 @@ -import { getCurrentHub, Scope } from '@sentry/core'; -import { Integration } from '@sentry/types'; +import type { Scope } from '@sentry/core'; +import { getCurrentHub } from '@sentry/core'; +import type { Integration } from '@sentry/types'; import { logger } from '@sentry/utils'; -import { NodeClient } from '../client'; +import type { NodeClient } from '../client'; import { logAndExitProcess } from './utils/errorhandling'; type OnFatalErrorHandler = (firstError: Error, secondError?: Error) => void; diff --git a/packages/node/src/integrations/onunhandledrejection.ts b/packages/node/src/integrations/onunhandledrejection.ts index 2ba9a3d8f205..aa3916931ba7 100644 --- a/packages/node/src/integrations/onunhandledrejection.ts +++ b/packages/node/src/integrations/onunhandledrejection.ts @@ -1,5 +1,6 @@ -import { getCurrentHub, Scope } from '@sentry/core'; -import { Integration } from '@sentry/types'; +import type { Scope } from '@sentry/core'; +import { getCurrentHub } from '@sentry/core'; +import type { Integration } from '@sentry/types'; import { consoleSandbox } from '@sentry/utils'; import { logAndExitProcess } from './utils/errorhandling'; diff --git a/packages/node/src/integrations/requestdata.ts b/packages/node/src/integrations/requestdata.ts index ec091d6907f7..2994b797b221 100644 --- a/packages/node/src/integrations/requestdata.ts +++ b/packages/node/src/integrations/requestdata.ts @@ -1,10 +1,11 @@ // TODO (v8 or v9): Whenever this becomes a default integration for `@sentry/browser`, move this to `@sentry/core`. For // now, we leave it in `@sentry/integrations` so that it doesn't contribute bytes to our CDN bundles. -import { Event, EventProcessor, Hub, Integration, PolymorphicRequest, Transaction } from '@sentry/types'; +import type { Event, EventProcessor, Hub, Integration, PolymorphicRequest, Transaction } from '@sentry/types'; import { extractPathForTransaction } from '@sentry/utils'; -import { addRequestDataToEvent, AddRequestDataToEventOptions, TransactionNamingScheme } from '../requestdata'; +import type { AddRequestDataToEventOptions, TransactionNamingScheme } from '../requestdata'; +import { addRequestDataToEvent } from '../requestdata'; export type RequestDataIntegrationOptions = { /** diff --git a/packages/node/src/integrations/utils/errorhandling.ts b/packages/node/src/integrations/utils/errorhandling.ts index 912b01c55ba9..e4c7a14924a1 100644 --- a/packages/node/src/integrations/utils/errorhandling.ts +++ b/packages/node/src/integrations/utils/errorhandling.ts @@ -1,7 +1,7 @@ import { getCurrentHub } from '@sentry/core'; import { logger } from '@sentry/utils'; -import { NodeClient } from '../../client'; +import type { NodeClient } from '../../client'; const DEFAULT_SHUTDOWN_TIMEOUT = 2000; diff --git a/packages/node/src/integrations/utils/http.ts b/packages/node/src/integrations/utils/http.ts index 067b2db2da9e..1281f9d6329d 100644 --- a/packages/node/src/integrations/utils/http.ts +++ b/packages/node/src/integrations/utils/http.ts @@ -1,7 +1,7 @@ import { getCurrentHub } from '@sentry/core'; import { parseSemver } from '@sentry/utils'; -import * as http from 'http'; -import * as https from 'https'; +import type * as http from 'http'; +import type * as https from 'https'; import { URL } from 'url'; const NODE_VERSION = parseSemver(process.versions.node); diff --git a/packages/node/src/requestDataDeprecated.ts b/packages/node/src/requestDataDeprecated.ts index 7e7ce5c5d241..2a45e71f8e47 100644 --- a/packages/node/src/requestDataDeprecated.ts +++ b/packages/node/src/requestDataDeprecated.ts @@ -6,13 +6,10 @@ /* eslint-disable deprecation/deprecation */ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { Event, ExtractedNodeRequestData, PolymorphicRequest } from '@sentry/types'; +import type { Event, ExtractedNodeRequestData, PolymorphicRequest } from '@sentry/types'; -import { - addRequestDataToEvent, - AddRequestDataToEventOptions, - extractRequestData as _extractRequestData, -} from './requestdata'; +import type { AddRequestDataToEventOptions } from './requestdata'; +import { addRequestDataToEvent, extractRequestData as _extractRequestData } from './requestdata'; /** * @deprecated `Handlers.ExpressRequest` is deprecated and will be removed in v8. Use `PolymorphicRequest` instead. diff --git a/packages/node/src/requestdata.ts b/packages/node/src/requestdata.ts index d72880df9680..d13aa78700f2 100644 --- a/packages/node/src/requestdata.ts +++ b/packages/node/src/requestdata.ts @@ -1,4 +1,10 @@ -import { Event, ExtractedNodeRequestData, PolymorphicRequest, Transaction, TransactionSource } from '@sentry/types'; +import type { + Event, + ExtractedNodeRequestData, + PolymorphicRequest, + Transaction, + TransactionSource, +} from '@sentry/types'; import { isPlainObject, isString, normalize, stripUrlQueryAndFragment } from '@sentry/utils'; import * as cookie from 'cookie'; import * as url from 'url'; diff --git a/packages/node/src/sdk.ts b/packages/node/src/sdk.ts index d70d8ec73180..71ae258826c8 100644 --- a/packages/node/src/sdk.ts +++ b/packages/node/src/sdk.ts @@ -7,7 +7,7 @@ import { Integrations as CoreIntegrations, setHubOnCarrier, } from '@sentry/core'; -import { SessionStatus, StackParser } from '@sentry/types'; +import type { SessionStatus, StackParser } from '@sentry/types'; import { createStackParser, GLOBAL_OBJ, @@ -32,7 +32,7 @@ import { } from './integrations'; import { getModule } from './module'; import { makeNodeTransport } from './transports'; -import { NodeClientOptions, NodeOptions } from './types'; +import type { NodeClientOptions, NodeOptions } from './types'; export const defaultIntegrations = [ // Common diff --git a/packages/node/src/transports/http-module.ts b/packages/node/src/transports/http-module.ts index d3856bcb4b8e..64b255cc869c 100644 --- a/packages/node/src/transports/http-module.ts +++ b/packages/node/src/transports/http-module.ts @@ -1,7 +1,7 @@ -import { IncomingHttpHeaders, RequestOptions as HTTPRequestOptions } from 'http'; -import { RequestOptions as HTTPSRequestOptions } from 'https'; -import { Writable } from 'stream'; -import { URL } from 'url'; +import type { IncomingHttpHeaders, RequestOptions as HTTPRequestOptions } from 'http'; +import type { RequestOptions as HTTPSRequestOptions } from 'https'; +import type { Writable } from 'stream'; +import type { URL } from 'url'; export type HTTPModuleRequestOptions = HTTPRequestOptions | HTTPSRequestOptions | string | URL; diff --git a/packages/node/src/transports/http.ts b/packages/node/src/transports/http.ts index 8116f194cd6b..a16f78e79a6f 100644 --- a/packages/node/src/transports/http.ts +++ b/packages/node/src/transports/http.ts @@ -1,5 +1,5 @@ import { createTransport } from '@sentry/core'; -import { +import type { BaseTransportOptions, Transport, TransportMakeRequestResponse, @@ -12,7 +12,7 @@ import { Readable } from 'stream'; import { URL } from 'url'; import { createGzip } from 'zlib'; -import { HTTPModule } from './http-module'; +import type { HTTPModule } from './http-module'; export interface NodeTransportOptions extends BaseTransportOptions { /** Define custom headers */ diff --git a/packages/node/src/types.ts b/packages/node/src/types.ts index 7b71d7bab988..6acdbb4b2566 100644 --- a/packages/node/src/types.ts +++ b/packages/node/src/types.ts @@ -1,6 +1,6 @@ -import { ClientOptions, Options, TracePropagationTargets } from '@sentry/types'; +import type { ClientOptions, Options, TracePropagationTargets } from '@sentry/types'; -import { NodeTransportOptions } from './transports'; +import type { NodeTransportOptions } from './transports'; export interface BaseNodeOptions { /** diff --git a/packages/node/test/client.test.ts b/packages/node/test/client.test.ts index a219a307f9d9..a996b5408288 100644 --- a/packages/node/test/client.test.ts +++ b/packages/node/test/client.test.ts @@ -1,5 +1,5 @@ import { Scope, SessionFlusher } from '@sentry/core'; -import { Event, EventHint } from '@sentry/types'; +import type { Event, EventHint } from '@sentry/types'; import * as os from 'os'; import { NodeClient } from '../src'; diff --git a/packages/node/test/context-lines.test.ts b/packages/node/test/context-lines.test.ts index 2b0fef8206b8..de6f6f382cd8 100644 --- a/packages/node/test/context-lines.test.ts +++ b/packages/node/test/context-lines.test.ts @@ -1,4 +1,4 @@ -import { StackFrame } from '@sentry/types'; +import type { StackFrame } from '@sentry/types'; import * as fs from 'fs'; import { parseStackFrames } from '../src/eventbuilder'; diff --git a/packages/node/test/eventbuilders.test.ts b/packages/node/test/eventbuilders.test.ts index 49f8e8156c5c..46dfc02a3c33 100644 --- a/packages/node/test/eventbuilders.test.ts +++ b/packages/node/test/eventbuilders.test.ts @@ -1,4 +1,4 @@ -import { Client } from '@sentry/types'; +import type { Client } from '@sentry/types'; import { defaultStackParser, Scope } from '../src'; import { eventFromUnknownInput } from '../src/eventbuilder'; diff --git a/packages/node/test/handlers.test.ts b/packages/node/test/handlers.test.ts index 1bf422f59621..9145305d6c1f 100644 --- a/packages/node/test/handlers.test.ts +++ b/packages/node/test/handlers.test.ts @@ -1,7 +1,7 @@ import * as sentryCore from '@sentry/core'; import { Hub, makeMain, Scope } from '@sentry/core'; import { Transaction } from '@sentry/tracing'; -import { Event } from '@sentry/types'; +import type { Event } from '@sentry/types'; import { SentryError } from '@sentry/utils'; import * as http from 'http'; diff --git a/packages/node/test/helper/node-client-options.ts b/packages/node/test/helper/node-client-options.ts index d6973e4a437d..9103174ad813 100644 --- a/packages/node/test/helper/node-client-options.ts +++ b/packages/node/test/helper/node-client-options.ts @@ -1,7 +1,7 @@ import { createTransport } from '@sentry/core'; import { resolvedSyncPromise } from '@sentry/utils'; -import { NodeClientOptions } from '../../src/types'; +import type { NodeClientOptions } from '../../src/types'; export function getDefaultNodeClientOptions(options: Partial = {}): NodeClientOptions { return { diff --git a/packages/node/test/index.test.ts b/packages/node/test/index.test.ts index d345998d3628..6ca834874d2e 100644 --- a/packages/node/test/index.test.ts +++ b/packages/node/test/index.test.ts @@ -1,22 +1,21 @@ import { getMainCarrier, initAndBind, SDK_VERSION } from '@sentry/core'; -import { EventHint, Integration } from '@sentry/types'; +import type { EventHint, Integration } from '@sentry/types'; import * as domain from 'domain'; +import type { Event, Scope } from '../src'; import { addBreadcrumb, captureEvent, captureException, captureMessage, configureScope, - Event, getCurrentHub, init, NodeClient, - Scope, } from '../src'; import { ContextLines, LinkedErrors } from '../src/integrations'; import { defaultStackParser } from '../src/sdk'; -import { NodeClientOptions } from '../src/types'; +import type { NodeClientOptions } from '../src/types'; import { getDefaultNodeClientOptions } from './helper/node-client-options'; jest.mock('@sentry/core', () => { diff --git a/packages/node/test/integrations/http.test.ts b/packages/node/test/integrations/http.test.ts index c717550710cf..ab2370ec19c6 100644 --- a/packages/node/test/integrations/http.test.ts +++ b/packages/node/test/integrations/http.test.ts @@ -1,17 +1,18 @@ import * as sentryCore from '@sentry/core'; import { Hub } from '@sentry/core'; -import { addExtensionMethods, Span, TRACEPARENT_REGEXP, Transaction } from '@sentry/tracing'; -import { TransactionContext } from '@sentry/types'; +import type { Span, Transaction } from '@sentry/tracing'; +import { addExtensionMethods, TRACEPARENT_REGEXP } from '@sentry/tracing'; +import type { TransactionContext } from '@sentry/types'; import { logger, parseSemver } from '@sentry/utils'; import * as http from 'http'; import * as https from 'https'; import * as HttpsProxyAgent from 'https-proxy-agent'; import * as nock from 'nock'; -import { Breadcrumb } from '../../src'; +import type { Breadcrumb } from '../../src'; import { NodeClient } from '../../src/client'; import { Http as HttpIntegration } from '../../src/integrations/http'; -import { NodeClientOptions } from '../../src/types'; +import type { NodeClientOptions } from '../../src/types'; import { getDefaultNodeClientOptions } from '../helper/node-client-options'; const NODE_VERSION = parseSemver(process.versions.node); diff --git a/packages/node/test/integrations/linkederrors.test.ts b/packages/node/test/integrations/linkederrors.test.ts index 390a847eea34..6582c0f6e9e5 100644 --- a/packages/node/test/integrations/linkederrors.test.ts +++ b/packages/node/test/integrations/linkederrors.test.ts @@ -1,6 +1,7 @@ -import { ExtendedError } from '@sentry/types'; +import type { ExtendedError } from '@sentry/types'; -import { Event, NodeClient } from '../../src'; +import type { Event } from '../../src'; +import { NodeClient } from '../../src'; import { LinkedErrors } from '../../src/integrations/linkederrors'; import { defaultStackParser as stackParser } from '../../src/sdk'; import { getDefaultNodeClientOptions } from '../helper/node-client-options'; diff --git a/packages/node/test/integrations/localvariables.test.ts b/packages/node/test/integrations/localvariables.test.ts index a9756e11d623..5324c2908b0f 100644 --- a/packages/node/test/integrations/localvariables.test.ts +++ b/packages/node/test/integrations/localvariables.test.ts @@ -1,9 +1,10 @@ -import { ClientOptions, EventProcessor } from '@sentry/types'; -import { Debugger, InspectorNotification } from 'inspector'; -import { LRUMap } from 'lru_map'; +import type { ClientOptions, EventProcessor } from '@sentry/types'; +import type { Debugger, InspectorNotification } from 'inspector'; +import type { LRUMap } from 'lru_map'; import { defaultStackParser } from '../../src'; -import { DebugSession, FrameVariables, LocalVariables } from '../../src/integrations/localvariables'; +import type { DebugSession, FrameVariables } from '../../src/integrations/localvariables'; +import { LocalVariables } from '../../src/integrations/localvariables'; import { getDefaultNodeClientOptions } from '../../test/helper/node-client-options'; interface ThrowOn { diff --git a/packages/node/test/integrations/requestdata.test.ts b/packages/node/test/integrations/requestdata.test.ts index 270c04cdfa86..91d9870f8292 100644 --- a/packages/node/test/integrations/requestdata.test.ts +++ b/packages/node/test/integrations/requestdata.test.ts @@ -1,10 +1,11 @@ import { getCurrentHub, Hub, makeMain } from '@sentry/core'; -import { Event, EventProcessor, PolymorphicRequest } from '@sentry/types'; +import type { Event, EventProcessor, PolymorphicRequest } from '@sentry/types'; import * as http from 'http'; import { NodeClient } from '../../src/client'; import { requestHandler } from '../../src/handlers'; -import { RequestData, RequestDataIntegrationOptions } from '../../src/integrations/requestdata'; +import type { RequestDataIntegrationOptions } from '../../src/integrations/requestdata'; +import { RequestData } from '../../src/integrations/requestdata'; import * as requestDataModule from '../../src/requestdata'; import { getDefaultNodeClientOptions } from '../helper/node-client-options'; diff --git a/packages/node/test/requestdata.test.ts b/packages/node/test/requestdata.test.ts index 9c79f2cd59bd..744cc9077c37 100644 --- a/packages/node/test/requestdata.test.ts +++ b/packages/node/test/requestdata.test.ts @@ -2,20 +2,17 @@ // TODO (v8 / #5257): Remove everything related to the deprecated functions -import { Event, PolymorphicRequest, TransactionSource, User } from '@sentry/types'; -import * as net from 'net'; +import type { Event, PolymorphicRequest, TransactionSource, User } from '@sentry/types'; +import type * as net from 'net'; +import type { AddRequestDataToEventOptions } from '../src/requestdata'; import { addRequestDataToEvent, - AddRequestDataToEventOptions, extractPathForTransaction, extractRequestData as newExtractRequestData, } from '../src/requestdata'; -import { - ExpressRequest, - extractRequestData as oldExtractRequestData, - parseRequest, -} from '../src/requestDataDeprecated'; +import type { ExpressRequest } from '../src/requestDataDeprecated'; +import { extractRequestData as oldExtractRequestData, parseRequest } from '../src/requestDataDeprecated'; // TODO (v8 / #5257): Remove `describe.each` wrapper, remove `formatArgs` wrapper, reformat args in tests, and use only // `addRequestDataToEvent` diff --git a/packages/node/test/sdk.test.ts b/packages/node/test/sdk.test.ts index 48e5accee439..abd0265b62c4 100644 --- a/packages/node/test/sdk.test.ts +++ b/packages/node/test/sdk.test.ts @@ -1,4 +1,4 @@ -import { Integration } from '@sentry/types'; +import type { Integration } from '@sentry/types'; import { init } from '../src/sdk'; import * as sdk from '../src/sdk'; diff --git a/packages/node/test/transports/http.test.ts b/packages/node/test/transports/http.test.ts index 80ab74b9f673..d03e917c4f42 100644 --- a/packages/node/test/transports/http.test.ts +++ b/packages/node/test/transports/http.test.ts @@ -1,5 +1,5 @@ import { createTransport } from '@sentry/core'; -import { EventEnvelope, EventItem } from '@sentry/types'; +import type { EventEnvelope, EventItem } from '@sentry/types'; import { addItemToEnvelope, createAttachmentEnvelopeItem, createEnvelope, serializeEnvelope } from '@sentry/utils'; import * as http from 'http'; import { TextEncoder } from 'util'; diff --git a/packages/node/test/transports/https.test.ts b/packages/node/test/transports/https.test.ts index 77286e32cbb6..d43b8306bed0 100644 --- a/packages/node/test/transports/https.test.ts +++ b/packages/node/test/transports/https.test.ts @@ -1,12 +1,12 @@ import { createTransport } from '@sentry/core'; -import { EventEnvelope, EventItem } from '@sentry/types'; +import type { EventEnvelope, EventItem } from '@sentry/types'; import { createEnvelope, serializeEnvelope } from '@sentry/utils'; import * as http from 'http'; import * as https from 'https'; import { TextEncoder } from 'util'; import { makeNodeTransport } from '../../src/transports'; -import { HTTPModule, HTTPModuleRequestIncomingMessage } from '../../src/transports/http-module'; +import type { HTTPModule, HTTPModuleRequestIncomingMessage } from '../../src/transports/http-module'; import testServerCerts from './test-server-certs'; const textEncoder = new TextEncoder(); diff --git a/packages/opentelemetry-node/src/propagator.ts b/packages/opentelemetry-node/src/propagator.ts index 039dcc04a30d..913e1ac218df 100644 --- a/packages/opentelemetry-node/src/propagator.ts +++ b/packages/opentelemetry-node/src/propagator.ts @@ -1,13 +1,5 @@ -import { - Baggage, - Context, - isSpanContextValid, - propagation, - TextMapGetter, - TextMapSetter, - trace, - TraceFlags, -} from '@opentelemetry/api'; +import type { Baggage, Context, TextMapGetter, TextMapSetter } from '@opentelemetry/api'; +import { isSpanContextValid, propagation, trace, TraceFlags } from '@opentelemetry/api'; import { isTracingSuppressed, W3CBaggagePropagator } from '@opentelemetry/core'; import { baggageHeaderToDynamicSamplingContext, diff --git a/packages/opentelemetry-node/src/spanprocessor.ts b/packages/opentelemetry-node/src/spanprocessor.ts index 04cd452a1a82..8aac3e1c53be 100644 --- a/packages/opentelemetry-node/src/spanprocessor.ts +++ b/packages/opentelemetry-node/src/spanprocessor.ts @@ -1,8 +1,9 @@ -import { Context, trace } from '@opentelemetry/api'; -import { Span as OtelSpan, SpanProcessor as OtelSpanProcessor } from '@opentelemetry/sdk-trace-base'; +import type { Context } from '@opentelemetry/api'; +import { trace } from '@opentelemetry/api'; +import type { Span as OtelSpan, SpanProcessor as OtelSpanProcessor } from '@opentelemetry/sdk-trace-base'; import { addGlobalEventProcessor, getCurrentHub } from '@sentry/core'; import { Transaction } from '@sentry/tracing'; -import { DynamicSamplingContext, Span as SentrySpan, TraceparentData, TransactionContext } from '@sentry/types'; +import type { DynamicSamplingContext, Span as SentrySpan, TraceparentData, TransactionContext } from '@sentry/types'; import { logger } from '@sentry/utils'; import { SENTRY_DYNAMIC_SAMPLING_CONTEXT_KEY, SENTRY_TRACE_PARENT_CONTEXT_KEY } from './constants'; diff --git a/packages/opentelemetry-node/src/utils/is-sentry-request.ts b/packages/opentelemetry-node/src/utils/is-sentry-request.ts index 1a504f3c3820..b02e3b4cb588 100644 --- a/packages/opentelemetry-node/src/utils/is-sentry-request.ts +++ b/packages/opentelemetry-node/src/utils/is-sentry-request.ts @@ -1,4 +1,4 @@ -import { Span as OtelSpan } from '@opentelemetry/sdk-trace-base'; +import type { Span as OtelSpan } from '@opentelemetry/sdk-trace-base'; import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; import { getCurrentHub } from '@sentry/core'; diff --git a/packages/opentelemetry-node/src/utils/map-otel-status.ts b/packages/opentelemetry-node/src/utils/map-otel-status.ts index 869d80862d13..789cd7706622 100644 --- a/packages/opentelemetry-node/src/utils/map-otel-status.ts +++ b/packages/opentelemetry-node/src/utils/map-otel-status.ts @@ -1,6 +1,6 @@ -import { Span as OtelSpan } from '@opentelemetry/sdk-trace-base'; +import type { Span as OtelSpan } from '@opentelemetry/sdk-trace-base'; import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; -import { SpanStatusType as SentryStatus } from '@sentry/tracing'; +import type { SpanStatusType as SentryStatus } from '@sentry/tracing'; // canonicalCodesHTTPMap maps some HTTP codes to Sentry's span statuses. See possible mapping in https://develop.sentry.dev/sdk/event-payloads/span/ const canonicalCodesHTTPMap: Record = { diff --git a/packages/opentelemetry-node/src/utils/parse-otel-span-description.ts b/packages/opentelemetry-node/src/utils/parse-otel-span-description.ts index d95fc7028223..396e37a29ced 100644 --- a/packages/opentelemetry-node/src/utils/parse-otel-span-description.ts +++ b/packages/opentelemetry-node/src/utils/parse-otel-span-description.ts @@ -1,7 +1,8 @@ -import { AttributeValue, SpanKind } from '@opentelemetry/api'; -import { Span as OtelSpan } from '@opentelemetry/sdk-trace-base'; +import type { AttributeValue } from '@opentelemetry/api'; +import { SpanKind } from '@opentelemetry/api'; +import type { Span as OtelSpan } from '@opentelemetry/sdk-trace-base'; import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; -import { TransactionSource } from '@sentry/types'; +import type { TransactionSource } from '@sentry/types'; interface SpanDescription { op: string | undefined; diff --git a/packages/opentelemetry-node/test/propagator.test.ts b/packages/opentelemetry-node/test/propagator.test.ts index ec7a047d7735..27c633e61c06 100644 --- a/packages/opentelemetry-node/test/propagator.test.ts +++ b/packages/opentelemetry-node/test/propagator.test.ts @@ -9,7 +9,7 @@ import { import { suppressTracing } from '@opentelemetry/core'; import { Hub, makeMain } from '@sentry/core'; import { addExtensionMethods, Transaction } from '@sentry/tracing'; -import { TransactionContext } from '@sentry/types'; +import type { TransactionContext } from '@sentry/types'; import { SENTRY_BAGGAGE_HEADER, diff --git a/packages/opentelemetry-node/test/spanprocessor.test.ts b/packages/opentelemetry-node/test/spanprocessor.test.ts index bd970bb737fd..d07c0b950f64 100644 --- a/packages/opentelemetry-node/test/spanprocessor.test.ts +++ b/packages/opentelemetry-node/test/spanprocessor.test.ts @@ -1,12 +1,13 @@ -import * as OpenTelemetry from '@opentelemetry/api'; +import type * as OpenTelemetry from '@opentelemetry/api'; import { SpanKind } from '@opentelemetry/api'; import { Resource } from '@opentelemetry/resources'; -import { Span as OtelSpan } from '@opentelemetry/sdk-trace-base'; +import type { Span as OtelSpan } from '@opentelemetry/sdk-trace-base'; import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'; import { SemanticAttributes, SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; import { createTransport, Hub, makeMain } from '@sentry/core'; import { NodeClient } from '@sentry/node'; -import { addExtensionMethods, Span as SentrySpan, SpanStatusType, Transaction } from '@sentry/tracing'; +import type { SpanStatusType } from '@sentry/tracing'; +import { addExtensionMethods, Span as SentrySpan, Transaction } from '@sentry/tracing'; import { resolvedSyncPromise } from '@sentry/utils'; import { SENTRY_SPAN_PROCESSOR_MAP, SentrySpanProcessor } from '../src/spanprocessor'; diff --git a/packages/react/src/errorboundary.tsx b/packages/react/src/errorboundary.tsx index 7aae1fbf6d08..907f7e029314 100644 --- a/packages/react/src/errorboundary.tsx +++ b/packages/react/src/errorboundary.tsx @@ -1,4 +1,5 @@ -import { captureException, ReportDialogOptions, Scope, showReportDialog, withScope } from '@sentry/browser'; +import type { ReportDialogOptions, Scope} from '@sentry/browser'; +import { captureException, showReportDialog, withScope } from '@sentry/browser'; import { isError, logger } from '@sentry/utils'; import hoistNonReactStatics from 'hoist-non-react-statics'; import * as React from 'react'; diff --git a/packages/react/src/profiler.tsx b/packages/react/src/profiler.tsx index 3f8f397f5ad9..d4c0f0a277e4 100644 --- a/packages/react/src/profiler.tsx +++ b/packages/react/src/profiler.tsx @@ -1,7 +1,8 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { getCurrentHub, Hub } from '@sentry/browser'; -import { Span, Transaction } from '@sentry/types'; +import type { Hub } from '@sentry/browser'; +import { getCurrentHub } from '@sentry/browser'; +import type { Span, Transaction } from '@sentry/types'; import { timestampWithMs } from '@sentry/utils'; import hoistNonReactStatics from 'hoist-non-react-statics'; import * as React from 'react'; diff --git a/packages/react/src/reactrouter.tsx b/packages/react/src/reactrouter.tsx index bad84fabf5a8..bfe89afd0890 100644 --- a/packages/react/src/reactrouter.tsx +++ b/packages/react/src/reactrouter.tsx @@ -1,9 +1,9 @@ import { WINDOW } from '@sentry/browser'; -import { Transaction, TransactionSource } from '@sentry/types'; +import type { Transaction, TransactionSource } from '@sentry/types'; import hoistNonReactStatics from 'hoist-non-react-statics'; import * as React from 'react'; -import { Action, Location, ReactRouterInstrumentation } from './types'; +import type { Action, Location, ReactRouterInstrumentation } from './types'; // We need to disable eslint no-explict-any because any is required for the // react-router typings. diff --git a/packages/react/src/reactrouterv3.ts b/packages/react/src/reactrouterv3.ts index e916784ede4e..4d12b3581e53 100644 --- a/packages/react/src/reactrouterv3.ts +++ b/packages/react/src/reactrouterv3.ts @@ -1,7 +1,7 @@ import { WINDOW } from '@sentry/browser'; -import { Primitive, Transaction, TransactionContext, TransactionSource } from '@sentry/types'; +import type { Primitive, Transaction, TransactionContext, TransactionSource } from '@sentry/types'; -import { Location, ReactRouterInstrumentation } from './types'; +import type { Location, ReactRouterInstrumentation } from './types'; // Many of the types below had to be mocked out to prevent typescript issues // these types are required for correct functionality. diff --git a/packages/react/src/reactrouterv6.tsx b/packages/react/src/reactrouterv6.tsx index 47db243cd8ab..877cd770dc7a 100644 --- a/packages/react/src/reactrouterv6.tsx +++ b/packages/react/src/reactrouterv6.tsx @@ -2,12 +2,12 @@ // https://gist.github.com/wontondon/e8c4bdf2888875e4c755712e99279536 import { WINDOW } from '@sentry/browser'; -import { Transaction, TransactionContext, TransactionSource } from '@sentry/types'; +import type { Transaction, TransactionContext, TransactionSource } from '@sentry/types'; import { getNumberOfUrlSegments, logger } from '@sentry/utils'; import hoistNonReactStatics from 'hoist-non-react-statics'; import React from 'react'; -import { +import type { Action, AgnosticDataRouteMatch, CreateRouterFunction, diff --git a/packages/react/src/redux.ts b/packages/react/src/redux.ts index 35f5348c5e08..5ef64a3d2522 100644 --- a/packages/react/src/redux.ts +++ b/packages/react/src/redux.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { configureScope } from '@sentry/browser'; -import { Scope } from '@sentry/types'; +import type { Scope } from '@sentry/types'; interface Action { type: T; diff --git a/packages/react/src/sdk.ts b/packages/react/src/sdk.ts index 038a509c03d7..e224d72945bb 100644 --- a/packages/react/src/sdk.ts +++ b/packages/react/src/sdk.ts @@ -1,4 +1,5 @@ -import { BrowserOptions, init as browserInit, SDK_VERSION } from '@sentry/browser'; +import type { BrowserOptions } from '@sentry/browser'; +import { init as browserInit, SDK_VERSION } from '@sentry/browser'; /** * Inits the React SDK diff --git a/packages/react/src/types.ts b/packages/react/src/types.ts index 57b2814463ec..0b908a800ff7 100644 --- a/packages/react/src/types.ts +++ b/packages/react/src/types.ts @@ -1,6 +1,6 @@ // Disabling `no-explicit-any` for the whole file as `any` has became common requirement. /* eslint-disable @typescript-eslint/no-explicit-any */ -import { Transaction, TransactionContext } from '@sentry/types'; +import type { Transaction, TransactionContext } from '@sentry/types'; export type Action = 'PUSH' | 'REPLACE' | 'POP'; diff --git a/packages/react/test/errorboundary.test.tsx b/packages/react/test/errorboundary.test.tsx index 7b0c25dc311a..d37a1de49116 100644 --- a/packages/react/test/errorboundary.test.tsx +++ b/packages/react/test/errorboundary.test.tsx @@ -3,9 +3,10 @@ import { fireEvent, render, screen } from '@testing-library/react'; import * as React from 'react'; import { useState } from 'react'; +import type { + ErrorBoundaryProps} from '../src/errorboundary'; import { ErrorBoundary, - ErrorBoundaryProps, isAtLeastReact17, UNKNOWN_COMPONENT, withErrorBoundary, diff --git a/packages/react/test/profiler.test.tsx b/packages/react/test/profiler.test.tsx index 96988f24b44b..20dc1f2962c5 100644 --- a/packages/react/test/profiler.test.tsx +++ b/packages/react/test/profiler.test.tsx @@ -1,4 +1,4 @@ -import { SpanContext } from '@sentry/types'; +import type { SpanContext } from '@sentry/types'; import { render } from '@testing-library/react'; import { renderHook } from '@testing-library/react-hooks'; import * as React from 'react'; diff --git a/packages/react/test/reactrouterv3.test.tsx b/packages/react/test/reactrouterv3.test.tsx index 336f52b74268..a4cbe463adbc 100644 --- a/packages/react/test/reactrouterv3.test.tsx +++ b/packages/react/test/reactrouterv3.test.tsx @@ -2,7 +2,8 @@ import { act, render } from '@testing-library/react'; import * as React from 'react'; import { createMemoryHistory, createRoutes, IndexRoute, match, Route, Router } from 'react-router-3'; -import { Match, reactRouterV3Instrumentation, Route as RouteType } from '../src/reactrouterv3'; +import type { Match, Route as RouteType } from '../src/reactrouterv3'; +import { reactRouterV3Instrumentation } from '../src/reactrouterv3'; // Have to manually set types because we are using package-alias declare module 'react-router-3' { diff --git a/packages/react/test/reactrouterv4.test.tsx b/packages/react/test/reactrouterv4.test.tsx index 7e2dfd14a763..343f78c175e5 100644 --- a/packages/react/test/reactrouterv4.test.tsx +++ b/packages/react/test/reactrouterv4.test.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import { matchPath, Route, Router, Switch } from 'react-router-4'; import { reactRouterV4Instrumentation, withSentryRouting } from '../src'; -import { RouteConfig } from '../src/reactrouter'; +import type { RouteConfig } from '../src/reactrouter'; describe('React Router v4', () => { function createInstrumentation(_opts?: { diff --git a/packages/react/test/reactrouterv5.test.tsx b/packages/react/test/reactrouterv5.test.tsx index f37dc10edea7..568e630ccc25 100644 --- a/packages/react/test/reactrouterv5.test.tsx +++ b/packages/react/test/reactrouterv5.test.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import { matchPath, Route, Router, Switch } from 'react-router-5'; import { reactRouterV5Instrumentation, withSentryRouting } from '../src'; -import { RouteConfig } from '../src/reactrouter'; +import type { RouteConfig } from '../src/reactrouter'; describe('React Router v5', () => { function createInstrumentation(_opts?: { diff --git a/packages/react/test/reactrouterv6.4.test.tsx b/packages/react/test/reactrouterv6.4.test.tsx index aeb7953ea1ab..0863535b38d5 100644 --- a/packages/react/test/reactrouterv6.4.test.tsx +++ b/packages/react/test/reactrouterv6.4.test.tsx @@ -13,7 +13,7 @@ import { } from 'react-router-6.4'; import { reactRouterV6Instrumentation,wrapCreateBrowserRouter } from '../src'; -import { CreateRouterFunction } from '../src/types'; +import type { CreateRouterFunction } from '../src/types'; beforeAll(() => { // @ts-ignore need to override global Request because it's not in the jest environment (even with an diff --git a/packages/react/test/redux.test.ts b/packages/react/test/redux.test.ts index 9be231adaddb..bf9fc31853c4 100644 --- a/packages/react/test/redux.test.ts +++ b/packages/react/test/redux.test.ts @@ -1,5 +1,5 @@ import * as Sentry from '@sentry/browser'; -import { Scope } from '@sentry/types'; +import type { Scope } from '@sentry/types'; import * as Redux from 'redux'; import { createReduxEnhancer } from '../src/redux'; diff --git a/packages/remix/src/index.client.tsx b/packages/remix/src/index.client.tsx index 68e6c2027141..22d9ce73fe82 100644 --- a/packages/remix/src/index.client.tsx +++ b/packages/remix/src/index.client.tsx @@ -2,7 +2,7 @@ import { configureScope, init as reactInit, Integrations } from '@sentry/react'; import { buildMetadata } from './utils/metadata'; -import { RemixOptions } from './utils/remixOptions'; +import type { RemixOptions } from './utils/remixOptions'; export { remixRouterInstrumentation, withSentry } from './performance/client'; export { BrowserTracing } from '@sentry/tracing'; export * from '@sentry/react'; diff --git a/packages/remix/src/index.server.ts b/packages/remix/src/index.server.ts index 2576c9a293f1..b2ad73866fde 100644 --- a/packages/remix/src/index.server.ts +++ b/packages/remix/src/index.server.ts @@ -4,7 +4,7 @@ import { logger } from '@sentry/utils'; import { instrumentServer } from './utils/instrumentServer'; import { buildMetadata } from './utils/metadata'; -import { RemixOptions } from './utils/remixOptions'; +import type { RemixOptions } from './utils/remixOptions'; export { ErrorBoundary, withErrorBoundary } from '@sentry/react'; export { remixRouterInstrumentation, withSentry } from './performance/client'; diff --git a/packages/remix/src/index.types.ts b/packages/remix/src/index.types.ts index 0d3e8a8633a1..079f524e3b61 100644 --- a/packages/remix/src/index.types.ts +++ b/packages/remix/src/index.types.ts @@ -9,7 +9,7 @@ import type { Integration, StackParser } from '@sentry/types'; import * as clientSdk from './index.client'; import * as serverSdk from './index.server'; -import { RemixOptions } from './utils/remixOptions'; +import type { RemixOptions } from './utils/remixOptions'; /** Initializes Sentry Remix SDK */ export declare function init(options: RemixOptions): void; diff --git a/packages/remix/src/performance/client.tsx b/packages/remix/src/performance/client.tsx index 2308c64d0727..0f88a67c2592 100644 --- a/packages/remix/src/performance/client.tsx +++ b/packages/remix/src/performance/client.tsx @@ -1,5 +1,6 @@ -import { ErrorBoundaryProps, WINDOW, withErrorBoundary } from '@sentry/react'; -import { Transaction, TransactionContext } from '@sentry/types'; +import type { ErrorBoundaryProps} from '@sentry/react'; +import { WINDOW, withErrorBoundary } from '@sentry/react'; +import type { Transaction, TransactionContext } from '@sentry/types'; import { logger } from '@sentry/utils'; import * as React from 'react'; diff --git a/packages/remix/src/utils/instrumentServer.ts b/packages/remix/src/utils/instrumentServer.ts index 4aee67a174b4..d80158a7b4bd 100644 --- a/packages/remix/src/utils/instrumentServer.ts +++ b/packages/remix/src/utils/instrumentServer.ts @@ -1,7 +1,8 @@ /* eslint-disable max-lines */ -import { captureException, getCurrentHub, Hub } from '@sentry/node'; +import type { Hub } from '@sentry/node'; +import { captureException, getCurrentHub } from '@sentry/node'; import { getActiveTransaction, hasTracingEnabled } from '@sentry/tracing'; -import { Transaction, TransactionSource, WrappedFunction } from '@sentry/types'; +import type { Transaction, TransactionSource, WrappedFunction } from '@sentry/types'; import { addExceptionMechanism, baggageHeaderToDynamicSamplingContext, @@ -14,7 +15,7 @@ import { } from '@sentry/utils'; import * as domain from 'domain'; -import { +import type { AppData, CreateRequestHandlerFunction, DataFunction, diff --git a/packages/remix/src/utils/metadata.ts b/packages/remix/src/utils/metadata.ts index 243cdcc3826f..382eff9c3e26 100644 --- a/packages/remix/src/utils/metadata.ts +++ b/packages/remix/src/utils/metadata.ts @@ -1,5 +1,5 @@ import { SDK_VERSION } from '@sentry/core'; -import { Options, SdkInfo } from '@sentry/types'; +import type { Options, SdkInfo } from '@sentry/types'; const PACKAGE_NAME_PREFIX = 'npm:@sentry/'; diff --git a/packages/remix/src/utils/remixOptions.ts b/packages/remix/src/utils/remixOptions.ts index 9534ed57de3b..4a1fe13e18e1 100644 --- a/packages/remix/src/utils/remixOptions.ts +++ b/packages/remix/src/utils/remixOptions.ts @@ -1,5 +1,5 @@ -import { NodeOptions } from '@sentry/node'; -import { BrowserOptions } from '@sentry/react'; -import { Options } from '@sentry/types'; +import type { NodeOptions } from '@sentry/node'; +import type { BrowserOptions } from '@sentry/react'; +import type { Options } from '@sentry/types'; export type RemixOptions = Options | BrowserOptions | NodeOptions; diff --git a/packages/remix/src/utils/serverAdapters/express.ts b/packages/remix/src/utils/serverAdapters/express.ts index 136246e60dde..e2484e4a6d6d 100644 --- a/packages/remix/src/utils/serverAdapters/express.ts +++ b/packages/remix/src/utils/serverAdapters/express.ts @@ -1,7 +1,7 @@ import { getCurrentHub } from '@sentry/core'; import { flush } from '@sentry/node'; import { hasTracingEnabled } from '@sentry/tracing'; -import { Transaction } from '@sentry/types'; +import type { Transaction } from '@sentry/types'; import { extractRequestData, isString, logger } from '@sentry/utils'; import { cwd } from 'process'; @@ -12,7 +12,7 @@ import { isRequestHandlerWrapped, startRequestHandlerTransaction, } from '../instrumentServer'; -import { +import type { ExpressCreateRequestHandler, ExpressCreateRequestHandlerOptions, ExpressNextFunction, diff --git a/packages/remix/src/utils/types.ts b/packages/remix/src/utils/types.ts index f6fe6e8d30e9..225a3ea1ad56 100644 --- a/packages/remix/src/utils/types.ts +++ b/packages/remix/src/utils/types.ts @@ -3,7 +3,7 @@ // Types vendored from @remix-run/server-runtime@1.6.0: // https://github.com/remix-run/remix/blob/f3691d51027b93caa3fd2cdfe146d7b62a6eb8f2/packages/remix-server-runtime/server.ts import type * as Express from 'express'; -import { Agent } from 'https'; +import type { Agent } from 'https'; import type { ComponentType } from 'react'; export type RemixRequestState = { diff --git a/packages/remix/src/utils/web-fetch.ts b/packages/remix/src/utils/web-fetch.ts index 72ce4177827b..853eac775c84 100644 --- a/packages/remix/src/utils/web-fetch.ts +++ b/packages/remix/src/utils/web-fetch.ts @@ -4,7 +4,7 @@ import { logger } from '@sentry/utils'; import { getClientIPAddress } from './getIpAddress'; -import { RemixRequest } from './types'; +import type { RemixRequest } from './types'; /* * Symbol extractor utility to be able to access internal fields of Remix requests. diff --git a/packages/replay/jest.setup.ts b/packages/replay/jest.setup.ts index 9d165d8c428e..db17db23f761 100644 --- a/packages/replay/jest.setup.ts +++ b/packages/replay/jest.setup.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { getCurrentHub } from '@sentry/core'; -import { ReplayRecordingData,Transport } from '@sentry/types'; +import type { ReplayRecordingData,Transport } from '@sentry/types'; import type { ReplayContainer, Session } from './src/types'; diff --git a/packages/replay/src/coreHandlers/breadcrumbHandler.ts b/packages/replay/src/coreHandlers/breadcrumbHandler.ts index f790e28db3c1..919c46fa7ad1 100644 --- a/packages/replay/src/coreHandlers/breadcrumbHandler.ts +++ b/packages/replay/src/coreHandlers/breadcrumbHandler.ts @@ -1,7 +1,8 @@ -import { Breadcrumb, Scope } from '@sentry/types'; +import type { Breadcrumb, Scope } from '@sentry/types'; import type { InstrumentationTypeBreadcrumb } from '../types'; -import { DomHandlerData, handleDom } from './handleDom'; +import type { DomHandlerData } from './handleDom'; +import { handleDom } from './handleDom'; import { handleScope } from './handleScope'; /** diff --git a/packages/replay/src/coreHandlers/handleDom.ts b/packages/replay/src/coreHandlers/handleDom.ts index 976adf7761c7..7cc2dc684b51 100644 --- a/packages/replay/src/coreHandlers/handleDom.ts +++ b/packages/replay/src/coreHandlers/handleDom.ts @@ -1,4 +1,4 @@ -import { Breadcrumb } from '@sentry/types'; +import type { Breadcrumb } from '@sentry/types'; import { htmlTreeAsString } from '@sentry/utils'; import { record } from 'rrweb'; diff --git a/packages/replay/src/coreHandlers/handleGlobalEvent.ts b/packages/replay/src/coreHandlers/handleGlobalEvent.ts index 59a286c3a770..719391e25491 100644 --- a/packages/replay/src/coreHandlers/handleGlobalEvent.ts +++ b/packages/replay/src/coreHandlers/handleGlobalEvent.ts @@ -1,5 +1,5 @@ import { addBreadcrumb } from '@sentry/core'; -import { Event } from '@sentry/types'; +import type { Event } from '@sentry/types'; import { logger } from '@sentry/utils'; import { REPLAY_EVENT_NAME, UNABLE_TO_SEND_REPLAY } from '../constants'; diff --git a/packages/replay/src/coreHandlers/handleScope.ts b/packages/replay/src/coreHandlers/handleScope.ts index 429fc9d9a3bb..6b5a8157cd9c 100644 --- a/packages/replay/src/coreHandlers/handleScope.ts +++ b/packages/replay/src/coreHandlers/handleScope.ts @@ -1,4 +1,4 @@ -import { Breadcrumb, Scope } from '@sentry/types'; +import type { Breadcrumb, Scope } from '@sentry/types'; import { createBreadcrumb } from '../util/createBreadcrumb'; diff --git a/packages/replay/src/eventBuffer.ts b/packages/replay/src/eventBuffer.ts index a755ab0676e8..f953b693bc6d 100644 --- a/packages/replay/src/eventBuffer.ts +++ b/packages/replay/src/eventBuffer.ts @@ -2,7 +2,7 @@ // TODO: figure out member access types and remove the line above import { captureException } from '@sentry/core'; -import { ReplayRecordingData } from '@sentry/types'; +import type { ReplayRecordingData } from '@sentry/types'; import { logger } from '@sentry/utils'; import type { EventBuffer, RecordingEvent, WorkerRequest, WorkerResponse } from './types'; diff --git a/packages/replay/src/integration.ts b/packages/replay/src/integration.ts index 865472c1571c..a7cadab5ccfd 100644 --- a/packages/replay/src/integration.ts +++ b/packages/replay/src/integration.ts @@ -1,6 +1,5 @@ import { getCurrentHub } from '@sentry/core'; -import type { BrowserClientReplayOptions } from '@sentry/types'; -import { Integration } from '@sentry/types'; +import type { BrowserClientReplayOptions, Integration } from '@sentry/types'; import { DEFAULT_ERROR_SAMPLE_RATE, diff --git a/packages/replay/src/util/createRecordingData.ts b/packages/replay/src/util/createRecordingData.ts index 63c1db5f6e7b..a0d9835c1094 100644 --- a/packages/replay/src/util/createRecordingData.ts +++ b/packages/replay/src/util/createRecordingData.ts @@ -1,4 +1,4 @@ -import { ReplayRecordingData } from '@sentry/types'; +import type { ReplayRecordingData } from '@sentry/types'; import type { RecordedEvents } from '../types'; diff --git a/packages/replay/src/util/createReplayEnvelope.ts b/packages/replay/src/util/createReplayEnvelope.ts index 3a052f32fdd6..afc445e83d71 100644 --- a/packages/replay/src/util/createReplayEnvelope.ts +++ b/packages/replay/src/util/createReplayEnvelope.ts @@ -1,4 +1,4 @@ -import { DsnComponents, ReplayEnvelope, ReplayEvent, ReplayRecordingData } from '@sentry/types'; +import type { DsnComponents, ReplayEnvelope, ReplayEvent, ReplayRecordingData } from '@sentry/types'; import { createEnvelope, createEventEnvelopeHeaders, getSdkMetadataForEnvelopeHeader } from '@sentry/utils'; /** diff --git a/packages/replay/src/util/isRrwebError.ts b/packages/replay/src/util/isRrwebError.ts index 1607f55255ef..b706e27f97cd 100644 --- a/packages/replay/src/util/isRrwebError.ts +++ b/packages/replay/src/util/isRrwebError.ts @@ -1,4 +1,4 @@ -import { Event } from '@sentry/types'; +import type { Event } from '@sentry/types'; /** * Returns true if we think the given event is an error originating inside of rrweb. diff --git a/packages/replay/src/util/monkeyPatchRecordDroppedEvent.ts b/packages/replay/src/util/monkeyPatchRecordDroppedEvent.ts index 70cf11faf450..6a819ef917b2 100644 --- a/packages/replay/src/util/monkeyPatchRecordDroppedEvent.ts +++ b/packages/replay/src/util/monkeyPatchRecordDroppedEvent.ts @@ -1,5 +1,5 @@ import { getCurrentHub } from '@sentry/core'; -import { Client, DataCategory, Event, EventDropReason } from '@sentry/types'; +import type { Client, DataCategory, Event, EventDropReason } from '@sentry/types'; let _originalRecordDroppedEvent: Client['recordDroppedEvent'] | undefined; diff --git a/packages/replay/src/util/prepareReplayEvent.ts b/packages/replay/src/util/prepareReplayEvent.ts index a7e2fd6da995..d9227f50cfca 100644 --- a/packages/replay/src/util/prepareReplayEvent.ts +++ b/packages/replay/src/util/prepareReplayEvent.ts @@ -1,5 +1,6 @@ -import { prepareEvent, Scope } from '@sentry/core'; -import { Client, ReplayEvent } from '@sentry/types'; +import type { Scope } from '@sentry/core'; +import { prepareEvent } from '@sentry/core'; +import type { Client, ReplayEvent } from '@sentry/types'; /** * Prepare a replay event & enrich it with the SDK metadata. diff --git a/packages/replay/test/fixtures/error.ts b/packages/replay/test/fixtures/error.ts index 31827cec3929..c4a0b93144ed 100644 --- a/packages/replay/test/fixtures/error.ts +++ b/packages/replay/test/fixtures/error.ts @@ -1,5 +1,5 @@ -import { SeverityLevel } from '@sentry/browser'; -import { Event } from '@sentry/types'; +import type { SeverityLevel } from '@sentry/browser'; +import type { Event } from '@sentry/types'; export function Error(obj?: Event): any { const timestamp = new Date().getTime() / 1000; diff --git a/packages/replay/test/fixtures/transaction.ts b/packages/replay/test/fixtures/transaction.ts index 37ffcc4ac49d..24f89cdc9fb8 100644 --- a/packages/replay/test/fixtures/transaction.ts +++ b/packages/replay/test/fixtures/transaction.ts @@ -1,4 +1,4 @@ -import { Event, SeverityLevel } from '@sentry/types'; +import type { Event, SeverityLevel } from '@sentry/types'; export function Transaction(obj?: Partial): any { const timestamp = new Date().getTime() / 1000; diff --git a/packages/replay/test/integration/coreHandlers/handleGlobalEvent.test.ts b/packages/replay/test/integration/coreHandlers/handleGlobalEvent.test.ts index a3d3ca17ad4f..e612d1210000 100644 --- a/packages/replay/test/integration/coreHandlers/handleGlobalEvent.test.ts +++ b/packages/replay/test/integration/coreHandlers/handleGlobalEvent.test.ts @@ -1,9 +1,9 @@ import { getCurrentHub } from '@sentry/core'; -import { Event } from '@sentry/types'; +import type { Event } from '@sentry/types'; import { REPLAY_EVENT_NAME } from '../../../src/constants'; import { handleGlobalEventListener } from '../../../src/coreHandlers/handleGlobalEvent'; -import { ReplayContainer } from '../../../src/replay'; +import type { ReplayContainer } from '../../../src/replay'; import { overwriteRecordDroppedEvent, restoreRecordDroppedEvent, diff --git a/packages/replay/test/integration/errorSampleRate.test.ts b/packages/replay/test/integration/errorSampleRate.test.ts index c8a7237a7951..2730560093a4 100644 --- a/packages/replay/test/integration/errorSampleRate.test.ts +++ b/packages/replay/test/integration/errorSampleRate.test.ts @@ -1,12 +1,13 @@ import { captureException } from '@sentry/core'; import { DEFAULT_FLUSH_MIN_DELAY, REPLAY_SESSION_KEY, VISIBILITY_CHANGE_TIMEOUT, WINDOW } from '../../src/constants'; -import { ReplayContainer } from '../../src/replay'; +import type { ReplayContainer } from '../../src/replay'; import { addEvent } from '../../src/util/addEvent'; import { PerformanceEntryResource } from '../fixtures/performanceEntry/resource'; -import { BASE_TIMESTAMP, RecordMock } from '../index'; +import type { RecordMock } from '../index'; +import { BASE_TIMESTAMP } from '../index'; import { resetSdkMock } from '../mocks/resetSdkMock'; -import { DomHandler } from '../types'; +import type { DomHandler } from '../types'; import { clearSession } from '../utils/clearSession'; import { useFakeTimers } from '../utils/use-fake-timers'; diff --git a/packages/replay/test/integration/eventProcessors.test.ts b/packages/replay/test/integration/eventProcessors.test.ts index 5b4d96077f08..363c5f2d5a74 100644 --- a/packages/replay/test/integration/eventProcessors.test.ts +++ b/packages/replay/test/integration/eventProcessors.test.ts @@ -1,5 +1,5 @@ import { getCurrentHub } from '@sentry/core'; -import { Event, Hub, Scope } from '@sentry/types'; +import type { Event, Hub, Scope } from '@sentry/types'; import { BASE_TIMESTAMP } from '..'; import { resetSdkMock } from '../mocks/resetSdkMock'; diff --git a/packages/replay/test/integration/events.test.ts b/packages/replay/test/integration/events.test.ts index e2770101d395..ed74b6978fe6 100644 --- a/packages/replay/test/integration/events.test.ts +++ b/packages/replay/test/integration/events.test.ts @@ -1,10 +1,11 @@ import { getCurrentHub } from '@sentry/core'; import { WINDOW } from '../../src/constants'; -import { ReplayContainer } from '../../src/replay'; +import type { ReplayContainer } from '../../src/replay'; import { addEvent } from '../../src/util/addEvent'; import { PerformanceEntryResource } from '../fixtures/performanceEntry/resource'; -import { BASE_TIMESTAMP, RecordMock } from '../index'; +import type { RecordMock } from '../index'; +import { BASE_TIMESTAMP } from '../index'; import { resetSdkMock } from '../mocks/resetSdkMock'; import { clearSession } from '../utils/clearSession'; import { useFakeTimers } from '../utils/use-fake-timers'; diff --git a/packages/replay/test/integration/flush.test.ts b/packages/replay/test/integration/flush.test.ts index c43729d79de1..8865aee30705 100644 --- a/packages/replay/test/integration/flush.test.ts +++ b/packages/replay/test/integration/flush.test.ts @@ -1,7 +1,7 @@ import * as SentryUtils from '@sentry/utils'; import { DEFAULT_FLUSH_MIN_DELAY, SESSION_IDLE_DURATION, WINDOW } from '../../src/constants'; -import { ReplayContainer } from '../../src/replay'; +import type { ReplayContainer } from '../../src/replay'; import * as AddMemoryEntry from '../../src/util/addMemoryEntry'; import { createPerformanceEntries } from '../../src/util/createPerformanceEntries'; import { createPerformanceSpans } from '../../src/util/createPerformanceSpans'; diff --git a/packages/replay/test/integration/sendReplayEvent.test.ts b/packages/replay/test/integration/sendReplayEvent.test.ts index 1701a0a48826..1f747723a11d 100644 --- a/packages/replay/test/integration/sendReplayEvent.test.ts +++ b/packages/replay/test/integration/sendReplayEvent.test.ts @@ -1,9 +1,9 @@ import { getCurrentHub } from '@sentry/core'; -import { Transport } from '@sentry/types'; +import type { Transport } from '@sentry/types'; import * as SentryUtils from '@sentry/utils'; import { DEFAULT_FLUSH_MIN_DELAY, SESSION_IDLE_DURATION, WINDOW } from '../../src/constants'; -import { ReplayContainer } from '../../src/replay'; +import type { ReplayContainer } from '../../src/replay'; import { addEvent } from '../../src/util/addEvent'; import { BASE_TIMESTAMP, mockRrweb, mockSdk } from '../index'; import { clearSession } from '../utils/clearSession'; diff --git a/packages/replay/test/integration/session.test.ts b/packages/replay/test/integration/session.test.ts index 61b08e292582..46ef58622372 100644 --- a/packages/replay/test/integration/session.test.ts +++ b/packages/replay/test/integration/session.test.ts @@ -1,5 +1,5 @@ import { getCurrentHub } from '@sentry/core'; -import { Transport } from '@sentry/types'; +import type { Transport } from '@sentry/types'; import { DEFAULT_FLUSH_MIN_DELAY, @@ -8,11 +8,11 @@ import { VISIBILITY_CHANGE_TIMEOUT, WINDOW, } from '../../src/constants'; -import { ReplayContainer } from '../../src/replay'; +import type { ReplayContainer } from '../../src/replay'; import { addEvent } from '../../src/util/addEvent'; import { createPerformanceSpans } from '../../src/util/createPerformanceSpans'; import { BASE_TIMESTAMP } from '../index'; -import { RecordMock } from '../mocks/mockRrweb'; +import type { RecordMock } from '../mocks/mockRrweb'; import { resetSdkMock } from '../mocks/resetSdkMock'; import { clearSession } from '../utils/clearSession'; import { useFakeTimers } from '../utils/use-fake-timers'; diff --git a/packages/replay/test/integration/stop.test.ts b/packages/replay/test/integration/stop.test.ts index 46c43e8afea5..189156abe015 100644 --- a/packages/replay/test/integration/stop.test.ts +++ b/packages/replay/test/integration/stop.test.ts @@ -1,8 +1,8 @@ import * as SentryUtils from '@sentry/utils'; -import { Replay } from '../../src'; +import type { Replay } from '../../src'; import { SESSION_IDLE_DURATION, WINDOW } from '../../src/constants'; -import { ReplayContainer } from '../../src/replay'; +import type { ReplayContainer } from '../../src/replay'; import { addEvent } from '../../src/util/addEvent'; // mock functions need to be imported first import { BASE_TIMESTAMP, mockRrweb, mockSdk } from '../index'; diff --git a/packages/replay/test/mocks/mockSdk.ts b/packages/replay/test/mocks/mockSdk.ts index 159627e656a2..d81d2820a9e3 100644 --- a/packages/replay/test/mocks/mockSdk.ts +++ b/packages/replay/test/mocks/mockSdk.ts @@ -1,8 +1,9 @@ -import { BrowserOptions, init } from '@sentry/browser'; -import { Envelope, Transport } from '@sentry/types'; +import type { BrowserOptions } from '@sentry/browser'; +import { init } from '@sentry/browser'; +import type { Envelope, Transport } from '@sentry/types'; -import { Replay as ReplayIntegration } from '../../src'; -import { ReplayContainer } from '../../src/replay'; +import type { Replay as ReplayIntegration } from '../../src'; +import type { ReplayContainer } from '../../src/replay'; import type { ReplayConfiguration } from '../../src/types'; export interface MockSdkParams { diff --git a/packages/replay/test/mocks/resetSdkMock.ts b/packages/replay/test/mocks/resetSdkMock.ts index f2104975045c..24d90678f400 100644 --- a/packages/replay/test/mocks/resetSdkMock.ts +++ b/packages/replay/test/mocks/resetSdkMock.ts @@ -1,7 +1,9 @@ import type { ReplayContainer } from '../../src/replay'; -import { BASE_TIMESTAMP, RecordMock } from './../index'; +import type { RecordMock } from './../index'; +import { BASE_TIMESTAMP } from './../index'; import type { DomHandler } from './../types'; -import { mockSdk, MockSdkParams } from './mockSdk'; +import type { MockSdkParams } from './mockSdk'; +import { mockSdk } from './mockSdk'; export async function resetSdkMock({ replayOptions, sentryOptions }: MockSdkParams): Promise<{ domHandler: DomHandler; diff --git a/packages/replay/test/unit/eventBuffer.test.ts b/packages/replay/test/unit/eventBuffer.test.ts index ac5a3be70183..a4a319da68bd 100644 --- a/packages/replay/test/unit/eventBuffer.test.ts +++ b/packages/replay/test/unit/eventBuffer.test.ts @@ -2,7 +2,8 @@ import 'jsdom-worker'; import pako from 'pako'; -import { createEventBuffer, EventBufferCompressionWorker } from './../../src/eventBuffer'; +import type { EventBufferCompressionWorker } from './../../src/eventBuffer'; +import { createEventBuffer } from './../../src/eventBuffer'; import { BASE_TIMESTAMP } from './../index'; const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 3 }; diff --git a/packages/replay/test/unit/util/createReplayEnvelope.test.ts b/packages/replay/test/unit/util/createReplayEnvelope.test.ts index d5c5f2e5b75f..7de41649147b 100644 --- a/packages/replay/test/unit/util/createReplayEnvelope.test.ts +++ b/packages/replay/test/unit/util/createReplayEnvelope.test.ts @@ -1,4 +1,4 @@ -import { ReplayEvent } from '@sentry/types'; +import type { ReplayEvent } from '@sentry/types'; import { makeDsn } from '@sentry/utils'; import { createReplayEnvelope } from '../../../src/util/createReplayEnvelope'; diff --git a/packages/replay/test/unit/util/getReplayEvent.test.ts b/packages/replay/test/unit/util/getReplayEvent.test.ts index f9f6dcdfc79e..49bb832206c6 100644 --- a/packages/replay/test/unit/util/getReplayEvent.test.ts +++ b/packages/replay/test/unit/util/getReplayEvent.test.ts @@ -1,6 +1,7 @@ import { BrowserClient } from '@sentry/browser'; -import { getCurrentHub, Hub, Scope } from '@sentry/core'; -import { Client, ReplayEvent } from '@sentry/types'; +import type { Hub, Scope } from '@sentry/core'; +import { getCurrentHub } from '@sentry/core'; +import type { Client, ReplayEvent } from '@sentry/types'; import { REPLAY_EVENT_NAME } from '../../../src/constants'; import { prepareReplayEvent } from '../../../src/util/prepareReplayEvent'; diff --git a/packages/replay/test/unit/util/prepareReplayEvent.test.ts b/packages/replay/test/unit/util/prepareReplayEvent.test.ts index 1213548cddb2..899a2ec5b35f 100644 --- a/packages/replay/test/unit/util/prepareReplayEvent.test.ts +++ b/packages/replay/test/unit/util/prepareReplayEvent.test.ts @@ -1,6 +1,7 @@ import { BrowserClient } from '@sentry/browser'; -import { getCurrentHub, Hub, Scope } from '@sentry/core'; -import { Client, ReplayEvent } from '@sentry/types'; +import type { Hub, Scope } from '@sentry/core'; +import { getCurrentHub } from '@sentry/core'; +import type { Client, ReplayEvent } from '@sentry/types'; import { REPLAY_EVENT_NAME } from '../../../src/constants'; import { prepareReplayEvent } from '../../../src/util/prepareReplayEvent'; diff --git a/packages/replay/test/utils/clearSession.ts b/packages/replay/test/utils/clearSession.ts index cb9c44b62493..b5b64ac04531 100644 --- a/packages/replay/test/utils/clearSession.ts +++ b/packages/replay/test/utils/clearSession.ts @@ -1,5 +1,5 @@ import { REPLAY_SESSION_KEY, WINDOW } from '../../src/constants'; -import { ReplayContainer } from '../../src/types'; +import type { ReplayContainer } from '../../src/types'; export function clearSession(replay: ReplayContainer) { deleteSession(); diff --git a/packages/replay/test/utils/getDefaultBrowserClientOptions.ts b/packages/replay/test/utils/getDefaultBrowserClientOptions.ts index 74aafee7a1b3..63ad424b6202 100644 --- a/packages/replay/test/utils/getDefaultBrowserClientOptions.ts +++ b/packages/replay/test/utils/getDefaultBrowserClientOptions.ts @@ -1,5 +1,5 @@ import { createTransport } from '@sentry/core'; -import { ClientOptions } from '@sentry/types'; +import type { ClientOptions } from '@sentry/types'; import { resolvedSyncPromise } from '@sentry/utils'; export function getDefaultBrowserClientOptions(options: Partial = {}): ClientOptions { diff --git a/packages/serverless/src/awslambda.ts b/packages/serverless/src/awslambda.ts index 18b9ea2387d4..29dd10da6602 100644 --- a/packages/serverless/src/awslambda.ts +++ b/packages/serverless/src/awslambda.ts @@ -1,12 +1,13 @@ /* eslint-disable max-lines */ +import type { Scope } from '@sentry/node'; import * as Sentry from '@sentry/node'; -import { captureException, captureMessage, flush, getCurrentHub, Scope, withScope } from '@sentry/node'; +import { captureException, captureMessage, flush, getCurrentHub, withScope } from '@sentry/node'; import { extractTraceparentData } from '@sentry/tracing'; -import { Integration } from '@sentry/types'; +import type { Integration } from '@sentry/types'; import { baggageHeaderToDynamicSamplingContext, dsnFromString, dsnToString, isString, logger } 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 -import { Context, Handler } from 'aws-lambda'; +import type { Context, Handler } from 'aws-lambda'; import { existsSync } from 'fs'; import { hostname } from 'os'; import { basename, resolve } from 'path'; diff --git a/packages/serverless/src/awsservices.ts b/packages/serverless/src/awsservices.ts index 66b2ea93c52d..431d0653ee69 100644 --- a/packages/serverless/src/awsservices.ts +++ b/packages/serverless/src/awsservices.ts @@ -1,9 +1,9 @@ import { getCurrentHub } from '@sentry/node'; -import { Integration, Span, Transaction } from '@sentry/types'; +import type { Integration, Span, Transaction } from '@sentry/types'; import { fill } from '@sentry/utils'; // 'aws-sdk/global' import is expected to be type-only so it's erased in the final .js file. // When TypeScript compiler is upgraded, use `import type` syntax to explicitly assert that we don't want to load a module here. -import * as AWS from 'aws-sdk/global'; +import type * as AWS from 'aws-sdk/global'; type GenericParams = { [key: string]: any }; // eslint-disable-line @typescript-eslint/no-explicit-any type MakeRequestCallback = (err: AWS.AWSError, data: TResult) => void; diff --git a/packages/serverless/src/gcpfunction/cloud_events.ts b/packages/serverless/src/gcpfunction/cloud_events.ts index 6ab7b173784c..00088687bec6 100644 --- a/packages/serverless/src/gcpfunction/cloud_events.ts +++ b/packages/serverless/src/gcpfunction/cloud_events.ts @@ -2,7 +2,7 @@ import { captureException, flush, getCurrentHub } from '@sentry/node'; import { logger } from '@sentry/utils'; import { domainify, getActiveDomain, proxyFunction } from '../utils'; -import { CloudEventFunction, CloudEventFunctionWithCallback, WrapperOptions } from './general'; +import type { CloudEventFunction, CloudEventFunctionWithCallback, WrapperOptions } from './general'; export type CloudEventFunctionWrapperOptions = WrapperOptions; diff --git a/packages/serverless/src/gcpfunction/events.ts b/packages/serverless/src/gcpfunction/events.ts index 6b83fd8bfba0..f1ad73379ca1 100644 --- a/packages/serverless/src/gcpfunction/events.ts +++ b/packages/serverless/src/gcpfunction/events.ts @@ -2,7 +2,7 @@ import { captureException, flush, getCurrentHub } from '@sentry/node'; import { logger } from '@sentry/utils'; import { domainify, getActiveDomain, proxyFunction } from '../utils'; -import { EventFunction, EventFunctionWithCallback, WrapperOptions } from './general'; +import type { EventFunction, EventFunctionWithCallback, WrapperOptions } from './general'; export type EventFunctionWrapperOptions = WrapperOptions; diff --git a/packages/serverless/src/gcpfunction/http.ts b/packages/serverless/src/gcpfunction/http.ts index 3b62d8301bf1..6636ccdab3d6 100644 --- a/packages/serverless/src/gcpfunction/http.ts +++ b/packages/serverless/src/gcpfunction/http.ts @@ -1,9 +1,10 @@ -import { AddRequestDataToEventOptions, captureException, flush, getCurrentHub } from '@sentry/node'; +import type { AddRequestDataToEventOptions } from '@sentry/node'; +import { captureException, flush, getCurrentHub } from '@sentry/node'; import { extractTraceparentData } from '@sentry/tracing'; import { baggageHeaderToDynamicSamplingContext, isString, logger, stripUrlQueryAndFragment } from '@sentry/utils'; import { domainify, getActiveDomain, proxyFunction } from './../utils'; -import { HttpFunction, WrapperOptions } from './general'; +import type { HttpFunction, WrapperOptions } from './general'; // TODO (v8 / #5257): Remove this whole old/new business and just use the new stuff type ParseRequestOptions = AddRequestDataToEventOptions['include'] & { diff --git a/packages/serverless/src/gcpfunction/index.ts b/packages/serverless/src/gcpfunction/index.ts index 2cd8a0bec73f..12e912d45b77 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 { Integration } from '@sentry/types'; +import type { Integration } from '@sentry/types'; import { GoogleCloudGrpc } from '../google-cloud-grpc'; import { GoogleCloudHttp } from '../google-cloud-http'; diff --git a/packages/serverless/src/google-cloud-grpc.ts b/packages/serverless/src/google-cloud-grpc.ts index 4037877cb3bc..f18aee42e002 100644 --- a/packages/serverless/src/google-cloud-grpc.ts +++ b/packages/serverless/src/google-cloud-grpc.ts @@ -1,7 +1,7 @@ import { getCurrentHub } from '@sentry/node'; -import { Integration, Span, Transaction } from '@sentry/types'; +import type { Integration, Span, Transaction } from '@sentry/types'; import { fill } from '@sentry/utils'; -import { EventEmitter } from 'events'; +import type { EventEmitter } from 'events'; interface GrpcFunction extends CallableFunction { (...args: unknown[]): EventEmitter; diff --git a/packages/serverless/src/google-cloud-http.ts b/packages/serverless/src/google-cloud-http.ts index 81b8d6a69155..04dec4bbe134 100644 --- a/packages/serverless/src/google-cloud-http.ts +++ b/packages/serverless/src/google-cloud-http.ts @@ -1,8 +1,8 @@ // '@google-cloud/common' import is expected to be type-only so it's erased in the final .js file. // When TypeScript compiler is upgraded, use `import type` syntax to explicitly assert that we don't want to load a module here. -import * as common from '@google-cloud/common'; +import type * as common from '@google-cloud/common'; import { getCurrentHub } from '@sentry/node'; -import { Integration, Span, Transaction } from '@sentry/types'; +import type { Integration, Span, Transaction } from '@sentry/types'; import { fill } from '@sentry/utils'; type RequestOptions = common.DecorateRequestOptions; diff --git a/packages/serverless/src/utils.ts b/packages/serverless/src/utils.ts index 0fc9a1a776aa..38e85c6a1c4e 100644 --- a/packages/serverless/src/utils.ts +++ b/packages/serverless/src/utils.ts @@ -1,4 +1,4 @@ -import { Event } from '@sentry/node'; +import type { Event } from '@sentry/node'; import { addExceptionMechanism } from '@sentry/utils'; import * as domain from 'domain'; diff --git a/packages/serverless/test/awslambda.test.ts b/packages/serverless/test/awslambda.test.ts index 419974e73bfe..36b3ed5626e6 100644 --- a/packages/serverless/test/awslambda.test.ts +++ b/packages/serverless/test/awslambda.test.ts @@ -1,6 +1,6 @@ // 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 -import { Callback, Handler } from 'aws-lambda'; +import type { Callback, Handler } from 'aws-lambda'; import * as Sentry from '../src'; diff --git a/packages/serverless/test/gcpfunction.test.ts b/packages/serverless/test/gcpfunction.test.ts index 20f878565a56..a6a58ebb2d8b 100644 --- a/packages/serverless/test/gcpfunction.test.ts +++ b/packages/serverless/test/gcpfunction.test.ts @@ -3,7 +3,7 @@ import * as domain from 'domain'; import * as Sentry from '../src'; import { wrapCloudEventFunction, wrapEventFunction, wrapHttpFunction } from '../src/gcpfunction'; -import { +import type { CloudEventFunction, CloudEventFunctionWithCallback, EventFunction, diff --git a/packages/svelte/src/config.ts b/packages/svelte/src/config.ts index 03c6c0dc1f01..4a62cecfbed5 100644 --- a/packages/svelte/src/config.ts +++ b/packages/svelte/src/config.ts @@ -1,7 +1,7 @@ -import { PreprocessorGroup } from 'svelte/types/compiler/preprocess'; +import type { PreprocessorGroup } from 'svelte/types/compiler/preprocess'; import { componentTrackingPreprocessor, defaultComponentTrackingOptions } from './preprocessors'; -import { SentryPreprocessorGroup, SentrySvelteConfigOptions, SvelteConfig } from './types'; +import type { SentryPreprocessorGroup, SentrySvelteConfigOptions, SvelteConfig } from './types'; const DEFAULT_SENTRY_OPTIONS: SentrySvelteConfigOptions = { componentTracking: defaultComponentTrackingOptions, diff --git a/packages/svelte/src/performance.ts b/packages/svelte/src/performance.ts index 359950e41264..5cb9e0254557 100644 --- a/packages/svelte/src/performance.ts +++ b/packages/svelte/src/performance.ts @@ -1,10 +1,10 @@ import { getCurrentHub } from '@sentry/browser'; -import { Span, Transaction } from '@sentry/types'; +import type { Span, Transaction } from '@sentry/types'; import { afterUpdate, beforeUpdate, onMount } from 'svelte'; import { current_component } from 'svelte/internal'; import { DEFAULT_COMPONENT_NAME, UI_SVELTE_INIT, UI_SVELTE_UPDATE } from './constants'; -import { TrackComponentOptions } from './types'; +import type { TrackComponentOptions } from './types'; const defaultTrackComponentOptions: { trackInit: boolean; diff --git a/packages/svelte/src/preprocessors.ts b/packages/svelte/src/preprocessors.ts index 49f8346aa47d..57722b1dad15 100644 --- a/packages/svelte/src/preprocessors.ts +++ b/packages/svelte/src/preprocessors.ts @@ -1,7 +1,7 @@ import MagicString from 'magic-string'; -import { PreprocessorGroup } from 'svelte/types/compiler/preprocess'; +import type { PreprocessorGroup } from 'svelte/types/compiler/preprocess'; -import { ComponentTrackingInitOptions, SentryPreprocessorGroup, TrackComponentOptions } from './types'; +import type { ComponentTrackingInitOptions, SentryPreprocessorGroup, TrackComponentOptions } from './types'; export const defaultComponentTrackingOptions: Required = { trackComponents: true, diff --git a/packages/svelte/src/sdk.ts b/packages/svelte/src/sdk.ts index 4857163ec137..448ccacbf046 100644 --- a/packages/svelte/src/sdk.ts +++ b/packages/svelte/src/sdk.ts @@ -1,4 +1,5 @@ -import { addGlobalEventProcessor, BrowserOptions, init as browserInit, SDK_VERSION } from '@sentry/browser'; +import type { BrowserOptions } from '@sentry/browser'; +import { addGlobalEventProcessor, init as browserInit, SDK_VERSION } from '@sentry/browser'; import type { EventProcessor } from '@sentry/types'; import { getDomElement } from '@sentry/utils'; /** diff --git a/packages/svelte/src/types.ts b/packages/svelte/src/types.ts index e601b239667d..4f4516606483 100644 --- a/packages/svelte/src/types.ts +++ b/packages/svelte/src/types.ts @@ -1,5 +1,5 @@ -import { CompileOptions } from 'svelte/types/compiler'; -import { PreprocessorGroup } from 'svelte/types/compiler/preprocess'; +import type { CompileOptions } from 'svelte/types/compiler'; +import type { PreprocessorGroup } from 'svelte/types/compiler/preprocess'; // Adds an id property to the preprocessor object we can use to check for duplication // in the preprocessors array diff --git a/packages/svelte/test/config.test.ts b/packages/svelte/test/config.test.ts index db8a699b5ab7..5e33355a4994 100644 --- a/packages/svelte/test/config.test.ts +++ b/packages/svelte/test/config.test.ts @@ -1,6 +1,6 @@ import { withSentryConfig } from '../src/config'; import { componentTrackingPreprocessor, FIRST_PASS_COMPONENT_TRACKING_PREPROC_ID } from '../src/preprocessors'; -import { SentryPreprocessorGroup, SentrySvelteConfigOptions, SvelteConfig } from '../src/types'; +import type { SentryPreprocessorGroup, SentrySvelteConfigOptions, SvelteConfig } from '../src/types'; describe('withSentryConfig', () => { it.each([ diff --git a/packages/svelte/test/performance.test.ts b/packages/svelte/test/performance.test.ts index cb039870a719..cbfd320f35bd 100644 --- a/packages/svelte/test/performance.test.ts +++ b/packages/svelte/test/performance.test.ts @@ -1,4 +1,4 @@ -import { Scope } from '@sentry/core'; +import type { Scope } from '@sentry/core'; import { act, render } from '@testing-library/svelte'; // linter doesn't like Svelte component imports diff --git a/packages/svelte/test/preprocessors.test.ts b/packages/svelte/test/preprocessors.test.ts index dd3d3cff1130..57a235ce4cfd 100644 --- a/packages/svelte/test/preprocessors.test.ts +++ b/packages/svelte/test/preprocessors.test.ts @@ -6,7 +6,7 @@ import { defaultComponentTrackingOptions, FIRST_PASS_COMPONENT_TRACKING_PREPROC_ID, } from '../src/preprocessors'; -import { SentryPreprocessorGroup } from '../src/types'; +import type { SentryPreprocessorGroup } from '../src/types'; function expectComponentCodeToBeModified( preprocessedComponents: { diff --git a/packages/svelte/test/sdk.test.ts b/packages/svelte/test/sdk.test.ts index a716cb35105e..8203b025a437 100644 --- a/packages/svelte/test/sdk.test.ts +++ b/packages/svelte/test/sdk.test.ts @@ -1,5 +1,5 @@ import { addGlobalEventProcessor, init as browserInit, SDK_VERSION } from '@sentry/browser'; -import { EventProcessor } from '@sentry/types'; +import type { EventProcessor } from '@sentry/types'; import { detectAndReportSvelteKit, init as svelteInit, isSvelteKitApp } from '../src/sdk'; diff --git a/packages/tracing/src/browser/backgroundtab.ts b/packages/tracing/src/browser/backgroundtab.ts index 62df53a9c9b9..8c55e9853901 100644 --- a/packages/tracing/src/browser/backgroundtab.ts +++ b/packages/tracing/src/browser/backgroundtab.ts @@ -1,7 +1,7 @@ import { logger } from '@sentry/utils'; -import { IdleTransaction } from '../idletransaction'; -import { SpanStatusType } from '../span'; +import type { IdleTransaction } from '../idletransaction'; +import type { SpanStatusType } from '../span'; import { getActiveTransaction } from '../utils'; import { WINDOW } from './types'; diff --git a/packages/tracing/src/browser/browsertracing.ts b/packages/tracing/src/browser/browsertracing.ts index c9ca077fb179..df275bc7357d 100644 --- a/packages/tracing/src/browser/browsertracing.ts +++ b/packages/tracing/src/browser/browsertracing.ts @@ -1,23 +1,16 @@ /* eslint-disable max-lines */ -import { Hub } from '@sentry/core'; -import { EventProcessor, Integration, Transaction, TransactionContext, TransactionSource } from '@sentry/types'; +import type { Hub } from '@sentry/core'; +import type { EventProcessor, Integration, Transaction, TransactionContext, TransactionSource } from '@sentry/types'; import { baggageHeaderToDynamicSamplingContext, getDomElement, logger } from '@sentry/utils'; import { startIdleTransaction } from '../hubextensions'; -import { - DEFAULT_FINAL_TIMEOUT, - DEFAULT_HEARTBEAT_INTERVAL, - DEFAULT_IDLE_TIMEOUT, - IdleTransaction, -} from '../idletransaction'; +import type { IdleTransaction } from '../idletransaction'; +import { DEFAULT_FINAL_TIMEOUT, DEFAULT_HEARTBEAT_INTERVAL, DEFAULT_IDLE_TIMEOUT } from '../idletransaction'; import { extractTraceparentData } from '../utils'; import { registerBackgroundTabDetection } from './backgroundtab'; import { addPerformanceEntries, startTrackingLongTasks, startTrackingWebVitals } from './metrics'; -import { - defaultRequestInstrumentationOptions, - instrumentOutgoingRequests, - RequestInstrumentationOptions, -} from './request'; +import type { RequestInstrumentationOptions } from './request'; +import { defaultRequestInstrumentationOptions, instrumentOutgoingRequests } from './request'; import { instrumentRoutingWithDefaults } from './router'; import { WINDOW } from './types'; diff --git a/packages/tracing/src/browser/metrics/index.ts b/packages/tracing/src/browser/metrics/index.ts index 481a6ea59d83..7bd575e90024 100644 --- a/packages/tracing/src/browser/metrics/index.ts +++ b/packages/tracing/src/browser/metrics/index.ts @@ -1,9 +1,9 @@ /* eslint-disable max-lines */ -import { Measurements } from '@sentry/types'; +import type { Measurements } from '@sentry/types'; import { browserPerformanceTimeOrigin, htmlTreeAsString, logger } from '@sentry/utils'; -import { IdleTransaction } from '../../idletransaction'; -import { Transaction } from '../../transaction'; +import type { IdleTransaction } from '../../idletransaction'; +import type { Transaction } from '../../transaction'; import { getActiveTransaction, msToSec } from '../../utils'; import { WINDOW } from '../types'; import { onCLS } from '../web-vitals/getCLS'; @@ -11,7 +11,7 @@ import { onFID } from '../web-vitals/getFID'; import { onLCP } from '../web-vitals/getLCP'; import { getVisibilityWatcher } from '../web-vitals/lib/getVisibilityWatcher'; import { observe } from '../web-vitals/lib/observe'; -import { NavigatorDeviceMemory, NavigatorNetworkInformation } from '../web-vitals/types'; +import type { NavigatorDeviceMemory, NavigatorNetworkInformation } from '../web-vitals/types'; import { _startChild, isMeasurementValue } from './utils'; function getBrowserPerformanceAPI(): Performance | undefined { diff --git a/packages/tracing/src/browser/metrics/utils.ts b/packages/tracing/src/browser/metrics/utils.ts index 4894dbd3ec8e..7c30a9090245 100644 --- a/packages/tracing/src/browser/metrics/utils.ts +++ b/packages/tracing/src/browser/metrics/utils.ts @@ -1,6 +1,6 @@ -import { Span, SpanContext } from '@sentry/types'; +import type { Span, SpanContext } from '@sentry/types'; -import { Transaction } from '../../transaction'; +import type { Transaction } from '../../transaction'; /** * Checks if a given value is a valid measurement value. diff --git a/packages/tracing/src/browser/router.ts b/packages/tracing/src/browser/router.ts index f0b50a0d014a..b66fdee30e9d 100644 --- a/packages/tracing/src/browser/router.ts +++ b/packages/tracing/src/browser/router.ts @@ -1,4 +1,4 @@ -import { Transaction, TransactionContext } from '@sentry/types'; +import type { Transaction, TransactionContext } from '@sentry/types'; import { addInstrumentationHandler, logger } from '@sentry/utils'; import { WINDOW } from './types'; diff --git a/packages/tracing/src/browser/web-vitals/getCLS.ts b/packages/tracing/src/browser/web-vitals/getCLS.ts index 0980eb5cf06d..3abddfac07cb 100644 --- a/packages/tracing/src/browser/web-vitals/getCLS.ts +++ b/packages/tracing/src/browser/web-vitals/getCLS.ts @@ -18,7 +18,7 @@ import { bindReporter } from './lib/bindReporter'; import { initMetric } from './lib/initMetric'; import { observe } from './lib/observe'; import { onHidden } from './lib/onHidden'; -import { CLSMetric, ReportCallback } from './types'; +import type { CLSMetric, ReportCallback } from './types'; /** * Calculates the [CLS](https://web.dev/cls/) value for the current page and diff --git a/packages/tracing/src/browser/web-vitals/getFID.ts b/packages/tracing/src/browser/web-vitals/getFID.ts index fcf5d529669c..fd19e112121a 100644 --- a/packages/tracing/src/browser/web-vitals/getFID.ts +++ b/packages/tracing/src/browser/web-vitals/getFID.ts @@ -19,7 +19,7 @@ import { getVisibilityWatcher } from './lib/getVisibilityWatcher'; import { initMetric } from './lib/initMetric'; import { observe } from './lib/observe'; import { onHidden } from './lib/onHidden'; -import { FIDMetric, PerformanceEventTiming, ReportCallback } from './types'; +import type { FIDMetric, PerformanceEventTiming, ReportCallback } from './types'; /** * Calculates the [FID](https://web.dev/fid/) value for the current page and diff --git a/packages/tracing/src/browser/web-vitals/getLCP.ts b/packages/tracing/src/browser/web-vitals/getLCP.ts index 01ddb2465ebc..bf834c07ce4e 100644 --- a/packages/tracing/src/browser/web-vitals/getLCP.ts +++ b/packages/tracing/src/browser/web-vitals/getLCP.ts @@ -20,7 +20,7 @@ import { getVisibilityWatcher } from './lib/getVisibilityWatcher'; import { initMetric } from './lib/initMetric'; import { observe } from './lib/observe'; import { onHidden } from './lib/onHidden'; -import { LCPMetric, ReportCallback } from './types'; +import type { LCPMetric, ReportCallback } from './types'; const reportedMetricIDs: Record = {}; diff --git a/packages/tracing/src/browser/web-vitals/lib/bindReporter.ts b/packages/tracing/src/browser/web-vitals/lib/bindReporter.ts index 6e304d747c95..79f3f874e2d8 100644 --- a/packages/tracing/src/browser/web-vitals/lib/bindReporter.ts +++ b/packages/tracing/src/browser/web-vitals/lib/bindReporter.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Metric, ReportCallback } from '../types'; +import type { Metric, ReportCallback } from '../types'; export const bindReporter = ( callback: ReportCallback, diff --git a/packages/tracing/src/browser/web-vitals/lib/getNavigationEntry.ts b/packages/tracing/src/browser/web-vitals/lib/getNavigationEntry.ts index a639eee2bb88..9aaa8939b6dc 100644 --- a/packages/tracing/src/browser/web-vitals/lib/getNavigationEntry.ts +++ b/packages/tracing/src/browser/web-vitals/lib/getNavigationEntry.ts @@ -15,7 +15,7 @@ */ import { WINDOW } from '../../types'; -import { NavigationTimingPolyfillEntry } from '../types'; +import type { NavigationTimingPolyfillEntry } from '../types'; const getNavigationEntryFromPerformanceTiming = (): NavigationTimingPolyfillEntry => { // eslint-disable-next-line deprecation/deprecation diff --git a/packages/tracing/src/browser/web-vitals/lib/initMetric.ts b/packages/tracing/src/browser/web-vitals/lib/initMetric.ts index c486dac6b24d..2fa5854fd6db 100644 --- a/packages/tracing/src/browser/web-vitals/lib/initMetric.ts +++ b/packages/tracing/src/browser/web-vitals/lib/initMetric.ts @@ -15,7 +15,7 @@ */ import { WINDOW } from '../../types'; -import { Metric } from '../types'; +import type { Metric } from '../types'; import { generateUniqueID } from './generateUniqueID'; import { getActivationStart } from './getActivationStart'; import { getNavigationEntry } from './getNavigationEntry'; diff --git a/packages/tracing/src/browser/web-vitals/lib/observe.ts b/packages/tracing/src/browser/web-vitals/lib/observe.ts index d3f6a0f14153..685105d5c7dc 100644 --- a/packages/tracing/src/browser/web-vitals/lib/observe.ts +++ b/packages/tracing/src/browser/web-vitals/lib/observe.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { FirstInputPolyfillEntry, NavigationTimingPolyfillEntry, PerformancePaintTiming } from '../types'; +import type { FirstInputPolyfillEntry, NavigationTimingPolyfillEntry, PerformancePaintTiming } from '../types'; export interface PerformanceEntryHandler { (entry: PerformanceEntry): void; diff --git a/packages/tracing/src/browser/web-vitals/types.ts b/packages/tracing/src/browser/web-vitals/types.ts index c78152748f97..ef8de70c12bb 100644 --- a/packages/tracing/src/browser/web-vitals/types.ts +++ b/packages/tracing/src/browser/web-vitals/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { FirstInputPolyfillCallback } from './types/polyfills'; +import type { FirstInputPolyfillCallback } from './types/polyfills'; export * from './types/base'; export * from './types/polyfills'; diff --git a/packages/tracing/src/browser/web-vitals/types/base.ts b/packages/tracing/src/browser/web-vitals/types/base.ts index 5194a8fd623b..5dc45f00558d 100644 --- a/packages/tracing/src/browser/web-vitals/types/base.ts +++ b/packages/tracing/src/browser/web-vitals/types/base.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { FirstInputPolyfillEntry, NavigationTimingPolyfillEntry } from './polyfills'; +import type { FirstInputPolyfillEntry, NavigationTimingPolyfillEntry } from './polyfills'; export interface Metric { /** diff --git a/packages/tracing/src/browser/web-vitals/types/cls.ts b/packages/tracing/src/browser/web-vitals/types/cls.ts index c4252dc31916..0c97a5dde9aa 100644 --- a/packages/tracing/src/browser/web-vitals/types/cls.ts +++ b/packages/tracing/src/browser/web-vitals/types/cls.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { LoadState, Metric, ReportCallback } from './base'; +import type { LoadState, Metric, ReportCallback } from './base'; /** * A CLS-specific version of the Metric object. diff --git a/packages/tracing/src/browser/web-vitals/types/fid.ts b/packages/tracing/src/browser/web-vitals/types/fid.ts index 324c7e25ff66..926f0675b90a 100644 --- a/packages/tracing/src/browser/web-vitals/types/fid.ts +++ b/packages/tracing/src/browser/web-vitals/types/fid.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { LoadState, Metric, ReportCallback } from './base'; -import { FirstInputPolyfillEntry } from './polyfills'; +import type { LoadState, Metric, ReportCallback } from './base'; +import type { FirstInputPolyfillEntry } from './polyfills'; /** * An FID-specific version of the Metric object. diff --git a/packages/tracing/src/browser/web-vitals/types/lcp.ts b/packages/tracing/src/browser/web-vitals/types/lcp.ts index 841ddca1e6de..c94573c1caaf 100644 --- a/packages/tracing/src/browser/web-vitals/types/lcp.ts +++ b/packages/tracing/src/browser/web-vitals/types/lcp.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { Metric, ReportCallback } from './base'; -import { NavigationTimingPolyfillEntry } from './polyfills'; +import type { Metric, ReportCallback } from './base'; +import type { NavigationTimingPolyfillEntry } from './polyfills'; /** * An LCP-specific version of the Metric object. diff --git a/packages/tracing/src/errors.ts b/packages/tracing/src/errors.ts index 5a406500ffee..1952fb75a915 100644 --- a/packages/tracing/src/errors.ts +++ b/packages/tracing/src/errors.ts @@ -1,6 +1,6 @@ import { addInstrumentationHandler, logger } from '@sentry/utils'; -import { SpanStatusType } from './span'; +import type { SpanStatusType } from './span'; import { getActiveTransaction } from './utils'; /** diff --git a/packages/tracing/src/hubextensions.ts b/packages/tracing/src/hubextensions.ts index c3cc2967be9d..9514cb00b321 100644 --- a/packages/tracing/src/hubextensions.ts +++ b/packages/tracing/src/hubextensions.ts @@ -1,5 +1,6 @@ -import { getMainCarrier, Hub } from '@sentry/core'; -import { +import type { Hub } from '@sentry/core'; +import { getMainCarrier } from '@sentry/core'; +import type { ClientOptions, CustomSamplingContext, Integration, diff --git a/packages/tracing/src/idletransaction.ts b/packages/tracing/src/idletransaction.ts index f509ff931a45..395b116481b1 100644 --- a/packages/tracing/src/idletransaction.ts +++ b/packages/tracing/src/idletransaction.ts @@ -1,9 +1,10 @@ /* eslint-disable max-lines */ -import { Hub } from '@sentry/core'; -import { TransactionContext } from '@sentry/types'; +import type { Hub } from '@sentry/core'; +import type { TransactionContext } from '@sentry/types'; import { logger, timestampWithMs } from '@sentry/utils'; -import { Span, SpanRecorder } from './span'; +import type { Span } from './span'; +import { SpanRecorder } from './span'; import { Transaction } from './transaction'; export const DEFAULT_IDLE_TIMEOUT = 1000; diff --git a/packages/tracing/src/integrations/node/apollo.ts b/packages/tracing/src/integrations/node/apollo.ts index 09442216deb7..41b136abff42 100644 --- a/packages/tracing/src/integrations/node/apollo.ts +++ b/packages/tracing/src/integrations/node/apollo.ts @@ -1,5 +1,5 @@ -import { Hub } from '@sentry/core'; -import { EventProcessor, Integration } from '@sentry/types'; +import type { Hub } from '@sentry/core'; +import type { EventProcessor, Integration } from '@sentry/types'; import { arrayify, fill, isThenable, loadModule, logger } from '@sentry/utils'; import { shouldDisableAutoInstrumentation } from './utils/node-utils'; diff --git a/packages/tracing/src/integrations/node/express.ts b/packages/tracing/src/integrations/node/express.ts index cb405664600a..8b3bcb52fcf4 100644 --- a/packages/tracing/src/integrations/node/express.ts +++ b/packages/tracing/src/integrations/node/express.ts @@ -1,5 +1,5 @@ /* eslint-disable max-lines */ -import { Hub, Integration, PolymorphicRequest, Transaction } from '@sentry/types'; +import type { Hub, Integration, PolymorphicRequest, Transaction } from '@sentry/types'; import { extractPathForTransaction, getNumberOfUrlSegments, isRegExp, logger } from '@sentry/utils'; import { shouldDisableAutoInstrumentation } from './utils/node-utils'; diff --git a/packages/tracing/src/integrations/node/graphql.ts b/packages/tracing/src/integrations/node/graphql.ts index 0bf267850cec..12f04b7c1e57 100644 --- a/packages/tracing/src/integrations/node/graphql.ts +++ b/packages/tracing/src/integrations/node/graphql.ts @@ -1,5 +1,5 @@ -import { Hub } from '@sentry/core'; -import { EventProcessor, Integration } from '@sentry/types'; +import type { Hub } from '@sentry/core'; +import type { EventProcessor, Integration } from '@sentry/types'; import { fill, isThenable, loadModule, logger } from '@sentry/utils'; import { shouldDisableAutoInstrumentation } from './utils/node-utils'; diff --git a/packages/tracing/src/integrations/node/mongo.ts b/packages/tracing/src/integrations/node/mongo.ts index b2933ba37022..37335358c82e 100644 --- a/packages/tracing/src/integrations/node/mongo.ts +++ b/packages/tracing/src/integrations/node/mongo.ts @@ -1,5 +1,5 @@ -import { Hub } from '@sentry/core'; -import { EventProcessor, Integration, SpanContext } from '@sentry/types'; +import type { Hub } from '@sentry/core'; +import type { EventProcessor, Integration, SpanContext } from '@sentry/types'; import { fill, isThenable, loadModule, logger } from '@sentry/utils'; import { shouldDisableAutoInstrumentation } from './utils/node-utils'; diff --git a/packages/tracing/src/integrations/node/mysql.ts b/packages/tracing/src/integrations/node/mysql.ts index 9303522ea260..6e6a80ac59a6 100644 --- a/packages/tracing/src/integrations/node/mysql.ts +++ b/packages/tracing/src/integrations/node/mysql.ts @@ -1,5 +1,5 @@ -import { Hub } from '@sentry/core'; -import { EventProcessor, Integration } from '@sentry/types'; +import type { Hub } from '@sentry/core'; +import type { EventProcessor, Integration } from '@sentry/types'; import { fill, loadModule, logger } from '@sentry/utils'; import { shouldDisableAutoInstrumentation } from './utils/node-utils'; diff --git a/packages/tracing/src/integrations/node/postgres.ts b/packages/tracing/src/integrations/node/postgres.ts index 5883ef94527f..41ad31b20660 100644 --- a/packages/tracing/src/integrations/node/postgres.ts +++ b/packages/tracing/src/integrations/node/postgres.ts @@ -1,5 +1,5 @@ -import { Hub } from '@sentry/core'; -import { EventProcessor, Integration } from '@sentry/types'; +import type { Hub } from '@sentry/core'; +import type { EventProcessor, Integration } from '@sentry/types'; import { fill, isThenable, loadModule, logger } from '@sentry/utils'; import { shouldDisableAutoInstrumentation } from './utils/node-utils'; diff --git a/packages/tracing/src/integrations/node/prisma.ts b/packages/tracing/src/integrations/node/prisma.ts index 371052217911..2215cf2a817a 100644 --- a/packages/tracing/src/integrations/node/prisma.ts +++ b/packages/tracing/src/integrations/node/prisma.ts @@ -1,5 +1,5 @@ -import { Hub } from '@sentry/core'; -import { EventProcessor, Integration } from '@sentry/types'; +import type { Hub } from '@sentry/core'; +import type { EventProcessor, Integration } from '@sentry/types'; import { isThenable, logger } from '@sentry/utils'; import { shouldDisableAutoInstrumentation } from './utils/node-utils'; diff --git a/packages/tracing/src/integrations/node/utils/node-utils.ts b/packages/tracing/src/integrations/node/utils/node-utils.ts index 9cb6970c9ebc..11f8f2430b61 100644 --- a/packages/tracing/src/integrations/node/utils/node-utils.ts +++ b/packages/tracing/src/integrations/node/utils/node-utils.ts @@ -1,4 +1,4 @@ -import { Hub } from '@sentry/types'; +import type { Hub } from '@sentry/types'; /** * Check if Sentry auto-instrumentation should be disabled. diff --git a/packages/tracing/src/span.ts b/packages/tracing/src/span.ts index be070c0cbfa3..db11bb931df7 100644 --- a/packages/tracing/src/span.ts +++ b/packages/tracing/src/span.ts @@ -1,5 +1,12 @@ /* eslint-disable max-lines */ -import { Instrumenter, Primitive, Span as SpanInterface, SpanContext, TraceContext, Transaction } from '@sentry/types'; +import type { + Instrumenter, + Primitive, + Span as SpanInterface, + SpanContext, + TraceContext, + Transaction, +} from '@sentry/types'; import { dropUndefinedKeys, logger, timestampWithMs, uuid4 } from '@sentry/utils'; /** diff --git a/packages/tracing/src/transaction.ts b/packages/tracing/src/transaction.ts index 78a33fd17313..b93d52b81c38 100644 --- a/packages/tracing/src/transaction.ts +++ b/packages/tracing/src/transaction.ts @@ -1,5 +1,6 @@ -import { getCurrentHub, Hub } from '@sentry/core'; -import { +import type { Hub } from '@sentry/core'; +import { getCurrentHub } from '@sentry/core'; +import type { Context, Contexts, DynamicSamplingContext, diff --git a/packages/tracing/src/utils.ts b/packages/tracing/src/utils.ts index d79a4f91a2df..66cba77843b7 100644 --- a/packages/tracing/src/utils.ts +++ b/packages/tracing/src/utils.ts @@ -1,5 +1,6 @@ -import { getCurrentHub, Hub } from '@sentry/core'; -import { Options, Transaction } from '@sentry/types'; +import type { Hub } from '@sentry/core'; +import { getCurrentHub } from '@sentry/core'; +import type { Options, Transaction } from '@sentry/types'; /** * The `extractTraceparentData` function and `TRACEPARENT_REGEXP` constant used diff --git a/packages/tracing/test/browser/browsertracing.test.ts b/packages/tracing/test/browser/browsertracing.test.ts index 3840ea75d6d6..a00025811874 100644 --- a/packages/tracing/test/browser/browsertracing.test.ts +++ b/packages/tracing/test/browser/browsertracing.test.ts @@ -1,19 +1,16 @@ import { BrowserClient, WINDOW } from '@sentry/browser'; import { Hub, makeMain } from '@sentry/core'; import type { BaseTransportOptions, ClientOptions, DsnComponents } from '@sentry/types'; -import { InstrumentHandlerCallback, InstrumentHandlerType } from '@sentry/utils'; +import type { InstrumentHandlerCallback, InstrumentHandlerType } from '@sentry/utils'; import { JSDOM } from 'jsdom'; -import { BrowserTracing, BrowserTracingOptions, getMetaContent } from '../../src/browser/browsertracing'; +import type { BrowserTracingOptions } from '../../src/browser/browsertracing'; +import { BrowserTracing, getMetaContent } from '../../src/browser/browsertracing'; import { defaultRequestInstrumentationOptions } from '../../src/browser/request'; import { instrumentRoutingWithDefaults } from '../../src/browser/router'; import * as hubExtensions from '../../src/hubextensions'; -import { - DEFAULT_FINAL_TIMEOUT, - DEFAULT_HEARTBEAT_INTERVAL, - DEFAULT_IDLE_TIMEOUT, - IdleTransaction, -} from '../../src/idletransaction'; +import type { IdleTransaction } from '../../src/idletransaction'; +import { DEFAULT_FINAL_TIMEOUT, DEFAULT_HEARTBEAT_INTERVAL, DEFAULT_IDLE_TIMEOUT } from '../../src/idletransaction'; import { getActiveTransaction } from '../../src/utils'; import { getDefaultBrowserClientOptions } from '../testutils'; diff --git a/packages/tracing/test/browser/metrics/index.test.ts b/packages/tracing/test/browser/metrics/index.test.ts index 2bda341e3b29..4820a70a5c9b 100644 --- a/packages/tracing/test/browser/metrics/index.test.ts +++ b/packages/tracing/test/browser/metrics/index.test.ts @@ -1,5 +1,6 @@ import { Transaction } from '../../../src'; -import { _addMeasureSpans, _addResourceSpans, ResourceEntry } from '../../../src/browser/metrics'; +import type { ResourceEntry } from '../../../src/browser/metrics'; +import { _addMeasureSpans, _addResourceSpans } from '../../../src/browser/metrics'; describe('_addMeasureSpans', () => { const transaction = new Transaction({ op: 'pageload', name: '/' }); diff --git a/packages/tracing/test/browser/request.test.ts b/packages/tracing/test/browser/request.test.ts index 732d6ea9b4f8..e2d60aa9edb2 100644 --- a/packages/tracing/test/browser/request.test.ts +++ b/packages/tracing/test/browser/request.test.ts @@ -2,15 +2,10 @@ import { BrowserClient } from '@sentry/browser'; import { Hub, makeMain } from '@sentry/core'; import * as utils from '@sentry/utils'; -import { Span, spanStatusfromHttpCode, Transaction } from '../../src'; -import { - fetchCallback, - FetchData, - instrumentOutgoingRequests, - shouldAttachHeaders, - xhrCallback, - XHRData, -} from '../../src/browser/request'; +import type { Transaction } from '../../src'; +import { Span, spanStatusfromHttpCode } from '../../src'; +import type { FetchData, XHRData } from '../../src/browser/request'; +import { fetchCallback, instrumentOutgoingRequests, shouldAttachHeaders, xhrCallback } from '../../src/browser/request'; import { addExtensionMethods } from '../../src/hubextensions'; import * as tracingUtils from '../../src/utils'; import { getDefaultBrowserClientOptions } from '../testutils'; diff --git a/packages/tracing/test/browser/router.test.ts b/packages/tracing/test/browser/router.test.ts index f0e3fec29084..65ce7e90af48 100644 --- a/packages/tracing/test/browser/router.test.ts +++ b/packages/tracing/test/browser/router.test.ts @@ -1,4 +1,4 @@ -import { InstrumentHandlerCallback, InstrumentHandlerType } from '@sentry/utils'; +import type { InstrumentHandlerCallback, InstrumentHandlerType } from '@sentry/utils'; import { JSDOM } from 'jsdom'; import { instrumentRoutingWithDefaults } from '../../src/browser/router'; diff --git a/packages/tracing/test/errors.test.ts b/packages/tracing/test/errors.test.ts index 7942bf2b85e9..554ee3b3d8c7 100644 --- a/packages/tracing/test/errors.test.ts +++ b/packages/tracing/test/errors.test.ts @@ -1,6 +1,6 @@ import { BrowserClient } from '@sentry/browser'; import { Hub, makeMain } from '@sentry/core'; -import { InstrumentHandlerCallback, InstrumentHandlerType } from '@sentry/utils'; +import type { InstrumentHandlerCallback, InstrumentHandlerType } from '@sentry/utils'; import { registerErrorInstrumentation } from '../src/errors'; import { _addTracingExtensions } from '../src/hubextensions'; diff --git a/packages/tracing/test/span.test.ts b/packages/tracing/test/span.test.ts index 378fac467069..6de0d79b7125 100644 --- a/packages/tracing/test/span.test.ts +++ b/packages/tracing/test/span.test.ts @@ -1,6 +1,6 @@ import { BrowserClient } from '@sentry/browser'; import { Hub, makeMain, Scope } from '@sentry/core'; -import { BaseTransportOptions, ClientOptions, TransactionSource } from '@sentry/types'; +import type { BaseTransportOptions, ClientOptions, TransactionSource } from '@sentry/types'; import { Span, Transaction } from '../src'; import { TRACEPARENT_REGEXP } from '../src/utils'; diff --git a/packages/tracing/test/testutils.ts b/packages/tracing/test/testutils.ts index 4ddd042a1240..071ecdf5111a 100644 --- a/packages/tracing/test/testutils.ts +++ b/packages/tracing/test/testutils.ts @@ -1,5 +1,5 @@ import { createTransport } from '@sentry/browser'; -import { Client, ClientOptions } from '@sentry/types'; +import type { Client, ClientOptions } from '@sentry/types'; import { GLOBAL_OBJ, resolvedSyncPromise } from '@sentry/utils'; import { JSDOM } from 'jsdom'; diff --git a/packages/types/src/breadcrumb.ts b/packages/types/src/breadcrumb.ts index 34fe2dfcd16a..b8e2552a2f34 100644 --- a/packages/types/src/breadcrumb.ts +++ b/packages/types/src/breadcrumb.ts @@ -1,4 +1,4 @@ -import { Severity, SeverityLevel } from './severity'; +import type { Severity, SeverityLevel } from './severity'; /** JSDoc */ export interface Breadcrumb { diff --git a/packages/types/src/client.ts b/packages/types/src/client.ts index 00fe9f5191ed..28d23025ce84 100644 --- a/packages/types/src/client.ts +++ b/packages/types/src/client.ts @@ -1,14 +1,14 @@ -import { EventDropReason } from './clientreport'; -import { DataCategory } from './datacategory'; -import { DsnComponents } from './dsn'; -import { Event, EventHint } from './event'; -import { Integration, IntegrationClass } from './integration'; -import { ClientOptions } from './options'; -import { Scope } from './scope'; -import { SdkMetadata } from './sdkmetadata'; -import { Session, SessionAggregates } from './session'; -import { Severity, SeverityLevel } from './severity'; -import { Transport } from './transport'; +import type { EventDropReason } from './clientreport'; +import type { DataCategory } from './datacategory'; +import type { DsnComponents } from './dsn'; +import type { Event, EventHint } from './event'; +import type { Integration, IntegrationClass } from './integration'; +import type { ClientOptions } from './options'; +import type { Scope } from './scope'; +import type { SdkMetadata } from './sdkmetadata'; +import type { Session, SessionAggregates } from './session'; +import type { Severity, SeverityLevel } from './severity'; +import type { Transport } from './transport'; /** * User-Facing Sentry SDK Client. diff --git a/packages/types/src/clientreport.ts b/packages/types/src/clientreport.ts index 79b3c2a4454c..7b4e181d1102 100644 --- a/packages/types/src/clientreport.ts +++ b/packages/types/src/clientreport.ts @@ -1,4 +1,4 @@ -import { DataCategory } from './datacategory'; +import type { DataCategory } from './datacategory'; export type EventDropReason = | 'before_send' diff --git a/packages/types/src/context.ts b/packages/types/src/context.ts index 120bd82cda9d..bab969899cea 100644 --- a/packages/types/src/context.ts +++ b/packages/types/src/context.ts @@ -1,4 +1,4 @@ -import { Primitive } from './misc'; +import type { Primitive } from './misc'; export type Context = Record; diff --git a/packages/types/src/envelope.ts b/packages/types/src/envelope.ts index 75c18498d5f1..60d67b89d0da 100644 --- a/packages/types/src/envelope.ts +++ b/packages/types/src/envelope.ts @@ -1,11 +1,11 @@ -import { ClientReport } from './clientreport'; -import { DsnComponents } from './dsn'; -import { Event } from './event'; -import { ReplayEvent, ReplayRecordingData } from './replay'; -import { SdkInfo } from './sdkinfo'; -import { Session, SessionAggregates } from './session'; -import { Transaction } from './transaction'; -import { UserFeedback } from './user'; +import type { ClientReport } from './clientreport'; +import type { DsnComponents } from './dsn'; +import type { Event } from './event'; +import type { ReplayEvent, ReplayRecordingData } from './replay'; +import type { SdkInfo } from './sdkinfo'; +import type { Session, SessionAggregates } from './session'; +import type { Transaction } from './transaction'; +import type { UserFeedback } from './user'; // Based on: https://develop.sentry.dev/sdk/envelopes/ diff --git a/packages/types/src/event.ts b/packages/types/src/event.ts index e9df8d364c68..86280fc7e65b 100644 --- a/packages/types/src/event.ts +++ b/packages/types/src/event.ts @@ -1,19 +1,19 @@ -import { Attachment } from './attachment'; -import { Breadcrumb } from './breadcrumb'; -import { Contexts } from './context'; -import { DebugMeta } from './debugMeta'; -import { Exception } from './exception'; -import { Extras } from './extra'; -import { Measurements } from './measurement'; -import { Primitive } from './misc'; -import { Request } from './request'; -import { CaptureContext } from './scope'; -import { SdkInfo } from './sdkinfo'; -import { Severity, SeverityLevel } from './severity'; -import { Span } from './span'; -import { Thread } from './thread'; -import { TransactionNameChange, TransactionSource } from './transaction'; -import { User } from './user'; +import type { Attachment } from './attachment'; +import type { Breadcrumb } from './breadcrumb'; +import type { Contexts } from './context'; +import type { DebugMeta } from './debugMeta'; +import type { Exception } from './exception'; +import type { Extras } from './extra'; +import type { Measurements } from './measurement'; +import type { Primitive } from './misc'; +import type { Request } from './request'; +import type { CaptureContext } from './scope'; +import type { SdkInfo } from './sdkinfo'; +import type { Severity, SeverityLevel } from './severity'; +import type { Span } from './span'; +import type { Thread } from './thread'; +import type { TransactionNameChange, TransactionSource } from './transaction'; +import type { User } from './user'; /** JSDoc */ export interface Event { diff --git a/packages/types/src/eventprocessor.ts b/packages/types/src/eventprocessor.ts index 543c0163ea5f..60a983fa0fdc 100644 --- a/packages/types/src/eventprocessor.ts +++ b/packages/types/src/eventprocessor.ts @@ -1,4 +1,4 @@ -import { Event, EventHint } from './event'; +import type { Event, EventHint } from './event'; /** * Event processors are used to change the event before it will be send. diff --git a/packages/types/src/exception.ts b/packages/types/src/exception.ts index 4b98f3a326d3..a74adf6c1603 100644 --- a/packages/types/src/exception.ts +++ b/packages/types/src/exception.ts @@ -1,5 +1,5 @@ -import { Mechanism } from './mechanism'; -import { Stacktrace } from './stacktrace'; +import type { Mechanism } from './mechanism'; +import type { Stacktrace } from './stacktrace'; /** JSDoc */ export interface Exception { diff --git a/packages/types/src/hub.ts b/packages/types/src/hub.ts index 555da1ef94ab..35a3f9f4a82e 100644 --- a/packages/types/src/hub.ts +++ b/packages/types/src/hub.ts @@ -1,14 +1,14 @@ -import { Breadcrumb, BreadcrumbHint } from './breadcrumb'; -import { Client } from './client'; -import { Event, EventHint } from './event'; -import { Extra, Extras } from './extra'; -import { Integration, IntegrationClass } from './integration'; -import { Primitive } from './misc'; -import { Scope } from './scope'; -import { Session } from './session'; -import { Severity, SeverityLevel } from './severity'; -import { CustomSamplingContext, Transaction, TransactionContext } from './transaction'; -import { User } from './user'; +import type { Breadcrumb, BreadcrumbHint } from './breadcrumb'; +import type { Client } from './client'; +import type { Event, EventHint } from './event'; +import type { Extra, Extras } from './extra'; +import type { Integration, IntegrationClass } from './integration'; +import type { Primitive } from './misc'; +import type { Scope } from './scope'; +import type { Session } from './session'; +import type { Severity, SeverityLevel } from './severity'; +import type { CustomSamplingContext, Transaction, TransactionContext } from './transaction'; +import type { User } from './user'; /** * Internal class used to make sure we always have the latest internal functions diff --git a/packages/types/src/integration.ts b/packages/types/src/integration.ts index 251c135973fd..c7672effc185 100644 --- a/packages/types/src/integration.ts +++ b/packages/types/src/integration.ts @@ -1,5 +1,5 @@ -import { EventProcessor } from './eventprocessor'; -import { Hub } from './hub'; +import type { EventProcessor } from './eventprocessor'; +import type { Hub } from './hub'; /** Integration Class Interface */ export interface IntegrationClass { diff --git a/packages/types/src/misc.ts b/packages/types/src/misc.ts index 3476befa78c6..af9ed8fc6bd7 100644 --- a/packages/types/src/misc.ts +++ b/packages/types/src/misc.ts @@ -1,4 +1,4 @@ -import { QueryParams } from './request'; +import type { QueryParams } from './request'; /** * Data extracted from an incoming request to a node server diff --git a/packages/types/src/options.ts b/packages/types/src/options.ts index 8e6a8465d298..ca53b19f607a 100644 --- a/packages/types/src/options.ts +++ b/packages/types/src/options.ts @@ -1,12 +1,12 @@ -import { Breadcrumb, BreadcrumbHint } from './breadcrumb'; -import { ErrorEvent, Event, EventHint, TransactionEvent } from './event'; -import { Instrumenter } from './instrumenter'; -import { Integration } from './integration'; -import { CaptureContext } from './scope'; -import { SdkMetadata } from './sdkmetadata'; -import { StackLineParser, StackParser } from './stacktrace'; -import { SamplingContext } from './transaction'; -import { BaseTransportOptions, Transport } from './transport'; +import type { Breadcrumb, BreadcrumbHint } from './breadcrumb'; +import type { ErrorEvent, Event, EventHint, TransactionEvent } from './event'; +import type { Instrumenter } from './instrumenter'; +import type { Integration } from './integration'; +import type { CaptureContext } from './scope'; +import type { SdkMetadata } from './sdkmetadata'; +import type { StackLineParser, StackParser } from './stacktrace'; +import type { SamplingContext } from './transaction'; +import type { BaseTransportOptions, Transport } from './transport'; export interface ClientOptions { /** diff --git a/packages/types/src/replay.ts b/packages/types/src/replay.ts index 2027a373bb33..79bbce4c6eb4 100644 --- a/packages/types/src/replay.ts +++ b/packages/types/src/replay.ts @@ -1,4 +1,4 @@ -import { Event } from './event'; +import type { Event } from './event'; /** * NOTE: These types are still considered Beta and subject to change. diff --git a/packages/types/src/scope.ts b/packages/types/src/scope.ts index 1b7be6f5b938..4ed11b287421 100644 --- a/packages/types/src/scope.ts +++ b/packages/types/src/scope.ts @@ -1,14 +1,14 @@ -import { Attachment } from './attachment'; -import { Breadcrumb } from './breadcrumb'; -import { Context, Contexts } from './context'; -import { EventProcessor } from './eventprocessor'; -import { Extra, Extras } from './extra'; -import { Primitive } from './misc'; -import { RequestSession, Session } from './session'; -import { Severity, SeverityLevel } from './severity'; -import { Span } from './span'; -import { Transaction } from './transaction'; -import { User } from './user'; +import type { Attachment } from './attachment'; +import type { Breadcrumb } from './breadcrumb'; +import type { Context, Contexts } from './context'; +import type { EventProcessor } from './eventprocessor'; +import type { Extra, Extras } from './extra'; +import type { Primitive } from './misc'; +import type { RequestSession, Session } from './session'; +import type { Severity, SeverityLevel } from './severity'; +import type { Span } from './span'; +import type { Transaction } from './transaction'; +import type { User } from './user'; /** JSDocs */ export type CaptureContext = Scope | Partial | ((scope: Scope) => Scope); diff --git a/packages/types/src/sdkinfo.ts b/packages/types/src/sdkinfo.ts index 0a839b0c7c95..b287ef0674f5 100644 --- a/packages/types/src/sdkinfo.ts +++ b/packages/types/src/sdkinfo.ts @@ -1,4 +1,4 @@ -import { Package } from './package'; +import type { Package } from './package'; export interface SdkInfo { name?: string; diff --git a/packages/types/src/sdkmetadata.ts b/packages/types/src/sdkmetadata.ts index d65e989cb4c1..40df8046fe3d 100644 --- a/packages/types/src/sdkmetadata.ts +++ b/packages/types/src/sdkmetadata.ts @@ -1,4 +1,4 @@ -import { SdkInfo } from './sdkinfo'; +import type { SdkInfo } from './sdkinfo'; export interface SdkMetadata { sdk?: SdkInfo; diff --git a/packages/types/src/session.ts b/packages/types/src/session.ts index 35d5e0840a3e..3d62a8085444 100644 --- a/packages/types/src/session.ts +++ b/packages/types/src/session.ts @@ -1,4 +1,4 @@ -import { User } from './user'; +import type { User } from './user'; export interface RequestSession { status?: RequestSessionStatus; diff --git a/packages/types/src/span.ts b/packages/types/src/span.ts index 2b0a91ed9ee6..756a6808910c 100644 --- a/packages/types/src/span.ts +++ b/packages/types/src/span.ts @@ -1,6 +1,6 @@ -import { Instrumenter } from './instrumenter'; -import { Primitive } from './misc'; -import { Transaction } from './transaction'; +import type { Instrumenter } from './instrumenter'; +import type { Primitive } from './misc'; +import type { Transaction } from './transaction'; /** Interface holding all properties that can be set on a Span on creation. */ export interface SpanContext { diff --git a/packages/types/src/stacktrace.ts b/packages/types/src/stacktrace.ts index ae2f350f716b..c27cbf00a12c 100644 --- a/packages/types/src/stacktrace.ts +++ b/packages/types/src/stacktrace.ts @@ -1,4 +1,4 @@ -import { StackFrame } from './stackframe'; +import type { StackFrame } from './stackframe'; /** JSDoc */ export interface Stacktrace { diff --git a/packages/types/src/thread.ts b/packages/types/src/thread.ts index 5cc1a2b63891..1cfad253a299 100644 --- a/packages/types/src/thread.ts +++ b/packages/types/src/thread.ts @@ -1,4 +1,4 @@ -import { Stacktrace } from './stacktrace'; +import type { Stacktrace } from './stacktrace'; /** JSDoc */ export interface Thread { diff --git a/packages/types/src/transaction.ts b/packages/types/src/transaction.ts index ae2dd54e2629..e60132a8e550 100644 --- a/packages/types/src/transaction.ts +++ b/packages/types/src/transaction.ts @@ -1,10 +1,10 @@ -import { Context } from './context'; -import { DynamicSamplingContext } from './envelope'; -import { Instrumenter } from './instrumenter'; -import { MeasurementUnit } from './measurement'; -import { ExtractedNodeRequestData, Primitive, WorkerLocation } from './misc'; -import { PolymorphicRequest } from './polymorphics'; -import { Span, SpanContext } from './span'; +import type { Context } from './context'; +import type { DynamicSamplingContext } from './envelope'; +import type { Instrumenter } from './instrumenter'; +import type { MeasurementUnit } from './measurement'; +import type { ExtractedNodeRequestData, Primitive, WorkerLocation } from './misc'; +import type { PolymorphicRequest } from './polymorphics'; +import type { Span, SpanContext } from './span'; /** * Interface holding Transaction-specific properties diff --git a/packages/types/src/transport.ts b/packages/types/src/transport.ts index b254edecc6df..05638b67228e 100644 --- a/packages/types/src/transport.ts +++ b/packages/types/src/transport.ts @@ -1,6 +1,6 @@ -import { Client } from './client'; -import { Envelope } from './envelope'; -import { TextEncoderInternal } from './textencoder'; +import type { Client } from './client'; +import type { Envelope } from './envelope'; +import type { TextEncoderInternal } from './textencoder'; export type TransportRequest = { body: string | Uint8Array; diff --git a/packages/utils/src/baggage.ts b/packages/utils/src/baggage.ts index 1a2fa58d52a6..406ee3adc819 100644 --- a/packages/utils/src/baggage.ts +++ b/packages/utils/src/baggage.ts @@ -1,4 +1,4 @@ -import { DynamicSamplingContext } from '@sentry/types'; +import type { DynamicSamplingContext } from '@sentry/types'; import { isString } from './is'; import { logger } from './logger'; diff --git a/packages/utils/src/buildPolyfills/_asyncOptionalChain.ts b/packages/utils/src/buildPolyfills/_asyncOptionalChain.ts index 10a10aa03f3a..71c0382451b5 100644 --- a/packages/utils/src/buildPolyfills/_asyncOptionalChain.ts +++ b/packages/utils/src/buildPolyfills/_asyncOptionalChain.ts @@ -1,4 +1,4 @@ -import { GenericFunction } from './types'; +import type { GenericFunction } from './types'; /** * Polyfill for the optional chain operator, `?.`, given previous conversion of the expression into an array of values, diff --git a/packages/utils/src/buildPolyfills/_createNamedExportFrom.ts b/packages/utils/src/buildPolyfills/_createNamedExportFrom.ts index 0c632e586aba..678e7024b7a1 100644 --- a/packages/utils/src/buildPolyfills/_createNamedExportFrom.ts +++ b/packages/utils/src/buildPolyfills/_createNamedExportFrom.ts @@ -1,4 +1,4 @@ -import { GenericObject } from './types'; +import type { GenericObject } from './types'; declare const exports: GenericObject; diff --git a/packages/utils/src/buildPolyfills/_createStarExport.ts b/packages/utils/src/buildPolyfills/_createStarExport.ts index f4f36c8c041a..38d4ae340a85 100644 --- a/packages/utils/src/buildPolyfills/_createStarExport.ts +++ b/packages/utils/src/buildPolyfills/_createStarExport.ts @@ -1,4 +1,4 @@ -import { GenericObject } from './types'; +import type { GenericObject } from './types'; declare const exports: GenericObject; diff --git a/packages/utils/src/buildPolyfills/_interopDefault.ts b/packages/utils/src/buildPolyfills/_interopDefault.ts index 5bed0ef4e3f1..2b56c164b698 100644 --- a/packages/utils/src/buildPolyfills/_interopDefault.ts +++ b/packages/utils/src/buildPolyfills/_interopDefault.ts @@ -1,4 +1,4 @@ -import { RequireResult } from './types'; +import type { RequireResult } from './types'; /** * Unwraps a module if it has been wrapped in an object under the key `default`. diff --git a/packages/utils/src/buildPolyfills/_interopNamespace.ts b/packages/utils/src/buildPolyfills/_interopNamespace.ts index 2211e21accfa..1401d986e9a1 100644 --- a/packages/utils/src/buildPolyfills/_interopNamespace.ts +++ b/packages/utils/src/buildPolyfills/_interopNamespace.ts @@ -1,4 +1,4 @@ -import { RequireResult } from './types'; +import type { RequireResult } from './types'; /** * Adds a self-referential `default` property to CJS modules which aren't the result of transpilation from ESM modules. diff --git a/packages/utils/src/buildPolyfills/_interopNamespaceDefaultOnly.ts b/packages/utils/src/buildPolyfills/_interopNamespaceDefaultOnly.ts index 66785a79e92f..98d96e329119 100644 --- a/packages/utils/src/buildPolyfills/_interopNamespaceDefaultOnly.ts +++ b/packages/utils/src/buildPolyfills/_interopNamespaceDefaultOnly.ts @@ -1,4 +1,4 @@ -import { RequireResult } from './types'; +import type { RequireResult } from './types'; /** * Wrap a module in an object, as the value under the key `default`. diff --git a/packages/utils/src/buildPolyfills/_interopRequireDefault.ts b/packages/utils/src/buildPolyfills/_interopRequireDefault.ts index 9d9a7767cb7c..480d3561fc4e 100644 --- a/packages/utils/src/buildPolyfills/_interopRequireDefault.ts +++ b/packages/utils/src/buildPolyfills/_interopRequireDefault.ts @@ -1,4 +1,4 @@ -import { RequireResult } from './types'; +import type { RequireResult } from './types'; /** * Wraps modules which aren't the result of transpiling an ESM module in an object under the key `default` diff --git a/packages/utils/src/buildPolyfills/_interopRequireWildcard.ts b/packages/utils/src/buildPolyfills/_interopRequireWildcard.ts index 411939ab68d6..b545d90c5652 100644 --- a/packages/utils/src/buildPolyfills/_interopRequireWildcard.ts +++ b/packages/utils/src/buildPolyfills/_interopRequireWildcard.ts @@ -1,4 +1,4 @@ -import { RequireResult } from './types'; +import type { RequireResult } from './types'; /** * Adds a `default` property to CJS modules which aren't the result of transpilation from ESM modules. diff --git a/packages/utils/src/buildPolyfills/_optionalChain.ts b/packages/utils/src/buildPolyfills/_optionalChain.ts index 452c6ac110b0..3bb00a465c53 100644 --- a/packages/utils/src/buildPolyfills/_optionalChain.ts +++ b/packages/utils/src/buildPolyfills/_optionalChain.ts @@ -1,4 +1,4 @@ -import { GenericFunction } from './types'; +import type { GenericFunction } from './types'; /** * Polyfill for the optional chain operator, `?.`, given previous conversion of the expression into an array of values, diff --git a/packages/utils/src/buildPolyfills/types.ts b/packages/utils/src/buildPolyfills/types.ts index 41f1a8a31ee5..806f302460a8 100644 --- a/packages/utils/src/buildPolyfills/types.ts +++ b/packages/utils/src/buildPolyfills/types.ts @@ -1,4 +1,4 @@ -import { Primitive } from '@sentry/types'; +import type { Primitive } from '@sentry/types'; export type GenericObject = { [key: string]: Value }; export type GenericFunction = (...args: unknown[]) => Value; diff --git a/packages/utils/src/clientreport.ts b/packages/utils/src/clientreport.ts index f91c79ab5c5c..c0dbb6cb3aba 100644 --- a/packages/utils/src/clientreport.ts +++ b/packages/utils/src/clientreport.ts @@ -1,4 +1,4 @@ -import { ClientReport, ClientReportEnvelope, ClientReportItem } from '@sentry/types'; +import type { ClientReport, ClientReportEnvelope, ClientReportItem } from '@sentry/types'; import { createEnvelope } from './envelope'; import { dateTimestampInSeconds } from './time'; diff --git a/packages/utils/src/dsn.ts b/packages/utils/src/dsn.ts index d6dd4a7da2b7..f506a8fcb9be 100644 --- a/packages/utils/src/dsn.ts +++ b/packages/utils/src/dsn.ts @@ -1,4 +1,4 @@ -import { DsnComponents, DsnLike, DsnProtocol } from '@sentry/types'; +import type { DsnComponents, DsnLike, DsnProtocol } from '@sentry/types'; import { SentryError } from './error'; diff --git a/packages/utils/src/envelope.ts b/packages/utils/src/envelope.ts index 5cb1a675645e..8dcb7c492fed 100644 --- a/packages/utils/src/envelope.ts +++ b/packages/utils/src/envelope.ts @@ -1,4 +1,4 @@ -import { +import type { Attachment, AttachmentItem, BaseEnvelopeHeaders, diff --git a/packages/utils/src/instrument.ts b/packages/utils/src/instrument.ts index 319ac5e538a3..cc3c1986b06d 100644 --- a/packages/utils/src/instrument.ts +++ b/packages/utils/src/instrument.ts @@ -1,7 +1,7 @@ /* eslint-disable max-lines */ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/ban-types */ -import { WrappedFunction } from '@sentry/types'; +import type { WrappedFunction } from '@sentry/types'; import { isInstanceOf, isString } from './is'; import { CONSOLE_LEVELS, logger } from './logger'; diff --git a/packages/utils/src/is.ts b/packages/utils/src/is.ts index 8ec83089e4d5..350826cb567c 100644 --- a/packages/utils/src/is.ts +++ b/packages/utils/src/is.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ -import { PolymorphicEvent, Primitive } from '@sentry/types'; +import type { PolymorphicEvent, Primitive } from '@sentry/types'; // eslint-disable-next-line @typescript-eslint/unbound-method const objectToString = Object.prototype.toString; diff --git a/packages/utils/src/logger.ts b/packages/utils/src/logger.ts index efa8f49716b9..b3cc6ddacdea 100644 --- a/packages/utils/src/logger.ts +++ b/packages/utils/src/logger.ts @@ -1,4 +1,4 @@ -import { WrappedFunction } from '@sentry/types'; +import type { WrappedFunction } from '@sentry/types'; import { getGlobalSingleton, GLOBAL_OBJ } from './worldwide'; diff --git a/packages/utils/src/misc.ts b/packages/utils/src/misc.ts index 39a61bd478e7..640c526db7b2 100644 --- a/packages/utils/src/misc.ts +++ b/packages/utils/src/misc.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { Event, Exception, Mechanism, StackFrame } from '@sentry/types'; +import type { Event, Exception, Mechanism, StackFrame } from '@sentry/types'; import { addNonEnumerableProperty } from './object'; import { snipLine } from './string'; diff --git a/packages/utils/src/normalize.ts b/packages/utils/src/normalize.ts index ebeb5f0ff318..0b203d3749e0 100644 --- a/packages/utils/src/normalize.ts +++ b/packages/utils/src/normalize.ts @@ -1,7 +1,8 @@ -import { Primitive } from '@sentry/types'; +import type { Primitive } from '@sentry/types'; import { isNaN, isSyntheticEvent } from './is'; -import { memoBuilder, MemoFunc } from './memo'; +import type { MemoFunc } from './memo'; +import { memoBuilder } from './memo'; import { convertToPlainObject } from './object'; import { getFunctionName } from './stacktrace'; diff --git a/packages/utils/src/object.ts b/packages/utils/src/object.ts index 1e02d1fd163e..3a22e2aa49a3 100644 --- a/packages/utils/src/object.ts +++ b/packages/utils/src/object.ts @@ -1,6 +1,6 @@ /* eslint-disable max-lines */ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { WrappedFunction } from '@sentry/types'; +import type { WrappedFunction } from '@sentry/types'; import { htmlTreeAsString } from './browser'; import { isElement, isError, isEvent, isInstanceOf, isPlainObject, isPrimitive } from './is'; diff --git a/packages/utils/src/ratelimit.ts b/packages/utils/src/ratelimit.ts index 78720c888576..da4e3bbdb08e 100644 --- a/packages/utils/src/ratelimit.ts +++ b/packages/utils/src/ratelimit.ts @@ -1,4 +1,4 @@ -import { TransportMakeRequestResponse } from '@sentry/types'; +import type { TransportMakeRequestResponse } from '@sentry/types'; // Intentionally keeping the key broad, as we don't know for sure what rate limit headers get returned from backend export type RateLimits = Record; diff --git a/packages/utils/src/requestdata.ts b/packages/utils/src/requestdata.ts index 4f914bfe16c0..f6b40c3ab8bd 100644 --- a/packages/utils/src/requestdata.ts +++ b/packages/utils/src/requestdata.ts @@ -14,7 +14,13 @@ /* eslint-disable max-lines */ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { Event, ExtractedNodeRequestData, PolymorphicRequest, Transaction, TransactionSource } from '@sentry/types'; +import type { + Event, + ExtractedNodeRequestData, + PolymorphicRequest, + Transaction, + TransactionSource, +} from '@sentry/types'; import { isPlainObject, isString } from './is'; import { normalize } from './normalize'; diff --git a/packages/utils/src/severity.ts b/packages/utils/src/severity.ts index c40f0c2a6004..6d631b590a88 100644 --- a/packages/utils/src/severity.ts +++ b/packages/utils/src/severity.ts @@ -1,5 +1,5 @@ /* eslint-disable deprecation/deprecation */ -import { Severity, SeverityLevel } from '@sentry/types'; +import type { Severity, SeverityLevel } from '@sentry/types'; // Note: Ideally the `SeverityLevel` type would be derived from `validSeverityLevels`, but that would mean either // diff --git a/packages/utils/src/stacktrace.ts b/packages/utils/src/stacktrace.ts index fabbc9f43575..742a28049695 100644 --- a/packages/utils/src/stacktrace.ts +++ b/packages/utils/src/stacktrace.ts @@ -1,4 +1,4 @@ -import { StackFrame, StackLineParser, StackLineParserFn, StackParser } from '@sentry/types'; +import type { StackFrame, StackLineParser, StackLineParserFn, StackParser } from '@sentry/types'; const STACKTRACE_LIMIT = 50; diff --git a/packages/utils/src/tracing.ts b/packages/utils/src/tracing.ts index cef6e334f7e7..7eee5312ce6d 100644 --- a/packages/utils/src/tracing.ts +++ b/packages/utils/src/tracing.ts @@ -1,4 +1,4 @@ -import { TraceparentData } from '@sentry/types'; +import type { TraceparentData } from '@sentry/types'; export const TRACEPARENT_REGEXP = new RegExp( '^[ \\t]*' + // whitespace diff --git a/packages/utils/src/worldwide.ts b/packages/utils/src/worldwide.ts index 464ca794b4a4..e341339bc980 100644 --- a/packages/utils/src/worldwide.ts +++ b/packages/utils/src/worldwide.ts @@ -12,7 +12,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { Integration } from '@sentry/types'; +import type { Integration } from '@sentry/types'; /** Internal global with common properties and Sentry extensions */ export interface InternalGlobal { diff --git a/packages/utils/test/buildPolyfills/interop.test.ts b/packages/utils/test/buildPolyfills/interop.test.ts index 917d62daee2f..a53c64eb0979 100644 --- a/packages/utils/test/buildPolyfills/interop.test.ts +++ b/packages/utils/test/buildPolyfills/interop.test.ts @@ -5,7 +5,7 @@ import { _interopRequireDefault, _interopRequireWildcard, } from '../../src/buildPolyfills'; -import { RequireResult } from '../../src/buildPolyfills/types'; +import type { RequireResult } from '../../src/buildPolyfills/types'; import { _interopDefault as _interopDefaultOrig, _interopNamespace as _interopNamespaceOrig, diff --git a/packages/utils/test/buildPolyfills/nullishCoalesce.test.ts b/packages/utils/test/buildPolyfills/nullishCoalesce.test.ts index 55fd5ee9c996..d2a1d98d8a05 100644 --- a/packages/utils/test/buildPolyfills/nullishCoalesce.test.ts +++ b/packages/utils/test/buildPolyfills/nullishCoalesce.test.ts @@ -1,5 +1,5 @@ import { _nullishCoalesce } from '../../src/buildPolyfills'; -import { Value } from '../../src/buildPolyfills/types'; +import type { Value } from '../../src/buildPolyfills/types'; import { _nullishCoalesce as _nullishCoalesceOrig } from './originals'; const dogStr = 'dogs are great!'; diff --git a/packages/utils/test/buildPolyfills/optionalChain.test.ts b/packages/utils/test/buildPolyfills/optionalChain.test.ts index 4fec5ee1dc63..4ad6f14befde 100644 --- a/packages/utils/test/buildPolyfills/optionalChain.test.ts +++ b/packages/utils/test/buildPolyfills/optionalChain.test.ts @@ -1,7 +1,7 @@ import { default as arrayFlat } from 'array.prototype.flat'; import { _optionalChain } from '../../src/buildPolyfills'; -import { GenericFunction, GenericObject, Value } from '../../src/buildPolyfills/types'; +import type { GenericFunction, GenericObject, Value } from '../../src/buildPolyfills/types'; import { _optionalChain as _optionalChainOrig } from './originals'; // Older versions of Node don't have `Array.prototype.flat`, which crashes these tests. On newer versions that do have diff --git a/packages/utils/test/clientreport.test.ts b/packages/utils/test/clientreport.test.ts index 03026fd16eaf..b783886771dc 100644 --- a/packages/utils/test/clientreport.test.ts +++ b/packages/utils/test/clientreport.test.ts @@ -1,4 +1,4 @@ -import { ClientReport } from '@sentry/types'; +import type { ClientReport } from '@sentry/types'; import { TextDecoder, TextEncoder } from 'util'; import { createClientReportEnvelope } from '../src/clientreport'; diff --git a/packages/utils/test/envelope.test.ts b/packages/utils/test/envelope.test.ts index b88400dbe503..2996b6dcde06 100644 --- a/packages/utils/test/envelope.test.ts +++ b/packages/utils/test/envelope.test.ts @@ -1,4 +1,4 @@ -import { EventEnvelope } from '@sentry/types'; +import type { EventEnvelope } from '@sentry/types'; import { TextDecoder, TextEncoder } from 'util'; const encoder = new TextEncoder(); diff --git a/packages/utils/test/misc.test.ts b/packages/utils/test/misc.test.ts index ded5a7a4d037..77c6441e31a1 100644 --- a/packages/utils/test/misc.test.ts +++ b/packages/utils/test/misc.test.ts @@ -1,4 +1,4 @@ -import { Event, Mechanism, StackFrame } from '@sentry/types'; +import type { Event, Mechanism, StackFrame } from '@sentry/types'; import { addContextToFrame, diff --git a/packages/utils/test/ratelimit.test.ts b/packages/utils/test/ratelimit.test.ts index 52a04683563f..978774c78ea3 100644 --- a/packages/utils/test/ratelimit.test.ts +++ b/packages/utils/test/ratelimit.test.ts @@ -1,9 +1,9 @@ +import type { RateLimits } from '../src/ratelimit'; import { DEFAULT_RETRY_AFTER, disabledUntil, isRateLimited, parseRetryAfterHeader, - RateLimits, updateRateLimits, } from '../src/ratelimit'; diff --git a/packages/vue/src/components.ts b/packages/vue/src/components.ts index 86c7e8f2f65f..c52962aade41 100644 --- a/packages/vue/src/components.ts +++ b/packages/vue/src/components.ts @@ -1,4 +1,4 @@ -import { ViewModel } from './types'; +import type { ViewModel } from './types'; // Vendored directly from https://github.com/vuejs/vue/blob/master/src/core/util/debug.js with types only changes. const classifyRE = /(?:^|[-_])(\w)/g; diff --git a/packages/vue/src/constants.ts b/packages/vue/src/constants.ts index 13c9d7af9c83..e254d988c40c 100644 --- a/packages/vue/src/constants.ts +++ b/packages/vue/src/constants.ts @@ -1,3 +1,3 @@ -import { Operation } from './types'; +import type { Operation } from './types'; export const DEFAULT_HOOKS: Operation[] = ['activate', 'mount', 'update']; diff --git a/packages/vue/src/errorhandler.ts b/packages/vue/src/errorhandler.ts index 16cb09a723ab..032ecc2386c8 100644 --- a/packages/vue/src/errorhandler.ts +++ b/packages/vue/src/errorhandler.ts @@ -1,7 +1,7 @@ import { getCurrentHub } from '@sentry/browser'; import { formatComponentName, generateComponentTrace } from './components'; -import { Options, ViewModel, Vue } from './types'; +import type { Options, ViewModel, Vue } from './types'; type UnknownFunc = (...args: unknown[]) => void; diff --git a/packages/vue/src/router.ts b/packages/vue/src/router.ts index f782246e8fb2..2bdb088a24cf 100644 --- a/packages/vue/src/router.ts +++ b/packages/vue/src/router.ts @@ -1,5 +1,5 @@ import { captureException, WINDOW } from '@sentry/browser'; -import { Transaction, TransactionContext, TransactionSource } from '@sentry/types'; +import type { Transaction, TransactionContext, TransactionSource } from '@sentry/types'; import { getActiveTransaction } from './tracing'; diff --git a/packages/vue/src/sdk.ts b/packages/vue/src/sdk.ts index a2063038402c..9c10dbec28e6 100644 --- a/packages/vue/src/sdk.ts +++ b/packages/vue/src/sdk.ts @@ -4,7 +4,7 @@ import { arrayify, GLOBAL_OBJ } from '@sentry/utils'; import { DEFAULT_HOOKS } from './constants'; import { attachErrorHandler } from './errorhandler'; import { createTracingMixins } from './tracing'; -import { Options, TracingOptions, Vue } from './types'; +import type { Options, TracingOptions, Vue } from './types'; const globalWithVue = GLOBAL_OBJ as typeof GLOBAL_OBJ & { Vue: Vue }; diff --git a/packages/vue/src/tracing.ts b/packages/vue/src/tracing.ts index 889cbe30dca6..9f56586aa717 100644 --- a/packages/vue/src/tracing.ts +++ b/packages/vue/src/tracing.ts @@ -1,10 +1,10 @@ import { getCurrentHub } from '@sentry/browser'; -import { Span, Transaction } from '@sentry/types'; +import type { Span, Transaction } from '@sentry/types'; import { logger, timestampInSeconds } from '@sentry/utils'; import { formatComponentName } from './components'; import { DEFAULT_HOOKS } from './constants'; -import { Hook, Operation, TracingOptions, ViewModel, Vue } from './types'; +import type { Hook, Operation, TracingOptions, ViewModel, Vue } from './types'; const VUE_OP = 'ui.vue'; diff --git a/packages/vue/src/types.ts b/packages/vue/src/types.ts index eb386d6f3e4b..1cc39b97b887 100644 --- a/packages/vue/src/types.ts +++ b/packages/vue/src/types.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { BrowserOptions } from '@sentry/browser'; +import type { BrowserOptions } from '@sentry/browser'; // This is not great, but kinda necessary to make it woth with Vue@2 and Vue@3 at the same time. export interface Vue { diff --git a/packages/vue/test/errorHandler.test.ts b/packages/vue/test/errorHandler.test.ts index 1c41166fd167..e7530c91304e 100644 --- a/packages/vue/test/errorHandler.test.ts +++ b/packages/vue/test/errorHandler.test.ts @@ -2,7 +2,7 @@ import { getCurrentHub } from '@sentry/browser'; import { generateComponentTrace } from '../src/components'; import { attachErrorHandler } from '../src/errorhandler'; -import { Operation, Options, ViewModel, Vue } from '../src/types'; +import type { Operation, Options, ViewModel, Vue } from '../src/types'; describe('attachErrorHandler', () => { describe('attachProps', () => { diff --git a/packages/vue/test/router.test.ts b/packages/vue/test/router.test.ts index c4072616d9af..65482f80f10b 100644 --- a/packages/vue/test/router.test.ts +++ b/packages/vue/test/router.test.ts @@ -1,8 +1,8 @@ import * as SentryBrowser from '@sentry/browser'; -import { Transaction } from '@sentry/types'; +import type { Transaction } from '@sentry/types'; import { vueRouterInstrumentation } from '../src'; -import { Route } from '../src/router'; +import type { Route } from '../src/router'; import * as vueTracing from '../src/tracing'; const captureExceptionSpy = jest.spyOn(SentryBrowser, 'captureException'); diff --git a/packages/wasm/src/index.ts b/packages/wasm/src/index.ts index cc5e678ee123..436668e73ac2 100644 --- a/packages/wasm/src/index.ts +++ b/packages/wasm/src/index.ts @@ -1,4 +1,4 @@ -import { Event, EventProcessor, Hub, Integration, StackFrame } from '@sentry/types'; +import type { Event, EventProcessor, Hub, Integration, StackFrame } from '@sentry/types'; import { patchWebAssembly } from './patchWebAssembly'; import { getImage, getImages } from './registry'; diff --git a/packages/wasm/src/registry.ts b/packages/wasm/src/registry.ts index 56609b389cca..f000dbff7c53 100644 --- a/packages/wasm/src/registry.ts +++ b/packages/wasm/src/registry.ts @@ -1,4 +1,4 @@ -import { DebugImage } from '@sentry/types'; +import type { DebugImage } from '@sentry/types'; export const IMAGES: Array = []; From 770a33dba89ceb06ef33228396cc117d7f403d15 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Wed, 11 Jan 2023 19:09:49 +0100 Subject: [PATCH 044/113] feat(replay): Change `addEvent` to be async (#6695) `addEvent` should be async as it waits for a response from Compression worker. We don't necessarily always await `addEvent()`, but it's more correct that it is marked as `async` and can maybe save some headaches down the line. --- packages/replay/src/eventBuffer.ts | 20 +++--- packages/replay/src/replay.ts | 11 ++-- packages/replay/src/types.ts | 20 +++++- packages/replay/src/util/addEvent.ts | 16 +++-- packages/replay/src/util/addMemoryEntry.ts | 15 +++-- .../replay/src/util/createPerformanceSpans.ts | 9 ++- packages/replay/src/worker/worker.js | 2 +- .../replay/test/integration/flush.test.ts | 64 ++++++++++--------- packages/replay/test/unit/eventBuffer.test.ts | 2 + .../test/unit/worker/Compressor.test.ts | 20 ++---- packages/replay/worker/src/Compressor.ts | 7 +- packages/replay/worker/src/handleMessage.ts | 5 +- 12 files changed, 105 insertions(+), 86 deletions(-) diff --git a/packages/replay/src/eventBuffer.ts b/packages/replay/src/eventBuffer.ts index f953b693bc6d..74ef2b77bd27 100644 --- a/packages/replay/src/eventBuffer.ts +++ b/packages/replay/src/eventBuffer.ts @@ -2,10 +2,9 @@ // TODO: figure out member access types and remove the line above import { captureException } from '@sentry/core'; -import type { ReplayRecordingData } from '@sentry/types'; import { logger } from '@sentry/utils'; -import type { EventBuffer, RecordingEvent, WorkerRequest, WorkerResponse } from './types'; +import type { AddEventResult, EventBuffer, RecordingEvent, WorkerRequest } from './types'; import workerString from './worker/worker.js'; interface CreateEventBufferParams { @@ -54,13 +53,14 @@ class EventBufferArray implements EventBuffer { this._events = []; } - public addEvent(event: RecordingEvent, isCheckout?: boolean): void { + public async addEvent(event: RecordingEvent, isCheckout?: boolean): Promise { if (isCheckout) { this._events = [event]; return; } this._events.push(event); + return; } public finish(): Promise { @@ -107,8 +107,10 @@ export class EventBufferCompressionWorker implements EventBuffer { /** * Add an event to the event buffer. + * + * Returns true if event was successfuly received and processed by worker. */ - public async addEvent(event: RecordingEvent, isCheckout?: boolean): Promise { + public async addEvent(event: RecordingEvent, isCheckout?: boolean): Promise { if (isCheckout) { // This event is a checkout, make sure worker buffer is cleared before // proceeding. @@ -132,7 +134,7 @@ export class EventBufferCompressionWorker implements EventBuffer { /** * Post message to worker and wait for response before resolving promise. */ - private _postMessage({ id, method, args }: WorkerRequest): Promise { + private _postMessage({ id, method, args }: WorkerRequest): Promise { return new Promise((resolve, reject) => { // eslint-disable-next-line @typescript-eslint/explicit-function-return-type const listener = ({ data }: MessageEvent) => { @@ -178,8 +180,8 @@ export class EventBufferCompressionWorker implements EventBuffer { /** * Send the event to the worker. */ - private _sendEventToWorker(event: RecordingEvent): Promise { - const promise = this._postMessage({ + private async _sendEventToWorker(event: RecordingEvent): Promise { + const promise = this._postMessage({ id: this._getAndIncrementId(), method: 'addEvent', args: [event], @@ -195,12 +197,12 @@ export class EventBufferCompressionWorker implements EventBuffer { * Finish the request and return the compressed data from the worker. */ private async _finishRequest(id: number): Promise { - const promise = this._postMessage({ id, method: 'finish', args: [] }); + const promise = this._postMessage({ id, method: 'finish', args: [] }); // XXX: See note in `get length()` this._eventBufferItemLength = 0; - return promise as Promise; + return promise; } /** Get the current ID and increment it for the next call. */ diff --git a/packages/replay/src/replay.ts b/packages/replay/src/replay.ts index 561d3f10b58b..e324a43b10d1 100644 --- a/packages/replay/src/replay.ts +++ b/packages/replay/src/replay.ts @@ -22,6 +22,7 @@ import { createEventBuffer } from './eventBuffer'; import { getSession } from './session/getSession'; import { saveSession } from './session/saveSession'; import type { + AddEventResult, AddUpdateCallback, AllPerformanceEntry, EventBuffer, @@ -450,7 +451,7 @@ export class ReplayContainer implements ReplayContainerInterface { // We need to clear existing events on a checkout, otherwise they are // incremental event updates and should be appended - addEvent(this, event, isCheckout); + void addEvent(this, event, isCheckout); // Different behavior for full snapshots (type=2), ignore other event types // See https://github.com/rrweb-io/rrweb/blob/d8f9290ca496712aa1e7d472549480c4e7876594/packages/rrweb/src/types.ts#L16 @@ -556,7 +557,7 @@ export class ReplayContainer implements ReplayContainerInterface { } this.addUpdate(() => { - addEvent(this, { + void addEvent(this, { type: EventType.Custom, // TODO: We were converting from ms to seconds for breadcrumbs, spans, // but maybe we should just keep them as milliseconds @@ -674,7 +675,7 @@ export class ReplayContainer implements ReplayContainerInterface { */ createCustomBreadcrumb(breadcrumb: Breadcrumb): void { this.addUpdate(() => { - addEvent(this, { + void addEvent(this, { type: EventType.Custom, timestamp: breadcrumb.timestamp || 0, data: { @@ -689,12 +690,12 @@ export class ReplayContainer implements ReplayContainerInterface { * Observed performance events are added to `this.performanceEvents`. These * are included in the replay event before it is finished and sent to Sentry. */ - addPerformanceEntries(): void { + addPerformanceEntries(): Promise> { // Copy and reset entries before processing const entries = [...this.performanceEvents]; this.performanceEvents = []; - createPerformanceSpans(this, createPerformanceEntries(entries)); + return Promise.all(createPerformanceSpans(this, createPerformanceEntries(entries))); } /** diff --git a/packages/replay/src/types.ts b/packages/replay/src/types.ts index 0c05bdeaabcf..8a9737611eda 100644 --- a/packages/replay/src/types.ts +++ b/packages/replay/src/types.ts @@ -48,9 +48,11 @@ export interface WorkerResponse { id: number; method: string; success: boolean; - response: ReplayRecordingData; + response: unknown; } +export type AddEventResult = void; + export interface SampleRates { /** * The sample rate for session-long replays. 1.0 will record all sessions and @@ -210,8 +212,22 @@ export interface Session { export interface EventBuffer { readonly length: number; + + /** + * Destroy the event buffer. + */ destroy(): void; - addEvent(event: RecordingEvent, isCheckout?: boolean): void; + + /** + * Add an event to the event buffer. + * + * Returns true if event was successfully added. + */ + addEvent(event: RecordingEvent, isCheckout?: boolean): Promise; + + /** + * Clears and returns the contents and the buffer. + */ finish(): Promise; } diff --git a/packages/replay/src/util/addEvent.ts b/packages/replay/src/util/addEvent.ts index 34086a4725c5..3bcc16f03af5 100644 --- a/packages/replay/src/util/addEvent.ts +++ b/packages/replay/src/util/addEvent.ts @@ -1,18 +1,22 @@ import { SESSION_IDLE_DURATION } from '../constants'; -import type { RecordingEvent, ReplayContainer } from '../types'; +import type { AddEventResult, RecordingEvent, ReplayContainer } from '../types'; /** * Add an event to the event buffer */ -export function addEvent(replay: ReplayContainer, event: RecordingEvent, isCheckout?: boolean): void { +export async function addEvent( + replay: ReplayContainer, + event: RecordingEvent, + isCheckout?: boolean, +): Promise { if (!replay.eventBuffer) { // This implies that `_isEnabled` is false - return; + return null; } if (replay.isPaused()) { // Do not add to event buffer when recording is paused - return; + return null; } // TODO: sadness -- we will want to normalize timestamps to be in ms - @@ -25,7 +29,7 @@ export function addEvent(replay: ReplayContainer, event: RecordingEvent, isCheck // comes back to trigger a new session. The performance entries rely on // `performance.timeOrigin`, which is when the page first opened. if (timestampInMs + SESSION_IDLE_DURATION < new Date().getTime()) { - return; + return null; } // Only record earliest event if a new session was created, otherwise it @@ -35,5 +39,5 @@ export function addEvent(replay: ReplayContainer, event: RecordingEvent, isCheck replay.getContext().earliestEvent = timestampInMs; } - replay.eventBuffer.addEvent(event, isCheckout); + return replay.eventBuffer.addEvent(event, isCheckout); } diff --git a/packages/replay/src/util/addMemoryEntry.ts b/packages/replay/src/util/addMemoryEntry.ts index 0b847a25a42d..8d31e06cd9d9 100644 --- a/packages/replay/src/util/addMemoryEntry.ts +++ b/packages/replay/src/util/addMemoryEntry.ts @@ -1,5 +1,5 @@ import { WINDOW } from '../constants'; -import type { ReplayContainer, ReplayPerformanceEntry } from '../types'; +import type { AddEventResult, ReplayContainer, ReplayPerformanceEntry } from '../types'; import { createPerformanceSpans } from './createPerformanceSpans'; type ReplayMemoryEntry = ReplayPerformanceEntry & { data: { memory: MemoryInfo } }; @@ -14,15 +14,18 @@ interface MemoryInfo { * Create a "span" for the total amount of memory being used by JS objects * (including v8 internal objects). */ -export function addMemoryEntry(replay: ReplayContainer): void { +export async function addMemoryEntry(replay: ReplayContainer): Promise> { // window.performance.memory is a non-standard API and doesn't work on all browsers, so we try-catch this try { - createPerformanceSpans(replay, [ - // @ts-ignore memory doesn't exist on type Performance as the API is non-standard (we check that it exists above) - createMemoryEntry(WINDOW.performance.memory), - ]); + return Promise.all( + createPerformanceSpans(replay, [ + // @ts-ignore memory doesn't exist on type Performance as the API is non-standard (we check that it exists above) + createMemoryEntry(WINDOW.performance.memory), + ]), + ); } catch (error) { // Do nothing + return []; } } diff --git a/packages/replay/src/util/createPerformanceSpans.ts b/packages/replay/src/util/createPerformanceSpans.ts index f7327ff488b0..ba8e86577972 100644 --- a/packages/replay/src/util/createPerformanceSpans.ts +++ b/packages/replay/src/util/createPerformanceSpans.ts @@ -1,13 +1,16 @@ import { EventType } from 'rrweb'; -import type { ReplayContainer, ReplayPerformanceEntry } from '../types'; +import type { AddEventResult, ReplayContainer, ReplayPerformanceEntry } from '../types'; import { addEvent } from './addEvent'; /** * Create a "span" for each performance entry. The parent transaction is `this.replayEvent`. */ -export function createPerformanceSpans(replay: ReplayContainer, entries: ReplayPerformanceEntry[]): void { - entries.map(({ type, start, end, name, data }) => +export function createPerformanceSpans( + replay: ReplayContainer, + entries: ReplayPerformanceEntry[], +): Promise[] { + return entries.map(({ type, start, end, name, data }) => addEvent(replay, { type: EventType.Custom, timestamp: start, diff --git a/packages/replay/src/worker/worker.js b/packages/replay/src/worker/worker.js index 9d043a3c5857..3598eedfd68a 100644 --- a/packages/replay/src/worker/worker.js +++ b/packages/replay/src/worker/worker.js @@ -1,2 +1,2 @@ export default `/*! pako 2.1.0 https://github.com/nodeca/pako @license (MIT AND Zlib) */ -function t(t){let e=t.length;for(;--e>=0;)t[e]=0}const e=new Uint8Array([0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0]),a=new Uint8Array([0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13]),i=new Uint8Array([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,3,7]),n=new Uint8Array([16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]),s=new Array(576);t(s);const r=new Array(60);t(r);const o=new Array(512);t(o);const l=new Array(256);t(l);const h=new Array(29);t(h);const d=new Array(30);function _(t,e,a,i,n){this.static_tree=t,this.extra_bits=e,this.extra_base=a,this.elems=i,this.max_length=n,this.has_stree=t&&t.length}let f,c,u;function w(t,e){this.dyn_tree=t,this.max_code=0,this.stat_desc=e}t(d);const m=t=>t<256?o[t]:o[256+(t>>>7)],b=(t,e)=>{t.pending_buf[t.pending++]=255&e,t.pending_buf[t.pending++]=e>>>8&255},g=(t,e,a)=>{t.bi_valid>16-a?(t.bi_buf|=e<>16-t.bi_valid,t.bi_valid+=a-16):(t.bi_buf|=e<{g(t,a[2*e],a[2*e+1])},k=(t,e)=>{let a=0;do{a|=1&t,t>>>=1,a<<=1}while(--e>0);return a>>>1},v=(t,e,a)=>{const i=new Array(16);let n,s,r=0;for(n=1;n<=15;n++)r=r+a[n-1]<<1,i[n]=r;for(s=0;s<=e;s++){let e=t[2*s+1];0!==e&&(t[2*s]=k(i[e]++,e))}},y=t=>{let e;for(e=0;e<286;e++)t.dyn_ltree[2*e]=0;for(e=0;e<30;e++)t.dyn_dtree[2*e]=0;for(e=0;e<19;e++)t.bl_tree[2*e]=0;t.dyn_ltree[512]=1,t.opt_len=t.static_len=0,t.sym_next=t.matches=0},x=t=>{t.bi_valid>8?b(t,t.bi_buf):t.bi_valid>0&&(t.pending_buf[t.pending++]=t.bi_buf),t.bi_buf=0,t.bi_valid=0},z=(t,e,a,i)=>{const n=2*e,s=2*a;return t[n]{const i=t.heap[a];let n=a<<1;for(;n<=t.heap_len&&(n{let s,r,o,_,f=0;if(0!==t.sym_next)do{s=255&t.pending_buf[t.sym_buf+f++],s+=(255&t.pending_buf[t.sym_buf+f++])<<8,r=t.pending_buf[t.sym_buf+f++],0===s?p(t,r,i):(o=l[r],p(t,o+256+1,i),_=e[o],0!==_&&(r-=h[o],g(t,r,_)),s--,o=m(s),p(t,o,n),_=a[o],0!==_&&(s-=d[o],g(t,s,_)))}while(f{const a=e.dyn_tree,i=e.stat_desc.static_tree,n=e.stat_desc.has_stree,s=e.stat_desc.elems;let r,o,l,h=-1;for(t.heap_len=0,t.heap_max=573,r=0;r>1;r>=1;r--)A(t,a,r);l=s;do{r=t.heap[1],t.heap[1]=t.heap[t.heap_len--],A(t,a,1),o=t.heap[1],t.heap[--t.heap_max]=r,t.heap[--t.heap_max]=o,a[2*l]=a[2*r]+a[2*o],t.depth[l]=(t.depth[r]>=t.depth[o]?t.depth[r]:t.depth[o])+1,a[2*r+1]=a[2*o+1]=l,t.heap[1]=l++,A(t,a,1)}while(t.heap_len>=2);t.heap[--t.heap_max]=t.heap[1],((t,e)=>{const a=e.dyn_tree,i=e.max_code,n=e.stat_desc.static_tree,s=e.stat_desc.has_stree,r=e.stat_desc.extra_bits,o=e.stat_desc.extra_base,l=e.stat_desc.max_length;let h,d,_,f,c,u,w=0;for(f=0;f<=15;f++)t.bl_count[f]=0;for(a[2*t.heap[t.heap_max]+1]=0,h=t.heap_max+1;h<573;h++)d=t.heap[h],f=a[2*a[2*d+1]+1]+1,f>l&&(f=l,w++),a[2*d+1]=f,d>i||(t.bl_count[f]++,c=0,d>=o&&(c=r[d-o]),u=a[2*d],t.opt_len+=u*(f+c),s&&(t.static_len+=u*(n[2*d+1]+c)));if(0!==w){do{for(f=l-1;0===t.bl_count[f];)f--;t.bl_count[f]--,t.bl_count[f+1]+=2,t.bl_count[l]--,w-=2}while(w>0);for(f=l;0!==f;f--)for(d=t.bl_count[f];0!==d;)_=t.heap[--h],_>i||(a[2*_+1]!==f&&(t.opt_len+=(f-a[2*_+1])*a[2*_],a[2*_+1]=f),d--)}})(t,e),v(a,h,t.bl_count)},Z=(t,e,a)=>{let i,n,s=-1,r=e[1],o=0,l=7,h=4;for(0===r&&(l=138,h=3),e[2*(a+1)+1]=65535,i=0;i<=a;i++)n=r,r=e[2*(i+1)+1],++o{let i,n,s=-1,r=e[1],o=0,l=7,h=4;for(0===r&&(l=138,h=3),i=0;i<=a;i++)if(n=r,r=e[2*(i+1)+1],!(++o{g(t,0+(i?1:0),3),x(t),b(t,a),b(t,~a),a&&t.pending_buf.set(t.window.subarray(e,e+a),t.pending),t.pending+=a};var O=(t,e,a,i)=>{let o,l,h=0;t.level>0?(2===t.strm.data_type&&(t.strm.data_type=(t=>{let e,a=4093624447;for(e=0;e<=31;e++,a>>>=1)if(1&a&&0!==t.dyn_ltree[2*e])return 0;if(0!==t.dyn_ltree[18]||0!==t.dyn_ltree[20]||0!==t.dyn_ltree[26])return 1;for(e=32;e<256;e++)if(0!==t.dyn_ltree[2*e])return 1;return 0})(t)),R(t,t.l_desc),R(t,t.d_desc),h=(t=>{let e;for(Z(t,t.dyn_ltree,t.l_desc.max_code),Z(t,t.dyn_dtree,t.d_desc.max_code),R(t,t.bl_desc),e=18;e>=3&&0===t.bl_tree[2*n[e]+1];e--);return t.opt_len+=3*(e+1)+5+5+4,e})(t),o=t.opt_len+3+7>>>3,l=t.static_len+3+7>>>3,l<=o&&(o=l)):o=l=a+5,a+4<=o&&-1!==e?D(t,e,a,i):4===t.strategy||l===o?(g(t,2+(i?1:0),3),E(t,s,r)):(g(t,4+(i?1:0),3),((t,e,a,i)=>{let s;for(g(t,e-257,5),g(t,a-1,5),g(t,i-4,4),s=0;s{U||((()=>{let t,n,w,m,b;const g=new Array(16);for(w=0,m=0;m<28;m++)for(h[m]=w,t=0;t<1<>=7;m<30;m++)for(d[m]=b<<7,t=0;t<1<(t.pending_buf[t.sym_buf+t.sym_next++]=e,t.pending_buf[t.sym_buf+t.sym_next++]=e>>8,t.pending_buf[t.sym_buf+t.sym_next++]=a,0===e?t.dyn_ltree[2*a]++:(t.matches++,e--,t.dyn_ltree[2*(l[a]+256+1)]++,t.dyn_dtree[2*m(e)]++),t.sym_next===t.sym_end),_tr_align:t=>{g(t,2,3),p(t,256,s),(t=>{16===t.bi_valid?(b(t,t.bi_buf),t.bi_buf=0,t.bi_valid=0):t.bi_valid>=8&&(t.pending_buf[t.pending++]=255&t.bi_buf,t.bi_buf>>=8,t.bi_valid-=8)})(t)}};var N=(t,e,a,i)=>{let n=65535&t|0,s=t>>>16&65535|0,r=0;for(;0!==a;){r=a>2e3?2e3:a,a-=r;do{n=n+e[i++]|0,s=s+n|0}while(--r);n%=65521,s%=65521}return n|s<<16|0};const F=new Uint32Array((()=>{let t,e=[];for(var a=0;a<256;a++){t=a;for(var i=0;i<8;i++)t=1&t?3988292384^t>>>1:t>>>1;e[a]=t}return e})());var L=(t,e,a,i)=>{const n=F,s=i+a;t^=-1;for(let a=i;a>>8^n[255&(t^e[a])];return-1^t},I={2:"need dictionary",1:"stream end",0:"","-1":"file error","-2":"stream error","-3":"data error","-4":"insufficient memory","-5":"buffer error","-6":"incompatible version"},B={Z_NO_FLUSH:0,Z_PARTIAL_FLUSH:1,Z_SYNC_FLUSH:2,Z_FULL_FLUSH:3,Z_FINISH:4,Z_BLOCK:5,Z_TREES:6,Z_OK:0,Z_STREAM_END:1,Z_NEED_DICT:2,Z_ERRNO:-1,Z_STREAM_ERROR:-2,Z_DATA_ERROR:-3,Z_MEM_ERROR:-4,Z_BUF_ERROR:-5,Z_NO_COMPRESSION:0,Z_BEST_SPEED:1,Z_BEST_COMPRESSION:9,Z_DEFAULT_COMPRESSION:-1,Z_FILTERED:1,Z_HUFFMAN_ONLY:2,Z_RLE:3,Z_FIXED:4,Z_DEFAULT_STRATEGY:0,Z_BINARY:0,Z_TEXT:1,Z_UNKNOWN:2,Z_DEFLATED:8};const{_tr_init:C,_tr_stored_block:H,_tr_flush_block:M,_tr_tally:j,_tr_align:K}=T,{Z_NO_FLUSH:P,Z_PARTIAL_FLUSH:Y,Z_FULL_FLUSH:G,Z_FINISH:X,Z_BLOCK:J,Z_OK:W,Z_STREAM_END:q,Z_STREAM_ERROR:Q,Z_DATA_ERROR:V,Z_BUF_ERROR:$,Z_DEFAULT_COMPRESSION:tt,Z_FILTERED:et,Z_HUFFMAN_ONLY:at,Z_RLE:it,Z_FIXED:nt,Z_DEFAULT_STRATEGY:st,Z_UNKNOWN:rt,Z_DEFLATED:ot}=B,lt=(t,e)=>(t.msg=I[e],e),ht=t=>2*t-(t>4?9:0),dt=t=>{let e=t.length;for(;--e>=0;)t[e]=0},_t=t=>{let e,a,i,n=t.w_size;e=t.hash_size,i=e;do{a=t.head[--i],t.head[i]=a>=n?a-n:0}while(--e);e=n,i=e;do{a=t.prev[--i],t.prev[i]=a>=n?a-n:0}while(--e)};let ft=(t,e,a)=>(e<{const e=t.state;let a=e.pending;a>t.avail_out&&(a=t.avail_out),0!==a&&(t.output.set(e.pending_buf.subarray(e.pending_out,e.pending_out+a),t.next_out),t.next_out+=a,e.pending_out+=a,t.total_out+=a,t.avail_out-=a,e.pending-=a,0===e.pending&&(e.pending_out=0))},ut=(t,e)=>{M(t,t.block_start>=0?t.block_start:-1,t.strstart-t.block_start,e),t.block_start=t.strstart,ct(t.strm)},wt=(t,e)=>{t.pending_buf[t.pending++]=e},mt=(t,e)=>{t.pending_buf[t.pending++]=e>>>8&255,t.pending_buf[t.pending++]=255&e},bt=(t,e,a,i)=>{let n=t.avail_in;return n>i&&(n=i),0===n?0:(t.avail_in-=n,e.set(t.input.subarray(t.next_in,t.next_in+n),a),1===t.state.wrap?t.adler=N(t.adler,e,n,a):2===t.state.wrap&&(t.adler=L(t.adler,e,n,a)),t.next_in+=n,t.total_in+=n,n)},gt=(t,e)=>{let a,i,n=t.max_chain_length,s=t.strstart,r=t.prev_length,o=t.nice_match;const l=t.strstart>t.w_size-262?t.strstart-(t.w_size-262):0,h=t.window,d=t.w_mask,_=t.prev,f=t.strstart+258;let c=h[s+r-1],u=h[s+r];t.prev_length>=t.good_match&&(n>>=2),o>t.lookahead&&(o=t.lookahead);do{if(a=e,h[a+r]===u&&h[a+r-1]===c&&h[a]===h[s]&&h[++a]===h[s+1]){s+=2,a++;do{}while(h[++s]===h[++a]&&h[++s]===h[++a]&&h[++s]===h[++a]&&h[++s]===h[++a]&&h[++s]===h[++a]&&h[++s]===h[++a]&&h[++s]===h[++a]&&h[++s]===h[++a]&&sr){if(t.match_start=e,r=i,i>=o)break;c=h[s+r-1],u=h[s+r]}}}while((e=_[e&d])>l&&0!=--n);return r<=t.lookahead?r:t.lookahead},pt=t=>{const e=t.w_size;let a,i,n;do{if(i=t.window_size-t.lookahead-t.strstart,t.strstart>=e+(e-262)&&(t.window.set(t.window.subarray(e,e+e-i),0),t.match_start-=e,t.strstart-=e,t.block_start-=e,t.insert>t.strstart&&(t.insert=t.strstart),_t(t),i+=e),0===t.strm.avail_in)break;if(a=bt(t.strm,t.window,t.strstart+t.lookahead,i),t.lookahead+=a,t.lookahead+t.insert>=3)for(n=t.strstart-t.insert,t.ins_h=t.window[n],t.ins_h=ft(t,t.ins_h,t.window[n+1]);t.insert&&(t.ins_h=ft(t,t.ins_h,t.window[n+3-1]),t.prev[n&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=n,n++,t.insert--,!(t.lookahead+t.insert<3)););}while(t.lookahead<262&&0!==t.strm.avail_in)},kt=(t,e)=>{let a,i,n,s=t.pending_buf_size-5>t.w_size?t.w_size:t.pending_buf_size-5,r=0,o=t.strm.avail_in;do{if(a=65535,n=t.bi_valid+42>>3,t.strm.avail_outi+t.strm.avail_in&&(a=i+t.strm.avail_in),a>n&&(a=n),a>8,t.pending_buf[t.pending-2]=~a,t.pending_buf[t.pending-1]=~a>>8,ct(t.strm),i&&(i>a&&(i=a),t.strm.output.set(t.window.subarray(t.block_start,t.block_start+i),t.strm.next_out),t.strm.next_out+=i,t.strm.avail_out-=i,t.strm.total_out+=i,t.block_start+=i,a-=i),a&&(bt(t.strm,t.strm.output,t.strm.next_out,a),t.strm.next_out+=a,t.strm.avail_out-=a,t.strm.total_out+=a)}while(0===r);return o-=t.strm.avail_in,o&&(o>=t.w_size?(t.matches=2,t.window.set(t.strm.input.subarray(t.strm.next_in-t.w_size,t.strm.next_in),0),t.strstart=t.w_size,t.insert=t.strstart):(t.window_size-t.strstart<=o&&(t.strstart-=t.w_size,t.window.set(t.window.subarray(t.w_size,t.w_size+t.strstart),0),t.matches<2&&t.matches++,t.insert>t.strstart&&(t.insert=t.strstart)),t.window.set(t.strm.input.subarray(t.strm.next_in-o,t.strm.next_in),t.strstart),t.strstart+=o,t.insert+=o>t.w_size-t.insert?t.w_size-t.insert:o),t.block_start=t.strstart),t.high_watern&&t.block_start>=t.w_size&&(t.block_start-=t.w_size,t.strstart-=t.w_size,t.window.set(t.window.subarray(t.w_size,t.w_size+t.strstart),0),t.matches<2&&t.matches++,n+=t.w_size,t.insert>t.strstart&&(t.insert=t.strstart)),n>t.strm.avail_in&&(n=t.strm.avail_in),n&&(bt(t.strm,t.window,t.strstart,n),t.strstart+=n,t.insert+=n>t.w_size-t.insert?t.w_size-t.insert:n),t.high_water>3,n=t.pending_buf_size-n>65535?65535:t.pending_buf_size-n,s=n>t.w_size?t.w_size:n,i=t.strstart-t.block_start,(i>=s||(i||e===X)&&e!==P&&0===t.strm.avail_in&&i<=n)&&(a=i>n?n:i,r=e===X&&0===t.strm.avail_in&&a===i?1:0,H(t,t.block_start,a,r),t.block_start+=a,ct(t.strm)),r?3:1)},vt=(t,e)=>{let a,i;for(;;){if(t.lookahead<262){if(pt(t),t.lookahead<262&&e===P)return 1;if(0===t.lookahead)break}if(a=0,t.lookahead>=3&&(t.ins_h=ft(t,t.ins_h,t.window[t.strstart+3-1]),a=t.prev[t.strstart&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=t.strstart),0!==a&&t.strstart-a<=t.w_size-262&&(t.match_length=gt(t,a)),t.match_length>=3)if(i=j(t,t.strstart-t.match_start,t.match_length-3),t.lookahead-=t.match_length,t.match_length<=t.max_lazy_match&&t.lookahead>=3){t.match_length--;do{t.strstart++,t.ins_h=ft(t,t.ins_h,t.window[t.strstart+3-1]),a=t.prev[t.strstart&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=t.strstart}while(0!=--t.match_length);t.strstart++}else t.strstart+=t.match_length,t.match_length=0,t.ins_h=t.window[t.strstart],t.ins_h=ft(t,t.ins_h,t.window[t.strstart+1]);else i=j(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++;if(i&&(ut(t,!1),0===t.strm.avail_out))return 1}return t.insert=t.strstart<2?t.strstart:2,e===X?(ut(t,!0),0===t.strm.avail_out?3:4):t.sym_next&&(ut(t,!1),0===t.strm.avail_out)?1:2},yt=(t,e)=>{let a,i,n;for(;;){if(t.lookahead<262){if(pt(t),t.lookahead<262&&e===P)return 1;if(0===t.lookahead)break}if(a=0,t.lookahead>=3&&(t.ins_h=ft(t,t.ins_h,t.window[t.strstart+3-1]),a=t.prev[t.strstart&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=t.strstart),t.prev_length=t.match_length,t.prev_match=t.match_start,t.match_length=2,0!==a&&t.prev_length4096)&&(t.match_length=2)),t.prev_length>=3&&t.match_length<=t.prev_length){n=t.strstart+t.lookahead-3,i=j(t,t.strstart-1-t.prev_match,t.prev_length-3),t.lookahead-=t.prev_length-1,t.prev_length-=2;do{++t.strstart<=n&&(t.ins_h=ft(t,t.ins_h,t.window[t.strstart+3-1]),a=t.prev[t.strstart&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=t.strstart)}while(0!=--t.prev_length);if(t.match_available=0,t.match_length=2,t.strstart++,i&&(ut(t,!1),0===t.strm.avail_out))return 1}else if(t.match_available){if(i=j(t,0,t.window[t.strstart-1]),i&&ut(t,!1),t.strstart++,t.lookahead--,0===t.strm.avail_out)return 1}else t.match_available=1,t.strstart++,t.lookahead--}return t.match_available&&(i=j(t,0,t.window[t.strstart-1]),t.match_available=0),t.insert=t.strstart<2?t.strstart:2,e===X?(ut(t,!0),0===t.strm.avail_out?3:4):t.sym_next&&(ut(t,!1),0===t.strm.avail_out)?1:2};function xt(t,e,a,i,n){this.good_length=t,this.max_lazy=e,this.nice_length=a,this.max_chain=i,this.func=n}const zt=[new xt(0,0,0,0,kt),new xt(4,4,8,4,vt),new xt(4,5,16,8,vt),new xt(4,6,32,32,vt),new xt(4,4,16,16,yt),new xt(8,16,32,32,yt),new xt(8,16,128,128,yt),new xt(8,32,128,256,yt),new xt(32,128,258,1024,yt),new xt(32,258,258,4096,yt)];function At(){this.strm=null,this.status=0,this.pending_buf=null,this.pending_buf_size=0,this.pending_out=0,this.pending=0,this.wrap=0,this.gzhead=null,this.gzindex=0,this.method=ot,this.last_flush=-1,this.w_size=0,this.w_bits=0,this.w_mask=0,this.window=null,this.window_size=0,this.prev=null,this.head=null,this.ins_h=0,this.hash_size=0,this.hash_bits=0,this.hash_mask=0,this.hash_shift=0,this.block_start=0,this.match_length=0,this.prev_match=0,this.match_available=0,this.strstart=0,this.match_start=0,this.lookahead=0,this.prev_length=0,this.max_chain_length=0,this.max_lazy_match=0,this.level=0,this.strategy=0,this.good_match=0,this.nice_match=0,this.dyn_ltree=new Uint16Array(1146),this.dyn_dtree=new Uint16Array(122),this.bl_tree=new Uint16Array(78),dt(this.dyn_ltree),dt(this.dyn_dtree),dt(this.bl_tree),this.l_desc=null,this.d_desc=null,this.bl_desc=null,this.bl_count=new Uint16Array(16),this.heap=new Uint16Array(573),dt(this.heap),this.heap_len=0,this.heap_max=0,this.depth=new Uint16Array(573),dt(this.depth),this.sym_buf=0,this.lit_bufsize=0,this.sym_next=0,this.sym_end=0,this.opt_len=0,this.static_len=0,this.matches=0,this.insert=0,this.bi_buf=0,this.bi_valid=0}const Et=t=>{if(!t)return 1;const e=t.state;return!e||e.strm!==t||42!==e.status&&57!==e.status&&69!==e.status&&73!==e.status&&91!==e.status&&103!==e.status&&113!==e.status&&666!==e.status?1:0},Rt=t=>{if(Et(t))return lt(t,Q);t.total_in=t.total_out=0,t.data_type=rt;const e=t.state;return e.pending=0,e.pending_out=0,e.wrap<0&&(e.wrap=-e.wrap),e.status=2===e.wrap?57:e.wrap?42:113,t.adler=2===e.wrap?0:1,e.last_flush=-2,C(e),W},Zt=t=>{const e=Rt(t);var a;return e===W&&((a=t.state).window_size=2*a.w_size,dt(a.head),a.max_lazy_match=zt[a.level].max_lazy,a.good_match=zt[a.level].good_length,a.nice_match=zt[a.level].nice_length,a.max_chain_length=zt[a.level].max_chain,a.strstart=0,a.block_start=0,a.lookahead=0,a.insert=0,a.match_length=a.prev_length=2,a.match_available=0,a.ins_h=0),e},St=(t,e,a,i,n,s)=>{if(!t)return Q;let r=1;if(e===tt&&(e=6),i<0?(r=0,i=-i):i>15&&(r=2,i-=16),n<1||n>9||a!==ot||i<8||i>15||e<0||e>9||s<0||s>nt||8===i&&1!==r)return lt(t,Q);8===i&&(i=9);const o=new At;return t.state=o,o.strm=t,o.status=42,o.wrap=r,o.gzhead=null,o.w_bits=i,o.w_size=1<St(t,e,ot,15,8,st),deflateInit2:St,deflateReset:Zt,deflateResetKeep:Rt,deflateSetHeader:(t,e)=>Et(t)||2!==t.state.wrap?Q:(t.state.gzhead=e,W),deflate:(t,e)=>{if(Et(t)||e>J||e<0)return t?lt(t,Q):Q;const a=t.state;if(!t.output||0!==t.avail_in&&!t.input||666===a.status&&e!==X)return lt(t,0===t.avail_out?$:Q);const i=a.last_flush;if(a.last_flush=e,0!==a.pending){if(ct(t),0===t.avail_out)return a.last_flush=-1,W}else if(0===t.avail_in&&ht(e)<=ht(i)&&e!==X)return lt(t,$);if(666===a.status&&0!==t.avail_in)return lt(t,$);if(42===a.status&&0===a.wrap&&(a.status=113),42===a.status){let e=ot+(a.w_bits-8<<4)<<8,i=-1;if(i=a.strategy>=at||a.level<2?0:a.level<6?1:6===a.level?2:3,e|=i<<6,0!==a.strstart&&(e|=32),e+=31-e%31,mt(a,e),0!==a.strstart&&(mt(a,t.adler>>>16),mt(a,65535&t.adler)),t.adler=1,a.status=113,ct(t),0!==a.pending)return a.last_flush=-1,W}if(57===a.status)if(t.adler=0,wt(a,31),wt(a,139),wt(a,8),a.gzhead)wt(a,(a.gzhead.text?1:0)+(a.gzhead.hcrc?2:0)+(a.gzhead.extra?4:0)+(a.gzhead.name?8:0)+(a.gzhead.comment?16:0)),wt(a,255&a.gzhead.time),wt(a,a.gzhead.time>>8&255),wt(a,a.gzhead.time>>16&255),wt(a,a.gzhead.time>>24&255),wt(a,9===a.level?2:a.strategy>=at||a.level<2?4:0),wt(a,255&a.gzhead.os),a.gzhead.extra&&a.gzhead.extra.length&&(wt(a,255&a.gzhead.extra.length),wt(a,a.gzhead.extra.length>>8&255)),a.gzhead.hcrc&&(t.adler=L(t.adler,a.pending_buf,a.pending,0)),a.gzindex=0,a.status=69;else if(wt(a,0),wt(a,0),wt(a,0),wt(a,0),wt(a,0),wt(a,9===a.level?2:a.strategy>=at||a.level<2?4:0),wt(a,3),a.status=113,ct(t),0!==a.pending)return a.last_flush=-1,W;if(69===a.status){if(a.gzhead.extra){let e=a.pending,i=(65535&a.gzhead.extra.length)-a.gzindex;for(;a.pending+i>a.pending_buf_size;){let n=a.pending_buf_size-a.pending;if(a.pending_buf.set(a.gzhead.extra.subarray(a.gzindex,a.gzindex+n),a.pending),a.pending=a.pending_buf_size,a.gzhead.hcrc&&a.pending>e&&(t.adler=L(t.adler,a.pending_buf,a.pending-e,e)),a.gzindex+=n,ct(t),0!==a.pending)return a.last_flush=-1,W;e=0,i-=n}let n=new Uint8Array(a.gzhead.extra);a.pending_buf.set(n.subarray(a.gzindex,a.gzindex+i),a.pending),a.pending+=i,a.gzhead.hcrc&&a.pending>e&&(t.adler=L(t.adler,a.pending_buf,a.pending-e,e)),a.gzindex=0}a.status=73}if(73===a.status){if(a.gzhead.name){let e,i=a.pending;do{if(a.pending===a.pending_buf_size){if(a.gzhead.hcrc&&a.pending>i&&(t.adler=L(t.adler,a.pending_buf,a.pending-i,i)),ct(t),0!==a.pending)return a.last_flush=-1,W;i=0}e=a.gzindexi&&(t.adler=L(t.adler,a.pending_buf,a.pending-i,i)),a.gzindex=0}a.status=91}if(91===a.status){if(a.gzhead.comment){let e,i=a.pending;do{if(a.pending===a.pending_buf_size){if(a.gzhead.hcrc&&a.pending>i&&(t.adler=L(t.adler,a.pending_buf,a.pending-i,i)),ct(t),0!==a.pending)return a.last_flush=-1,W;i=0}e=a.gzindexi&&(t.adler=L(t.adler,a.pending_buf,a.pending-i,i))}a.status=103}if(103===a.status){if(a.gzhead.hcrc){if(a.pending+2>a.pending_buf_size&&(ct(t),0!==a.pending))return a.last_flush=-1,W;wt(a,255&t.adler),wt(a,t.adler>>8&255),t.adler=0}if(a.status=113,ct(t),0!==a.pending)return a.last_flush=-1,W}if(0!==t.avail_in||0!==a.lookahead||e!==P&&666!==a.status){let i=0===a.level?kt(a,e):a.strategy===at?((t,e)=>{let a;for(;;){if(0===t.lookahead&&(pt(t),0===t.lookahead)){if(e===P)return 1;break}if(t.match_length=0,a=j(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++,a&&(ut(t,!1),0===t.strm.avail_out))return 1}return t.insert=0,e===X?(ut(t,!0),0===t.strm.avail_out?3:4):t.sym_next&&(ut(t,!1),0===t.strm.avail_out)?1:2})(a,e):a.strategy===it?((t,e)=>{let a,i,n,s;const r=t.window;for(;;){if(t.lookahead<=258){if(pt(t),t.lookahead<=258&&e===P)return 1;if(0===t.lookahead)break}if(t.match_length=0,t.lookahead>=3&&t.strstart>0&&(n=t.strstart-1,i=r[n],i===r[++n]&&i===r[++n]&&i===r[++n])){s=t.strstart+258;do{}while(i===r[++n]&&i===r[++n]&&i===r[++n]&&i===r[++n]&&i===r[++n]&&i===r[++n]&&i===r[++n]&&i===r[++n]&&nt.lookahead&&(t.match_length=t.lookahead)}if(t.match_length>=3?(a=j(t,1,t.match_length-3),t.lookahead-=t.match_length,t.strstart+=t.match_length,t.match_length=0):(a=j(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++),a&&(ut(t,!1),0===t.strm.avail_out))return 1}return t.insert=0,e===X?(ut(t,!0),0===t.strm.avail_out?3:4):t.sym_next&&(ut(t,!1),0===t.strm.avail_out)?1:2})(a,e):zt[a.level].func(a,e);if(3!==i&&4!==i||(a.status=666),1===i||3===i)return 0===t.avail_out&&(a.last_flush=-1),W;if(2===i&&(e===Y?K(a):e!==J&&(H(a,0,0,!1),e===G&&(dt(a.head),0===a.lookahead&&(a.strstart=0,a.block_start=0,a.insert=0))),ct(t),0===t.avail_out))return a.last_flush=-1,W}return e!==X?W:a.wrap<=0?q:(2===a.wrap?(wt(a,255&t.adler),wt(a,t.adler>>8&255),wt(a,t.adler>>16&255),wt(a,t.adler>>24&255),wt(a,255&t.total_in),wt(a,t.total_in>>8&255),wt(a,t.total_in>>16&255),wt(a,t.total_in>>24&255)):(mt(a,t.adler>>>16),mt(a,65535&t.adler)),ct(t),a.wrap>0&&(a.wrap=-a.wrap),0!==a.pending?W:q)},deflateEnd:t=>{if(Et(t))return Q;const e=t.state.status;return t.state=null,113===e?lt(t,V):W},deflateSetDictionary:(t,e)=>{let a=e.length;if(Et(t))return Q;const i=t.state,n=i.wrap;if(2===n||1===n&&42!==i.status||i.lookahead)return Q;if(1===n&&(t.adler=N(t.adler,e,a,0)),i.wrap=0,a>=i.w_size){0===n&&(dt(i.head),i.strstart=0,i.block_start=0,i.insert=0);let t=new Uint8Array(i.w_size);t.set(e.subarray(a-i.w_size,a),0),e=t,a=i.w_size}const s=t.avail_in,r=t.next_in,o=t.input;for(t.avail_in=a,t.next_in=0,t.input=e,pt(i);i.lookahead>=3;){let t=i.strstart,e=i.lookahead-2;do{i.ins_h=ft(i,i.ins_h,i.window[t+3-1]),i.prev[t&i.w_mask]=i.head[i.ins_h],i.head[i.ins_h]=t,t++}while(--e);i.strstart=t,i.lookahead=2,pt(i)}return i.strstart+=i.lookahead,i.block_start=i.strstart,i.insert=i.lookahead,i.lookahead=0,i.match_length=i.prev_length=2,i.match_available=0,t.next_in=r,t.input=o,t.avail_in=s,i.wrap=n,W},deflateInfo:"pako deflate (from Nodeca project)"};const Dt=(t,e)=>Object.prototype.hasOwnProperty.call(t,e);var Ot=function(t){const e=Array.prototype.slice.call(arguments,1);for(;e.length;){const a=e.shift();if(a){if("object"!=typeof a)throw new TypeError(a+"must be non-object");for(const e in a)Dt(a,e)&&(t[e]=a[e])}}return t},Tt=t=>{let e=0;for(let a=0,i=t.length;a=252?6:t>=248?5:t>=240?4:t>=224?3:t>=192?2:1;Ft[254]=Ft[254]=1;var Lt=t=>{if("function"==typeof TextEncoder&&TextEncoder.prototype.encode)return(new TextEncoder).encode(t);let e,a,i,n,s,r=t.length,o=0;for(n=0;n>>6,e[s++]=128|63&a):a<65536?(e[s++]=224|a>>>12,e[s++]=128|a>>>6&63,e[s++]=128|63&a):(e[s++]=240|a>>>18,e[s++]=128|a>>>12&63,e[s++]=128|a>>>6&63,e[s++]=128|63&a);return e},It=(t,e)=>{const a=e||t.length;if("function"==typeof TextDecoder&&TextDecoder.prototype.decode)return(new TextDecoder).decode(t.subarray(0,e));let i,n;const s=new Array(2*a);for(n=0,i=0;i4)s[n++]=65533,i+=r-1;else{for(e&=2===r?31:3===r?15:7;r>1&&i1?s[n++]=65533:e<65536?s[n++]=e:(e-=65536,s[n++]=55296|e>>10&1023,s[n++]=56320|1023&e)}}return((t,e)=>{if(e<65534&&t.subarray&&Nt)return String.fromCharCode.apply(null,t.length===e?t:t.subarray(0,e));let a="";for(let i=0;i{(e=e||t.length)>t.length&&(e=t.length);let a=e-1;for(;a>=0&&128==(192&t[a]);)a--;return a<0||0===a?e:a+Ft[t[a]]>e?a:e};var Ct=function(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0};const Ht=Object.prototype.toString,{Z_NO_FLUSH:Mt,Z_SYNC_FLUSH:jt,Z_FULL_FLUSH:Kt,Z_FINISH:Pt,Z_OK:Yt,Z_STREAM_END:Gt,Z_DEFAULT_COMPRESSION:Xt,Z_DEFAULT_STRATEGY:Jt,Z_DEFLATED:Wt}=B;function qt(t){this.options=Ot({level:Xt,method:Wt,chunkSize:16384,windowBits:15,memLevel:8,strategy:Jt},t||{});let e=this.options;e.raw&&e.windowBits>0?e.windowBits=-e.windowBits:e.gzip&&e.windowBits>0&&e.windowBits<16&&(e.windowBits+=16),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new Ct,this.strm.avail_out=0;let a=Ut.deflateInit2(this.strm,e.level,e.method,e.windowBits,e.memLevel,e.strategy);if(a!==Yt)throw new Error(I[a]);if(e.header&&Ut.deflateSetHeader(this.strm,e.header),e.dictionary){let t;if(t="string"==typeof e.dictionary?Lt(e.dictionary):"[object ArrayBuffer]"===Ht.call(e.dictionary)?new Uint8Array(e.dictionary):e.dictionary,a=Ut.deflateSetDictionary(this.strm,t),a!==Yt)throw new Error(I[a]);this._dict_set=!0}}function Qt(t,e){const a=new qt(e);if(a.push(t,!0),a.err)throw a.msg||I[a.err];return a.result}qt.prototype.push=function(t,e){const a=this.strm,i=this.options.chunkSize;let n,s;if(this.ended)return!1;for(s=e===~~e?e:!0===e?Pt:Mt,"string"==typeof t?a.input=Lt(t):"[object ArrayBuffer]"===Ht.call(t)?a.input=new Uint8Array(t):a.input=t,a.next_in=0,a.avail_in=a.input.length;;)if(0===a.avail_out&&(a.output=new Uint8Array(i),a.next_out=0,a.avail_out=i),(s===jt||s===Kt)&&a.avail_out<=6)this.onData(a.output.subarray(0,a.next_out)),a.avail_out=0;else{if(n=Ut.deflate(a,s),n===Gt)return a.next_out>0&&this.onData(a.output.subarray(0,a.next_out)),n=Ut.deflateEnd(this.strm),this.onEnd(n),this.ended=!0,n===Yt;if(0!==a.avail_out){if(s>0&&a.next_out>0)this.onData(a.output.subarray(0,a.next_out)),a.avail_out=0;else if(0===a.avail_in)break}else this.onData(a.output)}return!0},qt.prototype.onData=function(t){this.chunks.push(t)},qt.prototype.onEnd=function(t){t===Yt&&(this.result=Tt(this.chunks)),this.chunks=[],this.err=t,this.msg=this.strm.msg};var Vt={Deflate:qt,deflate:Qt,deflateRaw:function(t,e){return(e=e||{}).raw=!0,Qt(t,e)},gzip:function(t,e){return(e=e||{}).gzip=!0,Qt(t,e)},constants:B};var $t=function(t,e){let a,i,n,s,r,o,l,h,d,_,f,c,u,w,m,b,g,p,k,v,y,x,z,A;const E=t.state;a=t.next_in,z=t.input,i=a+(t.avail_in-5),n=t.next_out,A=t.output,s=n-(e-t.avail_out),r=n+(t.avail_out-257),o=E.dmax,l=E.wsize,h=E.whave,d=E.wnext,_=E.window,f=E.hold,c=E.bits,u=E.lencode,w=E.distcode,m=(1<>>24,f>>>=p,c-=p,p=g>>>16&255,0===p)A[n++]=65535&g;else{if(!(16&p)){if(0==(64&p)){g=u[(65535&g)+(f&(1<>>=p,c-=p),c<15&&(f+=z[a++]<>>24,f>>>=p,c-=p,p=g>>>16&255,!(16&p)){if(0==(64&p)){g=w[(65535&g)+(f&(1<o){t.msg="invalid distance too far back",E.mode=16209;break t}if(f>>>=p,c-=p,p=n-s,v>p){if(p=v-p,p>h&&E.sane){t.msg="invalid distance too far back",E.mode=16209;break t}if(y=0,x=_,0===d){if(y+=l-p,p2;)A[n++]=x[y++],A[n++]=x[y++],A[n++]=x[y++],k-=3;k&&(A[n++]=x[y++],k>1&&(A[n++]=x[y++]))}else{y=n-v;do{A[n++]=A[y++],A[n++]=A[y++],A[n++]=A[y++],k-=3}while(k>2);k&&(A[n++]=A[y++],k>1&&(A[n++]=A[y++]))}break}}break}}while(a>3,a-=k,c-=k<<3,f&=(1<{const l=o.bits;let h,d,_,f,c,u,w=0,m=0,b=0,g=0,p=0,k=0,v=0,y=0,x=0,z=0,A=null;const E=new Uint16Array(16),R=new Uint16Array(16);let Z,S,U,D=null;for(w=0;w<=15;w++)E[w]=0;for(m=0;m=1&&0===E[g];g--);if(p>g&&(p=g),0===g)return n[s++]=20971520,n[s++]=20971520,o.bits=1,0;for(b=1;b0&&(0===t||1!==g))return-1;for(R[1]=0,w=1;w<15;w++)R[w+1]=R[w]+E[w];for(m=0;m852||2===t&&x>592)return 1;for(;;){Z=w-v,r[m]+1=u?(S=D[r[m]-u],U=A[r[m]-u]):(S=96,U=0),h=1<>v)+d]=Z<<24|S<<16|U|0}while(0!==d);for(h=1<>=1;if(0!==h?(z&=h-1,z+=h):z=0,m++,0==--E[w]){if(w===g)break;w=e[a+r[m]]}if(w>p&&(z&f)!==_){for(0===v&&(v=p),c+=b,k=w-v,y=1<852||2===t&&x>592)return 1;_=z&f,n[_]=p<<24|k<<16|c-s|0}}return 0!==z&&(n[c+z]=w-v<<24|64<<16|0),o.bits=p,0};const{Z_FINISH:se,Z_BLOCK:re,Z_TREES:oe,Z_OK:le,Z_STREAM_END:he,Z_NEED_DICT:de,Z_STREAM_ERROR:_e,Z_DATA_ERROR:fe,Z_MEM_ERROR:ce,Z_BUF_ERROR:ue,Z_DEFLATED:we}=B,me=16209,be=t=>(t>>>24&255)+(t>>>8&65280)+((65280&t)<<8)+((255&t)<<24);function ge(){this.strm=null,this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new Uint16Array(320),this.work=new Uint16Array(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0}const pe=t=>{if(!t)return 1;const e=t.state;return!e||e.strm!==t||e.mode<16180||e.mode>16211?1:0},ke=t=>{if(pe(t))return _e;const e=t.state;return t.total_in=t.total_out=e.total=0,t.msg="",e.wrap&&(t.adler=1&e.wrap),e.mode=16180,e.last=0,e.havedict=0,e.flags=-1,e.dmax=32768,e.head=null,e.hold=0,e.bits=0,e.lencode=e.lendyn=new Int32Array(852),e.distcode=e.distdyn=new Int32Array(592),e.sane=1,e.back=-1,le},ve=t=>{if(pe(t))return _e;const e=t.state;return e.wsize=0,e.whave=0,e.wnext=0,ke(t)},ye=(t,e)=>{let a;if(pe(t))return _e;const i=t.state;return e<0?(a=0,e=-e):(a=5+(e>>4),e<48&&(e&=15)),e&&(e<8||e>15)?_e:(null!==i.window&&i.wbits!==e&&(i.window=null),i.wrap=a,i.wbits=e,ve(t))},xe=(t,e)=>{if(!t)return _e;const a=new ge;t.state=a,a.strm=t,a.window=null,a.mode=16180;const i=ye(t,e);return i!==le&&(t.state=null),i};let ze,Ae,Ee=!0;const Re=t=>{if(Ee){ze=new Int32Array(512),Ae=new Int32Array(32);let e=0;for(;e<144;)t.lens[e++]=8;for(;e<256;)t.lens[e++]=9;for(;e<280;)t.lens[e++]=7;for(;e<288;)t.lens[e++]=8;for(ne(1,t.lens,0,288,ze,0,t.work,{bits:9}),e=0;e<32;)t.lens[e++]=5;ne(2,t.lens,0,32,Ae,0,t.work,{bits:5}),Ee=!1}t.lencode=ze,t.lenbits=9,t.distcode=Ae,t.distbits=5},Ze=(t,e,a,i)=>{let n;const s=t.state;return null===s.window&&(s.wsize=1<=s.wsize?(s.window.set(e.subarray(a-s.wsize,a),0),s.wnext=0,s.whave=s.wsize):(n=s.wsize-s.wnext,n>i&&(n=i),s.window.set(e.subarray(a-i,a-i+n),s.wnext),(i-=n)?(s.window.set(e.subarray(a-i,a),0),s.wnext=i,s.whave=s.wsize):(s.wnext+=n,s.wnext===s.wsize&&(s.wnext=0),s.whavexe(t,15),inflateInit2:xe,inflate:(t,e)=>{let a,i,n,s,r,o,l,h,d,_,f,c,u,w,m,b,g,p,k,v,y,x,z=0;const A=new Uint8Array(4);let E,R;const Z=new Uint8Array([16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]);if(pe(t)||!t.output||!t.input&&0!==t.avail_in)return _e;a=t.state,16191===a.mode&&(a.mode=16192),r=t.next_out,n=t.output,l=t.avail_out,s=t.next_in,i=t.input,o=t.avail_in,h=a.hold,d=a.bits,_=o,f=l,x=le;t:for(;;)switch(a.mode){case 16180:if(0===a.wrap){a.mode=16192;break}for(;d<16;){if(0===o)break t;o--,h+=i[s++]<>>8&255,a.check=L(a.check,A,2,0),h=0,d=0,a.mode=16181;break}if(a.head&&(a.head.done=!1),!(1&a.wrap)||(((255&h)<<8)+(h>>8))%31){t.msg="incorrect header check",a.mode=me;break}if((15&h)!==we){t.msg="unknown compression method",a.mode=me;break}if(h>>>=4,d-=4,y=8+(15&h),0===a.wbits&&(a.wbits=y),y>15||y>a.wbits){t.msg="invalid window size",a.mode=me;break}a.dmax=1<>8&1),512&a.flags&&4&a.wrap&&(A[0]=255&h,A[1]=h>>>8&255,a.check=L(a.check,A,2,0)),h=0,d=0,a.mode=16182;case 16182:for(;d<32;){if(0===o)break t;o--,h+=i[s++]<>>8&255,A[2]=h>>>16&255,A[3]=h>>>24&255,a.check=L(a.check,A,4,0)),h=0,d=0,a.mode=16183;case 16183:for(;d<16;){if(0===o)break t;o--,h+=i[s++]<>8),512&a.flags&&4&a.wrap&&(A[0]=255&h,A[1]=h>>>8&255,a.check=L(a.check,A,2,0)),h=0,d=0,a.mode=16184;case 16184:if(1024&a.flags){for(;d<16;){if(0===o)break t;o--,h+=i[s++]<>>8&255,a.check=L(a.check,A,2,0)),h=0,d=0}else a.head&&(a.head.extra=null);a.mode=16185;case 16185:if(1024&a.flags&&(c=a.length,c>o&&(c=o),c&&(a.head&&(y=a.head.extra_len-a.length,a.head.extra||(a.head.extra=new Uint8Array(a.head.extra_len)),a.head.extra.set(i.subarray(s,s+c),y)),512&a.flags&&4&a.wrap&&(a.check=L(a.check,i,c,s)),o-=c,s+=c,a.length-=c),a.length))break t;a.length=0,a.mode=16186;case 16186:if(2048&a.flags){if(0===o)break t;c=0;do{y=i[s+c++],a.head&&y&&a.length<65536&&(a.head.name+=String.fromCharCode(y))}while(y&&c>9&1,a.head.done=!0),t.adler=a.check=0,a.mode=16191;break;case 16189:for(;d<32;){if(0===o)break t;o--,h+=i[s++]<>>=7&d,d-=7&d,a.mode=16206;break}for(;d<3;){if(0===o)break t;o--,h+=i[s++]<>>=1,d-=1,3&h){case 0:a.mode=16193;break;case 1:if(Re(a),a.mode=16199,e===oe){h>>>=2,d-=2;break t}break;case 2:a.mode=16196;break;case 3:t.msg="invalid block type",a.mode=me}h>>>=2,d-=2;break;case 16193:for(h>>>=7&d,d-=7&d;d<32;){if(0===o)break t;o--,h+=i[s++]<>>16^65535)){t.msg="invalid stored block lengths",a.mode=me;break}if(a.length=65535&h,h=0,d=0,a.mode=16194,e===oe)break t;case 16194:a.mode=16195;case 16195:if(c=a.length,c){if(c>o&&(c=o),c>l&&(c=l),0===c)break t;n.set(i.subarray(s,s+c),r),o-=c,s+=c,l-=c,r+=c,a.length-=c;break}a.mode=16191;break;case 16196:for(;d<14;){if(0===o)break t;o--,h+=i[s++]<>>=5,d-=5,a.ndist=1+(31&h),h>>>=5,d-=5,a.ncode=4+(15&h),h>>>=4,d-=4,a.nlen>286||a.ndist>30){t.msg="too many length or distance symbols",a.mode=me;break}a.have=0,a.mode=16197;case 16197:for(;a.have>>=3,d-=3}for(;a.have<19;)a.lens[Z[a.have++]]=0;if(a.lencode=a.lendyn,a.lenbits=7,E={bits:a.lenbits},x=ne(0,a.lens,0,19,a.lencode,0,a.work,E),a.lenbits=E.bits,x){t.msg="invalid code lengths set",a.mode=me;break}a.have=0,a.mode=16198;case 16198:for(;a.have>>24,b=z>>>16&255,g=65535&z,!(m<=d);){if(0===o)break t;o--,h+=i[s++]<>>=m,d-=m,a.lens[a.have++]=g;else{if(16===g){for(R=m+2;d>>=m,d-=m,0===a.have){t.msg="invalid bit length repeat",a.mode=me;break}y=a.lens[a.have-1],c=3+(3&h),h>>>=2,d-=2}else if(17===g){for(R=m+3;d>>=m,d-=m,y=0,c=3+(7&h),h>>>=3,d-=3}else{for(R=m+7;d>>=m,d-=m,y=0,c=11+(127&h),h>>>=7,d-=7}if(a.have+c>a.nlen+a.ndist){t.msg="invalid bit length repeat",a.mode=me;break}for(;c--;)a.lens[a.have++]=y}}if(a.mode===me)break;if(0===a.lens[256]){t.msg="invalid code -- missing end-of-block",a.mode=me;break}if(a.lenbits=9,E={bits:a.lenbits},x=ne(1,a.lens,0,a.nlen,a.lencode,0,a.work,E),a.lenbits=E.bits,x){t.msg="invalid literal/lengths set",a.mode=me;break}if(a.distbits=6,a.distcode=a.distdyn,E={bits:a.distbits},x=ne(2,a.lens,a.nlen,a.ndist,a.distcode,0,a.work,E),a.distbits=E.bits,x){t.msg="invalid distances set",a.mode=me;break}if(a.mode=16199,e===oe)break t;case 16199:a.mode=16200;case 16200:if(o>=6&&l>=258){t.next_out=r,t.avail_out=l,t.next_in=s,t.avail_in=o,a.hold=h,a.bits=d,$t(t,f),r=t.next_out,n=t.output,l=t.avail_out,s=t.next_in,i=t.input,o=t.avail_in,h=a.hold,d=a.bits,16191===a.mode&&(a.back=-1);break}for(a.back=0;z=a.lencode[h&(1<>>24,b=z>>>16&255,g=65535&z,!(m<=d);){if(0===o)break t;o--,h+=i[s++]<>p)],m=z>>>24,b=z>>>16&255,g=65535&z,!(p+m<=d);){if(0===o)break t;o--,h+=i[s++]<>>=p,d-=p,a.back+=p}if(h>>>=m,d-=m,a.back+=m,a.length=g,0===b){a.mode=16205;break}if(32&b){a.back=-1,a.mode=16191;break}if(64&b){t.msg="invalid literal/length code",a.mode=me;break}a.extra=15&b,a.mode=16201;case 16201:if(a.extra){for(R=a.extra;d>>=a.extra,d-=a.extra,a.back+=a.extra}a.was=a.length,a.mode=16202;case 16202:for(;z=a.distcode[h&(1<>>24,b=z>>>16&255,g=65535&z,!(m<=d);){if(0===o)break t;o--,h+=i[s++]<>p)],m=z>>>24,b=z>>>16&255,g=65535&z,!(p+m<=d);){if(0===o)break t;o--,h+=i[s++]<>>=p,d-=p,a.back+=p}if(h>>>=m,d-=m,a.back+=m,64&b){t.msg="invalid distance code",a.mode=me;break}a.offset=g,a.extra=15&b,a.mode=16203;case 16203:if(a.extra){for(R=a.extra;d>>=a.extra,d-=a.extra,a.back+=a.extra}if(a.offset>a.dmax){t.msg="invalid distance too far back",a.mode=me;break}a.mode=16204;case 16204:if(0===l)break t;if(c=f-l,a.offset>c){if(c=a.offset-c,c>a.whave&&a.sane){t.msg="invalid distance too far back",a.mode=me;break}c>a.wnext?(c-=a.wnext,u=a.wsize-c):u=a.wnext-c,c>a.length&&(c=a.length),w=a.window}else w=n,u=r-a.offset,c=a.length;c>l&&(c=l),l-=c,a.length-=c;do{n[r++]=w[u++]}while(--c);0===a.length&&(a.mode=16200);break;case 16205:if(0===l)break t;n[r++]=a.length,l--,a.mode=16200;break;case 16206:if(a.wrap){for(;d<32;){if(0===o)break t;o--,h|=i[s++]<{if(pe(t))return _e;let e=t.state;return e.window&&(e.window=null),t.state=null,le},inflateGetHeader:(t,e)=>{if(pe(t))return _e;const a=t.state;return 0==(2&a.wrap)?_e:(a.head=e,e.done=!1,le)},inflateSetDictionary:(t,e)=>{const a=e.length;let i,n,s;return pe(t)?_e:(i=t.state,0!==i.wrap&&16190!==i.mode?_e:16190===i.mode&&(n=1,n=N(n,e,a,0),n!==i.check)?fe:(s=Ze(t,e,a,a),s?(i.mode=16210,ce):(i.havedict=1,le)))},inflateInfo:"pako inflate (from Nodeca project)"};var Ue=function(){this.text=0,this.time=0,this.xflags=0,this.os=0,this.extra=null,this.extra_len=0,this.name="",this.comment="",this.hcrc=0,this.done=!1};const De=Object.prototype.toString,{Z_NO_FLUSH:Oe,Z_FINISH:Te,Z_OK:Ne,Z_STREAM_END:Fe,Z_NEED_DICT:Le,Z_STREAM_ERROR:Ie,Z_DATA_ERROR:Be,Z_MEM_ERROR:Ce}=B;function He(t){this.options=Ot({chunkSize:65536,windowBits:15,to:""},t||{});const e=this.options;e.raw&&e.windowBits>=0&&e.windowBits<16&&(e.windowBits=-e.windowBits,0===e.windowBits&&(e.windowBits=-15)),!(e.windowBits>=0&&e.windowBits<16)||t&&t.windowBits||(e.windowBits+=32),e.windowBits>15&&e.windowBits<48&&0==(15&e.windowBits)&&(e.windowBits|=15),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new Ct,this.strm.avail_out=0;let a=Se.inflateInit2(this.strm,e.windowBits);if(a!==Ne)throw new Error(I[a]);if(this.header=new Ue,Se.inflateGetHeader(this.strm,this.header),e.dictionary&&("string"==typeof e.dictionary?e.dictionary=Lt(e.dictionary):"[object ArrayBuffer]"===De.call(e.dictionary)&&(e.dictionary=new Uint8Array(e.dictionary)),e.raw&&(a=Se.inflateSetDictionary(this.strm,e.dictionary),a!==Ne)))throw new Error(I[a])}He.prototype.push=function(t,e){const a=this.strm,i=this.options.chunkSize,n=this.options.dictionary;let s,r,o;if(this.ended)return!1;for(r=e===~~e?e:!0===e?Te:Oe,"[object ArrayBuffer]"===De.call(t)?a.input=new Uint8Array(t):a.input=t,a.next_in=0,a.avail_in=a.input.length;;){for(0===a.avail_out&&(a.output=new Uint8Array(i),a.next_out=0,a.avail_out=i),s=Se.inflate(a,r),s===Le&&n&&(s=Se.inflateSetDictionary(a,n),s===Ne?s=Se.inflate(a,r):s===Be&&(s=Le));a.avail_in>0&&s===Fe&&a.state.wrap>0&&0!==t[a.next_in];)Se.inflateReset(a),s=Se.inflate(a,r);switch(s){case Ie:case Be:case Le:case Ce:return this.onEnd(s),this.ended=!0,!1}if(o=a.avail_out,a.next_out&&(0===a.avail_out||s===Fe))if("string"===this.options.to){let t=Bt(a.output,a.next_out),e=a.next_out-t,n=It(a.output,t);a.next_out=e,a.avail_out=i-e,e&&a.output.set(a.output.subarray(t,t+e),0),this.onData(n)}else this.onData(a.output.length===a.next_out?a.output:a.output.subarray(0,a.next_out));if(s!==Ne||0!==o){if(s===Fe)return s=Se.inflateEnd(this.strm),this.onEnd(s),this.ended=!0,!0;if(0===a.avail_in)break}}return!0},He.prototype.onData=function(t){this.chunks.push(t)},He.prototype.onEnd=function(t){t===Ne&&("string"===this.options.to?this.result=this.chunks.join(""):this.result=Tt(this.chunks)),this.chunks=[],this.err=t,this.msg=this.strm.msg};const{Deflate:Me,deflate:je,deflateRaw:Ke,gzip:Pe}=Vt;var Ye=Me,Ge=B;const Xe=new class{constructor(){this.added=0,this.init()}init(){this.added=0,this.deflate=new Ye,this.deflate.push("[",Ge.Z_NO_FLUSH)}addEvent(t){if(!t)return;const e=this.added>0?",":"";this.deflate.push(e+JSON.stringify(t),Ge.Z_NO_FLUSH),this.added++}finish(){if(this.deflate.push("]",Ge.Z_FINISH),this.deflate.err)throw this.deflate.err;const t=this.deflate.result;return this.init(),t}},Je={init:()=>(Xe.init(),""),addEvent:t=>(Xe.addEvent(t),""),finish:()=>Xe.finish()};addEventListener("message",(function(t){const e=t.data.method,a=t.data.id,[i]=t.data.args?JSON.parse(t.data.args):[];if(e in Je&&"function"==typeof Je[e])try{const t=Je[e](i);postMessage({id:a,method:e,success:!0,response:t})}catch(t){postMessage({id:a,method:e,success:!1,response:t}),console.error(t)}}));`; +function t(t){let e=t.length;for(;--e>=0;)t[e]=0}const e=new Uint8Array([0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0]),a=new Uint8Array([0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13]),i=new Uint8Array([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,3,7]),n=new Uint8Array([16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]),s=new Array(576);t(s);const r=new Array(60);t(r);const o=new Array(512);t(o);const l=new Array(256);t(l);const h=new Array(29);t(h);const d=new Array(30);function _(t,e,a,i,n){this.static_tree=t,this.extra_bits=e,this.extra_base=a,this.elems=i,this.max_length=n,this.has_stree=t&&t.length}let f,c,u;function w(t,e){this.dyn_tree=t,this.max_code=0,this.stat_desc=e}t(d);const m=t=>t<256?o[t]:o[256+(t>>>7)],b=(t,e)=>{t.pending_buf[t.pending++]=255&e,t.pending_buf[t.pending++]=e>>>8&255},g=(t,e,a)=>{t.bi_valid>16-a?(t.bi_buf|=e<>16-t.bi_valid,t.bi_valid+=a-16):(t.bi_buf|=e<{g(t,a[2*e],a[2*e+1])},k=(t,e)=>{let a=0;do{a|=1&t,t>>>=1,a<<=1}while(--e>0);return a>>>1},v=(t,e,a)=>{const i=new Array(16);let n,s,r=0;for(n=1;n<=15;n++)r=r+a[n-1]<<1,i[n]=r;for(s=0;s<=e;s++){let e=t[2*s+1];0!==e&&(t[2*s]=k(i[e]++,e))}},y=t=>{let e;for(e=0;e<286;e++)t.dyn_ltree[2*e]=0;for(e=0;e<30;e++)t.dyn_dtree[2*e]=0;for(e=0;e<19;e++)t.bl_tree[2*e]=0;t.dyn_ltree[512]=1,t.opt_len=t.static_len=0,t.sym_next=t.matches=0},x=t=>{t.bi_valid>8?b(t,t.bi_buf):t.bi_valid>0&&(t.pending_buf[t.pending++]=t.bi_buf),t.bi_buf=0,t.bi_valid=0},z=(t,e,a,i)=>{const n=2*e,s=2*a;return t[n]{const i=t.heap[a];let n=a<<1;for(;n<=t.heap_len&&(n{let s,r,o,_,f=0;if(0!==t.sym_next)do{s=255&t.pending_buf[t.sym_buf+f++],s+=(255&t.pending_buf[t.sym_buf+f++])<<8,r=t.pending_buf[t.sym_buf+f++],0===s?p(t,r,i):(o=l[r],p(t,o+256+1,i),_=e[o],0!==_&&(r-=h[o],g(t,r,_)),s--,o=m(s),p(t,o,n),_=a[o],0!==_&&(s-=d[o],g(t,s,_)))}while(f{const a=e.dyn_tree,i=e.stat_desc.static_tree,n=e.stat_desc.has_stree,s=e.stat_desc.elems;let r,o,l,h=-1;for(t.heap_len=0,t.heap_max=573,r=0;r>1;r>=1;r--)A(t,a,r);l=s;do{r=t.heap[1],t.heap[1]=t.heap[t.heap_len--],A(t,a,1),o=t.heap[1],t.heap[--t.heap_max]=r,t.heap[--t.heap_max]=o,a[2*l]=a[2*r]+a[2*o],t.depth[l]=(t.depth[r]>=t.depth[o]?t.depth[r]:t.depth[o])+1,a[2*r+1]=a[2*o+1]=l,t.heap[1]=l++,A(t,a,1)}while(t.heap_len>=2);t.heap[--t.heap_max]=t.heap[1],((t,e)=>{const a=e.dyn_tree,i=e.max_code,n=e.stat_desc.static_tree,s=e.stat_desc.has_stree,r=e.stat_desc.extra_bits,o=e.stat_desc.extra_base,l=e.stat_desc.max_length;let h,d,_,f,c,u,w=0;for(f=0;f<=15;f++)t.bl_count[f]=0;for(a[2*t.heap[t.heap_max]+1]=0,h=t.heap_max+1;h<573;h++)d=t.heap[h],f=a[2*a[2*d+1]+1]+1,f>l&&(f=l,w++),a[2*d+1]=f,d>i||(t.bl_count[f]++,c=0,d>=o&&(c=r[d-o]),u=a[2*d],t.opt_len+=u*(f+c),s&&(t.static_len+=u*(n[2*d+1]+c)));if(0!==w){do{for(f=l-1;0===t.bl_count[f];)f--;t.bl_count[f]--,t.bl_count[f+1]+=2,t.bl_count[l]--,w-=2}while(w>0);for(f=l;0!==f;f--)for(d=t.bl_count[f];0!==d;)_=t.heap[--h],_>i||(a[2*_+1]!==f&&(t.opt_len+=(f-a[2*_+1])*a[2*_],a[2*_+1]=f),d--)}})(t,e),v(a,h,t.bl_count)},Z=(t,e,a)=>{let i,n,s=-1,r=e[1],o=0,l=7,h=4;for(0===r&&(l=138,h=3),e[2*(a+1)+1]=65535,i=0;i<=a;i++)n=r,r=e[2*(i+1)+1],++o{let i,n,s=-1,r=e[1],o=0,l=7,h=4;for(0===r&&(l=138,h=3),i=0;i<=a;i++)if(n=r,r=e[2*(i+1)+1],!(++o{g(t,0+(i?1:0),3),x(t),b(t,a),b(t,~a),a&&t.pending_buf.set(t.window.subarray(e,e+a),t.pending),t.pending+=a};var O=(t,e,a,i)=>{let o,l,h=0;t.level>0?(2===t.strm.data_type&&(t.strm.data_type=(t=>{let e,a=4093624447;for(e=0;e<=31;e++,a>>>=1)if(1&a&&0!==t.dyn_ltree[2*e])return 0;if(0!==t.dyn_ltree[18]||0!==t.dyn_ltree[20]||0!==t.dyn_ltree[26])return 1;for(e=32;e<256;e++)if(0!==t.dyn_ltree[2*e])return 1;return 0})(t)),R(t,t.l_desc),R(t,t.d_desc),h=(t=>{let e;for(Z(t,t.dyn_ltree,t.l_desc.max_code),Z(t,t.dyn_dtree,t.d_desc.max_code),R(t,t.bl_desc),e=18;e>=3&&0===t.bl_tree[2*n[e]+1];e--);return t.opt_len+=3*(e+1)+5+5+4,e})(t),o=t.opt_len+3+7>>>3,l=t.static_len+3+7>>>3,l<=o&&(o=l)):o=l=a+5,a+4<=o&&-1!==e?D(t,e,a,i):4===t.strategy||l===o?(g(t,2+(i?1:0),3),E(t,s,r)):(g(t,4+(i?1:0),3),((t,e,a,i)=>{let s;for(g(t,e-257,5),g(t,a-1,5),g(t,i-4,4),s=0;s{U||((()=>{let t,n,w,m,b;const g=new Array(16);for(w=0,m=0;m<28;m++)for(h[m]=w,t=0;t<1<>=7;m<30;m++)for(d[m]=b<<7,t=0;t<1<(t.pending_buf[t.sym_buf+t.sym_next++]=e,t.pending_buf[t.sym_buf+t.sym_next++]=e>>8,t.pending_buf[t.sym_buf+t.sym_next++]=a,0===e?t.dyn_ltree[2*a]++:(t.matches++,e--,t.dyn_ltree[2*(l[a]+256+1)]++,t.dyn_dtree[2*m(e)]++),t.sym_next===t.sym_end),_tr_align:t=>{g(t,2,3),p(t,256,s),(t=>{16===t.bi_valid?(b(t,t.bi_buf),t.bi_buf=0,t.bi_valid=0):t.bi_valid>=8&&(t.pending_buf[t.pending++]=255&t.bi_buf,t.bi_buf>>=8,t.bi_valid-=8)})(t)}};var N=(t,e,a,i)=>{let n=65535&t|0,s=t>>>16&65535|0,r=0;for(;0!==a;){r=a>2e3?2e3:a,a-=r;do{n=n+e[i++]|0,s=s+n|0}while(--r);n%=65521,s%=65521}return n|s<<16|0};const F=new Uint32Array((()=>{let t,e=[];for(var a=0;a<256;a++){t=a;for(var i=0;i<8;i++)t=1&t?3988292384^t>>>1:t>>>1;e[a]=t}return e})());var L=(t,e,a,i)=>{const n=F,s=i+a;t^=-1;for(let a=i;a>>8^n[255&(t^e[a])];return-1^t},I={2:"need dictionary",1:"stream end",0:"","-1":"file error","-2":"stream error","-3":"data error","-4":"insufficient memory","-5":"buffer error","-6":"incompatible version"},B={Z_NO_FLUSH:0,Z_PARTIAL_FLUSH:1,Z_SYNC_FLUSH:2,Z_FULL_FLUSH:3,Z_FINISH:4,Z_BLOCK:5,Z_TREES:6,Z_OK:0,Z_STREAM_END:1,Z_NEED_DICT:2,Z_ERRNO:-1,Z_STREAM_ERROR:-2,Z_DATA_ERROR:-3,Z_MEM_ERROR:-4,Z_BUF_ERROR:-5,Z_NO_COMPRESSION:0,Z_BEST_SPEED:1,Z_BEST_COMPRESSION:9,Z_DEFAULT_COMPRESSION:-1,Z_FILTERED:1,Z_HUFFMAN_ONLY:2,Z_RLE:3,Z_FIXED:4,Z_DEFAULT_STRATEGY:0,Z_BINARY:0,Z_TEXT:1,Z_UNKNOWN:2,Z_DEFLATED:8};const{_tr_init:C,_tr_stored_block:H,_tr_flush_block:M,_tr_tally:j,_tr_align:K}=T,{Z_NO_FLUSH:P,Z_PARTIAL_FLUSH:Y,Z_FULL_FLUSH:G,Z_FINISH:X,Z_BLOCK:J,Z_OK:W,Z_STREAM_END:q,Z_STREAM_ERROR:Q,Z_DATA_ERROR:V,Z_BUF_ERROR:$,Z_DEFAULT_COMPRESSION:tt,Z_FILTERED:et,Z_HUFFMAN_ONLY:at,Z_RLE:it,Z_FIXED:nt,Z_DEFAULT_STRATEGY:st,Z_UNKNOWN:rt,Z_DEFLATED:ot}=B,lt=(t,e)=>(t.msg=I[e],e),ht=t=>2*t-(t>4?9:0),dt=t=>{let e=t.length;for(;--e>=0;)t[e]=0},_t=t=>{let e,a,i,n=t.w_size;e=t.hash_size,i=e;do{a=t.head[--i],t.head[i]=a>=n?a-n:0}while(--e);e=n,i=e;do{a=t.prev[--i],t.prev[i]=a>=n?a-n:0}while(--e)};let ft=(t,e,a)=>(e<{const e=t.state;let a=e.pending;a>t.avail_out&&(a=t.avail_out),0!==a&&(t.output.set(e.pending_buf.subarray(e.pending_out,e.pending_out+a),t.next_out),t.next_out+=a,e.pending_out+=a,t.total_out+=a,t.avail_out-=a,e.pending-=a,0===e.pending&&(e.pending_out=0))},ut=(t,e)=>{M(t,t.block_start>=0?t.block_start:-1,t.strstart-t.block_start,e),t.block_start=t.strstart,ct(t.strm)},wt=(t,e)=>{t.pending_buf[t.pending++]=e},mt=(t,e)=>{t.pending_buf[t.pending++]=e>>>8&255,t.pending_buf[t.pending++]=255&e},bt=(t,e,a,i)=>{let n=t.avail_in;return n>i&&(n=i),0===n?0:(t.avail_in-=n,e.set(t.input.subarray(t.next_in,t.next_in+n),a),1===t.state.wrap?t.adler=N(t.adler,e,n,a):2===t.state.wrap&&(t.adler=L(t.adler,e,n,a)),t.next_in+=n,t.total_in+=n,n)},gt=(t,e)=>{let a,i,n=t.max_chain_length,s=t.strstart,r=t.prev_length,o=t.nice_match;const l=t.strstart>t.w_size-262?t.strstart-(t.w_size-262):0,h=t.window,d=t.w_mask,_=t.prev,f=t.strstart+258;let c=h[s+r-1],u=h[s+r];t.prev_length>=t.good_match&&(n>>=2),o>t.lookahead&&(o=t.lookahead);do{if(a=e,h[a+r]===u&&h[a+r-1]===c&&h[a]===h[s]&&h[++a]===h[s+1]){s+=2,a++;do{}while(h[++s]===h[++a]&&h[++s]===h[++a]&&h[++s]===h[++a]&&h[++s]===h[++a]&&h[++s]===h[++a]&&h[++s]===h[++a]&&h[++s]===h[++a]&&h[++s]===h[++a]&&sr){if(t.match_start=e,r=i,i>=o)break;c=h[s+r-1],u=h[s+r]}}}while((e=_[e&d])>l&&0!=--n);return r<=t.lookahead?r:t.lookahead},pt=t=>{const e=t.w_size;let a,i,n;do{if(i=t.window_size-t.lookahead-t.strstart,t.strstart>=e+(e-262)&&(t.window.set(t.window.subarray(e,e+e-i),0),t.match_start-=e,t.strstart-=e,t.block_start-=e,t.insert>t.strstart&&(t.insert=t.strstart),_t(t),i+=e),0===t.strm.avail_in)break;if(a=bt(t.strm,t.window,t.strstart+t.lookahead,i),t.lookahead+=a,t.lookahead+t.insert>=3)for(n=t.strstart-t.insert,t.ins_h=t.window[n],t.ins_h=ft(t,t.ins_h,t.window[n+1]);t.insert&&(t.ins_h=ft(t,t.ins_h,t.window[n+3-1]),t.prev[n&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=n,n++,t.insert--,!(t.lookahead+t.insert<3)););}while(t.lookahead<262&&0!==t.strm.avail_in)},kt=(t,e)=>{let a,i,n,s=t.pending_buf_size-5>t.w_size?t.w_size:t.pending_buf_size-5,r=0,o=t.strm.avail_in;do{if(a=65535,n=t.bi_valid+42>>3,t.strm.avail_outi+t.strm.avail_in&&(a=i+t.strm.avail_in),a>n&&(a=n),a>8,t.pending_buf[t.pending-2]=~a,t.pending_buf[t.pending-1]=~a>>8,ct(t.strm),i&&(i>a&&(i=a),t.strm.output.set(t.window.subarray(t.block_start,t.block_start+i),t.strm.next_out),t.strm.next_out+=i,t.strm.avail_out-=i,t.strm.total_out+=i,t.block_start+=i,a-=i),a&&(bt(t.strm,t.strm.output,t.strm.next_out,a),t.strm.next_out+=a,t.strm.avail_out-=a,t.strm.total_out+=a)}while(0===r);return o-=t.strm.avail_in,o&&(o>=t.w_size?(t.matches=2,t.window.set(t.strm.input.subarray(t.strm.next_in-t.w_size,t.strm.next_in),0),t.strstart=t.w_size,t.insert=t.strstart):(t.window_size-t.strstart<=o&&(t.strstart-=t.w_size,t.window.set(t.window.subarray(t.w_size,t.w_size+t.strstart),0),t.matches<2&&t.matches++,t.insert>t.strstart&&(t.insert=t.strstart)),t.window.set(t.strm.input.subarray(t.strm.next_in-o,t.strm.next_in),t.strstart),t.strstart+=o,t.insert+=o>t.w_size-t.insert?t.w_size-t.insert:o),t.block_start=t.strstart),t.high_watern&&t.block_start>=t.w_size&&(t.block_start-=t.w_size,t.strstart-=t.w_size,t.window.set(t.window.subarray(t.w_size,t.w_size+t.strstart),0),t.matches<2&&t.matches++,n+=t.w_size,t.insert>t.strstart&&(t.insert=t.strstart)),n>t.strm.avail_in&&(n=t.strm.avail_in),n&&(bt(t.strm,t.window,t.strstart,n),t.strstart+=n,t.insert+=n>t.w_size-t.insert?t.w_size-t.insert:n),t.high_water>3,n=t.pending_buf_size-n>65535?65535:t.pending_buf_size-n,s=n>t.w_size?t.w_size:n,i=t.strstart-t.block_start,(i>=s||(i||e===X)&&e!==P&&0===t.strm.avail_in&&i<=n)&&(a=i>n?n:i,r=e===X&&0===t.strm.avail_in&&a===i?1:0,H(t,t.block_start,a,r),t.block_start+=a,ct(t.strm)),r?3:1)},vt=(t,e)=>{let a,i;for(;;){if(t.lookahead<262){if(pt(t),t.lookahead<262&&e===P)return 1;if(0===t.lookahead)break}if(a=0,t.lookahead>=3&&(t.ins_h=ft(t,t.ins_h,t.window[t.strstart+3-1]),a=t.prev[t.strstart&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=t.strstart),0!==a&&t.strstart-a<=t.w_size-262&&(t.match_length=gt(t,a)),t.match_length>=3)if(i=j(t,t.strstart-t.match_start,t.match_length-3),t.lookahead-=t.match_length,t.match_length<=t.max_lazy_match&&t.lookahead>=3){t.match_length--;do{t.strstart++,t.ins_h=ft(t,t.ins_h,t.window[t.strstart+3-1]),a=t.prev[t.strstart&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=t.strstart}while(0!=--t.match_length);t.strstart++}else t.strstart+=t.match_length,t.match_length=0,t.ins_h=t.window[t.strstart],t.ins_h=ft(t,t.ins_h,t.window[t.strstart+1]);else i=j(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++;if(i&&(ut(t,!1),0===t.strm.avail_out))return 1}return t.insert=t.strstart<2?t.strstart:2,e===X?(ut(t,!0),0===t.strm.avail_out?3:4):t.sym_next&&(ut(t,!1),0===t.strm.avail_out)?1:2},yt=(t,e)=>{let a,i,n;for(;;){if(t.lookahead<262){if(pt(t),t.lookahead<262&&e===P)return 1;if(0===t.lookahead)break}if(a=0,t.lookahead>=3&&(t.ins_h=ft(t,t.ins_h,t.window[t.strstart+3-1]),a=t.prev[t.strstart&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=t.strstart),t.prev_length=t.match_length,t.prev_match=t.match_start,t.match_length=2,0!==a&&t.prev_length4096)&&(t.match_length=2)),t.prev_length>=3&&t.match_length<=t.prev_length){n=t.strstart+t.lookahead-3,i=j(t,t.strstart-1-t.prev_match,t.prev_length-3),t.lookahead-=t.prev_length-1,t.prev_length-=2;do{++t.strstart<=n&&(t.ins_h=ft(t,t.ins_h,t.window[t.strstart+3-1]),a=t.prev[t.strstart&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=t.strstart)}while(0!=--t.prev_length);if(t.match_available=0,t.match_length=2,t.strstart++,i&&(ut(t,!1),0===t.strm.avail_out))return 1}else if(t.match_available){if(i=j(t,0,t.window[t.strstart-1]),i&&ut(t,!1),t.strstart++,t.lookahead--,0===t.strm.avail_out)return 1}else t.match_available=1,t.strstart++,t.lookahead--}return t.match_available&&(i=j(t,0,t.window[t.strstart-1]),t.match_available=0),t.insert=t.strstart<2?t.strstart:2,e===X?(ut(t,!0),0===t.strm.avail_out?3:4):t.sym_next&&(ut(t,!1),0===t.strm.avail_out)?1:2};function xt(t,e,a,i,n){this.good_length=t,this.max_lazy=e,this.nice_length=a,this.max_chain=i,this.func=n}const zt=[new xt(0,0,0,0,kt),new xt(4,4,8,4,vt),new xt(4,5,16,8,vt),new xt(4,6,32,32,vt),new xt(4,4,16,16,yt),new xt(8,16,32,32,yt),new xt(8,16,128,128,yt),new xt(8,32,128,256,yt),new xt(32,128,258,1024,yt),new xt(32,258,258,4096,yt)];function At(){this.strm=null,this.status=0,this.pending_buf=null,this.pending_buf_size=0,this.pending_out=0,this.pending=0,this.wrap=0,this.gzhead=null,this.gzindex=0,this.method=ot,this.last_flush=-1,this.w_size=0,this.w_bits=0,this.w_mask=0,this.window=null,this.window_size=0,this.prev=null,this.head=null,this.ins_h=0,this.hash_size=0,this.hash_bits=0,this.hash_mask=0,this.hash_shift=0,this.block_start=0,this.match_length=0,this.prev_match=0,this.match_available=0,this.strstart=0,this.match_start=0,this.lookahead=0,this.prev_length=0,this.max_chain_length=0,this.max_lazy_match=0,this.level=0,this.strategy=0,this.good_match=0,this.nice_match=0,this.dyn_ltree=new Uint16Array(1146),this.dyn_dtree=new Uint16Array(122),this.bl_tree=new Uint16Array(78),dt(this.dyn_ltree),dt(this.dyn_dtree),dt(this.bl_tree),this.l_desc=null,this.d_desc=null,this.bl_desc=null,this.bl_count=new Uint16Array(16),this.heap=new Uint16Array(573),dt(this.heap),this.heap_len=0,this.heap_max=0,this.depth=new Uint16Array(573),dt(this.depth),this.sym_buf=0,this.lit_bufsize=0,this.sym_next=0,this.sym_end=0,this.opt_len=0,this.static_len=0,this.matches=0,this.insert=0,this.bi_buf=0,this.bi_valid=0}const Et=t=>{if(!t)return 1;const e=t.state;return!e||e.strm!==t||42!==e.status&&57!==e.status&&69!==e.status&&73!==e.status&&91!==e.status&&103!==e.status&&113!==e.status&&666!==e.status?1:0},Rt=t=>{if(Et(t))return lt(t,Q);t.total_in=t.total_out=0,t.data_type=rt;const e=t.state;return e.pending=0,e.pending_out=0,e.wrap<0&&(e.wrap=-e.wrap),e.status=2===e.wrap?57:e.wrap?42:113,t.adler=2===e.wrap?0:1,e.last_flush=-2,C(e),W},Zt=t=>{const e=Rt(t);var a;return e===W&&((a=t.state).window_size=2*a.w_size,dt(a.head),a.max_lazy_match=zt[a.level].max_lazy,a.good_match=zt[a.level].good_length,a.nice_match=zt[a.level].nice_length,a.max_chain_length=zt[a.level].max_chain,a.strstart=0,a.block_start=0,a.lookahead=0,a.insert=0,a.match_length=a.prev_length=2,a.match_available=0,a.ins_h=0),e},St=(t,e,a,i,n,s)=>{if(!t)return Q;let r=1;if(e===tt&&(e=6),i<0?(r=0,i=-i):i>15&&(r=2,i-=16),n<1||n>9||a!==ot||i<8||i>15||e<0||e>9||s<0||s>nt||8===i&&1!==r)return lt(t,Q);8===i&&(i=9);const o=new At;return t.state=o,o.strm=t,o.status=42,o.wrap=r,o.gzhead=null,o.w_bits=i,o.w_size=1<St(t,e,ot,15,8,st),deflateInit2:St,deflateReset:Zt,deflateResetKeep:Rt,deflateSetHeader:(t,e)=>Et(t)||2!==t.state.wrap?Q:(t.state.gzhead=e,W),deflate:(t,e)=>{if(Et(t)||e>J||e<0)return t?lt(t,Q):Q;const a=t.state;if(!t.output||0!==t.avail_in&&!t.input||666===a.status&&e!==X)return lt(t,0===t.avail_out?$:Q);const i=a.last_flush;if(a.last_flush=e,0!==a.pending){if(ct(t),0===t.avail_out)return a.last_flush=-1,W}else if(0===t.avail_in&&ht(e)<=ht(i)&&e!==X)return lt(t,$);if(666===a.status&&0!==t.avail_in)return lt(t,$);if(42===a.status&&0===a.wrap&&(a.status=113),42===a.status){let e=ot+(a.w_bits-8<<4)<<8,i=-1;if(i=a.strategy>=at||a.level<2?0:a.level<6?1:6===a.level?2:3,e|=i<<6,0!==a.strstart&&(e|=32),e+=31-e%31,mt(a,e),0!==a.strstart&&(mt(a,t.adler>>>16),mt(a,65535&t.adler)),t.adler=1,a.status=113,ct(t),0!==a.pending)return a.last_flush=-1,W}if(57===a.status)if(t.adler=0,wt(a,31),wt(a,139),wt(a,8),a.gzhead)wt(a,(a.gzhead.text?1:0)+(a.gzhead.hcrc?2:0)+(a.gzhead.extra?4:0)+(a.gzhead.name?8:0)+(a.gzhead.comment?16:0)),wt(a,255&a.gzhead.time),wt(a,a.gzhead.time>>8&255),wt(a,a.gzhead.time>>16&255),wt(a,a.gzhead.time>>24&255),wt(a,9===a.level?2:a.strategy>=at||a.level<2?4:0),wt(a,255&a.gzhead.os),a.gzhead.extra&&a.gzhead.extra.length&&(wt(a,255&a.gzhead.extra.length),wt(a,a.gzhead.extra.length>>8&255)),a.gzhead.hcrc&&(t.adler=L(t.adler,a.pending_buf,a.pending,0)),a.gzindex=0,a.status=69;else if(wt(a,0),wt(a,0),wt(a,0),wt(a,0),wt(a,0),wt(a,9===a.level?2:a.strategy>=at||a.level<2?4:0),wt(a,3),a.status=113,ct(t),0!==a.pending)return a.last_flush=-1,W;if(69===a.status){if(a.gzhead.extra){let e=a.pending,i=(65535&a.gzhead.extra.length)-a.gzindex;for(;a.pending+i>a.pending_buf_size;){let n=a.pending_buf_size-a.pending;if(a.pending_buf.set(a.gzhead.extra.subarray(a.gzindex,a.gzindex+n),a.pending),a.pending=a.pending_buf_size,a.gzhead.hcrc&&a.pending>e&&(t.adler=L(t.adler,a.pending_buf,a.pending-e,e)),a.gzindex+=n,ct(t),0!==a.pending)return a.last_flush=-1,W;e=0,i-=n}let n=new Uint8Array(a.gzhead.extra);a.pending_buf.set(n.subarray(a.gzindex,a.gzindex+i),a.pending),a.pending+=i,a.gzhead.hcrc&&a.pending>e&&(t.adler=L(t.adler,a.pending_buf,a.pending-e,e)),a.gzindex=0}a.status=73}if(73===a.status){if(a.gzhead.name){let e,i=a.pending;do{if(a.pending===a.pending_buf_size){if(a.gzhead.hcrc&&a.pending>i&&(t.adler=L(t.adler,a.pending_buf,a.pending-i,i)),ct(t),0!==a.pending)return a.last_flush=-1,W;i=0}e=a.gzindexi&&(t.adler=L(t.adler,a.pending_buf,a.pending-i,i)),a.gzindex=0}a.status=91}if(91===a.status){if(a.gzhead.comment){let e,i=a.pending;do{if(a.pending===a.pending_buf_size){if(a.gzhead.hcrc&&a.pending>i&&(t.adler=L(t.adler,a.pending_buf,a.pending-i,i)),ct(t),0!==a.pending)return a.last_flush=-1,W;i=0}e=a.gzindexi&&(t.adler=L(t.adler,a.pending_buf,a.pending-i,i))}a.status=103}if(103===a.status){if(a.gzhead.hcrc){if(a.pending+2>a.pending_buf_size&&(ct(t),0!==a.pending))return a.last_flush=-1,W;wt(a,255&t.adler),wt(a,t.adler>>8&255),t.adler=0}if(a.status=113,ct(t),0!==a.pending)return a.last_flush=-1,W}if(0!==t.avail_in||0!==a.lookahead||e!==P&&666!==a.status){let i=0===a.level?kt(a,e):a.strategy===at?((t,e)=>{let a;for(;;){if(0===t.lookahead&&(pt(t),0===t.lookahead)){if(e===P)return 1;break}if(t.match_length=0,a=j(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++,a&&(ut(t,!1),0===t.strm.avail_out))return 1}return t.insert=0,e===X?(ut(t,!0),0===t.strm.avail_out?3:4):t.sym_next&&(ut(t,!1),0===t.strm.avail_out)?1:2})(a,e):a.strategy===it?((t,e)=>{let a,i,n,s;const r=t.window;for(;;){if(t.lookahead<=258){if(pt(t),t.lookahead<=258&&e===P)return 1;if(0===t.lookahead)break}if(t.match_length=0,t.lookahead>=3&&t.strstart>0&&(n=t.strstart-1,i=r[n],i===r[++n]&&i===r[++n]&&i===r[++n])){s=t.strstart+258;do{}while(i===r[++n]&&i===r[++n]&&i===r[++n]&&i===r[++n]&&i===r[++n]&&i===r[++n]&&i===r[++n]&&i===r[++n]&&nt.lookahead&&(t.match_length=t.lookahead)}if(t.match_length>=3?(a=j(t,1,t.match_length-3),t.lookahead-=t.match_length,t.strstart+=t.match_length,t.match_length=0):(a=j(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++),a&&(ut(t,!1),0===t.strm.avail_out))return 1}return t.insert=0,e===X?(ut(t,!0),0===t.strm.avail_out?3:4):t.sym_next&&(ut(t,!1),0===t.strm.avail_out)?1:2})(a,e):zt[a.level].func(a,e);if(3!==i&&4!==i||(a.status=666),1===i||3===i)return 0===t.avail_out&&(a.last_flush=-1),W;if(2===i&&(e===Y?K(a):e!==J&&(H(a,0,0,!1),e===G&&(dt(a.head),0===a.lookahead&&(a.strstart=0,a.block_start=0,a.insert=0))),ct(t),0===t.avail_out))return a.last_flush=-1,W}return e!==X?W:a.wrap<=0?q:(2===a.wrap?(wt(a,255&t.adler),wt(a,t.adler>>8&255),wt(a,t.adler>>16&255),wt(a,t.adler>>24&255),wt(a,255&t.total_in),wt(a,t.total_in>>8&255),wt(a,t.total_in>>16&255),wt(a,t.total_in>>24&255)):(mt(a,t.adler>>>16),mt(a,65535&t.adler)),ct(t),a.wrap>0&&(a.wrap=-a.wrap),0!==a.pending?W:q)},deflateEnd:t=>{if(Et(t))return Q;const e=t.state.status;return t.state=null,113===e?lt(t,V):W},deflateSetDictionary:(t,e)=>{let a=e.length;if(Et(t))return Q;const i=t.state,n=i.wrap;if(2===n||1===n&&42!==i.status||i.lookahead)return Q;if(1===n&&(t.adler=N(t.adler,e,a,0)),i.wrap=0,a>=i.w_size){0===n&&(dt(i.head),i.strstart=0,i.block_start=0,i.insert=0);let t=new Uint8Array(i.w_size);t.set(e.subarray(a-i.w_size,a),0),e=t,a=i.w_size}const s=t.avail_in,r=t.next_in,o=t.input;for(t.avail_in=a,t.next_in=0,t.input=e,pt(i);i.lookahead>=3;){let t=i.strstart,e=i.lookahead-2;do{i.ins_h=ft(i,i.ins_h,i.window[t+3-1]),i.prev[t&i.w_mask]=i.head[i.ins_h],i.head[i.ins_h]=t,t++}while(--e);i.strstart=t,i.lookahead=2,pt(i)}return i.strstart+=i.lookahead,i.block_start=i.strstart,i.insert=i.lookahead,i.lookahead=0,i.match_length=i.prev_length=2,i.match_available=0,t.next_in=r,t.input=o,t.avail_in=s,i.wrap=n,W},deflateInfo:"pako deflate (from Nodeca project)"};const Dt=(t,e)=>Object.prototype.hasOwnProperty.call(t,e);var Ot=function(t){const e=Array.prototype.slice.call(arguments,1);for(;e.length;){const a=e.shift();if(a){if("object"!=typeof a)throw new TypeError(a+"must be non-object");for(const e in a)Dt(a,e)&&(t[e]=a[e])}}return t},Tt=t=>{let e=0;for(let a=0,i=t.length;a=252?6:t>=248?5:t>=240?4:t>=224?3:t>=192?2:1;Ft[254]=Ft[254]=1;var Lt=t=>{if("function"==typeof TextEncoder&&TextEncoder.prototype.encode)return(new TextEncoder).encode(t);let e,a,i,n,s,r=t.length,o=0;for(n=0;n>>6,e[s++]=128|63&a):a<65536?(e[s++]=224|a>>>12,e[s++]=128|a>>>6&63,e[s++]=128|63&a):(e[s++]=240|a>>>18,e[s++]=128|a>>>12&63,e[s++]=128|a>>>6&63,e[s++]=128|63&a);return e},It=(t,e)=>{const a=e||t.length;if("function"==typeof TextDecoder&&TextDecoder.prototype.decode)return(new TextDecoder).decode(t.subarray(0,e));let i,n;const s=new Array(2*a);for(n=0,i=0;i4)s[n++]=65533,i+=r-1;else{for(e&=2===r?31:3===r?15:7;r>1&&i1?s[n++]=65533:e<65536?s[n++]=e:(e-=65536,s[n++]=55296|e>>10&1023,s[n++]=56320|1023&e)}}return((t,e)=>{if(e<65534&&t.subarray&&Nt)return String.fromCharCode.apply(null,t.length===e?t:t.subarray(0,e));let a="";for(let i=0;i{(e=e||t.length)>t.length&&(e=t.length);let a=e-1;for(;a>=0&&128==(192&t[a]);)a--;return a<0||0===a?e:a+Ft[t[a]]>e?a:e};var Ct=function(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0};const Ht=Object.prototype.toString,{Z_NO_FLUSH:Mt,Z_SYNC_FLUSH:jt,Z_FULL_FLUSH:Kt,Z_FINISH:Pt,Z_OK:Yt,Z_STREAM_END:Gt,Z_DEFAULT_COMPRESSION:Xt,Z_DEFAULT_STRATEGY:Jt,Z_DEFLATED:Wt}=B;function qt(t){this.options=Ot({level:Xt,method:Wt,chunkSize:16384,windowBits:15,memLevel:8,strategy:Jt},t||{});let e=this.options;e.raw&&e.windowBits>0?e.windowBits=-e.windowBits:e.gzip&&e.windowBits>0&&e.windowBits<16&&(e.windowBits+=16),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new Ct,this.strm.avail_out=0;let a=Ut.deflateInit2(this.strm,e.level,e.method,e.windowBits,e.memLevel,e.strategy);if(a!==Yt)throw new Error(I[a]);if(e.header&&Ut.deflateSetHeader(this.strm,e.header),e.dictionary){let t;if(t="string"==typeof e.dictionary?Lt(e.dictionary):"[object ArrayBuffer]"===Ht.call(e.dictionary)?new Uint8Array(e.dictionary):e.dictionary,a=Ut.deflateSetDictionary(this.strm,t),a!==Yt)throw new Error(I[a]);this._dict_set=!0}}function Qt(t,e){const a=new qt(e);if(a.push(t,!0),a.err)throw a.msg||I[a.err];return a.result}qt.prototype.push=function(t,e){const a=this.strm,i=this.options.chunkSize;let n,s;if(this.ended)return!1;for(s=e===~~e?e:!0===e?Pt:Mt,"string"==typeof t?a.input=Lt(t):"[object ArrayBuffer]"===Ht.call(t)?a.input=new Uint8Array(t):a.input=t,a.next_in=0,a.avail_in=a.input.length;;)if(0===a.avail_out&&(a.output=new Uint8Array(i),a.next_out=0,a.avail_out=i),(s===jt||s===Kt)&&a.avail_out<=6)this.onData(a.output.subarray(0,a.next_out)),a.avail_out=0;else{if(n=Ut.deflate(a,s),n===Gt)return a.next_out>0&&this.onData(a.output.subarray(0,a.next_out)),n=Ut.deflateEnd(this.strm),this.onEnd(n),this.ended=!0,n===Yt;if(0!==a.avail_out){if(s>0&&a.next_out>0)this.onData(a.output.subarray(0,a.next_out)),a.avail_out=0;else if(0===a.avail_in)break}else this.onData(a.output)}return!0},qt.prototype.onData=function(t){this.chunks.push(t)},qt.prototype.onEnd=function(t){t===Yt&&(this.result=Tt(this.chunks)),this.chunks=[],this.err=t,this.msg=this.strm.msg};var Vt={Deflate:qt,deflate:Qt,deflateRaw:function(t,e){return(e=e||{}).raw=!0,Qt(t,e)},gzip:function(t,e){return(e=e||{}).gzip=!0,Qt(t,e)},constants:B};var $t=function(t,e){let a,i,n,s,r,o,l,h,d,_,f,c,u,w,m,b,g,p,k,v,y,x,z,A;const E=t.state;a=t.next_in,z=t.input,i=a+(t.avail_in-5),n=t.next_out,A=t.output,s=n-(e-t.avail_out),r=n+(t.avail_out-257),o=E.dmax,l=E.wsize,h=E.whave,d=E.wnext,_=E.window,f=E.hold,c=E.bits,u=E.lencode,w=E.distcode,m=(1<>>24,f>>>=p,c-=p,p=g>>>16&255,0===p)A[n++]=65535&g;else{if(!(16&p)){if(0==(64&p)){g=u[(65535&g)+(f&(1<>>=p,c-=p),c<15&&(f+=z[a++]<>>24,f>>>=p,c-=p,p=g>>>16&255,!(16&p)){if(0==(64&p)){g=w[(65535&g)+(f&(1<o){t.msg="invalid distance too far back",E.mode=16209;break t}if(f>>>=p,c-=p,p=n-s,v>p){if(p=v-p,p>h&&E.sane){t.msg="invalid distance too far back",E.mode=16209;break t}if(y=0,x=_,0===d){if(y+=l-p,p2;)A[n++]=x[y++],A[n++]=x[y++],A[n++]=x[y++],k-=3;k&&(A[n++]=x[y++],k>1&&(A[n++]=x[y++]))}else{y=n-v;do{A[n++]=A[y++],A[n++]=A[y++],A[n++]=A[y++],k-=3}while(k>2);k&&(A[n++]=A[y++],k>1&&(A[n++]=A[y++]))}break}}break}}while(a>3,a-=k,c-=k<<3,f&=(1<{const l=o.bits;let h,d,_,f,c,u,w=0,m=0,b=0,g=0,p=0,k=0,v=0,y=0,x=0,z=0,A=null;const E=new Uint16Array(16),R=new Uint16Array(16);let Z,S,U,D=null;for(w=0;w<=15;w++)E[w]=0;for(m=0;m=1&&0===E[g];g--);if(p>g&&(p=g),0===g)return n[s++]=20971520,n[s++]=20971520,o.bits=1,0;for(b=1;b0&&(0===t||1!==g))return-1;for(R[1]=0,w=1;w<15;w++)R[w+1]=R[w]+E[w];for(m=0;m852||2===t&&x>592)return 1;for(;;){Z=w-v,r[m]+1=u?(S=D[r[m]-u],U=A[r[m]-u]):(S=96,U=0),h=1<>v)+d]=Z<<24|S<<16|U|0}while(0!==d);for(h=1<>=1;if(0!==h?(z&=h-1,z+=h):z=0,m++,0==--E[w]){if(w===g)break;w=e[a+r[m]]}if(w>p&&(z&f)!==_){for(0===v&&(v=p),c+=b,k=w-v,y=1<852||2===t&&x>592)return 1;_=z&f,n[_]=p<<24|k<<16|c-s|0}}return 0!==z&&(n[c+z]=w-v<<24|64<<16|0),o.bits=p,0};const{Z_FINISH:se,Z_BLOCK:re,Z_TREES:oe,Z_OK:le,Z_STREAM_END:he,Z_NEED_DICT:de,Z_STREAM_ERROR:_e,Z_DATA_ERROR:fe,Z_MEM_ERROR:ce,Z_BUF_ERROR:ue,Z_DEFLATED:we}=B,me=16209,be=t=>(t>>>24&255)+(t>>>8&65280)+((65280&t)<<8)+((255&t)<<24);function ge(){this.strm=null,this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new Uint16Array(320),this.work=new Uint16Array(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0}const pe=t=>{if(!t)return 1;const e=t.state;return!e||e.strm!==t||e.mode<16180||e.mode>16211?1:0},ke=t=>{if(pe(t))return _e;const e=t.state;return t.total_in=t.total_out=e.total=0,t.msg="",e.wrap&&(t.adler=1&e.wrap),e.mode=16180,e.last=0,e.havedict=0,e.flags=-1,e.dmax=32768,e.head=null,e.hold=0,e.bits=0,e.lencode=e.lendyn=new Int32Array(852),e.distcode=e.distdyn=new Int32Array(592),e.sane=1,e.back=-1,le},ve=t=>{if(pe(t))return _e;const e=t.state;return e.wsize=0,e.whave=0,e.wnext=0,ke(t)},ye=(t,e)=>{let a;if(pe(t))return _e;const i=t.state;return e<0?(a=0,e=-e):(a=5+(e>>4),e<48&&(e&=15)),e&&(e<8||e>15)?_e:(null!==i.window&&i.wbits!==e&&(i.window=null),i.wrap=a,i.wbits=e,ve(t))},xe=(t,e)=>{if(!t)return _e;const a=new ge;t.state=a,a.strm=t,a.window=null,a.mode=16180;const i=ye(t,e);return i!==le&&(t.state=null),i};let ze,Ae,Ee=!0;const Re=t=>{if(Ee){ze=new Int32Array(512),Ae=new Int32Array(32);let e=0;for(;e<144;)t.lens[e++]=8;for(;e<256;)t.lens[e++]=9;for(;e<280;)t.lens[e++]=7;for(;e<288;)t.lens[e++]=8;for(ne(1,t.lens,0,288,ze,0,t.work,{bits:9}),e=0;e<32;)t.lens[e++]=5;ne(2,t.lens,0,32,Ae,0,t.work,{bits:5}),Ee=!1}t.lencode=ze,t.lenbits=9,t.distcode=Ae,t.distbits=5},Ze=(t,e,a,i)=>{let n;const s=t.state;return null===s.window&&(s.wsize=1<=s.wsize?(s.window.set(e.subarray(a-s.wsize,a),0),s.wnext=0,s.whave=s.wsize):(n=s.wsize-s.wnext,n>i&&(n=i),s.window.set(e.subarray(a-i,a-i+n),s.wnext),(i-=n)?(s.window.set(e.subarray(a-i,a),0),s.wnext=i,s.whave=s.wsize):(s.wnext+=n,s.wnext===s.wsize&&(s.wnext=0),s.whavexe(t,15),inflateInit2:xe,inflate:(t,e)=>{let a,i,n,s,r,o,l,h,d,_,f,c,u,w,m,b,g,p,k,v,y,x,z=0;const A=new Uint8Array(4);let E,R;const Z=new Uint8Array([16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]);if(pe(t)||!t.output||!t.input&&0!==t.avail_in)return _e;a=t.state,16191===a.mode&&(a.mode=16192),r=t.next_out,n=t.output,l=t.avail_out,s=t.next_in,i=t.input,o=t.avail_in,h=a.hold,d=a.bits,_=o,f=l,x=le;t:for(;;)switch(a.mode){case 16180:if(0===a.wrap){a.mode=16192;break}for(;d<16;){if(0===o)break t;o--,h+=i[s++]<>>8&255,a.check=L(a.check,A,2,0),h=0,d=0,a.mode=16181;break}if(a.head&&(a.head.done=!1),!(1&a.wrap)||(((255&h)<<8)+(h>>8))%31){t.msg="incorrect header check",a.mode=me;break}if((15&h)!==we){t.msg="unknown compression method",a.mode=me;break}if(h>>>=4,d-=4,y=8+(15&h),0===a.wbits&&(a.wbits=y),y>15||y>a.wbits){t.msg="invalid window size",a.mode=me;break}a.dmax=1<>8&1),512&a.flags&&4&a.wrap&&(A[0]=255&h,A[1]=h>>>8&255,a.check=L(a.check,A,2,0)),h=0,d=0,a.mode=16182;case 16182:for(;d<32;){if(0===o)break t;o--,h+=i[s++]<>>8&255,A[2]=h>>>16&255,A[3]=h>>>24&255,a.check=L(a.check,A,4,0)),h=0,d=0,a.mode=16183;case 16183:for(;d<16;){if(0===o)break t;o--,h+=i[s++]<>8),512&a.flags&&4&a.wrap&&(A[0]=255&h,A[1]=h>>>8&255,a.check=L(a.check,A,2,0)),h=0,d=0,a.mode=16184;case 16184:if(1024&a.flags){for(;d<16;){if(0===o)break t;o--,h+=i[s++]<>>8&255,a.check=L(a.check,A,2,0)),h=0,d=0}else a.head&&(a.head.extra=null);a.mode=16185;case 16185:if(1024&a.flags&&(c=a.length,c>o&&(c=o),c&&(a.head&&(y=a.head.extra_len-a.length,a.head.extra||(a.head.extra=new Uint8Array(a.head.extra_len)),a.head.extra.set(i.subarray(s,s+c),y)),512&a.flags&&4&a.wrap&&(a.check=L(a.check,i,c,s)),o-=c,s+=c,a.length-=c),a.length))break t;a.length=0,a.mode=16186;case 16186:if(2048&a.flags){if(0===o)break t;c=0;do{y=i[s+c++],a.head&&y&&a.length<65536&&(a.head.name+=String.fromCharCode(y))}while(y&&c>9&1,a.head.done=!0),t.adler=a.check=0,a.mode=16191;break;case 16189:for(;d<32;){if(0===o)break t;o--,h+=i[s++]<>>=7&d,d-=7&d,a.mode=16206;break}for(;d<3;){if(0===o)break t;o--,h+=i[s++]<>>=1,d-=1,3&h){case 0:a.mode=16193;break;case 1:if(Re(a),a.mode=16199,e===oe){h>>>=2,d-=2;break t}break;case 2:a.mode=16196;break;case 3:t.msg="invalid block type",a.mode=me}h>>>=2,d-=2;break;case 16193:for(h>>>=7&d,d-=7&d;d<32;){if(0===o)break t;o--,h+=i[s++]<>>16^65535)){t.msg="invalid stored block lengths",a.mode=me;break}if(a.length=65535&h,h=0,d=0,a.mode=16194,e===oe)break t;case 16194:a.mode=16195;case 16195:if(c=a.length,c){if(c>o&&(c=o),c>l&&(c=l),0===c)break t;n.set(i.subarray(s,s+c),r),o-=c,s+=c,l-=c,r+=c,a.length-=c;break}a.mode=16191;break;case 16196:for(;d<14;){if(0===o)break t;o--,h+=i[s++]<>>=5,d-=5,a.ndist=1+(31&h),h>>>=5,d-=5,a.ncode=4+(15&h),h>>>=4,d-=4,a.nlen>286||a.ndist>30){t.msg="too many length or distance symbols",a.mode=me;break}a.have=0,a.mode=16197;case 16197:for(;a.have>>=3,d-=3}for(;a.have<19;)a.lens[Z[a.have++]]=0;if(a.lencode=a.lendyn,a.lenbits=7,E={bits:a.lenbits},x=ne(0,a.lens,0,19,a.lencode,0,a.work,E),a.lenbits=E.bits,x){t.msg="invalid code lengths set",a.mode=me;break}a.have=0,a.mode=16198;case 16198:for(;a.have>>24,b=z>>>16&255,g=65535&z,!(m<=d);){if(0===o)break t;o--,h+=i[s++]<>>=m,d-=m,a.lens[a.have++]=g;else{if(16===g){for(R=m+2;d>>=m,d-=m,0===a.have){t.msg="invalid bit length repeat",a.mode=me;break}y=a.lens[a.have-1],c=3+(3&h),h>>>=2,d-=2}else if(17===g){for(R=m+3;d>>=m,d-=m,y=0,c=3+(7&h),h>>>=3,d-=3}else{for(R=m+7;d>>=m,d-=m,y=0,c=11+(127&h),h>>>=7,d-=7}if(a.have+c>a.nlen+a.ndist){t.msg="invalid bit length repeat",a.mode=me;break}for(;c--;)a.lens[a.have++]=y}}if(a.mode===me)break;if(0===a.lens[256]){t.msg="invalid code -- missing end-of-block",a.mode=me;break}if(a.lenbits=9,E={bits:a.lenbits},x=ne(1,a.lens,0,a.nlen,a.lencode,0,a.work,E),a.lenbits=E.bits,x){t.msg="invalid literal/lengths set",a.mode=me;break}if(a.distbits=6,a.distcode=a.distdyn,E={bits:a.distbits},x=ne(2,a.lens,a.nlen,a.ndist,a.distcode,0,a.work,E),a.distbits=E.bits,x){t.msg="invalid distances set",a.mode=me;break}if(a.mode=16199,e===oe)break t;case 16199:a.mode=16200;case 16200:if(o>=6&&l>=258){t.next_out=r,t.avail_out=l,t.next_in=s,t.avail_in=o,a.hold=h,a.bits=d,$t(t,f),r=t.next_out,n=t.output,l=t.avail_out,s=t.next_in,i=t.input,o=t.avail_in,h=a.hold,d=a.bits,16191===a.mode&&(a.back=-1);break}for(a.back=0;z=a.lencode[h&(1<>>24,b=z>>>16&255,g=65535&z,!(m<=d);){if(0===o)break t;o--,h+=i[s++]<>p)],m=z>>>24,b=z>>>16&255,g=65535&z,!(p+m<=d);){if(0===o)break t;o--,h+=i[s++]<>>=p,d-=p,a.back+=p}if(h>>>=m,d-=m,a.back+=m,a.length=g,0===b){a.mode=16205;break}if(32&b){a.back=-1,a.mode=16191;break}if(64&b){t.msg="invalid literal/length code",a.mode=me;break}a.extra=15&b,a.mode=16201;case 16201:if(a.extra){for(R=a.extra;d>>=a.extra,d-=a.extra,a.back+=a.extra}a.was=a.length,a.mode=16202;case 16202:for(;z=a.distcode[h&(1<>>24,b=z>>>16&255,g=65535&z,!(m<=d);){if(0===o)break t;o--,h+=i[s++]<>p)],m=z>>>24,b=z>>>16&255,g=65535&z,!(p+m<=d);){if(0===o)break t;o--,h+=i[s++]<>>=p,d-=p,a.back+=p}if(h>>>=m,d-=m,a.back+=m,64&b){t.msg="invalid distance code",a.mode=me;break}a.offset=g,a.extra=15&b,a.mode=16203;case 16203:if(a.extra){for(R=a.extra;d>>=a.extra,d-=a.extra,a.back+=a.extra}if(a.offset>a.dmax){t.msg="invalid distance too far back",a.mode=me;break}a.mode=16204;case 16204:if(0===l)break t;if(c=f-l,a.offset>c){if(c=a.offset-c,c>a.whave&&a.sane){t.msg="invalid distance too far back",a.mode=me;break}c>a.wnext?(c-=a.wnext,u=a.wsize-c):u=a.wnext-c,c>a.length&&(c=a.length),w=a.window}else w=n,u=r-a.offset,c=a.length;c>l&&(c=l),l-=c,a.length-=c;do{n[r++]=w[u++]}while(--c);0===a.length&&(a.mode=16200);break;case 16205:if(0===l)break t;n[r++]=a.length,l--,a.mode=16200;break;case 16206:if(a.wrap){for(;d<32;){if(0===o)break t;o--,h|=i[s++]<{if(pe(t))return _e;let e=t.state;return e.window&&(e.window=null),t.state=null,le},inflateGetHeader:(t,e)=>{if(pe(t))return _e;const a=t.state;return 0==(2&a.wrap)?_e:(a.head=e,e.done=!1,le)},inflateSetDictionary:(t,e)=>{const a=e.length;let i,n,s;return pe(t)?_e:(i=t.state,0!==i.wrap&&16190!==i.mode?_e:16190===i.mode&&(n=1,n=N(n,e,a,0),n!==i.check)?fe:(s=Ze(t,e,a,a),s?(i.mode=16210,ce):(i.havedict=1,le)))},inflateInfo:"pako inflate (from Nodeca project)"};var Ue=function(){this.text=0,this.time=0,this.xflags=0,this.os=0,this.extra=null,this.extra_len=0,this.name="",this.comment="",this.hcrc=0,this.done=!1};const De=Object.prototype.toString,{Z_NO_FLUSH:Oe,Z_FINISH:Te,Z_OK:Ne,Z_STREAM_END:Fe,Z_NEED_DICT:Le,Z_STREAM_ERROR:Ie,Z_DATA_ERROR:Be,Z_MEM_ERROR:Ce}=B;function He(t){this.options=Ot({chunkSize:65536,windowBits:15,to:""},t||{});const e=this.options;e.raw&&e.windowBits>=0&&e.windowBits<16&&(e.windowBits=-e.windowBits,0===e.windowBits&&(e.windowBits=-15)),!(e.windowBits>=0&&e.windowBits<16)||t&&t.windowBits||(e.windowBits+=32),e.windowBits>15&&e.windowBits<48&&0==(15&e.windowBits)&&(e.windowBits|=15),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new Ct,this.strm.avail_out=0;let a=Se.inflateInit2(this.strm,e.windowBits);if(a!==Ne)throw new Error(I[a]);if(this.header=new Ue,Se.inflateGetHeader(this.strm,this.header),e.dictionary&&("string"==typeof e.dictionary?e.dictionary=Lt(e.dictionary):"[object ArrayBuffer]"===De.call(e.dictionary)&&(e.dictionary=new Uint8Array(e.dictionary)),e.raw&&(a=Se.inflateSetDictionary(this.strm,e.dictionary),a!==Ne)))throw new Error(I[a])}He.prototype.push=function(t,e){const a=this.strm,i=this.options.chunkSize,n=this.options.dictionary;let s,r,o;if(this.ended)return!1;for(r=e===~~e?e:!0===e?Te:Oe,"[object ArrayBuffer]"===De.call(t)?a.input=new Uint8Array(t):a.input=t,a.next_in=0,a.avail_in=a.input.length;;){for(0===a.avail_out&&(a.output=new Uint8Array(i),a.next_out=0,a.avail_out=i),s=Se.inflate(a,r),s===Le&&n&&(s=Se.inflateSetDictionary(a,n),s===Ne?s=Se.inflate(a,r):s===Be&&(s=Le));a.avail_in>0&&s===Fe&&a.state.wrap>0&&0!==t[a.next_in];)Se.inflateReset(a),s=Se.inflate(a,r);switch(s){case Ie:case Be:case Le:case Ce:return this.onEnd(s),this.ended=!0,!1}if(o=a.avail_out,a.next_out&&(0===a.avail_out||s===Fe))if("string"===this.options.to){let t=Bt(a.output,a.next_out),e=a.next_out-t,n=It(a.output,t);a.next_out=e,a.avail_out=i-e,e&&a.output.set(a.output.subarray(t,t+e),0),this.onData(n)}else this.onData(a.output.length===a.next_out?a.output:a.output.subarray(0,a.next_out));if(s!==Ne||0!==o){if(s===Fe)return s=Se.inflateEnd(this.strm),this.onEnd(s),this.ended=!0,!0;if(0===a.avail_in)break}}return!0},He.prototype.onData=function(t){this.chunks.push(t)},He.prototype.onEnd=function(t){t===Ne&&("string"===this.options.to?this.result=this.chunks.join(""):this.result=Tt(this.chunks)),this.chunks=[],this.err=t,this.msg=this.strm.msg};const{Deflate:Me,deflate:je,deflateRaw:Ke,gzip:Pe}=Vt;var Ye=Me,Ge=B;const Xe=new class{constructor(){this.added=0,this.init()}init(){this.added=0,this.deflate=new Ye,this.deflate.push("[",Ge.Z_NO_FLUSH)}addEvent(t){if(!t)return!1;const e=this.added>0?",":"";return this.deflate.push(e+JSON.stringify(t),Ge.Z_SYNC_FLUSH),this.added++,!0}finish(){if(this.deflate.push("]",Ge.Z_FINISH),this.deflate.err)throw this.deflate.err;const t=this.deflate.result;return this.init(),t}},Je={init:()=>(Xe.init(),""),addEvent:t=>Xe.addEvent(t),finish:()=>Xe.finish()};addEventListener("message",(function(t){const e=t.data.method,a=t.data.id,[i]=t.data.args?JSON.parse(t.data.args):[];if(e in Je&&"function"==typeof Je[e])try{const t=Je[e](i);postMessage({id:a,method:e,success:!0,response:t})}catch(t){postMessage({id:a,method:e,success:!1,response:t}),console.error(t)}}));`; diff --git a/packages/replay/test/integration/flush.test.ts b/packages/replay/test/integration/flush.test.ts index 8865aee30705..7e9277b9d47a 100644 --- a/packages/replay/test/integration/flush.test.ts +++ b/packages/replay/test/integration/flush.test.ts @@ -182,37 +182,39 @@ describe('Integration | flush', () => { }); // Add this to test that segment ID increases - mockAddPerformanceEntries.mockImplementationOnce(() => { - createPerformanceSpans( - replay, - createPerformanceEntries([ - { - name: 'https://sentry.io/foo.js', - entryType: 'resource', - startTime: 176.59999990463257, - duration: 5.600000023841858, - initiatorType: 'link', - nextHopProtocol: 'h2', - workerStart: 177.5, - redirectStart: 0, - redirectEnd: 0, - fetchStart: 177.69999992847443, - domainLookupStart: 177.69999992847443, - domainLookupEnd: 177.69999992847443, - connectStart: 177.69999992847443, - connectEnd: 177.69999992847443, - secureConnectionStart: 177.69999992847443, - requestStart: 177.5, - responseStart: 181, - responseEnd: 182.19999992847443, - transferSize: 0, - encodedBodySize: 0, - decodedBodySize: 0, - serverTiming: [], - } as unknown as PerformanceResourceTiming, - ]), - ); - }); + mockAddPerformanceEntries.mockImplementationOnce(() => + Promise.all( + createPerformanceSpans( + replay, + createPerformanceEntries([ + { + name: 'https://sentry.io/foo.js', + entryType: 'resource', + startTime: 176.59999990463257, + duration: 5.600000023841858, + initiatorType: 'link', + nextHopProtocol: 'h2', + workerStart: 177.5, + redirectStart: 0, + redirectEnd: 0, + fetchStart: 177.69999992847443, + domainLookupStart: 177.69999992847443, + domainLookupEnd: 177.69999992847443, + connectStart: 177.69999992847443, + connectEnd: 177.69999992847443, + secureConnectionStart: 177.69999992847443, + requestStart: 177.5, + responseStart: 181, + responseEnd: 182.19999992847443, + transferSize: 0, + encodedBodySize: 0, + decodedBodySize: 0, + serverTiming: [], + } as unknown as PerformanceResourceTiming, + ]), + ), + ), + ); // flush #5 @ t=25s - debounced flush calls `flush` // 20s + `flushMinDelay` which is 5 seconds await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); diff --git a/packages/replay/test/unit/eventBuffer.test.ts b/packages/replay/test/unit/eventBuffer.test.ts index a4a319da68bd..71006ca90e53 100644 --- a/packages/replay/test/unit/eventBuffer.test.ts +++ b/packages/replay/test/unit/eventBuffer.test.ts @@ -53,6 +53,8 @@ describe('Unit | eventBuffer', () => { }) as EventBufferCompressionWorker; buffer.addEvent(TEST_EVENT); + // @ts-ignore make sure it handles invalid data + buffer.addEvent(undefined); buffer.addEvent(TEST_EVENT); const result = await buffer.finish(); diff --git a/packages/replay/test/unit/worker/Compressor.test.ts b/packages/replay/test/unit/worker/Compressor.test.ts index afafa52da8cb..e8de4bd2f94a 100644 --- a/packages/replay/test/unit/worker/Compressor.test.ts +++ b/packages/replay/test/unit/worker/Compressor.test.ts @@ -26,28 +26,16 @@ describe('Unit | worker | Compressor', () => { expect(restored).toBe(JSON.stringify(events)); }); - it('ignores undefined events', () => { + it('throws on invalid/undefined events', () => { const compressor = new Compressor(); - const events = [ - { - id: 1, - foo: ['bar', 'baz'], - }, - undefined, - { - id: 2, - foo: [false], - }, - ] as Record[]; - - events.forEach(event => compressor.addEvent(event)); + // @ts-ignore ignoring type for test + expect(() => void compressor.addEvent(undefined)).toThrow(); const compressed = compressor.finish(); const restored = pako.inflate(compressed, { to: 'string' }); - const expected = [events[0], events[2]]; - expect(restored).toBe(JSON.stringify(expected)); + expect(restored).toBe(JSON.stringify([])); }); }); diff --git a/packages/replay/worker/src/Compressor.ts b/packages/replay/worker/src/Compressor.ts index 8f7698e313e3..a70eb51422ad 100644 --- a/packages/replay/worker/src/Compressor.ts +++ b/packages/replay/worker/src/Compressor.ts @@ -27,7 +27,7 @@ export class Compressor { public addEvent(data: Record): void { if (!data) { - return; + throw new Error('Adding invalid event'); } // If the event is not the first event, we need to prefix it with a `,` so // that we end up with a list of events @@ -35,10 +35,9 @@ export class Compressor { // TODO: We may want Z_SYNC_FLUSH or Z_FULL_FLUSH (not sure the difference) // Using NO_FLUSH here for now as we can create many attachments that our // web UI will get API rate limited. - this.deflate.push(prefix + JSON.stringify(data), constants.Z_NO_FLUSH); - this.added++; + this.deflate.push(prefix + JSON.stringify(data), constants.Z_SYNC_FLUSH); - return; + this.added++; } public finish(): Uint8Array { diff --git a/packages/replay/worker/src/handleMessage.ts b/packages/replay/worker/src/handleMessage.ts index 45796cd62141..0dd9e871c972 100644 --- a/packages/replay/worker/src/handleMessage.ts +++ b/packages/replay/worker/src/handleMessage.ts @@ -16,8 +16,7 @@ const handlers: Handlers = { }, addEvent: (data: Record) => { - compressor.addEvent(data); - return ''; + return compressor.addEvent(data); }, finish: () => { @@ -48,7 +47,7 @@ export function handleMessage(e: MessageEvent): void { id, method, success: false, - response: err, + response: err.message, }); // eslint-disable-next-line no-console From ef83db69cfed195b3f12582eb639b8666e0d6910 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Wed, 11 Jan 2023 22:17:41 +0100 Subject: [PATCH 045/113] feat(replay): Track pending events in `EventBuffer` (#6699) * Track raw pending events in `EventBuffer`. This can be helpful in the case where page is reloaded before the worker has a chance to close the compression stream. * Change `EventBuffer.length` to `EventBuffer.pendingLength` to better reflect what it is. In the case of compression worker, it is async, so recent events added to the buffer are not necessarily present in the workers compression stream. --- packages/replay/src/eventBuffer.ts | 40 +++++++++++++++++-- packages/replay/src/replay.ts | 2 +- packages/replay/src/types.ts | 12 +++++- packages/replay/src/worker/worker.js | 2 +- .../test/integration/sendReplayEvent.test.ts | 14 +++---- packages/replay/test/integration/stop.test.ts | 6 +-- packages/replay/test/unit/eventBuffer.test.ts | 4 +- 7 files changed, 60 insertions(+), 20 deletions(-) diff --git a/packages/replay/src/eventBuffer.ts b/packages/replay/src/eventBuffer.ts index 74ef2b77bd27..ec129550a009 100644 --- a/packages/replay/src/eventBuffer.ts +++ b/packages/replay/src/eventBuffer.ts @@ -45,10 +45,18 @@ class EventBufferArray implements EventBuffer { this._events = []; } - public get length(): number { + public get pendingLength(): number { return this._events.length; } + /** + * Returns the raw events that are buffered. In `EventBufferArray`, this is the + * same as `this._events`. + */ + public get pendingEvents(): RecordingEvent[] { + return this._events; + } + public destroy(): void { this._events = []; } @@ -80,6 +88,13 @@ class EventBufferArray implements EventBuffer { * Exported only for testing. */ export class EventBufferCompressionWorker implements EventBuffer { + /** + * Keeps track of the list of events since the last flush that have not been compressed. + * For example, page is reloaded and a flush attempt is made, but + * `finish()` (and thus the flush), does not complete. + */ + public _pendingEvents: RecordingEvent[] = []; + private _worker: null | Worker; private _eventBufferItemLength: number = 0; private _id: number = 0; @@ -89,13 +104,21 @@ export class EventBufferCompressionWorker implements EventBuffer { } /** - * Note that this may not reflect what is actually in the event buffer. This - * is only a local count of the buffer size since `addEvent` is async. + * The number of raw events that are buffered. This may not be the same as + * the number of events that have been compresed in the worker because + * `addEvent` is async. */ - public get length(): number { + public get pendingLength(): number { return this._eventBufferItemLength; } + /** + * Returns a list of the raw recording events that are being compressed. + */ + public get pendingEvents(): RecordingEvent[] { + return this._pendingEvents; + } + /** * Destroy the event buffer. */ @@ -121,6 +144,11 @@ export class EventBufferCompressionWorker implements EventBuffer { }); } + // Don't store checkout events in `_pendingEvents` because they are too large + if (!isCheckout) { + this._pendingEvents.push(event); + } + return this._sendEventToWorker(event); } @@ -202,6 +230,10 @@ export class EventBufferCompressionWorker implements EventBuffer { // XXX: See note in `get length()` this._eventBufferItemLength = 0; + await promise; + + this._pendingEvents = []; + return promise; } diff --git a/packages/replay/src/replay.ts b/packages/replay/src/replay.ts index e324a43b10d1..8bb123a36599 100644 --- a/packages/replay/src/replay.ts +++ b/packages/replay/src/replay.ts @@ -794,7 +794,7 @@ export class ReplayContainer implements ReplayContainerInterface { await this.addPerformanceEntries(); - if (!this.eventBuffer?.length) { + if (!this.eventBuffer?.pendingLength) { return; } diff --git a/packages/replay/src/types.ts b/packages/replay/src/types.ts index 8a9737611eda..21a528575380 100644 --- a/packages/replay/src/types.ts +++ b/packages/replay/src/types.ts @@ -211,7 +211,15 @@ export interface Session { } export interface EventBuffer { - readonly length: number; + /** + * The number of raw events that are buffered + */ + readonly pendingLength: number; + + /** + * The raw events that are buffered. + */ + readonly pendingEvents: RecordingEvent[]; /** * Destroy the event buffer. @@ -226,7 +234,7 @@ export interface EventBuffer { addEvent(event: RecordingEvent, isCheckout?: boolean): Promise; /** - * Clears and returns the contents and the buffer. + * Clears and returns the contents of the buffer. */ finish(): Promise; } diff --git a/packages/replay/src/worker/worker.js b/packages/replay/src/worker/worker.js index 3598eedfd68a..553a89077500 100644 --- a/packages/replay/src/worker/worker.js +++ b/packages/replay/src/worker/worker.js @@ -1,2 +1,2 @@ export default `/*! pako 2.1.0 https://github.com/nodeca/pako @license (MIT AND Zlib) */ -function t(t){let e=t.length;for(;--e>=0;)t[e]=0}const e=new Uint8Array([0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0]),a=new Uint8Array([0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13]),i=new Uint8Array([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,3,7]),n=new Uint8Array([16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]),s=new Array(576);t(s);const r=new Array(60);t(r);const o=new Array(512);t(o);const l=new Array(256);t(l);const h=new Array(29);t(h);const d=new Array(30);function _(t,e,a,i,n){this.static_tree=t,this.extra_bits=e,this.extra_base=a,this.elems=i,this.max_length=n,this.has_stree=t&&t.length}let f,c,u;function w(t,e){this.dyn_tree=t,this.max_code=0,this.stat_desc=e}t(d);const m=t=>t<256?o[t]:o[256+(t>>>7)],b=(t,e)=>{t.pending_buf[t.pending++]=255&e,t.pending_buf[t.pending++]=e>>>8&255},g=(t,e,a)=>{t.bi_valid>16-a?(t.bi_buf|=e<>16-t.bi_valid,t.bi_valid+=a-16):(t.bi_buf|=e<{g(t,a[2*e],a[2*e+1])},k=(t,e)=>{let a=0;do{a|=1&t,t>>>=1,a<<=1}while(--e>0);return a>>>1},v=(t,e,a)=>{const i=new Array(16);let n,s,r=0;for(n=1;n<=15;n++)r=r+a[n-1]<<1,i[n]=r;for(s=0;s<=e;s++){let e=t[2*s+1];0!==e&&(t[2*s]=k(i[e]++,e))}},y=t=>{let e;for(e=0;e<286;e++)t.dyn_ltree[2*e]=0;for(e=0;e<30;e++)t.dyn_dtree[2*e]=0;for(e=0;e<19;e++)t.bl_tree[2*e]=0;t.dyn_ltree[512]=1,t.opt_len=t.static_len=0,t.sym_next=t.matches=0},x=t=>{t.bi_valid>8?b(t,t.bi_buf):t.bi_valid>0&&(t.pending_buf[t.pending++]=t.bi_buf),t.bi_buf=0,t.bi_valid=0},z=(t,e,a,i)=>{const n=2*e,s=2*a;return t[n]{const i=t.heap[a];let n=a<<1;for(;n<=t.heap_len&&(n{let s,r,o,_,f=0;if(0!==t.sym_next)do{s=255&t.pending_buf[t.sym_buf+f++],s+=(255&t.pending_buf[t.sym_buf+f++])<<8,r=t.pending_buf[t.sym_buf+f++],0===s?p(t,r,i):(o=l[r],p(t,o+256+1,i),_=e[o],0!==_&&(r-=h[o],g(t,r,_)),s--,o=m(s),p(t,o,n),_=a[o],0!==_&&(s-=d[o],g(t,s,_)))}while(f{const a=e.dyn_tree,i=e.stat_desc.static_tree,n=e.stat_desc.has_stree,s=e.stat_desc.elems;let r,o,l,h=-1;for(t.heap_len=0,t.heap_max=573,r=0;r>1;r>=1;r--)A(t,a,r);l=s;do{r=t.heap[1],t.heap[1]=t.heap[t.heap_len--],A(t,a,1),o=t.heap[1],t.heap[--t.heap_max]=r,t.heap[--t.heap_max]=o,a[2*l]=a[2*r]+a[2*o],t.depth[l]=(t.depth[r]>=t.depth[o]?t.depth[r]:t.depth[o])+1,a[2*r+1]=a[2*o+1]=l,t.heap[1]=l++,A(t,a,1)}while(t.heap_len>=2);t.heap[--t.heap_max]=t.heap[1],((t,e)=>{const a=e.dyn_tree,i=e.max_code,n=e.stat_desc.static_tree,s=e.stat_desc.has_stree,r=e.stat_desc.extra_bits,o=e.stat_desc.extra_base,l=e.stat_desc.max_length;let h,d,_,f,c,u,w=0;for(f=0;f<=15;f++)t.bl_count[f]=0;for(a[2*t.heap[t.heap_max]+1]=0,h=t.heap_max+1;h<573;h++)d=t.heap[h],f=a[2*a[2*d+1]+1]+1,f>l&&(f=l,w++),a[2*d+1]=f,d>i||(t.bl_count[f]++,c=0,d>=o&&(c=r[d-o]),u=a[2*d],t.opt_len+=u*(f+c),s&&(t.static_len+=u*(n[2*d+1]+c)));if(0!==w){do{for(f=l-1;0===t.bl_count[f];)f--;t.bl_count[f]--,t.bl_count[f+1]+=2,t.bl_count[l]--,w-=2}while(w>0);for(f=l;0!==f;f--)for(d=t.bl_count[f];0!==d;)_=t.heap[--h],_>i||(a[2*_+1]!==f&&(t.opt_len+=(f-a[2*_+1])*a[2*_],a[2*_+1]=f),d--)}})(t,e),v(a,h,t.bl_count)},Z=(t,e,a)=>{let i,n,s=-1,r=e[1],o=0,l=7,h=4;for(0===r&&(l=138,h=3),e[2*(a+1)+1]=65535,i=0;i<=a;i++)n=r,r=e[2*(i+1)+1],++o{let i,n,s=-1,r=e[1],o=0,l=7,h=4;for(0===r&&(l=138,h=3),i=0;i<=a;i++)if(n=r,r=e[2*(i+1)+1],!(++o{g(t,0+(i?1:0),3),x(t),b(t,a),b(t,~a),a&&t.pending_buf.set(t.window.subarray(e,e+a),t.pending),t.pending+=a};var O=(t,e,a,i)=>{let o,l,h=0;t.level>0?(2===t.strm.data_type&&(t.strm.data_type=(t=>{let e,a=4093624447;for(e=0;e<=31;e++,a>>>=1)if(1&a&&0!==t.dyn_ltree[2*e])return 0;if(0!==t.dyn_ltree[18]||0!==t.dyn_ltree[20]||0!==t.dyn_ltree[26])return 1;for(e=32;e<256;e++)if(0!==t.dyn_ltree[2*e])return 1;return 0})(t)),R(t,t.l_desc),R(t,t.d_desc),h=(t=>{let e;for(Z(t,t.dyn_ltree,t.l_desc.max_code),Z(t,t.dyn_dtree,t.d_desc.max_code),R(t,t.bl_desc),e=18;e>=3&&0===t.bl_tree[2*n[e]+1];e--);return t.opt_len+=3*(e+1)+5+5+4,e})(t),o=t.opt_len+3+7>>>3,l=t.static_len+3+7>>>3,l<=o&&(o=l)):o=l=a+5,a+4<=o&&-1!==e?D(t,e,a,i):4===t.strategy||l===o?(g(t,2+(i?1:0),3),E(t,s,r)):(g(t,4+(i?1:0),3),((t,e,a,i)=>{let s;for(g(t,e-257,5),g(t,a-1,5),g(t,i-4,4),s=0;s{U||((()=>{let t,n,w,m,b;const g=new Array(16);for(w=0,m=0;m<28;m++)for(h[m]=w,t=0;t<1<>=7;m<30;m++)for(d[m]=b<<7,t=0;t<1<(t.pending_buf[t.sym_buf+t.sym_next++]=e,t.pending_buf[t.sym_buf+t.sym_next++]=e>>8,t.pending_buf[t.sym_buf+t.sym_next++]=a,0===e?t.dyn_ltree[2*a]++:(t.matches++,e--,t.dyn_ltree[2*(l[a]+256+1)]++,t.dyn_dtree[2*m(e)]++),t.sym_next===t.sym_end),_tr_align:t=>{g(t,2,3),p(t,256,s),(t=>{16===t.bi_valid?(b(t,t.bi_buf),t.bi_buf=0,t.bi_valid=0):t.bi_valid>=8&&(t.pending_buf[t.pending++]=255&t.bi_buf,t.bi_buf>>=8,t.bi_valid-=8)})(t)}};var N=(t,e,a,i)=>{let n=65535&t|0,s=t>>>16&65535|0,r=0;for(;0!==a;){r=a>2e3?2e3:a,a-=r;do{n=n+e[i++]|0,s=s+n|0}while(--r);n%=65521,s%=65521}return n|s<<16|0};const F=new Uint32Array((()=>{let t,e=[];for(var a=0;a<256;a++){t=a;for(var i=0;i<8;i++)t=1&t?3988292384^t>>>1:t>>>1;e[a]=t}return e})());var L=(t,e,a,i)=>{const n=F,s=i+a;t^=-1;for(let a=i;a>>8^n[255&(t^e[a])];return-1^t},I={2:"need dictionary",1:"stream end",0:"","-1":"file error","-2":"stream error","-3":"data error","-4":"insufficient memory","-5":"buffer error","-6":"incompatible version"},B={Z_NO_FLUSH:0,Z_PARTIAL_FLUSH:1,Z_SYNC_FLUSH:2,Z_FULL_FLUSH:3,Z_FINISH:4,Z_BLOCK:5,Z_TREES:6,Z_OK:0,Z_STREAM_END:1,Z_NEED_DICT:2,Z_ERRNO:-1,Z_STREAM_ERROR:-2,Z_DATA_ERROR:-3,Z_MEM_ERROR:-4,Z_BUF_ERROR:-5,Z_NO_COMPRESSION:0,Z_BEST_SPEED:1,Z_BEST_COMPRESSION:9,Z_DEFAULT_COMPRESSION:-1,Z_FILTERED:1,Z_HUFFMAN_ONLY:2,Z_RLE:3,Z_FIXED:4,Z_DEFAULT_STRATEGY:0,Z_BINARY:0,Z_TEXT:1,Z_UNKNOWN:2,Z_DEFLATED:8};const{_tr_init:C,_tr_stored_block:H,_tr_flush_block:M,_tr_tally:j,_tr_align:K}=T,{Z_NO_FLUSH:P,Z_PARTIAL_FLUSH:Y,Z_FULL_FLUSH:G,Z_FINISH:X,Z_BLOCK:J,Z_OK:W,Z_STREAM_END:q,Z_STREAM_ERROR:Q,Z_DATA_ERROR:V,Z_BUF_ERROR:$,Z_DEFAULT_COMPRESSION:tt,Z_FILTERED:et,Z_HUFFMAN_ONLY:at,Z_RLE:it,Z_FIXED:nt,Z_DEFAULT_STRATEGY:st,Z_UNKNOWN:rt,Z_DEFLATED:ot}=B,lt=(t,e)=>(t.msg=I[e],e),ht=t=>2*t-(t>4?9:0),dt=t=>{let e=t.length;for(;--e>=0;)t[e]=0},_t=t=>{let e,a,i,n=t.w_size;e=t.hash_size,i=e;do{a=t.head[--i],t.head[i]=a>=n?a-n:0}while(--e);e=n,i=e;do{a=t.prev[--i],t.prev[i]=a>=n?a-n:0}while(--e)};let ft=(t,e,a)=>(e<{const e=t.state;let a=e.pending;a>t.avail_out&&(a=t.avail_out),0!==a&&(t.output.set(e.pending_buf.subarray(e.pending_out,e.pending_out+a),t.next_out),t.next_out+=a,e.pending_out+=a,t.total_out+=a,t.avail_out-=a,e.pending-=a,0===e.pending&&(e.pending_out=0))},ut=(t,e)=>{M(t,t.block_start>=0?t.block_start:-1,t.strstart-t.block_start,e),t.block_start=t.strstart,ct(t.strm)},wt=(t,e)=>{t.pending_buf[t.pending++]=e},mt=(t,e)=>{t.pending_buf[t.pending++]=e>>>8&255,t.pending_buf[t.pending++]=255&e},bt=(t,e,a,i)=>{let n=t.avail_in;return n>i&&(n=i),0===n?0:(t.avail_in-=n,e.set(t.input.subarray(t.next_in,t.next_in+n),a),1===t.state.wrap?t.adler=N(t.adler,e,n,a):2===t.state.wrap&&(t.adler=L(t.adler,e,n,a)),t.next_in+=n,t.total_in+=n,n)},gt=(t,e)=>{let a,i,n=t.max_chain_length,s=t.strstart,r=t.prev_length,o=t.nice_match;const l=t.strstart>t.w_size-262?t.strstart-(t.w_size-262):0,h=t.window,d=t.w_mask,_=t.prev,f=t.strstart+258;let c=h[s+r-1],u=h[s+r];t.prev_length>=t.good_match&&(n>>=2),o>t.lookahead&&(o=t.lookahead);do{if(a=e,h[a+r]===u&&h[a+r-1]===c&&h[a]===h[s]&&h[++a]===h[s+1]){s+=2,a++;do{}while(h[++s]===h[++a]&&h[++s]===h[++a]&&h[++s]===h[++a]&&h[++s]===h[++a]&&h[++s]===h[++a]&&h[++s]===h[++a]&&h[++s]===h[++a]&&h[++s]===h[++a]&&sr){if(t.match_start=e,r=i,i>=o)break;c=h[s+r-1],u=h[s+r]}}}while((e=_[e&d])>l&&0!=--n);return r<=t.lookahead?r:t.lookahead},pt=t=>{const e=t.w_size;let a,i,n;do{if(i=t.window_size-t.lookahead-t.strstart,t.strstart>=e+(e-262)&&(t.window.set(t.window.subarray(e,e+e-i),0),t.match_start-=e,t.strstart-=e,t.block_start-=e,t.insert>t.strstart&&(t.insert=t.strstart),_t(t),i+=e),0===t.strm.avail_in)break;if(a=bt(t.strm,t.window,t.strstart+t.lookahead,i),t.lookahead+=a,t.lookahead+t.insert>=3)for(n=t.strstart-t.insert,t.ins_h=t.window[n],t.ins_h=ft(t,t.ins_h,t.window[n+1]);t.insert&&(t.ins_h=ft(t,t.ins_h,t.window[n+3-1]),t.prev[n&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=n,n++,t.insert--,!(t.lookahead+t.insert<3)););}while(t.lookahead<262&&0!==t.strm.avail_in)},kt=(t,e)=>{let a,i,n,s=t.pending_buf_size-5>t.w_size?t.w_size:t.pending_buf_size-5,r=0,o=t.strm.avail_in;do{if(a=65535,n=t.bi_valid+42>>3,t.strm.avail_outi+t.strm.avail_in&&(a=i+t.strm.avail_in),a>n&&(a=n),a>8,t.pending_buf[t.pending-2]=~a,t.pending_buf[t.pending-1]=~a>>8,ct(t.strm),i&&(i>a&&(i=a),t.strm.output.set(t.window.subarray(t.block_start,t.block_start+i),t.strm.next_out),t.strm.next_out+=i,t.strm.avail_out-=i,t.strm.total_out+=i,t.block_start+=i,a-=i),a&&(bt(t.strm,t.strm.output,t.strm.next_out,a),t.strm.next_out+=a,t.strm.avail_out-=a,t.strm.total_out+=a)}while(0===r);return o-=t.strm.avail_in,o&&(o>=t.w_size?(t.matches=2,t.window.set(t.strm.input.subarray(t.strm.next_in-t.w_size,t.strm.next_in),0),t.strstart=t.w_size,t.insert=t.strstart):(t.window_size-t.strstart<=o&&(t.strstart-=t.w_size,t.window.set(t.window.subarray(t.w_size,t.w_size+t.strstart),0),t.matches<2&&t.matches++,t.insert>t.strstart&&(t.insert=t.strstart)),t.window.set(t.strm.input.subarray(t.strm.next_in-o,t.strm.next_in),t.strstart),t.strstart+=o,t.insert+=o>t.w_size-t.insert?t.w_size-t.insert:o),t.block_start=t.strstart),t.high_watern&&t.block_start>=t.w_size&&(t.block_start-=t.w_size,t.strstart-=t.w_size,t.window.set(t.window.subarray(t.w_size,t.w_size+t.strstart),0),t.matches<2&&t.matches++,n+=t.w_size,t.insert>t.strstart&&(t.insert=t.strstart)),n>t.strm.avail_in&&(n=t.strm.avail_in),n&&(bt(t.strm,t.window,t.strstart,n),t.strstart+=n,t.insert+=n>t.w_size-t.insert?t.w_size-t.insert:n),t.high_water>3,n=t.pending_buf_size-n>65535?65535:t.pending_buf_size-n,s=n>t.w_size?t.w_size:n,i=t.strstart-t.block_start,(i>=s||(i||e===X)&&e!==P&&0===t.strm.avail_in&&i<=n)&&(a=i>n?n:i,r=e===X&&0===t.strm.avail_in&&a===i?1:0,H(t,t.block_start,a,r),t.block_start+=a,ct(t.strm)),r?3:1)},vt=(t,e)=>{let a,i;for(;;){if(t.lookahead<262){if(pt(t),t.lookahead<262&&e===P)return 1;if(0===t.lookahead)break}if(a=0,t.lookahead>=3&&(t.ins_h=ft(t,t.ins_h,t.window[t.strstart+3-1]),a=t.prev[t.strstart&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=t.strstart),0!==a&&t.strstart-a<=t.w_size-262&&(t.match_length=gt(t,a)),t.match_length>=3)if(i=j(t,t.strstart-t.match_start,t.match_length-3),t.lookahead-=t.match_length,t.match_length<=t.max_lazy_match&&t.lookahead>=3){t.match_length--;do{t.strstart++,t.ins_h=ft(t,t.ins_h,t.window[t.strstart+3-1]),a=t.prev[t.strstart&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=t.strstart}while(0!=--t.match_length);t.strstart++}else t.strstart+=t.match_length,t.match_length=0,t.ins_h=t.window[t.strstart],t.ins_h=ft(t,t.ins_h,t.window[t.strstart+1]);else i=j(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++;if(i&&(ut(t,!1),0===t.strm.avail_out))return 1}return t.insert=t.strstart<2?t.strstart:2,e===X?(ut(t,!0),0===t.strm.avail_out?3:4):t.sym_next&&(ut(t,!1),0===t.strm.avail_out)?1:2},yt=(t,e)=>{let a,i,n;for(;;){if(t.lookahead<262){if(pt(t),t.lookahead<262&&e===P)return 1;if(0===t.lookahead)break}if(a=0,t.lookahead>=3&&(t.ins_h=ft(t,t.ins_h,t.window[t.strstart+3-1]),a=t.prev[t.strstart&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=t.strstart),t.prev_length=t.match_length,t.prev_match=t.match_start,t.match_length=2,0!==a&&t.prev_length4096)&&(t.match_length=2)),t.prev_length>=3&&t.match_length<=t.prev_length){n=t.strstart+t.lookahead-3,i=j(t,t.strstart-1-t.prev_match,t.prev_length-3),t.lookahead-=t.prev_length-1,t.prev_length-=2;do{++t.strstart<=n&&(t.ins_h=ft(t,t.ins_h,t.window[t.strstart+3-1]),a=t.prev[t.strstart&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=t.strstart)}while(0!=--t.prev_length);if(t.match_available=0,t.match_length=2,t.strstart++,i&&(ut(t,!1),0===t.strm.avail_out))return 1}else if(t.match_available){if(i=j(t,0,t.window[t.strstart-1]),i&&ut(t,!1),t.strstart++,t.lookahead--,0===t.strm.avail_out)return 1}else t.match_available=1,t.strstart++,t.lookahead--}return t.match_available&&(i=j(t,0,t.window[t.strstart-1]),t.match_available=0),t.insert=t.strstart<2?t.strstart:2,e===X?(ut(t,!0),0===t.strm.avail_out?3:4):t.sym_next&&(ut(t,!1),0===t.strm.avail_out)?1:2};function xt(t,e,a,i,n){this.good_length=t,this.max_lazy=e,this.nice_length=a,this.max_chain=i,this.func=n}const zt=[new xt(0,0,0,0,kt),new xt(4,4,8,4,vt),new xt(4,5,16,8,vt),new xt(4,6,32,32,vt),new xt(4,4,16,16,yt),new xt(8,16,32,32,yt),new xt(8,16,128,128,yt),new xt(8,32,128,256,yt),new xt(32,128,258,1024,yt),new xt(32,258,258,4096,yt)];function At(){this.strm=null,this.status=0,this.pending_buf=null,this.pending_buf_size=0,this.pending_out=0,this.pending=0,this.wrap=0,this.gzhead=null,this.gzindex=0,this.method=ot,this.last_flush=-1,this.w_size=0,this.w_bits=0,this.w_mask=0,this.window=null,this.window_size=0,this.prev=null,this.head=null,this.ins_h=0,this.hash_size=0,this.hash_bits=0,this.hash_mask=0,this.hash_shift=0,this.block_start=0,this.match_length=0,this.prev_match=0,this.match_available=0,this.strstart=0,this.match_start=0,this.lookahead=0,this.prev_length=0,this.max_chain_length=0,this.max_lazy_match=0,this.level=0,this.strategy=0,this.good_match=0,this.nice_match=0,this.dyn_ltree=new Uint16Array(1146),this.dyn_dtree=new Uint16Array(122),this.bl_tree=new Uint16Array(78),dt(this.dyn_ltree),dt(this.dyn_dtree),dt(this.bl_tree),this.l_desc=null,this.d_desc=null,this.bl_desc=null,this.bl_count=new Uint16Array(16),this.heap=new Uint16Array(573),dt(this.heap),this.heap_len=0,this.heap_max=0,this.depth=new Uint16Array(573),dt(this.depth),this.sym_buf=0,this.lit_bufsize=0,this.sym_next=0,this.sym_end=0,this.opt_len=0,this.static_len=0,this.matches=0,this.insert=0,this.bi_buf=0,this.bi_valid=0}const Et=t=>{if(!t)return 1;const e=t.state;return!e||e.strm!==t||42!==e.status&&57!==e.status&&69!==e.status&&73!==e.status&&91!==e.status&&103!==e.status&&113!==e.status&&666!==e.status?1:0},Rt=t=>{if(Et(t))return lt(t,Q);t.total_in=t.total_out=0,t.data_type=rt;const e=t.state;return e.pending=0,e.pending_out=0,e.wrap<0&&(e.wrap=-e.wrap),e.status=2===e.wrap?57:e.wrap?42:113,t.adler=2===e.wrap?0:1,e.last_flush=-2,C(e),W},Zt=t=>{const e=Rt(t);var a;return e===W&&((a=t.state).window_size=2*a.w_size,dt(a.head),a.max_lazy_match=zt[a.level].max_lazy,a.good_match=zt[a.level].good_length,a.nice_match=zt[a.level].nice_length,a.max_chain_length=zt[a.level].max_chain,a.strstart=0,a.block_start=0,a.lookahead=0,a.insert=0,a.match_length=a.prev_length=2,a.match_available=0,a.ins_h=0),e},St=(t,e,a,i,n,s)=>{if(!t)return Q;let r=1;if(e===tt&&(e=6),i<0?(r=0,i=-i):i>15&&(r=2,i-=16),n<1||n>9||a!==ot||i<8||i>15||e<0||e>9||s<0||s>nt||8===i&&1!==r)return lt(t,Q);8===i&&(i=9);const o=new At;return t.state=o,o.strm=t,o.status=42,o.wrap=r,o.gzhead=null,o.w_bits=i,o.w_size=1<St(t,e,ot,15,8,st),deflateInit2:St,deflateReset:Zt,deflateResetKeep:Rt,deflateSetHeader:(t,e)=>Et(t)||2!==t.state.wrap?Q:(t.state.gzhead=e,W),deflate:(t,e)=>{if(Et(t)||e>J||e<0)return t?lt(t,Q):Q;const a=t.state;if(!t.output||0!==t.avail_in&&!t.input||666===a.status&&e!==X)return lt(t,0===t.avail_out?$:Q);const i=a.last_flush;if(a.last_flush=e,0!==a.pending){if(ct(t),0===t.avail_out)return a.last_flush=-1,W}else if(0===t.avail_in&&ht(e)<=ht(i)&&e!==X)return lt(t,$);if(666===a.status&&0!==t.avail_in)return lt(t,$);if(42===a.status&&0===a.wrap&&(a.status=113),42===a.status){let e=ot+(a.w_bits-8<<4)<<8,i=-1;if(i=a.strategy>=at||a.level<2?0:a.level<6?1:6===a.level?2:3,e|=i<<6,0!==a.strstart&&(e|=32),e+=31-e%31,mt(a,e),0!==a.strstart&&(mt(a,t.adler>>>16),mt(a,65535&t.adler)),t.adler=1,a.status=113,ct(t),0!==a.pending)return a.last_flush=-1,W}if(57===a.status)if(t.adler=0,wt(a,31),wt(a,139),wt(a,8),a.gzhead)wt(a,(a.gzhead.text?1:0)+(a.gzhead.hcrc?2:0)+(a.gzhead.extra?4:0)+(a.gzhead.name?8:0)+(a.gzhead.comment?16:0)),wt(a,255&a.gzhead.time),wt(a,a.gzhead.time>>8&255),wt(a,a.gzhead.time>>16&255),wt(a,a.gzhead.time>>24&255),wt(a,9===a.level?2:a.strategy>=at||a.level<2?4:0),wt(a,255&a.gzhead.os),a.gzhead.extra&&a.gzhead.extra.length&&(wt(a,255&a.gzhead.extra.length),wt(a,a.gzhead.extra.length>>8&255)),a.gzhead.hcrc&&(t.adler=L(t.adler,a.pending_buf,a.pending,0)),a.gzindex=0,a.status=69;else if(wt(a,0),wt(a,0),wt(a,0),wt(a,0),wt(a,0),wt(a,9===a.level?2:a.strategy>=at||a.level<2?4:0),wt(a,3),a.status=113,ct(t),0!==a.pending)return a.last_flush=-1,W;if(69===a.status){if(a.gzhead.extra){let e=a.pending,i=(65535&a.gzhead.extra.length)-a.gzindex;for(;a.pending+i>a.pending_buf_size;){let n=a.pending_buf_size-a.pending;if(a.pending_buf.set(a.gzhead.extra.subarray(a.gzindex,a.gzindex+n),a.pending),a.pending=a.pending_buf_size,a.gzhead.hcrc&&a.pending>e&&(t.adler=L(t.adler,a.pending_buf,a.pending-e,e)),a.gzindex+=n,ct(t),0!==a.pending)return a.last_flush=-1,W;e=0,i-=n}let n=new Uint8Array(a.gzhead.extra);a.pending_buf.set(n.subarray(a.gzindex,a.gzindex+i),a.pending),a.pending+=i,a.gzhead.hcrc&&a.pending>e&&(t.adler=L(t.adler,a.pending_buf,a.pending-e,e)),a.gzindex=0}a.status=73}if(73===a.status){if(a.gzhead.name){let e,i=a.pending;do{if(a.pending===a.pending_buf_size){if(a.gzhead.hcrc&&a.pending>i&&(t.adler=L(t.adler,a.pending_buf,a.pending-i,i)),ct(t),0!==a.pending)return a.last_flush=-1,W;i=0}e=a.gzindexi&&(t.adler=L(t.adler,a.pending_buf,a.pending-i,i)),a.gzindex=0}a.status=91}if(91===a.status){if(a.gzhead.comment){let e,i=a.pending;do{if(a.pending===a.pending_buf_size){if(a.gzhead.hcrc&&a.pending>i&&(t.adler=L(t.adler,a.pending_buf,a.pending-i,i)),ct(t),0!==a.pending)return a.last_flush=-1,W;i=0}e=a.gzindexi&&(t.adler=L(t.adler,a.pending_buf,a.pending-i,i))}a.status=103}if(103===a.status){if(a.gzhead.hcrc){if(a.pending+2>a.pending_buf_size&&(ct(t),0!==a.pending))return a.last_flush=-1,W;wt(a,255&t.adler),wt(a,t.adler>>8&255),t.adler=0}if(a.status=113,ct(t),0!==a.pending)return a.last_flush=-1,W}if(0!==t.avail_in||0!==a.lookahead||e!==P&&666!==a.status){let i=0===a.level?kt(a,e):a.strategy===at?((t,e)=>{let a;for(;;){if(0===t.lookahead&&(pt(t),0===t.lookahead)){if(e===P)return 1;break}if(t.match_length=0,a=j(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++,a&&(ut(t,!1),0===t.strm.avail_out))return 1}return t.insert=0,e===X?(ut(t,!0),0===t.strm.avail_out?3:4):t.sym_next&&(ut(t,!1),0===t.strm.avail_out)?1:2})(a,e):a.strategy===it?((t,e)=>{let a,i,n,s;const r=t.window;for(;;){if(t.lookahead<=258){if(pt(t),t.lookahead<=258&&e===P)return 1;if(0===t.lookahead)break}if(t.match_length=0,t.lookahead>=3&&t.strstart>0&&(n=t.strstart-1,i=r[n],i===r[++n]&&i===r[++n]&&i===r[++n])){s=t.strstart+258;do{}while(i===r[++n]&&i===r[++n]&&i===r[++n]&&i===r[++n]&&i===r[++n]&&i===r[++n]&&i===r[++n]&&i===r[++n]&&nt.lookahead&&(t.match_length=t.lookahead)}if(t.match_length>=3?(a=j(t,1,t.match_length-3),t.lookahead-=t.match_length,t.strstart+=t.match_length,t.match_length=0):(a=j(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++),a&&(ut(t,!1),0===t.strm.avail_out))return 1}return t.insert=0,e===X?(ut(t,!0),0===t.strm.avail_out?3:4):t.sym_next&&(ut(t,!1),0===t.strm.avail_out)?1:2})(a,e):zt[a.level].func(a,e);if(3!==i&&4!==i||(a.status=666),1===i||3===i)return 0===t.avail_out&&(a.last_flush=-1),W;if(2===i&&(e===Y?K(a):e!==J&&(H(a,0,0,!1),e===G&&(dt(a.head),0===a.lookahead&&(a.strstart=0,a.block_start=0,a.insert=0))),ct(t),0===t.avail_out))return a.last_flush=-1,W}return e!==X?W:a.wrap<=0?q:(2===a.wrap?(wt(a,255&t.adler),wt(a,t.adler>>8&255),wt(a,t.adler>>16&255),wt(a,t.adler>>24&255),wt(a,255&t.total_in),wt(a,t.total_in>>8&255),wt(a,t.total_in>>16&255),wt(a,t.total_in>>24&255)):(mt(a,t.adler>>>16),mt(a,65535&t.adler)),ct(t),a.wrap>0&&(a.wrap=-a.wrap),0!==a.pending?W:q)},deflateEnd:t=>{if(Et(t))return Q;const e=t.state.status;return t.state=null,113===e?lt(t,V):W},deflateSetDictionary:(t,e)=>{let a=e.length;if(Et(t))return Q;const i=t.state,n=i.wrap;if(2===n||1===n&&42!==i.status||i.lookahead)return Q;if(1===n&&(t.adler=N(t.adler,e,a,0)),i.wrap=0,a>=i.w_size){0===n&&(dt(i.head),i.strstart=0,i.block_start=0,i.insert=0);let t=new Uint8Array(i.w_size);t.set(e.subarray(a-i.w_size,a),0),e=t,a=i.w_size}const s=t.avail_in,r=t.next_in,o=t.input;for(t.avail_in=a,t.next_in=0,t.input=e,pt(i);i.lookahead>=3;){let t=i.strstart,e=i.lookahead-2;do{i.ins_h=ft(i,i.ins_h,i.window[t+3-1]),i.prev[t&i.w_mask]=i.head[i.ins_h],i.head[i.ins_h]=t,t++}while(--e);i.strstart=t,i.lookahead=2,pt(i)}return i.strstart+=i.lookahead,i.block_start=i.strstart,i.insert=i.lookahead,i.lookahead=0,i.match_length=i.prev_length=2,i.match_available=0,t.next_in=r,t.input=o,t.avail_in=s,i.wrap=n,W},deflateInfo:"pako deflate (from Nodeca project)"};const Dt=(t,e)=>Object.prototype.hasOwnProperty.call(t,e);var Ot=function(t){const e=Array.prototype.slice.call(arguments,1);for(;e.length;){const a=e.shift();if(a){if("object"!=typeof a)throw new TypeError(a+"must be non-object");for(const e in a)Dt(a,e)&&(t[e]=a[e])}}return t},Tt=t=>{let e=0;for(let a=0,i=t.length;a=252?6:t>=248?5:t>=240?4:t>=224?3:t>=192?2:1;Ft[254]=Ft[254]=1;var Lt=t=>{if("function"==typeof TextEncoder&&TextEncoder.prototype.encode)return(new TextEncoder).encode(t);let e,a,i,n,s,r=t.length,o=0;for(n=0;n>>6,e[s++]=128|63&a):a<65536?(e[s++]=224|a>>>12,e[s++]=128|a>>>6&63,e[s++]=128|63&a):(e[s++]=240|a>>>18,e[s++]=128|a>>>12&63,e[s++]=128|a>>>6&63,e[s++]=128|63&a);return e},It=(t,e)=>{const a=e||t.length;if("function"==typeof TextDecoder&&TextDecoder.prototype.decode)return(new TextDecoder).decode(t.subarray(0,e));let i,n;const s=new Array(2*a);for(n=0,i=0;i4)s[n++]=65533,i+=r-1;else{for(e&=2===r?31:3===r?15:7;r>1&&i1?s[n++]=65533:e<65536?s[n++]=e:(e-=65536,s[n++]=55296|e>>10&1023,s[n++]=56320|1023&e)}}return((t,e)=>{if(e<65534&&t.subarray&&Nt)return String.fromCharCode.apply(null,t.length===e?t:t.subarray(0,e));let a="";for(let i=0;i{(e=e||t.length)>t.length&&(e=t.length);let a=e-1;for(;a>=0&&128==(192&t[a]);)a--;return a<0||0===a?e:a+Ft[t[a]]>e?a:e};var Ct=function(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0};const Ht=Object.prototype.toString,{Z_NO_FLUSH:Mt,Z_SYNC_FLUSH:jt,Z_FULL_FLUSH:Kt,Z_FINISH:Pt,Z_OK:Yt,Z_STREAM_END:Gt,Z_DEFAULT_COMPRESSION:Xt,Z_DEFAULT_STRATEGY:Jt,Z_DEFLATED:Wt}=B;function qt(t){this.options=Ot({level:Xt,method:Wt,chunkSize:16384,windowBits:15,memLevel:8,strategy:Jt},t||{});let e=this.options;e.raw&&e.windowBits>0?e.windowBits=-e.windowBits:e.gzip&&e.windowBits>0&&e.windowBits<16&&(e.windowBits+=16),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new Ct,this.strm.avail_out=0;let a=Ut.deflateInit2(this.strm,e.level,e.method,e.windowBits,e.memLevel,e.strategy);if(a!==Yt)throw new Error(I[a]);if(e.header&&Ut.deflateSetHeader(this.strm,e.header),e.dictionary){let t;if(t="string"==typeof e.dictionary?Lt(e.dictionary):"[object ArrayBuffer]"===Ht.call(e.dictionary)?new Uint8Array(e.dictionary):e.dictionary,a=Ut.deflateSetDictionary(this.strm,t),a!==Yt)throw new Error(I[a]);this._dict_set=!0}}function Qt(t,e){const a=new qt(e);if(a.push(t,!0),a.err)throw a.msg||I[a.err];return a.result}qt.prototype.push=function(t,e){const a=this.strm,i=this.options.chunkSize;let n,s;if(this.ended)return!1;for(s=e===~~e?e:!0===e?Pt:Mt,"string"==typeof t?a.input=Lt(t):"[object ArrayBuffer]"===Ht.call(t)?a.input=new Uint8Array(t):a.input=t,a.next_in=0,a.avail_in=a.input.length;;)if(0===a.avail_out&&(a.output=new Uint8Array(i),a.next_out=0,a.avail_out=i),(s===jt||s===Kt)&&a.avail_out<=6)this.onData(a.output.subarray(0,a.next_out)),a.avail_out=0;else{if(n=Ut.deflate(a,s),n===Gt)return a.next_out>0&&this.onData(a.output.subarray(0,a.next_out)),n=Ut.deflateEnd(this.strm),this.onEnd(n),this.ended=!0,n===Yt;if(0!==a.avail_out){if(s>0&&a.next_out>0)this.onData(a.output.subarray(0,a.next_out)),a.avail_out=0;else if(0===a.avail_in)break}else this.onData(a.output)}return!0},qt.prototype.onData=function(t){this.chunks.push(t)},qt.prototype.onEnd=function(t){t===Yt&&(this.result=Tt(this.chunks)),this.chunks=[],this.err=t,this.msg=this.strm.msg};var Vt={Deflate:qt,deflate:Qt,deflateRaw:function(t,e){return(e=e||{}).raw=!0,Qt(t,e)},gzip:function(t,e){return(e=e||{}).gzip=!0,Qt(t,e)},constants:B};var $t=function(t,e){let a,i,n,s,r,o,l,h,d,_,f,c,u,w,m,b,g,p,k,v,y,x,z,A;const E=t.state;a=t.next_in,z=t.input,i=a+(t.avail_in-5),n=t.next_out,A=t.output,s=n-(e-t.avail_out),r=n+(t.avail_out-257),o=E.dmax,l=E.wsize,h=E.whave,d=E.wnext,_=E.window,f=E.hold,c=E.bits,u=E.lencode,w=E.distcode,m=(1<>>24,f>>>=p,c-=p,p=g>>>16&255,0===p)A[n++]=65535&g;else{if(!(16&p)){if(0==(64&p)){g=u[(65535&g)+(f&(1<>>=p,c-=p),c<15&&(f+=z[a++]<>>24,f>>>=p,c-=p,p=g>>>16&255,!(16&p)){if(0==(64&p)){g=w[(65535&g)+(f&(1<o){t.msg="invalid distance too far back",E.mode=16209;break t}if(f>>>=p,c-=p,p=n-s,v>p){if(p=v-p,p>h&&E.sane){t.msg="invalid distance too far back",E.mode=16209;break t}if(y=0,x=_,0===d){if(y+=l-p,p2;)A[n++]=x[y++],A[n++]=x[y++],A[n++]=x[y++],k-=3;k&&(A[n++]=x[y++],k>1&&(A[n++]=x[y++]))}else{y=n-v;do{A[n++]=A[y++],A[n++]=A[y++],A[n++]=A[y++],k-=3}while(k>2);k&&(A[n++]=A[y++],k>1&&(A[n++]=A[y++]))}break}}break}}while(a>3,a-=k,c-=k<<3,f&=(1<{const l=o.bits;let h,d,_,f,c,u,w=0,m=0,b=0,g=0,p=0,k=0,v=0,y=0,x=0,z=0,A=null;const E=new Uint16Array(16),R=new Uint16Array(16);let Z,S,U,D=null;for(w=0;w<=15;w++)E[w]=0;for(m=0;m=1&&0===E[g];g--);if(p>g&&(p=g),0===g)return n[s++]=20971520,n[s++]=20971520,o.bits=1,0;for(b=1;b0&&(0===t||1!==g))return-1;for(R[1]=0,w=1;w<15;w++)R[w+1]=R[w]+E[w];for(m=0;m852||2===t&&x>592)return 1;for(;;){Z=w-v,r[m]+1=u?(S=D[r[m]-u],U=A[r[m]-u]):(S=96,U=0),h=1<>v)+d]=Z<<24|S<<16|U|0}while(0!==d);for(h=1<>=1;if(0!==h?(z&=h-1,z+=h):z=0,m++,0==--E[w]){if(w===g)break;w=e[a+r[m]]}if(w>p&&(z&f)!==_){for(0===v&&(v=p),c+=b,k=w-v,y=1<852||2===t&&x>592)return 1;_=z&f,n[_]=p<<24|k<<16|c-s|0}}return 0!==z&&(n[c+z]=w-v<<24|64<<16|0),o.bits=p,0};const{Z_FINISH:se,Z_BLOCK:re,Z_TREES:oe,Z_OK:le,Z_STREAM_END:he,Z_NEED_DICT:de,Z_STREAM_ERROR:_e,Z_DATA_ERROR:fe,Z_MEM_ERROR:ce,Z_BUF_ERROR:ue,Z_DEFLATED:we}=B,me=16209,be=t=>(t>>>24&255)+(t>>>8&65280)+((65280&t)<<8)+((255&t)<<24);function ge(){this.strm=null,this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new Uint16Array(320),this.work=new Uint16Array(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0}const pe=t=>{if(!t)return 1;const e=t.state;return!e||e.strm!==t||e.mode<16180||e.mode>16211?1:0},ke=t=>{if(pe(t))return _e;const e=t.state;return t.total_in=t.total_out=e.total=0,t.msg="",e.wrap&&(t.adler=1&e.wrap),e.mode=16180,e.last=0,e.havedict=0,e.flags=-1,e.dmax=32768,e.head=null,e.hold=0,e.bits=0,e.lencode=e.lendyn=new Int32Array(852),e.distcode=e.distdyn=new Int32Array(592),e.sane=1,e.back=-1,le},ve=t=>{if(pe(t))return _e;const e=t.state;return e.wsize=0,e.whave=0,e.wnext=0,ke(t)},ye=(t,e)=>{let a;if(pe(t))return _e;const i=t.state;return e<0?(a=0,e=-e):(a=5+(e>>4),e<48&&(e&=15)),e&&(e<8||e>15)?_e:(null!==i.window&&i.wbits!==e&&(i.window=null),i.wrap=a,i.wbits=e,ve(t))},xe=(t,e)=>{if(!t)return _e;const a=new ge;t.state=a,a.strm=t,a.window=null,a.mode=16180;const i=ye(t,e);return i!==le&&(t.state=null),i};let ze,Ae,Ee=!0;const Re=t=>{if(Ee){ze=new Int32Array(512),Ae=new Int32Array(32);let e=0;for(;e<144;)t.lens[e++]=8;for(;e<256;)t.lens[e++]=9;for(;e<280;)t.lens[e++]=7;for(;e<288;)t.lens[e++]=8;for(ne(1,t.lens,0,288,ze,0,t.work,{bits:9}),e=0;e<32;)t.lens[e++]=5;ne(2,t.lens,0,32,Ae,0,t.work,{bits:5}),Ee=!1}t.lencode=ze,t.lenbits=9,t.distcode=Ae,t.distbits=5},Ze=(t,e,a,i)=>{let n;const s=t.state;return null===s.window&&(s.wsize=1<=s.wsize?(s.window.set(e.subarray(a-s.wsize,a),0),s.wnext=0,s.whave=s.wsize):(n=s.wsize-s.wnext,n>i&&(n=i),s.window.set(e.subarray(a-i,a-i+n),s.wnext),(i-=n)?(s.window.set(e.subarray(a-i,a),0),s.wnext=i,s.whave=s.wsize):(s.wnext+=n,s.wnext===s.wsize&&(s.wnext=0),s.whavexe(t,15),inflateInit2:xe,inflate:(t,e)=>{let a,i,n,s,r,o,l,h,d,_,f,c,u,w,m,b,g,p,k,v,y,x,z=0;const A=new Uint8Array(4);let E,R;const Z=new Uint8Array([16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]);if(pe(t)||!t.output||!t.input&&0!==t.avail_in)return _e;a=t.state,16191===a.mode&&(a.mode=16192),r=t.next_out,n=t.output,l=t.avail_out,s=t.next_in,i=t.input,o=t.avail_in,h=a.hold,d=a.bits,_=o,f=l,x=le;t:for(;;)switch(a.mode){case 16180:if(0===a.wrap){a.mode=16192;break}for(;d<16;){if(0===o)break t;o--,h+=i[s++]<>>8&255,a.check=L(a.check,A,2,0),h=0,d=0,a.mode=16181;break}if(a.head&&(a.head.done=!1),!(1&a.wrap)||(((255&h)<<8)+(h>>8))%31){t.msg="incorrect header check",a.mode=me;break}if((15&h)!==we){t.msg="unknown compression method",a.mode=me;break}if(h>>>=4,d-=4,y=8+(15&h),0===a.wbits&&(a.wbits=y),y>15||y>a.wbits){t.msg="invalid window size",a.mode=me;break}a.dmax=1<>8&1),512&a.flags&&4&a.wrap&&(A[0]=255&h,A[1]=h>>>8&255,a.check=L(a.check,A,2,0)),h=0,d=0,a.mode=16182;case 16182:for(;d<32;){if(0===o)break t;o--,h+=i[s++]<>>8&255,A[2]=h>>>16&255,A[3]=h>>>24&255,a.check=L(a.check,A,4,0)),h=0,d=0,a.mode=16183;case 16183:for(;d<16;){if(0===o)break t;o--,h+=i[s++]<>8),512&a.flags&&4&a.wrap&&(A[0]=255&h,A[1]=h>>>8&255,a.check=L(a.check,A,2,0)),h=0,d=0,a.mode=16184;case 16184:if(1024&a.flags){for(;d<16;){if(0===o)break t;o--,h+=i[s++]<>>8&255,a.check=L(a.check,A,2,0)),h=0,d=0}else a.head&&(a.head.extra=null);a.mode=16185;case 16185:if(1024&a.flags&&(c=a.length,c>o&&(c=o),c&&(a.head&&(y=a.head.extra_len-a.length,a.head.extra||(a.head.extra=new Uint8Array(a.head.extra_len)),a.head.extra.set(i.subarray(s,s+c),y)),512&a.flags&&4&a.wrap&&(a.check=L(a.check,i,c,s)),o-=c,s+=c,a.length-=c),a.length))break t;a.length=0,a.mode=16186;case 16186:if(2048&a.flags){if(0===o)break t;c=0;do{y=i[s+c++],a.head&&y&&a.length<65536&&(a.head.name+=String.fromCharCode(y))}while(y&&c>9&1,a.head.done=!0),t.adler=a.check=0,a.mode=16191;break;case 16189:for(;d<32;){if(0===o)break t;o--,h+=i[s++]<>>=7&d,d-=7&d,a.mode=16206;break}for(;d<3;){if(0===o)break t;o--,h+=i[s++]<>>=1,d-=1,3&h){case 0:a.mode=16193;break;case 1:if(Re(a),a.mode=16199,e===oe){h>>>=2,d-=2;break t}break;case 2:a.mode=16196;break;case 3:t.msg="invalid block type",a.mode=me}h>>>=2,d-=2;break;case 16193:for(h>>>=7&d,d-=7&d;d<32;){if(0===o)break t;o--,h+=i[s++]<>>16^65535)){t.msg="invalid stored block lengths",a.mode=me;break}if(a.length=65535&h,h=0,d=0,a.mode=16194,e===oe)break t;case 16194:a.mode=16195;case 16195:if(c=a.length,c){if(c>o&&(c=o),c>l&&(c=l),0===c)break t;n.set(i.subarray(s,s+c),r),o-=c,s+=c,l-=c,r+=c,a.length-=c;break}a.mode=16191;break;case 16196:for(;d<14;){if(0===o)break t;o--,h+=i[s++]<>>=5,d-=5,a.ndist=1+(31&h),h>>>=5,d-=5,a.ncode=4+(15&h),h>>>=4,d-=4,a.nlen>286||a.ndist>30){t.msg="too many length or distance symbols",a.mode=me;break}a.have=0,a.mode=16197;case 16197:for(;a.have>>=3,d-=3}for(;a.have<19;)a.lens[Z[a.have++]]=0;if(a.lencode=a.lendyn,a.lenbits=7,E={bits:a.lenbits},x=ne(0,a.lens,0,19,a.lencode,0,a.work,E),a.lenbits=E.bits,x){t.msg="invalid code lengths set",a.mode=me;break}a.have=0,a.mode=16198;case 16198:for(;a.have>>24,b=z>>>16&255,g=65535&z,!(m<=d);){if(0===o)break t;o--,h+=i[s++]<>>=m,d-=m,a.lens[a.have++]=g;else{if(16===g){for(R=m+2;d>>=m,d-=m,0===a.have){t.msg="invalid bit length repeat",a.mode=me;break}y=a.lens[a.have-1],c=3+(3&h),h>>>=2,d-=2}else if(17===g){for(R=m+3;d>>=m,d-=m,y=0,c=3+(7&h),h>>>=3,d-=3}else{for(R=m+7;d>>=m,d-=m,y=0,c=11+(127&h),h>>>=7,d-=7}if(a.have+c>a.nlen+a.ndist){t.msg="invalid bit length repeat",a.mode=me;break}for(;c--;)a.lens[a.have++]=y}}if(a.mode===me)break;if(0===a.lens[256]){t.msg="invalid code -- missing end-of-block",a.mode=me;break}if(a.lenbits=9,E={bits:a.lenbits},x=ne(1,a.lens,0,a.nlen,a.lencode,0,a.work,E),a.lenbits=E.bits,x){t.msg="invalid literal/lengths set",a.mode=me;break}if(a.distbits=6,a.distcode=a.distdyn,E={bits:a.distbits},x=ne(2,a.lens,a.nlen,a.ndist,a.distcode,0,a.work,E),a.distbits=E.bits,x){t.msg="invalid distances set",a.mode=me;break}if(a.mode=16199,e===oe)break t;case 16199:a.mode=16200;case 16200:if(o>=6&&l>=258){t.next_out=r,t.avail_out=l,t.next_in=s,t.avail_in=o,a.hold=h,a.bits=d,$t(t,f),r=t.next_out,n=t.output,l=t.avail_out,s=t.next_in,i=t.input,o=t.avail_in,h=a.hold,d=a.bits,16191===a.mode&&(a.back=-1);break}for(a.back=0;z=a.lencode[h&(1<>>24,b=z>>>16&255,g=65535&z,!(m<=d);){if(0===o)break t;o--,h+=i[s++]<>p)],m=z>>>24,b=z>>>16&255,g=65535&z,!(p+m<=d);){if(0===o)break t;o--,h+=i[s++]<>>=p,d-=p,a.back+=p}if(h>>>=m,d-=m,a.back+=m,a.length=g,0===b){a.mode=16205;break}if(32&b){a.back=-1,a.mode=16191;break}if(64&b){t.msg="invalid literal/length code",a.mode=me;break}a.extra=15&b,a.mode=16201;case 16201:if(a.extra){for(R=a.extra;d>>=a.extra,d-=a.extra,a.back+=a.extra}a.was=a.length,a.mode=16202;case 16202:for(;z=a.distcode[h&(1<>>24,b=z>>>16&255,g=65535&z,!(m<=d);){if(0===o)break t;o--,h+=i[s++]<>p)],m=z>>>24,b=z>>>16&255,g=65535&z,!(p+m<=d);){if(0===o)break t;o--,h+=i[s++]<>>=p,d-=p,a.back+=p}if(h>>>=m,d-=m,a.back+=m,64&b){t.msg="invalid distance code",a.mode=me;break}a.offset=g,a.extra=15&b,a.mode=16203;case 16203:if(a.extra){for(R=a.extra;d>>=a.extra,d-=a.extra,a.back+=a.extra}if(a.offset>a.dmax){t.msg="invalid distance too far back",a.mode=me;break}a.mode=16204;case 16204:if(0===l)break t;if(c=f-l,a.offset>c){if(c=a.offset-c,c>a.whave&&a.sane){t.msg="invalid distance too far back",a.mode=me;break}c>a.wnext?(c-=a.wnext,u=a.wsize-c):u=a.wnext-c,c>a.length&&(c=a.length),w=a.window}else w=n,u=r-a.offset,c=a.length;c>l&&(c=l),l-=c,a.length-=c;do{n[r++]=w[u++]}while(--c);0===a.length&&(a.mode=16200);break;case 16205:if(0===l)break t;n[r++]=a.length,l--,a.mode=16200;break;case 16206:if(a.wrap){for(;d<32;){if(0===o)break t;o--,h|=i[s++]<{if(pe(t))return _e;let e=t.state;return e.window&&(e.window=null),t.state=null,le},inflateGetHeader:(t,e)=>{if(pe(t))return _e;const a=t.state;return 0==(2&a.wrap)?_e:(a.head=e,e.done=!1,le)},inflateSetDictionary:(t,e)=>{const a=e.length;let i,n,s;return pe(t)?_e:(i=t.state,0!==i.wrap&&16190!==i.mode?_e:16190===i.mode&&(n=1,n=N(n,e,a,0),n!==i.check)?fe:(s=Ze(t,e,a,a),s?(i.mode=16210,ce):(i.havedict=1,le)))},inflateInfo:"pako inflate (from Nodeca project)"};var Ue=function(){this.text=0,this.time=0,this.xflags=0,this.os=0,this.extra=null,this.extra_len=0,this.name="",this.comment="",this.hcrc=0,this.done=!1};const De=Object.prototype.toString,{Z_NO_FLUSH:Oe,Z_FINISH:Te,Z_OK:Ne,Z_STREAM_END:Fe,Z_NEED_DICT:Le,Z_STREAM_ERROR:Ie,Z_DATA_ERROR:Be,Z_MEM_ERROR:Ce}=B;function He(t){this.options=Ot({chunkSize:65536,windowBits:15,to:""},t||{});const e=this.options;e.raw&&e.windowBits>=0&&e.windowBits<16&&(e.windowBits=-e.windowBits,0===e.windowBits&&(e.windowBits=-15)),!(e.windowBits>=0&&e.windowBits<16)||t&&t.windowBits||(e.windowBits+=32),e.windowBits>15&&e.windowBits<48&&0==(15&e.windowBits)&&(e.windowBits|=15),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new Ct,this.strm.avail_out=0;let a=Se.inflateInit2(this.strm,e.windowBits);if(a!==Ne)throw new Error(I[a]);if(this.header=new Ue,Se.inflateGetHeader(this.strm,this.header),e.dictionary&&("string"==typeof e.dictionary?e.dictionary=Lt(e.dictionary):"[object ArrayBuffer]"===De.call(e.dictionary)&&(e.dictionary=new Uint8Array(e.dictionary)),e.raw&&(a=Se.inflateSetDictionary(this.strm,e.dictionary),a!==Ne)))throw new Error(I[a])}He.prototype.push=function(t,e){const a=this.strm,i=this.options.chunkSize,n=this.options.dictionary;let s,r,o;if(this.ended)return!1;for(r=e===~~e?e:!0===e?Te:Oe,"[object ArrayBuffer]"===De.call(t)?a.input=new Uint8Array(t):a.input=t,a.next_in=0,a.avail_in=a.input.length;;){for(0===a.avail_out&&(a.output=new Uint8Array(i),a.next_out=0,a.avail_out=i),s=Se.inflate(a,r),s===Le&&n&&(s=Se.inflateSetDictionary(a,n),s===Ne?s=Se.inflate(a,r):s===Be&&(s=Le));a.avail_in>0&&s===Fe&&a.state.wrap>0&&0!==t[a.next_in];)Se.inflateReset(a),s=Se.inflate(a,r);switch(s){case Ie:case Be:case Le:case Ce:return this.onEnd(s),this.ended=!0,!1}if(o=a.avail_out,a.next_out&&(0===a.avail_out||s===Fe))if("string"===this.options.to){let t=Bt(a.output,a.next_out),e=a.next_out-t,n=It(a.output,t);a.next_out=e,a.avail_out=i-e,e&&a.output.set(a.output.subarray(t,t+e),0),this.onData(n)}else this.onData(a.output.length===a.next_out?a.output:a.output.subarray(0,a.next_out));if(s!==Ne||0!==o){if(s===Fe)return s=Se.inflateEnd(this.strm),this.onEnd(s),this.ended=!0,!0;if(0===a.avail_in)break}}return!0},He.prototype.onData=function(t){this.chunks.push(t)},He.prototype.onEnd=function(t){t===Ne&&("string"===this.options.to?this.result=this.chunks.join(""):this.result=Tt(this.chunks)),this.chunks=[],this.err=t,this.msg=this.strm.msg};const{Deflate:Me,deflate:je,deflateRaw:Ke,gzip:Pe}=Vt;var Ye=Me,Ge=B;const Xe=new class{constructor(){this.added=0,this.init()}init(){this.added=0,this.deflate=new Ye,this.deflate.push("[",Ge.Z_NO_FLUSH)}addEvent(t){if(!t)return!1;const e=this.added>0?",":"";return this.deflate.push(e+JSON.stringify(t),Ge.Z_SYNC_FLUSH),this.added++,!0}finish(){if(this.deflate.push("]",Ge.Z_FINISH),this.deflate.err)throw this.deflate.err;const t=this.deflate.result;return this.init(),t}},Je={init:()=>(Xe.init(),""),addEvent:t=>Xe.addEvent(t),finish:()=>Xe.finish()};addEventListener("message",(function(t){const e=t.data.method,a=t.data.id,[i]=t.data.args?JSON.parse(t.data.args):[];if(e in Je&&"function"==typeof Je[e])try{const t=Je[e](i);postMessage({id:a,method:e,success:!0,response:t})}catch(t){postMessage({id:a,method:e,success:!1,response:t}),console.error(t)}}));`; +function t(t){let e=t.length;for(;--e>=0;)t[e]=0}const e=new Uint8Array([0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0]),a=new Uint8Array([0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13]),i=new Uint8Array([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,3,7]),n=new Uint8Array([16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]),s=new Array(576);t(s);const r=new Array(60);t(r);const o=new Array(512);t(o);const l=new Array(256);t(l);const h=new Array(29);t(h);const d=new Array(30);function _(t,e,a,i,n){this.static_tree=t,this.extra_bits=e,this.extra_base=a,this.elems=i,this.max_length=n,this.has_stree=t&&t.length}let f,c,u;function w(t,e){this.dyn_tree=t,this.max_code=0,this.stat_desc=e}t(d);const m=t=>t<256?o[t]:o[256+(t>>>7)],b=(t,e)=>{t.pending_buf[t.pending++]=255&e,t.pending_buf[t.pending++]=e>>>8&255},g=(t,e,a)=>{t.bi_valid>16-a?(t.bi_buf|=e<>16-t.bi_valid,t.bi_valid+=a-16):(t.bi_buf|=e<{g(t,a[2*e],a[2*e+1])},k=(t,e)=>{let a=0;do{a|=1&t,t>>>=1,a<<=1}while(--e>0);return a>>>1},v=(t,e,a)=>{const i=new Array(16);let n,s,r=0;for(n=1;n<=15;n++)r=r+a[n-1]<<1,i[n]=r;for(s=0;s<=e;s++){let e=t[2*s+1];0!==e&&(t[2*s]=k(i[e]++,e))}},y=t=>{let e;for(e=0;e<286;e++)t.dyn_ltree[2*e]=0;for(e=0;e<30;e++)t.dyn_dtree[2*e]=0;for(e=0;e<19;e++)t.bl_tree[2*e]=0;t.dyn_ltree[512]=1,t.opt_len=t.static_len=0,t.sym_next=t.matches=0},x=t=>{t.bi_valid>8?b(t,t.bi_buf):t.bi_valid>0&&(t.pending_buf[t.pending++]=t.bi_buf),t.bi_buf=0,t.bi_valid=0},z=(t,e,a,i)=>{const n=2*e,s=2*a;return t[n]{const i=t.heap[a];let n=a<<1;for(;n<=t.heap_len&&(n{let s,r,o,_,f=0;if(0!==t.sym_next)do{s=255&t.pending_buf[t.sym_buf+f++],s+=(255&t.pending_buf[t.sym_buf+f++])<<8,r=t.pending_buf[t.sym_buf+f++],0===s?p(t,r,i):(o=l[r],p(t,o+256+1,i),_=e[o],0!==_&&(r-=h[o],g(t,r,_)),s--,o=m(s),p(t,o,n),_=a[o],0!==_&&(s-=d[o],g(t,s,_)))}while(f{const a=e.dyn_tree,i=e.stat_desc.static_tree,n=e.stat_desc.has_stree,s=e.stat_desc.elems;let r,o,l,h=-1;for(t.heap_len=0,t.heap_max=573,r=0;r>1;r>=1;r--)A(t,a,r);l=s;do{r=t.heap[1],t.heap[1]=t.heap[t.heap_len--],A(t,a,1),o=t.heap[1],t.heap[--t.heap_max]=r,t.heap[--t.heap_max]=o,a[2*l]=a[2*r]+a[2*o],t.depth[l]=(t.depth[r]>=t.depth[o]?t.depth[r]:t.depth[o])+1,a[2*r+1]=a[2*o+1]=l,t.heap[1]=l++,A(t,a,1)}while(t.heap_len>=2);t.heap[--t.heap_max]=t.heap[1],((t,e)=>{const a=e.dyn_tree,i=e.max_code,n=e.stat_desc.static_tree,s=e.stat_desc.has_stree,r=e.stat_desc.extra_bits,o=e.stat_desc.extra_base,l=e.stat_desc.max_length;let h,d,_,f,c,u,w=0;for(f=0;f<=15;f++)t.bl_count[f]=0;for(a[2*t.heap[t.heap_max]+1]=0,h=t.heap_max+1;h<573;h++)d=t.heap[h],f=a[2*a[2*d+1]+1]+1,f>l&&(f=l,w++),a[2*d+1]=f,d>i||(t.bl_count[f]++,c=0,d>=o&&(c=r[d-o]),u=a[2*d],t.opt_len+=u*(f+c),s&&(t.static_len+=u*(n[2*d+1]+c)));if(0!==w){do{for(f=l-1;0===t.bl_count[f];)f--;t.bl_count[f]--,t.bl_count[f+1]+=2,t.bl_count[l]--,w-=2}while(w>0);for(f=l;0!==f;f--)for(d=t.bl_count[f];0!==d;)_=t.heap[--h],_>i||(a[2*_+1]!==f&&(t.opt_len+=(f-a[2*_+1])*a[2*_],a[2*_+1]=f),d--)}})(t,e),v(a,h,t.bl_count)},Z=(t,e,a)=>{let i,n,s=-1,r=e[1],o=0,l=7,h=4;for(0===r&&(l=138,h=3),e[2*(a+1)+1]=65535,i=0;i<=a;i++)n=r,r=e[2*(i+1)+1],++o{let i,n,s=-1,r=e[1],o=0,l=7,h=4;for(0===r&&(l=138,h=3),i=0;i<=a;i++)if(n=r,r=e[2*(i+1)+1],!(++o{g(t,0+(i?1:0),3),x(t),b(t,a),b(t,~a),a&&t.pending_buf.set(t.window.subarray(e,e+a),t.pending),t.pending+=a};var O=(t,e,a,i)=>{let o,l,h=0;t.level>0?(2===t.strm.data_type&&(t.strm.data_type=(t=>{let e,a=4093624447;for(e=0;e<=31;e++,a>>>=1)if(1&a&&0!==t.dyn_ltree[2*e])return 0;if(0!==t.dyn_ltree[18]||0!==t.dyn_ltree[20]||0!==t.dyn_ltree[26])return 1;for(e=32;e<256;e++)if(0!==t.dyn_ltree[2*e])return 1;return 0})(t)),R(t,t.l_desc),R(t,t.d_desc),h=(t=>{let e;for(Z(t,t.dyn_ltree,t.l_desc.max_code),Z(t,t.dyn_dtree,t.d_desc.max_code),R(t,t.bl_desc),e=18;e>=3&&0===t.bl_tree[2*n[e]+1];e--);return t.opt_len+=3*(e+1)+5+5+4,e})(t),o=t.opt_len+3+7>>>3,l=t.static_len+3+7>>>3,l<=o&&(o=l)):o=l=a+5,a+4<=o&&-1!==e?D(t,e,a,i):4===t.strategy||l===o?(g(t,2+(i?1:0),3),E(t,s,r)):(g(t,4+(i?1:0),3),((t,e,a,i)=>{let s;for(g(t,e-257,5),g(t,a-1,5),g(t,i-4,4),s=0;s{U||((()=>{let t,n,w,m,b;const g=new Array(16);for(w=0,m=0;m<28;m++)for(h[m]=w,t=0;t<1<>=7;m<30;m++)for(d[m]=b<<7,t=0;t<1<(t.pending_buf[t.sym_buf+t.sym_next++]=e,t.pending_buf[t.sym_buf+t.sym_next++]=e>>8,t.pending_buf[t.sym_buf+t.sym_next++]=a,0===e?t.dyn_ltree[2*a]++:(t.matches++,e--,t.dyn_ltree[2*(l[a]+256+1)]++,t.dyn_dtree[2*m(e)]++),t.sym_next===t.sym_end),_tr_align:t=>{g(t,2,3),p(t,256,s),(t=>{16===t.bi_valid?(b(t,t.bi_buf),t.bi_buf=0,t.bi_valid=0):t.bi_valid>=8&&(t.pending_buf[t.pending++]=255&t.bi_buf,t.bi_buf>>=8,t.bi_valid-=8)})(t)}};var N=(t,e,a,i)=>{let n=65535&t|0,s=t>>>16&65535|0,r=0;for(;0!==a;){r=a>2e3?2e3:a,a-=r;do{n=n+e[i++]|0,s=s+n|0}while(--r);n%=65521,s%=65521}return n|s<<16|0};const F=new Uint32Array((()=>{let t,e=[];for(var a=0;a<256;a++){t=a;for(var i=0;i<8;i++)t=1&t?3988292384^t>>>1:t>>>1;e[a]=t}return e})());var L=(t,e,a,i)=>{const n=F,s=i+a;t^=-1;for(let a=i;a>>8^n[255&(t^e[a])];return-1^t},I={2:"need dictionary",1:"stream end",0:"","-1":"file error","-2":"stream error","-3":"data error","-4":"insufficient memory","-5":"buffer error","-6":"incompatible version"},B={Z_NO_FLUSH:0,Z_PARTIAL_FLUSH:1,Z_SYNC_FLUSH:2,Z_FULL_FLUSH:3,Z_FINISH:4,Z_BLOCK:5,Z_TREES:6,Z_OK:0,Z_STREAM_END:1,Z_NEED_DICT:2,Z_ERRNO:-1,Z_STREAM_ERROR:-2,Z_DATA_ERROR:-3,Z_MEM_ERROR:-4,Z_BUF_ERROR:-5,Z_NO_COMPRESSION:0,Z_BEST_SPEED:1,Z_BEST_COMPRESSION:9,Z_DEFAULT_COMPRESSION:-1,Z_FILTERED:1,Z_HUFFMAN_ONLY:2,Z_RLE:3,Z_FIXED:4,Z_DEFAULT_STRATEGY:0,Z_BINARY:0,Z_TEXT:1,Z_UNKNOWN:2,Z_DEFLATED:8};const{_tr_init:C,_tr_stored_block:H,_tr_flush_block:M,_tr_tally:j,_tr_align:K}=T,{Z_NO_FLUSH:P,Z_PARTIAL_FLUSH:Y,Z_FULL_FLUSH:G,Z_FINISH:X,Z_BLOCK:J,Z_OK:W,Z_STREAM_END:q,Z_STREAM_ERROR:Q,Z_DATA_ERROR:V,Z_BUF_ERROR:$,Z_DEFAULT_COMPRESSION:tt,Z_FILTERED:et,Z_HUFFMAN_ONLY:at,Z_RLE:it,Z_FIXED:nt,Z_DEFAULT_STRATEGY:st,Z_UNKNOWN:rt,Z_DEFLATED:ot}=B,lt=(t,e)=>(t.msg=I[e],e),ht=t=>2*t-(t>4?9:0),dt=t=>{let e=t.length;for(;--e>=0;)t[e]=0},_t=t=>{let e,a,i,n=t.w_size;e=t.hash_size,i=e;do{a=t.head[--i],t.head[i]=a>=n?a-n:0}while(--e);e=n,i=e;do{a=t.prev[--i],t.prev[i]=a>=n?a-n:0}while(--e)};let ft=(t,e,a)=>(e<{const e=t.state;let a=e.pending;a>t.avail_out&&(a=t.avail_out),0!==a&&(t.output.set(e.pending_buf.subarray(e.pending_out,e.pending_out+a),t.next_out),t.next_out+=a,e.pending_out+=a,t.total_out+=a,t.avail_out-=a,e.pending-=a,0===e.pending&&(e.pending_out=0))},ut=(t,e)=>{M(t,t.block_start>=0?t.block_start:-1,t.strstart-t.block_start,e),t.block_start=t.strstart,ct(t.strm)},wt=(t,e)=>{t.pending_buf[t.pending++]=e},mt=(t,e)=>{t.pending_buf[t.pending++]=e>>>8&255,t.pending_buf[t.pending++]=255&e},bt=(t,e,a,i)=>{let n=t.avail_in;return n>i&&(n=i),0===n?0:(t.avail_in-=n,e.set(t.input.subarray(t.next_in,t.next_in+n),a),1===t.state.wrap?t.adler=N(t.adler,e,n,a):2===t.state.wrap&&(t.adler=L(t.adler,e,n,a)),t.next_in+=n,t.total_in+=n,n)},gt=(t,e)=>{let a,i,n=t.max_chain_length,s=t.strstart,r=t.prev_length,o=t.nice_match;const l=t.strstart>t.w_size-262?t.strstart-(t.w_size-262):0,h=t.window,d=t.w_mask,_=t.prev,f=t.strstart+258;let c=h[s+r-1],u=h[s+r];t.prev_length>=t.good_match&&(n>>=2),o>t.lookahead&&(o=t.lookahead);do{if(a=e,h[a+r]===u&&h[a+r-1]===c&&h[a]===h[s]&&h[++a]===h[s+1]){s+=2,a++;do{}while(h[++s]===h[++a]&&h[++s]===h[++a]&&h[++s]===h[++a]&&h[++s]===h[++a]&&h[++s]===h[++a]&&h[++s]===h[++a]&&h[++s]===h[++a]&&h[++s]===h[++a]&&sr){if(t.match_start=e,r=i,i>=o)break;c=h[s+r-1],u=h[s+r]}}}while((e=_[e&d])>l&&0!=--n);return r<=t.lookahead?r:t.lookahead},pt=t=>{const e=t.w_size;let a,i,n;do{if(i=t.window_size-t.lookahead-t.strstart,t.strstart>=e+(e-262)&&(t.window.set(t.window.subarray(e,e+e-i),0),t.match_start-=e,t.strstart-=e,t.block_start-=e,t.insert>t.strstart&&(t.insert=t.strstart),_t(t),i+=e),0===t.strm.avail_in)break;if(a=bt(t.strm,t.window,t.strstart+t.lookahead,i),t.lookahead+=a,t.lookahead+t.insert>=3)for(n=t.strstart-t.insert,t.ins_h=t.window[n],t.ins_h=ft(t,t.ins_h,t.window[n+1]);t.insert&&(t.ins_h=ft(t,t.ins_h,t.window[n+3-1]),t.prev[n&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=n,n++,t.insert--,!(t.lookahead+t.insert<3)););}while(t.lookahead<262&&0!==t.strm.avail_in)},kt=(t,e)=>{let a,i,n,s=t.pending_buf_size-5>t.w_size?t.w_size:t.pending_buf_size-5,r=0,o=t.strm.avail_in;do{if(a=65535,n=t.bi_valid+42>>3,t.strm.avail_outi+t.strm.avail_in&&(a=i+t.strm.avail_in),a>n&&(a=n),a>8,t.pending_buf[t.pending-2]=~a,t.pending_buf[t.pending-1]=~a>>8,ct(t.strm),i&&(i>a&&(i=a),t.strm.output.set(t.window.subarray(t.block_start,t.block_start+i),t.strm.next_out),t.strm.next_out+=i,t.strm.avail_out-=i,t.strm.total_out+=i,t.block_start+=i,a-=i),a&&(bt(t.strm,t.strm.output,t.strm.next_out,a),t.strm.next_out+=a,t.strm.avail_out-=a,t.strm.total_out+=a)}while(0===r);return o-=t.strm.avail_in,o&&(o>=t.w_size?(t.matches=2,t.window.set(t.strm.input.subarray(t.strm.next_in-t.w_size,t.strm.next_in),0),t.strstart=t.w_size,t.insert=t.strstart):(t.window_size-t.strstart<=o&&(t.strstart-=t.w_size,t.window.set(t.window.subarray(t.w_size,t.w_size+t.strstart),0),t.matches<2&&t.matches++,t.insert>t.strstart&&(t.insert=t.strstart)),t.window.set(t.strm.input.subarray(t.strm.next_in-o,t.strm.next_in),t.strstart),t.strstart+=o,t.insert+=o>t.w_size-t.insert?t.w_size-t.insert:o),t.block_start=t.strstart),t.high_watern&&t.block_start>=t.w_size&&(t.block_start-=t.w_size,t.strstart-=t.w_size,t.window.set(t.window.subarray(t.w_size,t.w_size+t.strstart),0),t.matches<2&&t.matches++,n+=t.w_size,t.insert>t.strstart&&(t.insert=t.strstart)),n>t.strm.avail_in&&(n=t.strm.avail_in),n&&(bt(t.strm,t.window,t.strstart,n),t.strstart+=n,t.insert+=n>t.w_size-t.insert?t.w_size-t.insert:n),t.high_water>3,n=t.pending_buf_size-n>65535?65535:t.pending_buf_size-n,s=n>t.w_size?t.w_size:n,i=t.strstart-t.block_start,(i>=s||(i||e===X)&&e!==P&&0===t.strm.avail_in&&i<=n)&&(a=i>n?n:i,r=e===X&&0===t.strm.avail_in&&a===i?1:0,H(t,t.block_start,a,r),t.block_start+=a,ct(t.strm)),r?3:1)},vt=(t,e)=>{let a,i;for(;;){if(t.lookahead<262){if(pt(t),t.lookahead<262&&e===P)return 1;if(0===t.lookahead)break}if(a=0,t.lookahead>=3&&(t.ins_h=ft(t,t.ins_h,t.window[t.strstart+3-1]),a=t.prev[t.strstart&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=t.strstart),0!==a&&t.strstart-a<=t.w_size-262&&(t.match_length=gt(t,a)),t.match_length>=3)if(i=j(t,t.strstart-t.match_start,t.match_length-3),t.lookahead-=t.match_length,t.match_length<=t.max_lazy_match&&t.lookahead>=3){t.match_length--;do{t.strstart++,t.ins_h=ft(t,t.ins_h,t.window[t.strstart+3-1]),a=t.prev[t.strstart&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=t.strstart}while(0!=--t.match_length);t.strstart++}else t.strstart+=t.match_length,t.match_length=0,t.ins_h=t.window[t.strstart],t.ins_h=ft(t,t.ins_h,t.window[t.strstart+1]);else i=j(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++;if(i&&(ut(t,!1),0===t.strm.avail_out))return 1}return t.insert=t.strstart<2?t.strstart:2,e===X?(ut(t,!0),0===t.strm.avail_out?3:4):t.sym_next&&(ut(t,!1),0===t.strm.avail_out)?1:2},yt=(t,e)=>{let a,i,n;for(;;){if(t.lookahead<262){if(pt(t),t.lookahead<262&&e===P)return 1;if(0===t.lookahead)break}if(a=0,t.lookahead>=3&&(t.ins_h=ft(t,t.ins_h,t.window[t.strstart+3-1]),a=t.prev[t.strstart&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=t.strstart),t.prev_length=t.match_length,t.prev_match=t.match_start,t.match_length=2,0!==a&&t.prev_length4096)&&(t.match_length=2)),t.prev_length>=3&&t.match_length<=t.prev_length){n=t.strstart+t.lookahead-3,i=j(t,t.strstart-1-t.prev_match,t.prev_length-3),t.lookahead-=t.prev_length-1,t.prev_length-=2;do{++t.strstart<=n&&(t.ins_h=ft(t,t.ins_h,t.window[t.strstart+3-1]),a=t.prev[t.strstart&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=t.strstart)}while(0!=--t.prev_length);if(t.match_available=0,t.match_length=2,t.strstart++,i&&(ut(t,!1),0===t.strm.avail_out))return 1}else if(t.match_available){if(i=j(t,0,t.window[t.strstart-1]),i&&ut(t,!1),t.strstart++,t.lookahead--,0===t.strm.avail_out)return 1}else t.match_available=1,t.strstart++,t.lookahead--}return t.match_available&&(i=j(t,0,t.window[t.strstart-1]),t.match_available=0),t.insert=t.strstart<2?t.strstart:2,e===X?(ut(t,!0),0===t.strm.avail_out?3:4):t.sym_next&&(ut(t,!1),0===t.strm.avail_out)?1:2};function xt(t,e,a,i,n){this.good_length=t,this.max_lazy=e,this.nice_length=a,this.max_chain=i,this.func=n}const zt=[new xt(0,0,0,0,kt),new xt(4,4,8,4,vt),new xt(4,5,16,8,vt),new xt(4,6,32,32,vt),new xt(4,4,16,16,yt),new xt(8,16,32,32,yt),new xt(8,16,128,128,yt),new xt(8,32,128,256,yt),new xt(32,128,258,1024,yt),new xt(32,258,258,4096,yt)];function At(){this.strm=null,this.status=0,this.pending_buf=null,this.pending_buf_size=0,this.pending_out=0,this.pending=0,this.wrap=0,this.gzhead=null,this.gzindex=0,this.method=ot,this.last_flush=-1,this.w_size=0,this.w_bits=0,this.w_mask=0,this.window=null,this.window_size=0,this.prev=null,this.head=null,this.ins_h=0,this.hash_size=0,this.hash_bits=0,this.hash_mask=0,this.hash_shift=0,this.block_start=0,this.match_length=0,this.prev_match=0,this.match_available=0,this.strstart=0,this.match_start=0,this.lookahead=0,this.prev_length=0,this.max_chain_length=0,this.max_lazy_match=0,this.level=0,this.strategy=0,this.good_match=0,this.nice_match=0,this.dyn_ltree=new Uint16Array(1146),this.dyn_dtree=new Uint16Array(122),this.bl_tree=new Uint16Array(78),dt(this.dyn_ltree),dt(this.dyn_dtree),dt(this.bl_tree),this.l_desc=null,this.d_desc=null,this.bl_desc=null,this.bl_count=new Uint16Array(16),this.heap=new Uint16Array(573),dt(this.heap),this.heap_len=0,this.heap_max=0,this.depth=new Uint16Array(573),dt(this.depth),this.sym_buf=0,this.lit_bufsize=0,this.sym_next=0,this.sym_end=0,this.opt_len=0,this.static_len=0,this.matches=0,this.insert=0,this.bi_buf=0,this.bi_valid=0}const Et=t=>{if(!t)return 1;const e=t.state;return!e||e.strm!==t||42!==e.status&&57!==e.status&&69!==e.status&&73!==e.status&&91!==e.status&&103!==e.status&&113!==e.status&&666!==e.status?1:0},Rt=t=>{if(Et(t))return lt(t,Q);t.total_in=t.total_out=0,t.data_type=rt;const e=t.state;return e.pending=0,e.pending_out=0,e.wrap<0&&(e.wrap=-e.wrap),e.status=2===e.wrap?57:e.wrap?42:113,t.adler=2===e.wrap?0:1,e.last_flush=-2,C(e),W},Zt=t=>{const e=Rt(t);var a;return e===W&&((a=t.state).window_size=2*a.w_size,dt(a.head),a.max_lazy_match=zt[a.level].max_lazy,a.good_match=zt[a.level].good_length,a.nice_match=zt[a.level].nice_length,a.max_chain_length=zt[a.level].max_chain,a.strstart=0,a.block_start=0,a.lookahead=0,a.insert=0,a.match_length=a.prev_length=2,a.match_available=0,a.ins_h=0),e},St=(t,e,a,i,n,s)=>{if(!t)return Q;let r=1;if(e===tt&&(e=6),i<0?(r=0,i=-i):i>15&&(r=2,i-=16),n<1||n>9||a!==ot||i<8||i>15||e<0||e>9||s<0||s>nt||8===i&&1!==r)return lt(t,Q);8===i&&(i=9);const o=new At;return t.state=o,o.strm=t,o.status=42,o.wrap=r,o.gzhead=null,o.w_bits=i,o.w_size=1<St(t,e,ot,15,8,st),deflateInit2:St,deflateReset:Zt,deflateResetKeep:Rt,deflateSetHeader:(t,e)=>Et(t)||2!==t.state.wrap?Q:(t.state.gzhead=e,W),deflate:(t,e)=>{if(Et(t)||e>J||e<0)return t?lt(t,Q):Q;const a=t.state;if(!t.output||0!==t.avail_in&&!t.input||666===a.status&&e!==X)return lt(t,0===t.avail_out?$:Q);const i=a.last_flush;if(a.last_flush=e,0!==a.pending){if(ct(t),0===t.avail_out)return a.last_flush=-1,W}else if(0===t.avail_in&&ht(e)<=ht(i)&&e!==X)return lt(t,$);if(666===a.status&&0!==t.avail_in)return lt(t,$);if(42===a.status&&0===a.wrap&&(a.status=113),42===a.status){let e=ot+(a.w_bits-8<<4)<<8,i=-1;if(i=a.strategy>=at||a.level<2?0:a.level<6?1:6===a.level?2:3,e|=i<<6,0!==a.strstart&&(e|=32),e+=31-e%31,mt(a,e),0!==a.strstart&&(mt(a,t.adler>>>16),mt(a,65535&t.adler)),t.adler=1,a.status=113,ct(t),0!==a.pending)return a.last_flush=-1,W}if(57===a.status)if(t.adler=0,wt(a,31),wt(a,139),wt(a,8),a.gzhead)wt(a,(a.gzhead.text?1:0)+(a.gzhead.hcrc?2:0)+(a.gzhead.extra?4:0)+(a.gzhead.name?8:0)+(a.gzhead.comment?16:0)),wt(a,255&a.gzhead.time),wt(a,a.gzhead.time>>8&255),wt(a,a.gzhead.time>>16&255),wt(a,a.gzhead.time>>24&255),wt(a,9===a.level?2:a.strategy>=at||a.level<2?4:0),wt(a,255&a.gzhead.os),a.gzhead.extra&&a.gzhead.extra.length&&(wt(a,255&a.gzhead.extra.length),wt(a,a.gzhead.extra.length>>8&255)),a.gzhead.hcrc&&(t.adler=L(t.adler,a.pending_buf,a.pending,0)),a.gzindex=0,a.status=69;else if(wt(a,0),wt(a,0),wt(a,0),wt(a,0),wt(a,0),wt(a,9===a.level?2:a.strategy>=at||a.level<2?4:0),wt(a,3),a.status=113,ct(t),0!==a.pending)return a.last_flush=-1,W;if(69===a.status){if(a.gzhead.extra){let e=a.pending,i=(65535&a.gzhead.extra.length)-a.gzindex;for(;a.pending+i>a.pending_buf_size;){let n=a.pending_buf_size-a.pending;if(a.pending_buf.set(a.gzhead.extra.subarray(a.gzindex,a.gzindex+n),a.pending),a.pending=a.pending_buf_size,a.gzhead.hcrc&&a.pending>e&&(t.adler=L(t.adler,a.pending_buf,a.pending-e,e)),a.gzindex+=n,ct(t),0!==a.pending)return a.last_flush=-1,W;e=0,i-=n}let n=new Uint8Array(a.gzhead.extra);a.pending_buf.set(n.subarray(a.gzindex,a.gzindex+i),a.pending),a.pending+=i,a.gzhead.hcrc&&a.pending>e&&(t.adler=L(t.adler,a.pending_buf,a.pending-e,e)),a.gzindex=0}a.status=73}if(73===a.status){if(a.gzhead.name){let e,i=a.pending;do{if(a.pending===a.pending_buf_size){if(a.gzhead.hcrc&&a.pending>i&&(t.adler=L(t.adler,a.pending_buf,a.pending-i,i)),ct(t),0!==a.pending)return a.last_flush=-1,W;i=0}e=a.gzindexi&&(t.adler=L(t.adler,a.pending_buf,a.pending-i,i)),a.gzindex=0}a.status=91}if(91===a.status){if(a.gzhead.comment){let e,i=a.pending;do{if(a.pending===a.pending_buf_size){if(a.gzhead.hcrc&&a.pending>i&&(t.adler=L(t.adler,a.pending_buf,a.pending-i,i)),ct(t),0!==a.pending)return a.last_flush=-1,W;i=0}e=a.gzindexi&&(t.adler=L(t.adler,a.pending_buf,a.pending-i,i))}a.status=103}if(103===a.status){if(a.gzhead.hcrc){if(a.pending+2>a.pending_buf_size&&(ct(t),0!==a.pending))return a.last_flush=-1,W;wt(a,255&t.adler),wt(a,t.adler>>8&255),t.adler=0}if(a.status=113,ct(t),0!==a.pending)return a.last_flush=-1,W}if(0!==t.avail_in||0!==a.lookahead||e!==P&&666!==a.status){let i=0===a.level?kt(a,e):a.strategy===at?((t,e)=>{let a;for(;;){if(0===t.lookahead&&(pt(t),0===t.lookahead)){if(e===P)return 1;break}if(t.match_length=0,a=j(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++,a&&(ut(t,!1),0===t.strm.avail_out))return 1}return t.insert=0,e===X?(ut(t,!0),0===t.strm.avail_out?3:4):t.sym_next&&(ut(t,!1),0===t.strm.avail_out)?1:2})(a,e):a.strategy===it?((t,e)=>{let a,i,n,s;const r=t.window;for(;;){if(t.lookahead<=258){if(pt(t),t.lookahead<=258&&e===P)return 1;if(0===t.lookahead)break}if(t.match_length=0,t.lookahead>=3&&t.strstart>0&&(n=t.strstart-1,i=r[n],i===r[++n]&&i===r[++n]&&i===r[++n])){s=t.strstart+258;do{}while(i===r[++n]&&i===r[++n]&&i===r[++n]&&i===r[++n]&&i===r[++n]&&i===r[++n]&&i===r[++n]&&i===r[++n]&&nt.lookahead&&(t.match_length=t.lookahead)}if(t.match_length>=3?(a=j(t,1,t.match_length-3),t.lookahead-=t.match_length,t.strstart+=t.match_length,t.match_length=0):(a=j(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++),a&&(ut(t,!1),0===t.strm.avail_out))return 1}return t.insert=0,e===X?(ut(t,!0),0===t.strm.avail_out?3:4):t.sym_next&&(ut(t,!1),0===t.strm.avail_out)?1:2})(a,e):zt[a.level].func(a,e);if(3!==i&&4!==i||(a.status=666),1===i||3===i)return 0===t.avail_out&&(a.last_flush=-1),W;if(2===i&&(e===Y?K(a):e!==J&&(H(a,0,0,!1),e===G&&(dt(a.head),0===a.lookahead&&(a.strstart=0,a.block_start=0,a.insert=0))),ct(t),0===t.avail_out))return a.last_flush=-1,W}return e!==X?W:a.wrap<=0?q:(2===a.wrap?(wt(a,255&t.adler),wt(a,t.adler>>8&255),wt(a,t.adler>>16&255),wt(a,t.adler>>24&255),wt(a,255&t.total_in),wt(a,t.total_in>>8&255),wt(a,t.total_in>>16&255),wt(a,t.total_in>>24&255)):(mt(a,t.adler>>>16),mt(a,65535&t.adler)),ct(t),a.wrap>0&&(a.wrap=-a.wrap),0!==a.pending?W:q)},deflateEnd:t=>{if(Et(t))return Q;const e=t.state.status;return t.state=null,113===e?lt(t,V):W},deflateSetDictionary:(t,e)=>{let a=e.length;if(Et(t))return Q;const i=t.state,n=i.wrap;if(2===n||1===n&&42!==i.status||i.lookahead)return Q;if(1===n&&(t.adler=N(t.adler,e,a,0)),i.wrap=0,a>=i.w_size){0===n&&(dt(i.head),i.strstart=0,i.block_start=0,i.insert=0);let t=new Uint8Array(i.w_size);t.set(e.subarray(a-i.w_size,a),0),e=t,a=i.w_size}const s=t.avail_in,r=t.next_in,o=t.input;for(t.avail_in=a,t.next_in=0,t.input=e,pt(i);i.lookahead>=3;){let t=i.strstart,e=i.lookahead-2;do{i.ins_h=ft(i,i.ins_h,i.window[t+3-1]),i.prev[t&i.w_mask]=i.head[i.ins_h],i.head[i.ins_h]=t,t++}while(--e);i.strstart=t,i.lookahead=2,pt(i)}return i.strstart+=i.lookahead,i.block_start=i.strstart,i.insert=i.lookahead,i.lookahead=0,i.match_length=i.prev_length=2,i.match_available=0,t.next_in=r,t.input=o,t.avail_in=s,i.wrap=n,W},deflateInfo:"pako deflate (from Nodeca project)"};const Dt=(t,e)=>Object.prototype.hasOwnProperty.call(t,e);var Ot=function(t){const e=Array.prototype.slice.call(arguments,1);for(;e.length;){const a=e.shift();if(a){if("object"!=typeof a)throw new TypeError(a+"must be non-object");for(const e in a)Dt(a,e)&&(t[e]=a[e])}}return t},Tt=t=>{let e=0;for(let a=0,i=t.length;a=252?6:t>=248?5:t>=240?4:t>=224?3:t>=192?2:1;Ft[254]=Ft[254]=1;var Lt=t=>{if("function"==typeof TextEncoder&&TextEncoder.prototype.encode)return(new TextEncoder).encode(t);let e,a,i,n,s,r=t.length,o=0;for(n=0;n>>6,e[s++]=128|63&a):a<65536?(e[s++]=224|a>>>12,e[s++]=128|a>>>6&63,e[s++]=128|63&a):(e[s++]=240|a>>>18,e[s++]=128|a>>>12&63,e[s++]=128|a>>>6&63,e[s++]=128|63&a);return e},It=(t,e)=>{const a=e||t.length;if("function"==typeof TextDecoder&&TextDecoder.prototype.decode)return(new TextDecoder).decode(t.subarray(0,e));let i,n;const s=new Array(2*a);for(n=0,i=0;i4)s[n++]=65533,i+=r-1;else{for(e&=2===r?31:3===r?15:7;r>1&&i1?s[n++]=65533:e<65536?s[n++]=e:(e-=65536,s[n++]=55296|e>>10&1023,s[n++]=56320|1023&e)}}return((t,e)=>{if(e<65534&&t.subarray&&Nt)return String.fromCharCode.apply(null,t.length===e?t:t.subarray(0,e));let a="";for(let i=0;i{(e=e||t.length)>t.length&&(e=t.length);let a=e-1;for(;a>=0&&128==(192&t[a]);)a--;return a<0||0===a?e:a+Ft[t[a]]>e?a:e};var Ct=function(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0};const Ht=Object.prototype.toString,{Z_NO_FLUSH:Mt,Z_SYNC_FLUSH:jt,Z_FULL_FLUSH:Kt,Z_FINISH:Pt,Z_OK:Yt,Z_STREAM_END:Gt,Z_DEFAULT_COMPRESSION:Xt,Z_DEFAULT_STRATEGY:Jt,Z_DEFLATED:Wt}=B;function qt(t){this.options=Ot({level:Xt,method:Wt,chunkSize:16384,windowBits:15,memLevel:8,strategy:Jt},t||{});let e=this.options;e.raw&&e.windowBits>0?e.windowBits=-e.windowBits:e.gzip&&e.windowBits>0&&e.windowBits<16&&(e.windowBits+=16),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new Ct,this.strm.avail_out=0;let a=Ut.deflateInit2(this.strm,e.level,e.method,e.windowBits,e.memLevel,e.strategy);if(a!==Yt)throw new Error(I[a]);if(e.header&&Ut.deflateSetHeader(this.strm,e.header),e.dictionary){let t;if(t="string"==typeof e.dictionary?Lt(e.dictionary):"[object ArrayBuffer]"===Ht.call(e.dictionary)?new Uint8Array(e.dictionary):e.dictionary,a=Ut.deflateSetDictionary(this.strm,t),a!==Yt)throw new Error(I[a]);this._dict_set=!0}}function Qt(t,e){const a=new qt(e);if(a.push(t,!0),a.err)throw a.msg||I[a.err];return a.result}qt.prototype.push=function(t,e){const a=this.strm,i=this.options.chunkSize;let n,s;if(this.ended)return!1;for(s=e===~~e?e:!0===e?Pt:Mt,"string"==typeof t?a.input=Lt(t):"[object ArrayBuffer]"===Ht.call(t)?a.input=new Uint8Array(t):a.input=t,a.next_in=0,a.avail_in=a.input.length;;)if(0===a.avail_out&&(a.output=new Uint8Array(i),a.next_out=0,a.avail_out=i),(s===jt||s===Kt)&&a.avail_out<=6)this.onData(a.output.subarray(0,a.next_out)),a.avail_out=0;else{if(n=Ut.deflate(a,s),n===Gt)return a.next_out>0&&this.onData(a.output.subarray(0,a.next_out)),n=Ut.deflateEnd(this.strm),this.onEnd(n),this.ended=!0,n===Yt;if(0!==a.avail_out){if(s>0&&a.next_out>0)this.onData(a.output.subarray(0,a.next_out)),a.avail_out=0;else if(0===a.avail_in)break}else this.onData(a.output)}return!0},qt.prototype.onData=function(t){this.chunks.push(t)},qt.prototype.onEnd=function(t){t===Yt&&(this.result=Tt(this.chunks)),this.chunks=[],this.err=t,this.msg=this.strm.msg};var Vt={Deflate:qt,deflate:Qt,deflateRaw:function(t,e){return(e=e||{}).raw=!0,Qt(t,e)},gzip:function(t,e){return(e=e||{}).gzip=!0,Qt(t,e)},constants:B};var $t=function(t,e){let a,i,n,s,r,o,l,h,d,_,f,c,u,w,m,b,g,p,k,v,y,x,z,A;const E=t.state;a=t.next_in,z=t.input,i=a+(t.avail_in-5),n=t.next_out,A=t.output,s=n-(e-t.avail_out),r=n+(t.avail_out-257),o=E.dmax,l=E.wsize,h=E.whave,d=E.wnext,_=E.window,f=E.hold,c=E.bits,u=E.lencode,w=E.distcode,m=(1<>>24,f>>>=p,c-=p,p=g>>>16&255,0===p)A[n++]=65535&g;else{if(!(16&p)){if(0==(64&p)){g=u[(65535&g)+(f&(1<>>=p,c-=p),c<15&&(f+=z[a++]<>>24,f>>>=p,c-=p,p=g>>>16&255,!(16&p)){if(0==(64&p)){g=w[(65535&g)+(f&(1<o){t.msg="invalid distance too far back",E.mode=16209;break t}if(f>>>=p,c-=p,p=n-s,v>p){if(p=v-p,p>h&&E.sane){t.msg="invalid distance too far back",E.mode=16209;break t}if(y=0,x=_,0===d){if(y+=l-p,p2;)A[n++]=x[y++],A[n++]=x[y++],A[n++]=x[y++],k-=3;k&&(A[n++]=x[y++],k>1&&(A[n++]=x[y++]))}else{y=n-v;do{A[n++]=A[y++],A[n++]=A[y++],A[n++]=A[y++],k-=3}while(k>2);k&&(A[n++]=A[y++],k>1&&(A[n++]=A[y++]))}break}}break}}while(a>3,a-=k,c-=k<<3,f&=(1<{const l=o.bits;let h,d,_,f,c,u,w=0,m=0,b=0,g=0,p=0,k=0,v=0,y=0,x=0,z=0,A=null;const E=new Uint16Array(16),R=new Uint16Array(16);let Z,S,U,D=null;for(w=0;w<=15;w++)E[w]=0;for(m=0;m=1&&0===E[g];g--);if(p>g&&(p=g),0===g)return n[s++]=20971520,n[s++]=20971520,o.bits=1,0;for(b=1;b0&&(0===t||1!==g))return-1;for(R[1]=0,w=1;w<15;w++)R[w+1]=R[w]+E[w];for(m=0;m852||2===t&&x>592)return 1;for(;;){Z=w-v,r[m]+1=u?(S=D[r[m]-u],U=A[r[m]-u]):(S=96,U=0),h=1<>v)+d]=Z<<24|S<<16|U|0}while(0!==d);for(h=1<>=1;if(0!==h?(z&=h-1,z+=h):z=0,m++,0==--E[w]){if(w===g)break;w=e[a+r[m]]}if(w>p&&(z&f)!==_){for(0===v&&(v=p),c+=b,k=w-v,y=1<852||2===t&&x>592)return 1;_=z&f,n[_]=p<<24|k<<16|c-s|0}}return 0!==z&&(n[c+z]=w-v<<24|64<<16|0),o.bits=p,0};const{Z_FINISH:se,Z_BLOCK:re,Z_TREES:oe,Z_OK:le,Z_STREAM_END:he,Z_NEED_DICT:de,Z_STREAM_ERROR:_e,Z_DATA_ERROR:fe,Z_MEM_ERROR:ce,Z_BUF_ERROR:ue,Z_DEFLATED:we}=B,me=16209,be=t=>(t>>>24&255)+(t>>>8&65280)+((65280&t)<<8)+((255&t)<<24);function ge(){this.strm=null,this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new Uint16Array(320),this.work=new Uint16Array(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0}const pe=t=>{if(!t)return 1;const e=t.state;return!e||e.strm!==t||e.mode<16180||e.mode>16211?1:0},ke=t=>{if(pe(t))return _e;const e=t.state;return t.total_in=t.total_out=e.total=0,t.msg="",e.wrap&&(t.adler=1&e.wrap),e.mode=16180,e.last=0,e.havedict=0,e.flags=-1,e.dmax=32768,e.head=null,e.hold=0,e.bits=0,e.lencode=e.lendyn=new Int32Array(852),e.distcode=e.distdyn=new Int32Array(592),e.sane=1,e.back=-1,le},ve=t=>{if(pe(t))return _e;const e=t.state;return e.wsize=0,e.whave=0,e.wnext=0,ke(t)},ye=(t,e)=>{let a;if(pe(t))return _e;const i=t.state;return e<0?(a=0,e=-e):(a=5+(e>>4),e<48&&(e&=15)),e&&(e<8||e>15)?_e:(null!==i.window&&i.wbits!==e&&(i.window=null),i.wrap=a,i.wbits=e,ve(t))},xe=(t,e)=>{if(!t)return _e;const a=new ge;t.state=a,a.strm=t,a.window=null,a.mode=16180;const i=ye(t,e);return i!==le&&(t.state=null),i};let ze,Ae,Ee=!0;const Re=t=>{if(Ee){ze=new Int32Array(512),Ae=new Int32Array(32);let e=0;for(;e<144;)t.lens[e++]=8;for(;e<256;)t.lens[e++]=9;for(;e<280;)t.lens[e++]=7;for(;e<288;)t.lens[e++]=8;for(ne(1,t.lens,0,288,ze,0,t.work,{bits:9}),e=0;e<32;)t.lens[e++]=5;ne(2,t.lens,0,32,Ae,0,t.work,{bits:5}),Ee=!1}t.lencode=ze,t.lenbits=9,t.distcode=Ae,t.distbits=5},Ze=(t,e,a,i)=>{let n;const s=t.state;return null===s.window&&(s.wsize=1<=s.wsize?(s.window.set(e.subarray(a-s.wsize,a),0),s.wnext=0,s.whave=s.wsize):(n=s.wsize-s.wnext,n>i&&(n=i),s.window.set(e.subarray(a-i,a-i+n),s.wnext),(i-=n)?(s.window.set(e.subarray(a-i,a),0),s.wnext=i,s.whave=s.wsize):(s.wnext+=n,s.wnext===s.wsize&&(s.wnext=0),s.whavexe(t,15),inflateInit2:xe,inflate:(t,e)=>{let a,i,n,s,r,o,l,h,d,_,f,c,u,w,m,b,g,p,k,v,y,x,z=0;const A=new Uint8Array(4);let E,R;const Z=new Uint8Array([16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]);if(pe(t)||!t.output||!t.input&&0!==t.avail_in)return _e;a=t.state,16191===a.mode&&(a.mode=16192),r=t.next_out,n=t.output,l=t.avail_out,s=t.next_in,i=t.input,o=t.avail_in,h=a.hold,d=a.bits,_=o,f=l,x=le;t:for(;;)switch(a.mode){case 16180:if(0===a.wrap){a.mode=16192;break}for(;d<16;){if(0===o)break t;o--,h+=i[s++]<>>8&255,a.check=L(a.check,A,2,0),h=0,d=0,a.mode=16181;break}if(a.head&&(a.head.done=!1),!(1&a.wrap)||(((255&h)<<8)+(h>>8))%31){t.msg="incorrect header check",a.mode=me;break}if((15&h)!==we){t.msg="unknown compression method",a.mode=me;break}if(h>>>=4,d-=4,y=8+(15&h),0===a.wbits&&(a.wbits=y),y>15||y>a.wbits){t.msg="invalid window size",a.mode=me;break}a.dmax=1<>8&1),512&a.flags&&4&a.wrap&&(A[0]=255&h,A[1]=h>>>8&255,a.check=L(a.check,A,2,0)),h=0,d=0,a.mode=16182;case 16182:for(;d<32;){if(0===o)break t;o--,h+=i[s++]<>>8&255,A[2]=h>>>16&255,A[3]=h>>>24&255,a.check=L(a.check,A,4,0)),h=0,d=0,a.mode=16183;case 16183:for(;d<16;){if(0===o)break t;o--,h+=i[s++]<>8),512&a.flags&&4&a.wrap&&(A[0]=255&h,A[1]=h>>>8&255,a.check=L(a.check,A,2,0)),h=0,d=0,a.mode=16184;case 16184:if(1024&a.flags){for(;d<16;){if(0===o)break t;o--,h+=i[s++]<>>8&255,a.check=L(a.check,A,2,0)),h=0,d=0}else a.head&&(a.head.extra=null);a.mode=16185;case 16185:if(1024&a.flags&&(c=a.length,c>o&&(c=o),c&&(a.head&&(y=a.head.extra_len-a.length,a.head.extra||(a.head.extra=new Uint8Array(a.head.extra_len)),a.head.extra.set(i.subarray(s,s+c),y)),512&a.flags&&4&a.wrap&&(a.check=L(a.check,i,c,s)),o-=c,s+=c,a.length-=c),a.length))break t;a.length=0,a.mode=16186;case 16186:if(2048&a.flags){if(0===o)break t;c=0;do{y=i[s+c++],a.head&&y&&a.length<65536&&(a.head.name+=String.fromCharCode(y))}while(y&&c>9&1,a.head.done=!0),t.adler=a.check=0,a.mode=16191;break;case 16189:for(;d<32;){if(0===o)break t;o--,h+=i[s++]<>>=7&d,d-=7&d,a.mode=16206;break}for(;d<3;){if(0===o)break t;o--,h+=i[s++]<>>=1,d-=1,3&h){case 0:a.mode=16193;break;case 1:if(Re(a),a.mode=16199,e===oe){h>>>=2,d-=2;break t}break;case 2:a.mode=16196;break;case 3:t.msg="invalid block type",a.mode=me}h>>>=2,d-=2;break;case 16193:for(h>>>=7&d,d-=7&d;d<32;){if(0===o)break t;o--,h+=i[s++]<>>16^65535)){t.msg="invalid stored block lengths",a.mode=me;break}if(a.length=65535&h,h=0,d=0,a.mode=16194,e===oe)break t;case 16194:a.mode=16195;case 16195:if(c=a.length,c){if(c>o&&(c=o),c>l&&(c=l),0===c)break t;n.set(i.subarray(s,s+c),r),o-=c,s+=c,l-=c,r+=c,a.length-=c;break}a.mode=16191;break;case 16196:for(;d<14;){if(0===o)break t;o--,h+=i[s++]<>>=5,d-=5,a.ndist=1+(31&h),h>>>=5,d-=5,a.ncode=4+(15&h),h>>>=4,d-=4,a.nlen>286||a.ndist>30){t.msg="too many length or distance symbols",a.mode=me;break}a.have=0,a.mode=16197;case 16197:for(;a.have>>=3,d-=3}for(;a.have<19;)a.lens[Z[a.have++]]=0;if(a.lencode=a.lendyn,a.lenbits=7,E={bits:a.lenbits},x=ne(0,a.lens,0,19,a.lencode,0,a.work,E),a.lenbits=E.bits,x){t.msg="invalid code lengths set",a.mode=me;break}a.have=0,a.mode=16198;case 16198:for(;a.have>>24,b=z>>>16&255,g=65535&z,!(m<=d);){if(0===o)break t;o--,h+=i[s++]<>>=m,d-=m,a.lens[a.have++]=g;else{if(16===g){for(R=m+2;d>>=m,d-=m,0===a.have){t.msg="invalid bit length repeat",a.mode=me;break}y=a.lens[a.have-1],c=3+(3&h),h>>>=2,d-=2}else if(17===g){for(R=m+3;d>>=m,d-=m,y=0,c=3+(7&h),h>>>=3,d-=3}else{for(R=m+7;d>>=m,d-=m,y=0,c=11+(127&h),h>>>=7,d-=7}if(a.have+c>a.nlen+a.ndist){t.msg="invalid bit length repeat",a.mode=me;break}for(;c--;)a.lens[a.have++]=y}}if(a.mode===me)break;if(0===a.lens[256]){t.msg="invalid code -- missing end-of-block",a.mode=me;break}if(a.lenbits=9,E={bits:a.lenbits},x=ne(1,a.lens,0,a.nlen,a.lencode,0,a.work,E),a.lenbits=E.bits,x){t.msg="invalid literal/lengths set",a.mode=me;break}if(a.distbits=6,a.distcode=a.distdyn,E={bits:a.distbits},x=ne(2,a.lens,a.nlen,a.ndist,a.distcode,0,a.work,E),a.distbits=E.bits,x){t.msg="invalid distances set",a.mode=me;break}if(a.mode=16199,e===oe)break t;case 16199:a.mode=16200;case 16200:if(o>=6&&l>=258){t.next_out=r,t.avail_out=l,t.next_in=s,t.avail_in=o,a.hold=h,a.bits=d,$t(t,f),r=t.next_out,n=t.output,l=t.avail_out,s=t.next_in,i=t.input,o=t.avail_in,h=a.hold,d=a.bits,16191===a.mode&&(a.back=-1);break}for(a.back=0;z=a.lencode[h&(1<>>24,b=z>>>16&255,g=65535&z,!(m<=d);){if(0===o)break t;o--,h+=i[s++]<>p)],m=z>>>24,b=z>>>16&255,g=65535&z,!(p+m<=d);){if(0===o)break t;o--,h+=i[s++]<>>=p,d-=p,a.back+=p}if(h>>>=m,d-=m,a.back+=m,a.length=g,0===b){a.mode=16205;break}if(32&b){a.back=-1,a.mode=16191;break}if(64&b){t.msg="invalid literal/length code",a.mode=me;break}a.extra=15&b,a.mode=16201;case 16201:if(a.extra){for(R=a.extra;d>>=a.extra,d-=a.extra,a.back+=a.extra}a.was=a.length,a.mode=16202;case 16202:for(;z=a.distcode[h&(1<>>24,b=z>>>16&255,g=65535&z,!(m<=d);){if(0===o)break t;o--,h+=i[s++]<>p)],m=z>>>24,b=z>>>16&255,g=65535&z,!(p+m<=d);){if(0===o)break t;o--,h+=i[s++]<>>=p,d-=p,a.back+=p}if(h>>>=m,d-=m,a.back+=m,64&b){t.msg="invalid distance code",a.mode=me;break}a.offset=g,a.extra=15&b,a.mode=16203;case 16203:if(a.extra){for(R=a.extra;d>>=a.extra,d-=a.extra,a.back+=a.extra}if(a.offset>a.dmax){t.msg="invalid distance too far back",a.mode=me;break}a.mode=16204;case 16204:if(0===l)break t;if(c=f-l,a.offset>c){if(c=a.offset-c,c>a.whave&&a.sane){t.msg="invalid distance too far back",a.mode=me;break}c>a.wnext?(c-=a.wnext,u=a.wsize-c):u=a.wnext-c,c>a.length&&(c=a.length),w=a.window}else w=n,u=r-a.offset,c=a.length;c>l&&(c=l),l-=c,a.length-=c;do{n[r++]=w[u++]}while(--c);0===a.length&&(a.mode=16200);break;case 16205:if(0===l)break t;n[r++]=a.length,l--,a.mode=16200;break;case 16206:if(a.wrap){for(;d<32;){if(0===o)break t;o--,h|=i[s++]<{if(pe(t))return _e;let e=t.state;return e.window&&(e.window=null),t.state=null,le},inflateGetHeader:(t,e)=>{if(pe(t))return _e;const a=t.state;return 0==(2&a.wrap)?_e:(a.head=e,e.done=!1,le)},inflateSetDictionary:(t,e)=>{const a=e.length;let i,n,s;return pe(t)?_e:(i=t.state,0!==i.wrap&&16190!==i.mode?_e:16190===i.mode&&(n=1,n=N(n,e,a,0),n!==i.check)?fe:(s=Ze(t,e,a,a),s?(i.mode=16210,ce):(i.havedict=1,le)))},inflateInfo:"pako inflate (from Nodeca project)"};var Ue=function(){this.text=0,this.time=0,this.xflags=0,this.os=0,this.extra=null,this.extra_len=0,this.name="",this.comment="",this.hcrc=0,this.done=!1};const De=Object.prototype.toString,{Z_NO_FLUSH:Oe,Z_FINISH:Te,Z_OK:Ne,Z_STREAM_END:Fe,Z_NEED_DICT:Le,Z_STREAM_ERROR:Ie,Z_DATA_ERROR:Be,Z_MEM_ERROR:Ce}=B;function He(t){this.options=Ot({chunkSize:65536,windowBits:15,to:""},t||{});const e=this.options;e.raw&&e.windowBits>=0&&e.windowBits<16&&(e.windowBits=-e.windowBits,0===e.windowBits&&(e.windowBits=-15)),!(e.windowBits>=0&&e.windowBits<16)||t&&t.windowBits||(e.windowBits+=32),e.windowBits>15&&e.windowBits<48&&0==(15&e.windowBits)&&(e.windowBits|=15),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new Ct,this.strm.avail_out=0;let a=Se.inflateInit2(this.strm,e.windowBits);if(a!==Ne)throw new Error(I[a]);if(this.header=new Ue,Se.inflateGetHeader(this.strm,this.header),e.dictionary&&("string"==typeof e.dictionary?e.dictionary=Lt(e.dictionary):"[object ArrayBuffer]"===De.call(e.dictionary)&&(e.dictionary=new Uint8Array(e.dictionary)),e.raw&&(a=Se.inflateSetDictionary(this.strm,e.dictionary),a!==Ne)))throw new Error(I[a])}He.prototype.push=function(t,e){const a=this.strm,i=this.options.chunkSize,n=this.options.dictionary;let s,r,o;if(this.ended)return!1;for(r=e===~~e?e:!0===e?Te:Oe,"[object ArrayBuffer]"===De.call(t)?a.input=new Uint8Array(t):a.input=t,a.next_in=0,a.avail_in=a.input.length;;){for(0===a.avail_out&&(a.output=new Uint8Array(i),a.next_out=0,a.avail_out=i),s=Se.inflate(a,r),s===Le&&n&&(s=Se.inflateSetDictionary(a,n),s===Ne?s=Se.inflate(a,r):s===Be&&(s=Le));a.avail_in>0&&s===Fe&&a.state.wrap>0&&0!==t[a.next_in];)Se.inflateReset(a),s=Se.inflate(a,r);switch(s){case Ie:case Be:case Le:case Ce:return this.onEnd(s),this.ended=!0,!1}if(o=a.avail_out,a.next_out&&(0===a.avail_out||s===Fe))if("string"===this.options.to){let t=Bt(a.output,a.next_out),e=a.next_out-t,n=It(a.output,t);a.next_out=e,a.avail_out=i-e,e&&a.output.set(a.output.subarray(t,t+e),0),this.onData(n)}else this.onData(a.output.length===a.next_out?a.output:a.output.subarray(0,a.next_out));if(s!==Ne||0!==o){if(s===Fe)return s=Se.inflateEnd(this.strm),this.onEnd(s),this.ended=!0,!0;if(0===a.avail_in)break}}return!0},He.prototype.onData=function(t){this.chunks.push(t)},He.prototype.onEnd=function(t){t===Ne&&("string"===this.options.to?this.result=this.chunks.join(""):this.result=Tt(this.chunks)),this.chunks=[],this.err=t,this.msg=this.strm.msg};const{Deflate:Me,deflate:je,deflateRaw:Ke,gzip:Pe}=Vt;var Ye=Me,Ge=B;const Xe=new class{constructor(){this.added=0,this.init()}init(){this.added=0,this.deflate=new Ye,this.deflate.push("[",Ge.Z_NO_FLUSH)}addEvent(t){if(!t)throw new Error("Adding invalid event");const e=this.added>0?",":"";this.deflate.push(e+JSON.stringify(t),Ge.Z_SYNC_FLUSH),this.added++}finish(){if(this.deflate.push("]",Ge.Z_FINISH),this.deflate.err)throw this.deflate.err;const t=this.deflate.result;return this.init(),t}},Je={init:()=>(Xe.init(),""),addEvent:t=>Xe.addEvent(t),finish:()=>Xe.finish()};addEventListener("message",(function(t){const e=t.data.method,a=t.data.id,[i]=t.data.args?JSON.parse(t.data.args):[];if(e in Je&&"function"==typeof Je[e])try{const t=Je[e](i);postMessage({id:a,method:e,success:!0,response:t})}catch(t){postMessage({id:a,method:e,success:!1,response:t.message}),console.error(t)}}));`; diff --git a/packages/replay/test/integration/sendReplayEvent.test.ts b/packages/replay/test/integration/sendReplayEvent.test.ts index 1f747723a11d..d9db9ea42a28 100644 --- a/packages/replay/test/integration/sendReplayEvent.test.ts +++ b/packages/replay/test/integration/sendReplayEvent.test.ts @@ -102,7 +102,7 @@ describe('Integration | sendReplayEvent', () => { expect(replay.session?.segmentId).toBe(1); // events array should be empty - expect(replay.eventBuffer?.length).toBe(0); + expect(replay.eventBuffer?.pendingLength).toBe(0); }); it('update last activity when user clicks mouse', async () => { @@ -141,7 +141,7 @@ describe('Integration | sendReplayEvent', () => { expect(replay.session?.segmentId).toBe(1); // events array should be empty - expect(replay.eventBuffer?.length).toBe(0); + expect(replay.eventBuffer?.pendingLength).toBe(0); }); it('uploads a replay event if 15 seconds have elapsed since the last replay upload', async () => { @@ -169,7 +169,7 @@ describe('Integration | sendReplayEvent', () => { expect(replay.session?.lastActivity).toBe(BASE_TIMESTAMP); expect(replay.session?.segmentId).toBe(1); // events array should be empty - expect(replay.eventBuffer?.length).toBe(0); + expect(replay.eventBuffer?.pendingLength).toBe(0); // Let's make sure it continues to work mockTransportSend.mockClear(); @@ -214,7 +214,7 @@ describe('Integration | sendReplayEvent', () => { // Session's last activity should not be updated expect(replay.session?.lastActivity).toBe(BASE_TIMESTAMP); // events array should be empty - expect(replay.eventBuffer?.length).toBe(0); + expect(replay.eventBuffer?.pendingLength).toBe(0); }); it('uploads a replay event when document becomes hidden', async () => { @@ -242,7 +242,7 @@ describe('Integration | sendReplayEvent', () => { // visibilitystate as user being active expect(replay.session?.lastActivity).toBe(BASE_TIMESTAMP); // events array should be empty - expect(replay.eventBuffer?.length).toBe(0); + expect(replay.eventBuffer?.pendingLength).toBe(0); }); it('uploads a replay event if 5 seconds have elapsed since the last replay event occurred', async () => { @@ -261,7 +261,7 @@ describe('Integration | sendReplayEvent', () => { expect(replay.session?.segmentId).toBe(1); // events array should be empty - expect(replay.eventBuffer?.length).toBe(0); + expect(replay.eventBuffer?.pendingLength).toBe(0); }); it('uploads a replay event if 15 seconds have elapsed since the last replay upload', async () => { @@ -290,7 +290,7 @@ describe('Integration | sendReplayEvent', () => { expect(replay.session?.lastActivity).toBe(BASE_TIMESTAMP); expect(replay.session?.segmentId).toBe(1); // events array should be empty - expect(replay.eventBuffer?.length).toBe(0); + expect(replay.eventBuffer?.pendingLength).toBe(0); // Let's make sure it continues to work mockTransportSend.mockClear(); diff --git a/packages/replay/test/integration/stop.test.ts b/packages/replay/test/integration/stop.test.ts index 189156abe015..0b0e164cc8b8 100644 --- a/packages/replay/test/integration/stop.test.ts +++ b/packages/replay/test/integration/stop.test.ts @@ -128,17 +128,17 @@ describe('Integration | stop', () => { it('does not buffer events when stopped', async function () { WINDOW.dispatchEvent(new Event('blur')); - expect(replay.eventBuffer?.length).toBe(1); + expect(replay.eventBuffer?.pendingLength).toBe(1); // stop replays integration.stop(); - expect(replay.eventBuffer?.length).toBe(undefined); + expect(replay.eventBuffer?.pendingLength).toBe(undefined); WINDOW.dispatchEvent(new Event('blur')); await new Promise(process.nextTick); - expect(replay.eventBuffer?.length).toBe(undefined); + expect(replay.eventBuffer?.pendingLength).toBe(undefined); expect(replay).not.toHaveLastSentReplay(); }); diff --git a/packages/replay/test/unit/eventBuffer.test.ts b/packages/replay/test/unit/eventBuffer.test.ts index 71006ca90e53..395af37b7f05 100644 --- a/packages/replay/test/unit/eventBuffer.test.ts +++ b/packages/replay/test/unit/eventBuffer.test.ts @@ -53,10 +53,10 @@ describe('Unit | eventBuffer', () => { }) as EventBufferCompressionWorker; buffer.addEvent(TEST_EVENT); - // @ts-ignore make sure it handles invalid data - buffer.addEvent(undefined); buffer.addEvent(TEST_EVENT); + expect(buffer.pendingEvents).toEqual([TEST_EVENT, TEST_EVENT]); + const result = await buffer.finish(); const restored = pako.inflate(result, { to: 'string' }); From 476f51b84f11bc0b7b8116e88dc4e8ef60594f7b Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 12 Jan 2023 10:42:13 +0100 Subject: [PATCH 046/113] ref(replay): Remove circular `@sentry/browser` peerDependency (#6736) We have only been using this in tests, and can use our own client instead here. --- packages/replay/package.json | 3 -- packages/replay/test/fixtures/error.ts | 3 +- packages/replay/test/mocks/mockSdk.ts | 8 ++-- .../unit/util/createReplayEnvelope.test.ts | 10 ++--- .../test/unit/util/getReplayEvent.test.ts | 7 ++- .../test/unit/util/prepareReplayEvent.test.ts | 7 ++- packages/replay/test/utils/TestClient.ts | 44 +++++++++++++++++++ .../utils/getDefaultBrowserClientOptions.ts | 13 ------ 8 files changed, 60 insertions(+), 35 deletions(-) create mode 100644 packages/replay/test/utils/TestClient.ts delete mode 100644 packages/replay/test/utils/getDefaultBrowserClientOptions.ts diff --git a/packages/replay/package.json b/packages/replay/package.json index da2f247de08b..949dd4495ff8 100644 --- a/packages/replay/package.json +++ b/packages/replay/package.json @@ -57,9 +57,6 @@ "@sentry/types": "7.30.0", "@sentry/utils": "7.30.0" }, - "peerDependencies": { - "@sentry/browser": ">=7.24.0" - }, "engines": { "node": ">=12" }, diff --git a/packages/replay/test/fixtures/error.ts b/packages/replay/test/fixtures/error.ts index c4a0b93144ed..6008f81209e1 100644 --- a/packages/replay/test/fixtures/error.ts +++ b/packages/replay/test/fixtures/error.ts @@ -1,4 +1,3 @@ -import type { SeverityLevel } from '@sentry/browser'; import type { Event } from '@sentry/types'; export function Error(obj?: Event): any { @@ -31,7 +30,7 @@ export function Error(obj?: Event): any { }, ], }, - level: 'error' as SeverityLevel, + level: 'error', event_id: 'event_id', platform: 'javascript', timestamp, diff --git a/packages/replay/test/mocks/mockSdk.ts b/packages/replay/test/mocks/mockSdk.ts index d81d2820a9e3..9da56061071f 100644 --- a/packages/replay/test/mocks/mockSdk.ts +++ b/packages/replay/test/mocks/mockSdk.ts @@ -1,14 +1,14 @@ -import type { BrowserOptions } from '@sentry/browser'; -import { init } from '@sentry/browser'; import type { Envelope, Transport } from '@sentry/types'; import type { Replay as ReplayIntegration } from '../../src'; import type { ReplayContainer } from '../../src/replay'; import type { ReplayConfiguration } from '../../src/types'; +import type { TestClientOptions } from '../utils/TestClient'; +import { getDefaultClientOptions, init } from '../utils/TestClient'; export interface MockSdkParams { replayOptions?: ReplayConfiguration; - sentryOptions?: BrowserOptions; + sentryOptions?: Partial; autoStart?: boolean; } @@ -64,13 +64,13 @@ export async function mockSdk({ replayOptions, sentryOptions, autoStart = true } }); init({ + ...getDefaultClientOptions(), dsn: 'https://dsn@ingest.f00.f00/1', autoSessionTracking: false, sendClientReports: false, transport: () => new MockTransport(), replaysSessionSampleRate: 1.0, replaysOnErrorSampleRate: 0.0, - defaultIntegrations: false, ...sentryOptions, integrations: [replayIntegration], }); diff --git a/packages/replay/test/unit/util/createReplayEnvelope.test.ts b/packages/replay/test/unit/util/createReplayEnvelope.test.ts index 7de41649147b..d0245d0d2f5f 100644 --- a/packages/replay/test/unit/util/createReplayEnvelope.test.ts +++ b/packages/replay/test/unit/util/createReplayEnvelope.test.ts @@ -20,7 +20,7 @@ describe('Unit | util | createReplayEnvelope', () => { environment: 'production', sdk: { integrations: ['BrowserTracing', 'Replay'], - name: 'sentry.javascript.browser', + name: 'sentry.javascript.unknown', version: '7.25.0', }, replay_type: 'error', @@ -47,7 +47,7 @@ describe('Unit | util | createReplayEnvelope', () => { expect(envelope).toEqual([ { event_id: REPLAY_ID, - sdk: { name: 'sentry.javascript.browser', version: '7.25.0' }, + sdk: { name: 'sentry.javascript.unknown', version: '7.25.0' }, sent_at: expect.any(String), }, [ @@ -60,7 +60,7 @@ describe('Unit | util | createReplayEnvelope', () => { platform: 'javascript', replay_id: REPLAY_ID, replay_type: 'error', - sdk: { integrations: ['BrowserTracing', 'Replay'], name: 'sentry.javascript.browser', version: '7.25.0' }, + sdk: { integrations: ['BrowserTracing', 'Replay'], name: 'sentry.javascript.unknown', version: '7.25.0' }, segment_id: 3, tags: { errorSampleRate: 0, sessionSampleRate: 1 }, timestamp: 1670837008.634, @@ -80,7 +80,7 @@ describe('Unit | util | createReplayEnvelope', () => { expect(envelope).toEqual([ { event_id: REPLAY_ID, - sdk: { name: 'sentry.javascript.browser', version: '7.25.0' }, + sdk: { name: 'sentry.javascript.unknown', version: '7.25.0' }, sent_at: expect.any(String), dsn: 'https://abc@sentry.io:1234/123', }, @@ -93,7 +93,7 @@ describe('Unit | util | createReplayEnvelope', () => { event_id: REPLAY_ID, platform: 'javascript', replay_id: REPLAY_ID, - sdk: { integrations: ['BrowserTracing', 'Replay'], name: 'sentry.javascript.browser', version: '7.25.0' }, + sdk: { integrations: ['BrowserTracing', 'Replay'], name: 'sentry.javascript.unknown', version: '7.25.0' }, segment_id: 3, replay_type: 'error', tags: { errorSampleRate: 0, sessionSampleRate: 1 }, diff --git a/packages/replay/test/unit/util/getReplayEvent.test.ts b/packages/replay/test/unit/util/getReplayEvent.test.ts index 49bb832206c6..a7d18971c20e 100644 --- a/packages/replay/test/unit/util/getReplayEvent.test.ts +++ b/packages/replay/test/unit/util/getReplayEvent.test.ts @@ -1,11 +1,10 @@ -import { BrowserClient } from '@sentry/browser'; import type { Hub, Scope } from '@sentry/core'; import { getCurrentHub } from '@sentry/core'; import type { Client, ReplayEvent } from '@sentry/types'; import { REPLAY_EVENT_NAME } from '../../../src/constants'; import { prepareReplayEvent } from '../../../src/util/prepareReplayEvent'; -import { getDefaultBrowserClientOptions } from '../../utils/getDefaultBrowserClientOptions'; +import { getDefaultClientOptions, TestClient } from '../../utils/TestClient'; describe('Unit | util | getReplayEvent', () => { let hub: Hub; @@ -14,7 +13,7 @@ describe('Unit | util | getReplayEvent', () => { beforeEach(() => { hub = getCurrentHub(); - client = new BrowserClient(getDefaultBrowserClientOptions()); + client = new TestClient(getDefaultClientOptions()); hub.bindClient(client); client = hub.getClient()!; @@ -53,7 +52,7 @@ describe('Unit | util | getReplayEvent', () => { event_id: 'replay-ID', environment: 'production', sdk: { - name: 'sentry.javascript.browser', + name: 'sentry.javascript.unknown', version: 'version:Test', }, sdkProcessingMetadata: {}, diff --git a/packages/replay/test/unit/util/prepareReplayEvent.test.ts b/packages/replay/test/unit/util/prepareReplayEvent.test.ts index 899a2ec5b35f..c16c4c0afbb7 100644 --- a/packages/replay/test/unit/util/prepareReplayEvent.test.ts +++ b/packages/replay/test/unit/util/prepareReplayEvent.test.ts @@ -1,11 +1,10 @@ -import { BrowserClient } from '@sentry/browser'; import type { Hub, Scope } from '@sentry/core'; import { getCurrentHub } from '@sentry/core'; import type { Client, ReplayEvent } from '@sentry/types'; import { REPLAY_EVENT_NAME } from '../../../src/constants'; import { prepareReplayEvent } from '../../../src/util/prepareReplayEvent'; -import { getDefaultBrowserClientOptions } from '../../utils/getDefaultBrowserClientOptions'; +import { getDefaultClientOptions, TestClient } from '../../utils/TestClient'; describe('Unit | util | prepareReplayEvent', () => { let hub: Hub; @@ -14,7 +13,7 @@ describe('Unit | util | prepareReplayEvent', () => { beforeEach(() => { hub = getCurrentHub(); - client = new BrowserClient(getDefaultBrowserClientOptions()); + client = new TestClient(getDefaultClientOptions()); hub.bindClient(client); client = hub.getClient()!; @@ -53,7 +52,7 @@ describe('Unit | util | prepareReplayEvent', () => { event_id: 'replay-ID', environment: 'production', sdk: { - name: 'sentry.javascript.browser', + name: 'sentry.javascript.unknown', version: 'version:Test', }, sdkProcessingMetadata: {}, diff --git a/packages/replay/test/utils/TestClient.ts b/packages/replay/test/utils/TestClient.ts new file mode 100644 index 000000000000..ad39b82084a9 --- /dev/null +++ b/packages/replay/test/utils/TestClient.ts @@ -0,0 +1,44 @@ +import { BaseClient, createTransport, initAndBind } from '@sentry/core'; +import type { BrowserClientReplayOptions, ClientOptions, Event, SeverityLevel } from '@sentry/types'; +import { resolvedSyncPromise } from '@sentry/utils'; + +export interface TestClientOptions extends ClientOptions, BrowserClientReplayOptions {} + +export class TestClient extends BaseClient { + public constructor(options: TestClientOptions) { + super(options); + } + + public eventFromException(exception: any): PromiseLike { + return resolvedSyncPromise({ + exception: { + values: [ + { + /* eslint-disable @typescript-eslint/no-unsafe-member-access */ + type: exception.name, + value: exception.message, + /* eslint-enable @typescript-eslint/no-unsafe-member-access */ + }, + ], + }, + }); + } + + public eventFromMessage(message: string, level: SeverityLevel = 'info'): PromiseLike { + return resolvedSyncPromise({ message, level }); + } +} + +export function init(options: TestClientOptions): void { + initAndBind(TestClient, options); +} + +export function getDefaultClientOptions(options: Partial = {}): ClientOptions { + return { + integrations: [], + dsn: 'https://username@domain/123', + transport: () => createTransport({ recordDroppedEvent: () => undefined }, _ => resolvedSyncPromise({})), + stackParser: () => [], + ...options, + }; +} diff --git a/packages/replay/test/utils/getDefaultBrowserClientOptions.ts b/packages/replay/test/utils/getDefaultBrowserClientOptions.ts deleted file mode 100644 index 63ad424b6202..000000000000 --- a/packages/replay/test/utils/getDefaultBrowserClientOptions.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { createTransport } from '@sentry/core'; -import type { ClientOptions } from '@sentry/types'; -import { resolvedSyncPromise } from '@sentry/utils'; - -export function getDefaultBrowserClientOptions(options: Partial = {}): ClientOptions { - return { - integrations: [], - dsn: 'https://username@domain/123', - transport: () => createTransport({ recordDroppedEvent: () => undefined }, _ => resolvedSyncPromise({})), - stackParser: () => [], - ...options, - }; -} From 90cfe007da84aa4a4875f4c5f03ec5afbdd6df5e Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Thu, 12 Jan 2023 10:47:09 +0100 Subject: [PATCH 047/113] build: Turn unused vars from TS error into eslint error (#6747) --- packages/eslint-config-sdk/src/index.js | 2 +- packages/typescript/tsconfig.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/eslint-config-sdk/src/index.js b/packages/eslint-config-sdk/src/index.js index 4076f507c4b3..ef5d219bcc6e 100644 --- a/packages/eslint-config-sdk/src/index.js +++ b/packages/eslint-config-sdk/src/index.js @@ -24,7 +24,7 @@ module.exports = { '@sentry-internal/sdk/no-eq-empty': 'error', // Unused variables should be removed unless they are marked with and underscore (ex. _varName). - '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], + '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], // Make sure that all ts-ignore comments are given a description. '@typescript-eslint/ban-ts-comment': [ diff --git a/packages/typescript/tsconfig.json b/packages/typescript/tsconfig.json index 1eb5750178ce..107a5888e432 100644 --- a/packages/typescript/tsconfig.json +++ b/packages/typescript/tsconfig.json @@ -13,8 +13,8 @@ "noFallthroughCasesInSwitch": true, "noImplicitReturns": true, "noImplicitUseStrict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, + "noUnusedLocals": false, + "noUnusedParameters": false, "preserveWatchOutput": true, "sourceMap": true, "strict": true, From 7c8844f63ddeff16cc8a44a4e332673d09e5b948 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Thu, 12 Jan 2023 11:10:13 +0100 Subject: [PATCH 048/113] chore(utils): Adjust JSDoc of rate limit functions (#6748) --- packages/utils/src/ratelimit.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/utils/src/ratelimit.ts b/packages/utils/src/ratelimit.ts index da4e3bbdb08e..c3353c8034a6 100644 --- a/packages/utils/src/ratelimit.ts +++ b/packages/utils/src/ratelimit.ts @@ -26,7 +26,11 @@ export function parseRetryAfterHeader(header: string, now: number = Date.now()): } /** - * Gets the time that given category is disabled until for rate limiting + * Gets the time that the given category is disabled until for rate limiting. + * In case no category-specific limit is set but a general rate limit across all categories is active, + * that time is returned. + * + * @return the time in ms that the category is disabled until or 0 if there's no active rate limit. */ export function disabledUntil(limits: RateLimits, category: string): number { return limits[category] || limits.all || 0; @@ -41,7 +45,8 @@ export function isRateLimited(limits: RateLimits, category: string, now: number /** * Update ratelimits from incoming headers. - * Returns true if headers contains a non-empty rate limiting header. + * + * @return the updated RateLimits object. */ export function updateRateLimits( limits: RateLimits, From f432d09df5f0fcf09ca5404f97c89e1b4ea5c6db Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 12 Jan 2023 11:11:07 +0100 Subject: [PATCH 049/113] ref(replay): Mark all methods & properties as public/private (#6734) --- packages/replay/.eslintrc.js | 2 - packages/replay/src/integration.ts | 42 +-- packages/replay/src/replay.ts | 306 +++++++++--------- .../test/integration/autoSaveSession.test.ts | 4 +- .../replay/test/integration/events.test.ts | 12 +- .../replay/test/integration/flush.test.ts | 62 ++-- .../replay/test/integration/sampling.test.ts | 6 +- .../test/integration/sendReplayEvent.test.ts | 32 +- .../replay/test/integration/session.test.ts | 2 +- packages/replay/test/integration/stop.test.ts | 2 +- .../test/unit/util/getReplayEvent.test.ts | 62 ---- 11 files changed, 238 insertions(+), 294 deletions(-) delete mode 100644 packages/replay/test/unit/util/getReplayEvent.test.ts diff --git a/packages/replay/.eslintrc.js b/packages/replay/.eslintrc.js index cd80e893576c..85d8dbf13a1e 100644 --- a/packages/replay/.eslintrc.js +++ b/packages/replay/.eslintrc.js @@ -23,8 +23,6 @@ module.exports = { { files: ['*.ts', '*.tsx', '*.d.ts'], rules: { - // TODO (high-prio): Re-enable this after migration - '@typescript-eslint/explicit-member-accessibility': 'off', // Since we target only es6 here, we can leave this off '@sentry-internal/sdk/no-async-await': 'off', }, diff --git a/packages/replay/src/integration.ts b/packages/replay/src/integration.ts index a7cadab5ccfd..274ec6147e34 100644 --- a/packages/replay/src/integration.ts +++ b/packages/replay/src/integration.ts @@ -34,13 +34,13 @@ export class Replay implements Integration { /** * Options to pass to `rrweb.record()` */ - readonly recordingOptions: RecordingOptions; + private readonly _recordingOptions: RecordingOptions; - readonly options: ReplayPluginOptions; + private readonly _options: ReplayPluginOptions; private _replay?: ReplayContainer; - constructor({ + public constructor({ flushMinDelay = DEFAULT_FLUSH_MIN_DELAY, flushMaxDelay = DEFAULT_FLUSH_MAX_DELAY, initialFlushDelay = INITIAL_FLUSH_DELAY, @@ -57,19 +57,19 @@ export class Replay implements Integration { ignoreClass = 'sentry-ignore', maskTextClass = 'sentry-mask', blockSelector = '[data-sentry-block]', - ...recordingOptions + ..._recordingOptions }: ReplayConfiguration = {}) { - this.recordingOptions = { + this._recordingOptions = { maskAllInputs, blockClass, ignoreClass, maskTextClass, maskTextSelector, blockSelector, - ...recordingOptions, + ..._recordingOptions, }; - this.options = { + this._options = { flushMinDelay, flushMaxDelay, stickySession, @@ -91,7 +91,7 @@ Instead, configure \`replaysSessionSampleRate\` directly in the SDK init options Sentry.init({ replaysSessionSampleRate: ${sessionSampleRate} })`, ); - this.options.sessionSampleRate = sessionSampleRate; + this._options.sessionSampleRate = sessionSampleRate; } if (typeof errorSampleRate === 'number') { @@ -103,22 +103,22 @@ Instead, configure \`replaysOnErrorSampleRate\` directly in the SDK init options Sentry.init({ replaysOnErrorSampleRate: ${errorSampleRate} })`, ); - this.options.errorSampleRate = errorSampleRate; + this._options.errorSampleRate = errorSampleRate; } - if (this.options.maskAllText) { + if (this._options.maskAllText) { // `maskAllText` is a more user friendly option to configure // `maskTextSelector`. This means that all nodes will have their text // content masked. - this.recordingOptions.maskTextSelector = MASK_ALL_TEXT_SELECTOR; + this._recordingOptions.maskTextSelector = MASK_ALL_TEXT_SELECTOR; } - if (this.options.blockAllMedia) { + if (this._options.blockAllMedia) { // `blockAllMedia` is a more user friendly option to configure blocking // embedded media elements - this.recordingOptions.blockSelector = !this.recordingOptions.blockSelector + this._recordingOptions.blockSelector = !this._recordingOptions.blockSelector ? MEDIA_SELECTORS - : `${this.recordingOptions.blockSelector},${MEDIA_SELECTORS}`; + : `${this._recordingOptions.blockSelector},${MEDIA_SELECTORS}`; } if (this._isInitialized && isBrowser()) { @@ -148,7 +148,7 @@ Sentry.init({ replaysOnErrorSampleRate: ${errorSampleRate} })`, * global event processors to finish. This is no longer needed, but keeping it * here to avoid any future issues. */ - setupOnce(): void { + public setupOnce(): void { if (!isBrowser()) { return; } @@ -165,7 +165,7 @@ Sentry.init({ replaysOnErrorSampleRate: ${errorSampleRate} })`, * Creates or loads a session, attaches listeners to varying events (DOM, * PerformanceObserver, Recording, Sentry SDK, etc) */ - start(): void { + public start(): void { if (!this._replay) { return; } @@ -177,7 +177,7 @@ Sentry.init({ replaysOnErrorSampleRate: ${errorSampleRate} })`, * Currently, this needs to be manually called (e.g. for tests). Sentry SDK * does not support a teardown */ - stop(): void { + public stop(): void { if (!this._replay) { return; } @@ -191,8 +191,8 @@ Sentry.init({ replaysOnErrorSampleRate: ${errorSampleRate} })`, this._loadReplayOptionsFromClient(); this._replay = new ReplayContainer({ - options: this.options, - recordingOptions: this.recordingOptions, + options: this._options, + recordingOptions: this._recordingOptions, }); } @@ -202,11 +202,11 @@ Sentry.init({ replaysOnErrorSampleRate: ${errorSampleRate} })`, const opt = client && (client.getOptions() as BrowserClientReplayOptions | undefined); if (opt && typeof opt.replaysSessionSampleRate === 'number') { - this.options.sessionSampleRate = opt.replaysSessionSampleRate; + this._options.sessionSampleRate = opt.replaysSessionSampleRate; } if (opt && typeof opt.replaysOnErrorSampleRate === 'number') { - this.options.errorSampleRate = opt.replaysOnErrorSampleRate; + this._options.errorSampleRate = opt.replaysOnErrorSampleRate; } } } diff --git a/packages/replay/src/replay.ts b/packages/replay/src/replay.ts index 8bb123a36599..e82908defa58 100644 --- a/packages/replay/src/replay.ts +++ b/packages/replay/src/replay.ts @@ -128,11 +128,17 @@ export class ReplayContainer implements ReplayContainerInterface { initialUrl: '', }; - constructor({ options, recordingOptions }: { options: ReplayPluginOptions; recordingOptions: RecordingOptions }) { + public constructor({ + options, + recordingOptions, + }: { + options: ReplayPluginOptions; + recordingOptions: RecordingOptions; + }) { this._recordingOptions = recordingOptions; this._options = options; - this._debouncedFlush = debounce(() => this.flush(), this._options.flushMinDelay, { + this._debouncedFlush = debounce(() => this._flush(), this._options.flushMinDelay, { maxWait: this._options.flushMaxDelay, }); } @@ -163,14 +169,14 @@ export class ReplayContainer implements ReplayContainerInterface { * Creates or loads a session, attaches listeners to varying events (DOM, * _performanceObserver, Recording, Sentry SDK, etc) */ - start(): void { - this.setInitialState(); + public start(): void { + this._setInitialState(); - this.loadSession({ expiry: SESSION_IDLE_DURATION }); + this._loadSession({ expiry: SESSION_IDLE_DURATION }); // If there is no session, then something bad has happened - can't continue if (!this.session) { - this.handleException(new Error('No session found')); + this._handleException(new Error('No session found')); return; } @@ -187,13 +193,13 @@ export class ReplayContainer implements ReplayContainerInterface { // setup() is generally called on page load or manually - in both cases we // should treat it as an activity - this.updateSessionActivity(); + this._updateSessionActivity(); this.eventBuffer = createEventBuffer({ useCompression: Boolean(this._options.useCompression), }); - this.addListeners(); + this._addListeners(); // Need to set as enabled before we start recording, as `record()` can trigger a flush with a new checkout this._isEnabled = true; @@ -206,7 +212,7 @@ export class ReplayContainer implements ReplayContainerInterface { * * Note that this will cause a new DOM checkout */ - startRecording(): void { + public startRecording(): void { try { this._stopRecording = record({ ...this._recordingOptions, @@ -214,10 +220,10 @@ export class ReplayContainer implements ReplayContainerInterface { // Without this, it would record forever, until an error happens, which we don't want // instead, we'll always keep the last 60 seconds of replay before an error happened ...(this.recordingMode === 'error' && { checkoutEveryNms: 60000 }), - emit: this.handleRecordingEmit, + emit: this._handleRecordingEmit, }); } catch (err) { - this.handleException(err); + this._handleException(err); } } @@ -238,16 +244,16 @@ export class ReplayContainer implements ReplayContainerInterface { * Currently, this needs to be manually called (e.g. for tests). Sentry SDK * does not support a teardown */ - stop(): void { + public stop(): void { try { __DEBUG_BUILD__ && logger.log('[Replay] Stopping Replays'); this._isEnabled = false; - this.removeListeners(); + this._removeListeners(); this._stopRecording?.(); this.eventBuffer?.destroy(); this.eventBuffer = null; } catch (err) { - this.handleException(err); + this._handleException(err); } } @@ -256,7 +262,7 @@ export class ReplayContainer implements ReplayContainerInterface { * This differs from stop as this only stops DOM recording, it is * not as thorough of a shutdown as `stop()`. */ - pause(): void { + public pause(): void { this._isPaused = true; try { if (this._stopRecording) { @@ -264,7 +270,7 @@ export class ReplayContainer implements ReplayContainerInterface { this._stopRecording = undefined; } } catch (err) { - this.handleException(err); + this._handleException(err); } } @@ -274,13 +280,80 @@ export class ReplayContainer implements ReplayContainerInterface { * Note that calling `startRecording()` here will cause a * new DOM checkout.` */ - resume(): void { + public resume(): void { this._isPaused = false; this.startRecording(); } + /** + * We want to batch uploads of replay events. Save events only if + * `` milliseconds have elapsed since the last event + * *OR* if `` milliseconds have elapsed. + * + * Accepts a callback to perform side-effects and returns true to stop batch + * processing and hand back control to caller. + */ + public addUpdate(cb: AddUpdateCallback): void { + // We need to always run `cb` (e.g. in the case of `this.recordingMode == 'error'`) + const cbResult = cb?.(); + + // If this option is turned on then we will only want to call `flush` + // explicitly + if (this.recordingMode === 'error') { + return; + } + + // If callback is true, we do not want to continue with flushing -- the + // caller will need to handle it. + if (cbResult === true) { + return; + } + + // addUpdate is called quite frequently - use _debouncedFlush so that it + // respects the flush delays and does not flush immediately + this._debouncedFlush(); + } + + /** + * Updates the user activity timestamp and resumes recording. This should be + * called in an event handler for a user action that we consider as the user + * being "active" (e.g. a mouse click). + */ + public triggerUserActivity(): void { + this._updateUserActivity(); + + // This case means that recording was once stopped due to inactivity. + // Ensure that recording is resumed. + if (!this._stopRecording) { + // Create a new session, otherwise when the user action is flushed, it + // will get rejected due to an expired session. + this._loadSession({ expiry: SESSION_IDLE_DURATION }); + + // Note: This will cause a new DOM checkout + this.resume(); + return; + } + + // Otherwise... recording was never suspended, continue as normalish + this._checkAndHandleExpiredSession(); + + this._updateSessionActivity(); + } + + /** + * + * Always flush via `_debouncedFlush` so that we do not have flushes triggered + * from calling both `flush` and `_debouncedFlush`. Otherwise, there could be + * cases of mulitple flushes happening closely together. + */ + public flushImmediate(): Promise { + this._debouncedFlush(); + // `.flush` is provided by the debounced function, analogously to lodash.debounce + return this._debouncedFlush.flush() as Promise; + } + /** A wrapper to conditionally capture exceptions. */ - handleException(error: unknown): void { + private _handleException(error: unknown): void { __DEBUG_BUILD__ && logger.error('[Replay]', error); if (__DEBUG_BUILD__ && this._options._experiments && this._options._experiments.captureExceptions) { @@ -292,7 +365,7 @@ export class ReplayContainer implements ReplayContainerInterface { * Loads a session from storage, or creates a new one if it does not exist or * is expired. */ - loadSession({ expiry }: { expiry: number }): void { + private _loadSession({ expiry }: { expiry: number }): void { const { type, session } = getSession({ expiry, stickySession: Boolean(this._options.stickySession), @@ -304,7 +377,7 @@ export class ReplayContainer implements ReplayContainerInterface { // If session was newly created (i.e. was not loaded from storage), then // enable flag to create the root replay if (type === 'new') { - this.setInitialState(); + this._setInitialState(); } if (session.id !== this.session?.id) { @@ -319,14 +392,14 @@ export class ReplayContainer implements ReplayContainerInterface { * replay. This is required because otherwise they would be captured at the * first flush. */ - setInitialState(): void { + private _setInitialState(): void { const urlPath = `${WINDOW.location.pathname}${WINDOW.location.hash}${WINDOW.location.search}`; const url = `${WINDOW.location.origin}${urlPath}`; this.performanceEvents = []; // Reset _context as well - this.clearContext(); + this._clearContext(); this._context.initialUrl = url; this._context.initialTimestamp = new Date().getTime(); @@ -336,11 +409,11 @@ export class ReplayContainer implements ReplayContainerInterface { /** * Adds listeners to record events for the replay */ - addListeners(): void { + private _addListeners(): void { try { - WINDOW.document.addEventListener('visibilitychange', this.handleVisibilityChange); - WINDOW.addEventListener('blur', this.handleWindowBlur); - WINDOW.addEventListener('focus', this.handleWindowFocus); + WINDOW.document.addEventListener('visibilitychange', this._handleVisibilityChange); + WINDOW.addEventListener('blur', this._handleWindowBlur); + WINDOW.addEventListener('focus', this._handleWindowFocus); // We need to filter out dropped events captured by `addGlobalEventProcessor(this.handleGlobalEvent)` below overwriteRecordDroppedEvent(this._context.errorIds); @@ -349,8 +422,8 @@ export class ReplayContainer implements ReplayContainerInterface { if (!this._hasInitializedCoreListeners) { // Listeners from core SDK // const scope = getCurrentHub().getScope(); - scope?.addScopeListener(this.handleCoreBreadcrumbListener('scope')); - addInstrumentationHandler('dom', this.handleCoreBreadcrumbListener('dom')); + scope?.addScopeListener(this._handleCoreBreadcrumbListener('scope')); + addInstrumentationHandler('dom', this._handleCoreBreadcrumbListener('dom')); addInstrumentationHandler('fetch', handleFetchSpanListener(this)); addInstrumentationHandler('xhr', handleXhrSpanListener(this)); addInstrumentationHandler('history', handleHistorySpanListener(this)); @@ -362,7 +435,7 @@ export class ReplayContainer implements ReplayContainerInterface { this._hasInitializedCoreListeners = true; } } catch (err) { - this.handleException(err); + this._handleException(err); } // _performanceObserver // @@ -374,14 +447,14 @@ export class ReplayContainer implements ReplayContainerInterface { } /** - * Cleans up listeners that were created in `addListeners` + * Cleans up listeners that were created in `_addListeners` */ - removeListeners(): void { + private _removeListeners(): void { try { - WINDOW.document.removeEventListener('visibilitychange', this.handleVisibilityChange); + WINDOW.document.removeEventListener('visibilitychange', this._handleVisibilityChange); - WINDOW.removeEventListener('blur', this.handleWindowBlur); - WINDOW.removeEventListener('focus', this.handleWindowFocus); + WINDOW.removeEventListener('blur', this._handleWindowBlur); + WINDOW.removeEventListener('focus', this._handleWindowFocus); restoreRecordDroppedEvent(); @@ -390,50 +463,21 @@ export class ReplayContainer implements ReplayContainerInterface { this._performanceObserver = null; } } catch (err) { - this.handleException(err); + this._handleException(err); } } - /** - * We want to batch uploads of replay events. Save events only if - * `` milliseconds have elapsed since the last event - * *OR* if `` milliseconds have elapsed. - * - * Accepts a callback to perform side-effects and returns true to stop batch - * processing and hand back control to caller. - */ - addUpdate(cb: AddUpdateCallback): void { - // We need to always run `cb` (e.g. in the case of `this.recordingMode == 'error'`) - const cbResult = cb?.(); - - // If this option is turned on then we will only want to call `flush` - // explicitly - if (this.recordingMode === 'error') { - return; - } - - // If callback is true, we do not want to continue with flushing -- the - // caller will need to handle it. - if (cbResult === true) { - return; - } - - // addUpdate is called quite frequently - use _debouncedFlush so that it - // respects the flush delays and does not flush immediately - this._debouncedFlush(); - } - /** * Handler for recording events. * * Adds to event buffer, and has varying flushing behaviors if the event was a checkout. */ - handleRecordingEmit: (event: RecordingEvent, isCheckout?: boolean) => void = ( + private _handleRecordingEmit: (event: RecordingEvent, isCheckout?: boolean) => void = ( event: RecordingEvent, isCheckout?: boolean, ) => { // If this is false, it means session is expired, create and a new session and wait for checkout - if (!this.checkAndHandleExpiredSession()) { + if (!this._checkAndHandleExpiredSession()) { __DEBUG_BUILD__ && logger.error('[Replay] Received replay event after session expired.'); return; @@ -446,7 +490,7 @@ export class ReplayContainer implements ReplayContainerInterface { // checkout. This needs to happen before `addEvent()` which updates state // dependent on this reset. if (this.recordingMode === 'error' && event.type === 2) { - this.setInitialState(); + this._setInitialState(); } // We need to clear existing events on a checkout, otherwise they are @@ -494,38 +538,38 @@ export class ReplayContainer implements ReplayContainerInterface { * be hidden. Likewise, moving a different window to cover the contents of the * page will also trigger a change to a hidden state. */ - handleVisibilityChange: () => void = () => { + private _handleVisibilityChange: () => void = () => { if (WINDOW.document.visibilityState === 'visible') { - this.doChangeToForegroundTasks(); + this._doChangeToForegroundTasks(); } else { - this.doChangeToBackgroundTasks(); + this._doChangeToBackgroundTasks(); } }; /** * Handle when page is blurred */ - handleWindowBlur: () => void = () => { + private _handleWindowBlur: () => void = () => { const breadcrumb = createBreadcrumb({ category: 'ui.blur', }); // Do not count blur as a user action -- it's part of the process of them // leaving the page - this.doChangeToBackgroundTasks(breadcrumb); + this._doChangeToBackgroundTasks(breadcrumb); }; /** * Handle when page is focused */ - handleWindowFocus: () => void = () => { + private _handleWindowFocus: () => void = () => { const breadcrumb = createBreadcrumb({ category: 'ui.focus', }); // Do not count focus as a user action -- instead wait until they focus and // interactive with page - this.doChangeToForegroundTasks(breadcrumb); + this._doChangeToForegroundTasks(breadcrumb); }; /** @@ -533,7 +577,7 @@ export class ReplayContainer implements ReplayContainerInterface { * * These events will create breadcrumb-like objects in the recording. */ - handleCoreBreadcrumbListener: (type: InstrumentationTypeBreadcrumb) => (handlerData: unknown) => void = + private _handleCoreBreadcrumbListener: (type: InstrumentationTypeBreadcrumb) => (handlerData: unknown) => void = (type: InstrumentationTypeBreadcrumb) => (handlerData: unknown): void => { if (!this._isEnabled) { @@ -553,7 +597,7 @@ export class ReplayContainer implements ReplayContainerInterface { if (result.category === 'ui.click') { this.triggerUserActivity(); } else { - this.checkAndHandleExpiredSession(); + this._checkAndHandleExpiredSession(); } this.addUpdate(() => { @@ -576,7 +620,7 @@ export class ReplayContainer implements ReplayContainerInterface { /** * Tasks to run when we consider a page to be hidden (via blurring and/or visibility) */ - doChangeToBackgroundTasks(breadcrumb?: Breadcrumb): void { + private _doChangeToBackgroundTasks(breadcrumb?: Breadcrumb): void { if (!this.session) { return; } @@ -584,24 +628,24 @@ export class ReplayContainer implements ReplayContainerInterface { const expired = isSessionExpired(this.session, VISIBILITY_CHANGE_TIMEOUT); if (breadcrumb && !expired) { - this.createCustomBreadcrumb(breadcrumb); + this._createCustomBreadcrumb(breadcrumb); } // Send replay when the page/tab becomes hidden. There is no reason to send // replay if it becomes visible, since no actions we care about were done // while it was hidden - this.conditionalFlush(); + this._conditionalFlush(); } /** * Tasks to run when we consider a page to be visible (via focus and/or visibility) */ - doChangeToForegroundTasks(breadcrumb?: Breadcrumb): void { + private _doChangeToForegroundTasks(breadcrumb?: Breadcrumb): void { if (!this.session) { return; } - const isSessionActive = this.checkAndHandleExpiredSession({ + const isSessionActive = this._checkAndHandleExpiredSession({ expiry: VISIBILITY_CHANGE_TIMEOUT, }); @@ -614,7 +658,7 @@ export class ReplayContainer implements ReplayContainerInterface { } if (breadcrumb) { - this.createCustomBreadcrumb(breadcrumb); + this._createCustomBreadcrumb(breadcrumb); } } @@ -622,7 +666,7 @@ export class ReplayContainer implements ReplayContainerInterface { * Trigger rrweb to take a full snapshot which will cause this plugin to * create a new Replay event. */ - triggerFullSnapshot(): void { + private _triggerFullSnapshot(): void { __DEBUG_BUILD__ && logger.log('[Replay] Taking full rrweb snapshot'); record.takeFullSnapshot(true); } @@ -630,50 +674,24 @@ export class ReplayContainer implements ReplayContainerInterface { /** * Update user activity (across session lifespans) */ - updateUserActivity(_lastActivity: number = new Date().getTime()): void { + private _updateUserActivity(_lastActivity: number = new Date().getTime()): void { this._lastActivity = _lastActivity; } /** * Updates the session's last activity timestamp */ - updateSessionActivity(_lastActivity: number = new Date().getTime()): void { + private _updateSessionActivity(_lastActivity: number = new Date().getTime()): void { if (this.session) { this.session.lastActivity = _lastActivity; this._maybeSaveSession(); } } - /** - * Updates the user activity timestamp and resumes recording. This should be - * called in an event handler for a user action that we consider as the user - * being "active" (e.g. a mouse click). - */ - triggerUserActivity(): void { - this.updateUserActivity(); - - // This case means that recording was once stopped due to inactivity. - // Ensure that recording is resumed. - if (!this._stopRecording) { - // Create a new session, otherwise when the user action is flushed, it - // will get rejected due to an expired session. - this.loadSession({ expiry: SESSION_IDLE_DURATION }); - - // Note: This will cause a new DOM checkout - this.resume(); - return; - } - - // Otherwise... recording was never suspended, continue as normalish - this.checkAndHandleExpiredSession(); - - this.updateSessionActivity(); - } - /** * Helper to create (and buffer) a replay breadcrumb from a core SDK breadcrumb */ - createCustomBreadcrumb(breadcrumb: Breadcrumb): void { + private _createCustomBreadcrumb(breadcrumb: Breadcrumb): void { this.addUpdate(() => { void addEvent(this, { type: EventType.Custom, @@ -690,7 +708,7 @@ export class ReplayContainer implements ReplayContainerInterface { * Observed performance events are added to `this.performanceEvents`. These * are included in the replay event before it is finished and sent to Sentry. */ - addPerformanceEntries(): Promise> { + private _addPerformanceEntries(): Promise> { // Copy and reset entries before processing const entries = [...this.performanceEvents]; this.performanceEvents = []; @@ -705,7 +723,7 @@ export class ReplayContainer implements ReplayContainerInterface { * * Returns true if session is not expired, false otherwise. */ - checkAndHandleExpiredSession({ expiry = SESSION_IDLE_DURATION }: { expiry?: number } = {}): boolean | void { + private _checkAndHandleExpiredSession({ expiry = SESSION_IDLE_DURATION }: { expiry?: number } = {}): boolean | void { const oldSessionId = this.session?.id; // Prevent starting a new session if the last user activity is older than @@ -720,7 +738,7 @@ export class ReplayContainer implements ReplayContainerInterface { // --- There is recent user activity --- // // This will create a new session if expired, based on expiry length - this.loadSession({ expiry }); + this._loadSession({ expiry }); // Session was expired if session ids do not match const expired = oldSessionId !== this.session?.id; @@ -730,7 +748,7 @@ export class ReplayContainer implements ReplayContainerInterface { } // Session is expired, trigger a full snapshot (which will create a new session) - this.triggerFullSnapshot(); + this._triggerFullSnapshot(); return false; } @@ -738,7 +756,7 @@ export class ReplayContainer implements ReplayContainerInterface { /** * Only flush if `this.recordingMode === 'session'` */ - conditionalFlush(): void { + private _conditionalFlush(): void { if (this.recordingMode === 'error') { return; } @@ -749,7 +767,7 @@ export class ReplayContainer implements ReplayContainerInterface { /** * Clear _context */ - clearContext(): void { + private _clearContext(): void { // XXX: `initialTimestamp` and `initialUrl` do not get cleared this._context.errorIds.clear(); this._context.traceIds.clear(); @@ -760,7 +778,7 @@ export class ReplayContainer implements ReplayContainerInterface { /** * Return and clear _context */ - popEventContext(): PopEventContext { + private _popEventContext(): PopEventContext { if (this._context.earliestEvent && this._context.earliestEvent < this._context.initialTimestamp) { this._context.initialTimestamp = this._context.earliestEvent; } @@ -773,7 +791,7 @@ export class ReplayContainer implements ReplayContainerInterface { urls: this._context.urls, }; - this.clearContext(); + this._clearContext(); return _context; } @@ -786,13 +804,13 @@ export class ReplayContainer implements ReplayContainerInterface { * * Should never be called directly, only by `flush` */ - async runFlush(): Promise { + private async _runFlush(): Promise { if (!this.session) { __DEBUG_BUILD__ && logger.error('[Replay] No session found to flush.'); return; } - await this.addPerformanceEntries(); + await this._addPerformanceEntries(); if (!this.eventBuffer?.pendingLength) { return; @@ -808,12 +826,12 @@ export class ReplayContainer implements ReplayContainerInterface { // NOTE: Copy values from instance members, as it's possible they could // change before the flush finishes. const replayId = this.session.id; - const eventContext = this.popEventContext(); + const eventContext = this._popEventContext(); // Always increment segmentId regardless of outcome of sending replay const segmentId = this.session.segmentId++; this._maybeSaveSession(); - await this.sendReplay({ + await this._sendReplay({ replayId, events: recordingData, segmentId, @@ -821,7 +839,7 @@ export class ReplayContainer implements ReplayContainerInterface { eventContext, }); } catch (err) { - this.handleException(err); + this._handleException(err); } } @@ -829,14 +847,14 @@ export class ReplayContainer implements ReplayContainerInterface { * Flush recording data to Sentry. Creates a lock so that only a single flush * can be active at a time. Do not call this directly. */ - flush: () => Promise = async () => { + private _flush: () => Promise = async () => { if (!this._isEnabled) { // This is just a precaution, there should be no listeners that would // cause a flush. return; } - if (!this.checkAndHandleExpiredSession()) { + if (!this._checkAndHandleExpiredSession()) { __DEBUG_BUILD__ && logger.error('[Replay] Attempting to finish replay event after session expired.'); return; } @@ -849,16 +867,16 @@ export class ReplayContainer implements ReplayContainerInterface { // A flush is about to happen, cancel any queued flushes this._debouncedFlush?.cancel(); - // this._flushLock acts as a lock so that future calls to `flush()` + // this._flushLock acts as a lock so that future calls to `_flush()` // will be blocked until this promise resolves if (!this._flushLock) { - this._flushLock = this.runFlush(); + this._flushLock = this._runFlush(); await this._flushLock; this._flushLock = null; return; } - // Wait for previous flush to finish, then call the debounced `flush()`. + // Wait for previous flush to finish, then call the debounced `_flush()`. // It's possible there are other flush requests queued and waiting for it // to resolve. We want to reduce all outstanding requests (as well as any // new flush requests that occur within a second of the locked flush @@ -873,22 +891,10 @@ export class ReplayContainer implements ReplayContainerInterface { } }; - /** - * - * Always flush via `_debouncedFlush` so that we do not have flushes triggered - * from calling both `flush` and `_debouncedFlush`. Otherwise, there could be - * cases of mulitple flushes happening closely together. - */ - flushImmediate(): Promise { - this._debouncedFlush(); - // `.flush` is provided by the debounced function, analogously to lodash.debounce - return this._debouncedFlush.flush() as Promise; - } - /** * Send replay attachment using `fetch()` */ - async sendReplayRequest({ + private async _sendReplayRequest({ events, replayId, segmentId: segment_id, @@ -991,7 +997,7 @@ export class ReplayContainer implements ReplayContainerInterface { /** * Reset the counter of retries for sending replays. */ - resetRetries(): void { + private _resetRetries(): void { this._retryCount = 0; this._retryInterval = BASE_RETRY_INTERVAL; } @@ -999,34 +1005,34 @@ export class ReplayContainer implements ReplayContainerInterface { /** * Finalize and send the current replay event to Sentry */ - async sendReplay({ + private async _sendReplay({ replayId, events, segmentId, includeReplayStartTimestamp, eventContext, }: SendReplay): Promise { - // short circuit if there's no events to upload (this shouldn't happen as runFlush makes this check) + // short circuit if there's no events to upload (this shouldn't happen as _runFlush makes this check) if (!events.length) { return; } try { - await this.sendReplayRequest({ + await this._sendReplayRequest({ events, replayId, segmentId, includeReplayStartTimestamp, eventContext, }); - this.resetRetries(); + this._resetRetries(); return true; } catch (err) { // Capture error for every failed replay setContext('Replays', { _retryCount: this._retryCount, }); - this.handleException(err); + this._handleException(err); // If an error happened here, it's likely that uploading the attachment // failed, we'll can retry with the same events payload @@ -1041,7 +1047,7 @@ export class ReplayContainer implements ReplayContainerInterface { return await new Promise((resolve, reject) => { setTimeout(async () => { try { - await this.sendReplay({ + await this._sendReplay({ replayId, events, segmentId, diff --git a/packages/replay/test/integration/autoSaveSession.test.ts b/packages/replay/test/integration/autoSaveSession.test.ts index 5cdcf3a6525b..fb047e07bcc8 100644 --- a/packages/replay/test/integration/autoSaveSession.test.ts +++ b/packages/replay/test/integration/autoSaveSession.test.ts @@ -35,7 +35,7 @@ describe('Integration | autoSaveSession', () => { // Initially called up to three times: once for start, then once for replay.updateSessionActivity & once for segmentId increase expect(saveSessionSpy).toHaveBeenCalledTimes(addSummand * 3); - replay.updateSessionActivity(); + replay['_updateSessionActivity'](); expect(saveSessionSpy).toHaveBeenCalledTimes(addSummand * 4); @@ -50,7 +50,7 @@ describe('Integration | autoSaveSession', () => { addEvent(replay, event); - await replay.runFlush(); + await replay['_runFlush'](); expect(saveSessionSpy).toHaveBeenCalledTimes(addSummand * 5); }); diff --git a/packages/replay/test/integration/events.test.ts b/packages/replay/test/integration/events.test.ts index ed74b6978fe6..9bfe34484ce6 100644 --- a/packages/replay/test/integration/events.test.ts +++ b/packages/replay/test/integration/events.test.ts @@ -23,7 +23,7 @@ describe('Integration | events', () => { let mockTransportSend: jest.SpyInstance; const prevLocation = WINDOW.location; - type MockSendReplayRequest = jest.MockedFunction; + type MockSendReplayRequest = jest.MockedFunction; let mockSendReplayRequest: MockSendReplayRequest; beforeAll(async () => { @@ -40,16 +40,14 @@ describe('Integration | events', () => { mockTransportSend = jest.spyOn(getCurrentHub().getClient()!.getTransport()!, 'send'); - jest.spyOn(replay, 'flush'); - jest.spyOn(replay, 'runFlush'); - jest.spyOn(replay, 'sendReplayRequest'); + // @ts-ignore private API + mockSendReplayRequest = jest.spyOn(replay, '_sendReplayRequest'); // Create a new session and clear mocks because a segment (from initial // checkout) will have already been uploaded by the time the tests run clearSession(replay); - replay.loadSession({ expiry: 0 }); + replay['_loadSession']({ expiry: 0 }); mockTransportSend.mockClear(); - mockSendReplayRequest = replay.sendReplayRequest as MockSendReplayRequest; mockSendReplayRequest.mockClear(); }); @@ -103,7 +101,7 @@ describe('Integration | events', () => { it('has correct timestamps when there are events earlier than initial timestamp', async function () { clearSession(replay); - replay.loadSession({ expiry: 0 }); + replay['_loadSession']({ expiry: 0 }); mockTransportSend.mockClear(); Object.defineProperty(document, 'visibilityState', { configurable: true, diff --git a/packages/replay/test/integration/flush.test.ts b/packages/replay/test/integration/flush.test.ts index 7e9277b9d47a..f2760ef43371 100644 --- a/packages/replay/test/integration/flush.test.ts +++ b/packages/replay/test/integration/flush.test.ts @@ -2,6 +2,7 @@ import * as SentryUtils from '@sentry/utils'; import { DEFAULT_FLUSH_MIN_DELAY, SESSION_IDLE_DURATION, WINDOW } from '../../src/constants'; import type { ReplayContainer } from '../../src/replay'; +import type { EventBuffer } from '../../src/types'; import * as AddMemoryEntry from '../../src/util/addMemoryEntry'; import { createPerformanceEntries } from '../../src/util/createPerformanceEntries'; import { createPerformanceSpans } from '../../src/util/createPerformanceSpans'; @@ -16,12 +17,12 @@ async function advanceTimers(time: number) { await new Promise(process.nextTick); } -type MockSendReplay = jest.MockedFunction; -type MockAddPerformanceEntries = jest.MockedFunction; +type MockSendReplay = jest.MockedFunction; +type MockAddPerformanceEntries = jest.MockedFunction; type MockAddMemoryEntry = jest.SpyInstance; -type MockEventBufferFinish = jest.MockedFunction['finish']>; -type MockFlush = jest.MockedFunction; -type MockRunFlush = jest.MockedFunction; +type MockEventBufferFinish = jest.MockedFunction; +type MockFlush = jest.MockedFunction; +type MockRunFlush = jest.MockedFunction; const prevLocation = WINDOW.location; @@ -46,22 +47,23 @@ describe('Integration | flush', () => { }); ({ replay } = await mockSdk()); - jest.spyOn(replay, 'sendReplay'); - mockSendReplay = replay.sendReplay as MockSendReplay; + + // @ts-ignore private API + mockSendReplay = jest.spyOn(replay, '_sendReplay'); mockSendReplay.mockImplementation( jest.fn(async () => { return; }), ); - jest.spyOn(replay, 'flush'); - mockFlush = replay.flush as MockFlush; + // @ts-ignore private API + mockFlush = jest.spyOn(replay, '_flush'); - jest.spyOn(replay, 'runFlush'); - mockRunFlush = replay.runFlush as MockRunFlush; + // @ts-ignore private API + mockRunFlush = jest.spyOn(replay, '_runFlush'); - jest.spyOn(replay, 'addPerformanceEntries'); - mockAddPerformanceEntries = replay.addPerformanceEntries as MockAddPerformanceEntries; + // @ts-ignore private API + mockAddPerformanceEntries = jest.spyOn(replay, '_addPerformanceEntries'); mockAddPerformanceEntries.mockImplementation(async () => { return []; @@ -93,7 +95,7 @@ describe('Integration | flush', () => { jest.setSystemTime(new Date(BASE_TIMESTAMP)); sessionStorage.clear(); clearSession(replay); - replay.loadSession({ expiry: SESSION_IDLE_DURATION }); + replay['_loadSession']({ expiry: SESSION_IDLE_DURATION }); mockRecord.takeFullSnapshot.mockClear(); Object.defineProperty(WINDOW, 'location', { value: prevLocation, @@ -116,19 +118,19 @@ describe('Integration | flush', () => { WINDOW.dispatchEvent(new Event('blur')); WINDOW.dispatchEvent(new Event('blur')); - expect(replay.flush).toHaveBeenCalledTimes(4); + expect(mockFlush).toHaveBeenCalledTimes(4); jest.runAllTimers(); await new Promise(process.nextTick); - expect(replay.runFlush).toHaveBeenCalledTimes(1); + expect(mockRunFlush).toHaveBeenCalledTimes(1); jest.runAllTimers(); await new Promise(process.nextTick); - expect(replay.runFlush).toHaveBeenCalledTimes(2); + expect(mockRunFlush).toHaveBeenCalledTimes(2); jest.runAllTimers(); await new Promise(process.nextTick); - expect(replay.runFlush).toHaveBeenCalledTimes(2); + expect(mockRunFlush).toHaveBeenCalledTimes(2); }); it('long first flush enqueues following events', async () => { @@ -141,8 +143,8 @@ describe('Integration | flush', () => { // flush #1 @ t=0s - due to blur WINDOW.dispatchEvent(new Event('blur')); - expect(replay.flush).toHaveBeenCalledTimes(1); - expect(replay.runFlush).toHaveBeenCalledTimes(1); + expect(mockFlush).toHaveBeenCalledTimes(1); + expect(mockRunFlush).toHaveBeenCalledTimes(1); // This will attempt to flush in 5 seconds (flushMinDelay) domHandler({ @@ -150,28 +152,28 @@ describe('Integration | flush', () => { }); await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); // flush #2 @ t=5s - due to click - expect(replay.flush).toHaveBeenCalledTimes(2); + expect(mockFlush).toHaveBeenCalledTimes(2); await advanceTimers(1000); // flush #3 @ t=6s - due to blur WINDOW.dispatchEvent(new Event('blur')); - expect(replay.flush).toHaveBeenCalledTimes(3); + expect(mockFlush).toHaveBeenCalledTimes(3); // NOTE: Blur also adds a breadcrumb which calls `addUpdate`, meaning it will // flush after `flushMinDelay`, but this gets cancelled by the blur await advanceTimers(8000); - expect(replay.flush).toHaveBeenCalledTimes(3); + expect(mockFlush).toHaveBeenCalledTimes(3); // flush #4 @ t=14s - due to blur WINDOW.dispatchEvent(new Event('blur')); - expect(replay.flush).toHaveBeenCalledTimes(4); + expect(mockFlush).toHaveBeenCalledTimes(4); - expect(replay.runFlush).toHaveBeenCalledTimes(1); + expect(mockRunFlush).toHaveBeenCalledTimes(1); await advanceTimers(6000); // t=20s // addPerformanceEntries is finished, `flushLock` promise is resolved, calls // debouncedFlush, which will call `flush` in 1 second - expect(replay.flush).toHaveBeenCalledTimes(4); + expect(mockFlush).toHaveBeenCalledTimes(4); // sendReplay is called with replayId, events, segment expect(mockSendReplay).toHaveBeenLastCalledWith({ events: expect.any(String), @@ -218,8 +220,8 @@ describe('Integration | flush', () => { // flush #5 @ t=25s - debounced flush calls `flush` // 20s + `flushMinDelay` which is 5 seconds await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); - expect(replay.flush).toHaveBeenCalledTimes(5); - expect(replay.runFlush).toHaveBeenCalledTimes(2); + expect(mockFlush).toHaveBeenCalledTimes(5); + expect(mockRunFlush).toHaveBeenCalledTimes(2); expect(mockSendReplay).toHaveBeenLastCalledWith({ events: expect.any(String), replayId: expect.any(String), @@ -245,10 +247,10 @@ describe('Integration | flush', () => { mockRecord._emitter(TEST_EVENT); await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); - expect(replay.flush).toHaveBeenCalledTimes(1); + expect(mockFlush).toHaveBeenCalledTimes(1); // Make sure there's nothing queued up after await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); - expect(replay.flush).toHaveBeenCalledTimes(1); + expect(mockFlush).toHaveBeenCalledTimes(1); }); }); diff --git a/packages/replay/test/integration/sampling.test.ts b/packages/replay/test/integration/sampling.test.ts index d17052a4e7f6..049329ebda3e 100644 --- a/packages/replay/test/integration/sampling.test.ts +++ b/packages/replay/test/integration/sampling.test.ts @@ -16,8 +16,8 @@ describe('Integration | sampling', () => { }, }); - jest.spyOn(replay, 'loadSession'); - jest.spyOn(replay, 'addListeners'); + // @ts-ignore private API + const spyAddListeners = jest.spyOn(replay, '_addListeners'); jest.runAllTimers(); expect(replay.session?.sampled).toBe(false); @@ -28,6 +28,6 @@ describe('Integration | sampling', () => { }), ); expect(mockRecord).not.toHaveBeenCalled(); - expect(replay.addListeners).not.toHaveBeenCalled(); + expect(spyAddListeners).not.toHaveBeenCalled(); }); }); diff --git a/packages/replay/test/integration/sendReplayEvent.test.ts b/packages/replay/test/integration/sendReplayEvent.test.ts index d9db9ea42a28..ade380a1605a 100644 --- a/packages/replay/test/integration/sendReplayEvent.test.ts +++ b/packages/replay/test/integration/sendReplayEvent.test.ts @@ -17,7 +17,7 @@ async function advanceTimers(time: number) { } type MockTransportSend = jest.MockedFunction; -type MockSendReplayRequest = jest.MockedFunction; +type MockSendReplayRequest = jest.MockedFunction; describe('Integration | sendReplayEvent', () => { let replay: ReplayContainer; @@ -40,11 +40,11 @@ describe('Integration | sendReplayEvent', () => { }, })); - jest.spyOn(replay, 'sendReplayRequest'); + // @ts-ignore private API + mockSendReplayRequest = jest.spyOn(replay, '_sendReplayRequest'); jest.runAllTimers(); mockTransportSend = getCurrentHub()?.getClient()?.getTransport()?.send as MockTransportSend; - mockSendReplayRequest = replay.sendReplayRequest as MockSendReplayRequest; }); beforeEach(() => { @@ -55,7 +55,7 @@ describe('Integration | sendReplayEvent', () => { // Create a new session and clear mocks because a segment (from initial // checkout) will have already been uploaded by the time the tests run clearSession(replay); - replay.loadSession({ expiry: 0 }); + replay['_loadSession']({ expiry: 0 }); mockSendReplayRequest.mockClear(); }); @@ -65,7 +65,7 @@ describe('Integration | sendReplayEvent', () => { await new Promise(process.nextTick); jest.setSystemTime(new Date(BASE_TIMESTAMP)); clearSession(replay); - replay.loadSession({ expiry: SESSION_IDLE_DURATION }); + replay['_loadSession']({ expiry: SESSION_IDLE_DURATION }); }); afterAll(() => { @@ -382,13 +382,15 @@ describe('Integration | sendReplayEvent', () => { it('fails to upload data and hits retry max and stops', async () => { const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 3 }; - jest.spyOn(replay, 'sendReplay'); + + // @ts-ignore private API + const spySendReplay = jest.spyOn(replay, '_sendReplay'); // Suppress console.errors const mockConsole = jest.spyOn(console, 'error').mockImplementation(jest.fn()); - // Check errors - const spyHandleException = jest.spyOn(replay, 'handleException'); + // @ts-ignore privaye api - Check errors + const spyHandleException = jest.spyOn(replay, '_handleException'); expect(replay.session?.segmentId).toBe(0); @@ -402,24 +404,24 @@ describe('Integration | sendReplayEvent', () => { await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); - expect(replay.sendReplayRequest).toHaveBeenCalledTimes(1); + expect(mockSendReplayRequest).toHaveBeenCalledTimes(1); await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); - expect(replay.sendReplayRequest).toHaveBeenCalledTimes(2); + expect(mockSendReplayRequest).toHaveBeenCalledTimes(2); await advanceTimers(10000); - expect(replay.sendReplayRequest).toHaveBeenCalledTimes(3); + expect(mockSendReplayRequest).toHaveBeenCalledTimes(3); await advanceTimers(30000); - expect(replay.sendReplayRequest).toHaveBeenCalledTimes(4); - expect(replay.sendReplay).toHaveBeenCalledTimes(4); + expect(mockSendReplayRequest).toHaveBeenCalledTimes(4); + expect(spySendReplay).toHaveBeenCalledTimes(4); mockConsole.mockReset(); // Make sure it doesn't retry again jest.runAllTimers(); - expect(replay.sendReplayRequest).toHaveBeenCalledTimes(4); - expect(replay.sendReplay).toHaveBeenCalledTimes(4); + expect(mockSendReplayRequest).toHaveBeenCalledTimes(4); + expect(spySendReplay).toHaveBeenCalledTimes(4); // Retries = 3 (total tries = 4 including initial attempt) // + last exception is max retries exceeded diff --git a/packages/replay/test/integration/session.test.ts b/packages/replay/test/integration/session.test.ts index 46ef58622372..c07bac150751 100644 --- a/packages/replay/test/integration/session.test.ts +++ b/packages/replay/test/integration/session.test.ts @@ -451,7 +451,7 @@ describe('Integration | session', () => { it('increases segment id after each event', async () => { clearSession(replay); - replay.loadSession({ expiry: 0 }); + replay['_loadSession']({ expiry: 0 }); Object.defineProperty(document, 'visibilityState', { configurable: true, diff --git a/packages/replay/test/integration/stop.test.ts b/packages/replay/test/integration/stop.test.ts index 0b0e164cc8b8..42ccbe09ca68 100644 --- a/packages/replay/test/integration/stop.test.ts +++ b/packages/replay/test/integration/stop.test.ts @@ -44,7 +44,7 @@ describe('Integration | stop', () => { jest.setSystemTime(new Date(BASE_TIMESTAMP)); sessionStorage.clear(); clearSession(replay); - replay.loadSession({ expiry: SESSION_IDLE_DURATION }); + replay['_loadSession']({ expiry: SESSION_IDLE_DURATION }); mockRecord.takeFullSnapshot.mockClear(); mockAddInstrumentationHandler.mockClear(); Object.defineProperty(WINDOW, 'location', { diff --git a/packages/replay/test/unit/util/getReplayEvent.test.ts b/packages/replay/test/unit/util/getReplayEvent.test.ts deleted file mode 100644 index a7d18971c20e..000000000000 --- a/packages/replay/test/unit/util/getReplayEvent.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import type { Hub, Scope } from '@sentry/core'; -import { getCurrentHub } from '@sentry/core'; -import type { Client, ReplayEvent } from '@sentry/types'; - -import { REPLAY_EVENT_NAME } from '../../../src/constants'; -import { prepareReplayEvent } from '../../../src/util/prepareReplayEvent'; -import { getDefaultClientOptions, TestClient } from '../../utils/TestClient'; - -describe('Unit | util | getReplayEvent', () => { - let hub: Hub; - let client: Client; - let scope: Scope; - - beforeEach(() => { - hub = getCurrentHub(); - client = new TestClient(getDefaultClientOptions()); - hub.bindClient(client); - - client = hub.getClient()!; - scope = hub.getScope()!; - }); - - it('works', async () => { - expect(client).toBeDefined(); - expect(scope).toBeDefined(); - - const replayId = 'replay-ID'; - const event: ReplayEvent = { - // @ts-ignore private api - type: REPLAY_EVENT_NAME, - timestamp: 1670837008.634, - error_ids: ['error-ID'], - trace_ids: ['trace-ID'], - urls: ['https://sentry.io/'], - replay_id: replayId, - replay_type: 'session', - segment_id: 3, - }; - - const replayEvent = await prepareReplayEvent({ scope, client, replayId, event }); - - expect(replayEvent).toEqual({ - type: 'replay_event', - timestamp: 1670837008.634, - error_ids: ['error-ID'], - trace_ids: ['trace-ID'], - urls: ['https://sentry.io/'], - replay_id: 'replay-ID', - replay_type: 'session', - segment_id: 3, - platform: 'javascript', - event_id: 'replay-ID', - environment: 'production', - sdk: { - name: 'sentry.javascript.unknown', - version: 'version:Test', - }, - sdkProcessingMetadata: {}, - breadcrumbs: undefined, - }); - }); -}); From 2e281adc429422c3579be5aff67fb063cb2e01b4 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 12 Jan 2023 14:36:18 +0100 Subject: [PATCH 050/113] test(replay): Add basic replay integration tests (#6735) --- .../suites/replay/captureReplay/template.html | 9 +++ .../suites/replay/captureReplay/test.ts | 67 +++++++++++++++++++ .../integration-tests/suites/replay/init.js | 16 +++++ .../suites/replay/sampling/init.js | 16 +++++ .../suites/replay/sampling/template.html | 9 +++ .../suites/replay/sampling/test.ts | 34 ++++++++++ packages/integration-tests/utils/helpers.ts | 31 +++++++-- 7 files changed, 175 insertions(+), 7 deletions(-) create mode 100644 packages/integration-tests/suites/replay/captureReplay/template.html create mode 100644 packages/integration-tests/suites/replay/captureReplay/test.ts create mode 100644 packages/integration-tests/suites/replay/init.js create mode 100644 packages/integration-tests/suites/replay/sampling/init.js create mode 100644 packages/integration-tests/suites/replay/sampling/template.html create mode 100644 packages/integration-tests/suites/replay/sampling/test.ts diff --git a/packages/integration-tests/suites/replay/captureReplay/template.html b/packages/integration-tests/suites/replay/captureReplay/template.html new file mode 100644 index 000000000000..2b3e2f0b27b4 --- /dev/null +++ b/packages/integration-tests/suites/replay/captureReplay/template.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/packages/integration-tests/suites/replay/captureReplay/test.ts b/packages/integration-tests/suites/replay/captureReplay/test.ts new file mode 100644 index 000000000000..80a3f52201dd --- /dev/null +++ b/packages/integration-tests/suites/replay/captureReplay/test.ts @@ -0,0 +1,67 @@ +import { expect } from '@playwright/test'; +import { SDK_VERSION } from '@sentry/browser'; +import type { Event } from '@sentry/types'; + +import { sentryTest } from '../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest } from '../../../utils/helpers'; + +sentryTest('captureReplay', async ({ getLocalTestPath, page }) => { + // Currently bundle tests are not supported for replay + if (process.env.PW_BUNDLE && process.env.PW_BUNDLE.startsWith('bundle_')) { + sentryTest.skip(); + } + + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); + }); + + const url = await getLocalTestPath({ testDir: __dirname }); + await page.goto(url); + + await page.click('button'); + await page.waitForTimeout(200); + + const replayEvent = await getFirstSentryEnvelopeRequest(page, url); + + expect(replayEvent).toBeDefined(); + expect(replayEvent).toEqual({ + type: 'replay_event', + timestamp: expect.any(Number), + error_ids: [], + trace_ids: [], + urls: [expect.stringContaining('/dist/index.html')], + replay_id: expect.stringMatching(/\w{32}/), + segment_id: 2, + replay_type: 'session', + event_id: expect.stringMatching(/\w{32}/), + environment: 'production', + sdk: { + integrations: [ + 'InboundFilters', + 'FunctionToString', + 'TryCatch', + 'Breadcrumbs', + 'GlobalHandlers', + 'LinkedErrors', + 'Dedupe', + 'HttpContext', + 'Replay', + ], + version: SDK_VERSION, + name: 'sentry.javascript.browser', + }, + sdkProcessingMetadata: {}, + request: { + url: expect.stringContaining('/dist/index.html'), + headers: { + 'User-Agent': expect.stringContaining(''), + }, + }, + platform: 'javascript', + tags: { sessionSampleRate: 1, errorSampleRate: 0 }, + }); +}); diff --git a/packages/integration-tests/suites/replay/init.js b/packages/integration-tests/suites/replay/init.js new file mode 100644 index 000000000000..9050f274417c --- /dev/null +++ b/packages/integration-tests/suites/replay/init.js @@ -0,0 +1,16 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; +window.Replay = new Sentry.Replay({ + flushMinDelay: 200, + initialFlushDelay: 200, +}); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 0, + replaysSessionSampleRate: 1.0, + replaysOnErrorSampleRate: 0.0, + + integrations: [window.Replay], +}); diff --git a/packages/integration-tests/suites/replay/sampling/init.js b/packages/integration-tests/suites/replay/sampling/init.js new file mode 100644 index 000000000000..67b681515697 --- /dev/null +++ b/packages/integration-tests/suites/replay/sampling/init.js @@ -0,0 +1,16 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; +window.Replay = new Sentry.Replay({ + flushMinDelay: 200, + initialFlushDelay: 200, +}); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 0, + replaysSessionSampleRate: 0.0, + replaysOnErrorSampleRate: 0.0, + + integrations: [window.Replay], +}); diff --git a/packages/integration-tests/suites/replay/sampling/template.html b/packages/integration-tests/suites/replay/sampling/template.html new file mode 100644 index 000000000000..2b3e2f0b27b4 --- /dev/null +++ b/packages/integration-tests/suites/replay/sampling/template.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/packages/integration-tests/suites/replay/sampling/test.ts b/packages/integration-tests/suites/replay/sampling/test.ts new file mode 100644 index 000000000000..9a351ce30f6f --- /dev/null +++ b/packages/integration-tests/suites/replay/sampling/test.ts @@ -0,0 +1,34 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../utils/fixtures'; +import { getReplaySnapshot } from '../../../utils/helpers'; + +sentryTest('sampling', async ({ getLocalTestPath, page }) => { + // Currently bundle tests are not supported for replay + if (process.env.PW_BUNDLE && process.env.PW_BUNDLE.startsWith('bundle_')) { + sentryTest.skip(); + } + + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + // This should never be called! + expect(true).toBe(false); + + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); + }); + + const url = await getLocalTestPath({ testDir: __dirname }); + await page.goto(url); + + await page.click('button'); + await page.waitForTimeout(200); + + const replay = await getReplaySnapshot(page); + + expect(replay.session?.sampled).toBe(false); + + // Cannot wait on getFirstSentryEnvelopeRequest, as that never resolves +}); diff --git a/packages/integration-tests/utils/helpers.ts b/packages/integration-tests/utils/helpers.ts index 12078c09039e..54e1cd44a0bb 100644 --- a/packages/integration-tests/utils/helpers.ts +++ b/packages/integration-tests/utils/helpers.ts @@ -1,4 +1,5 @@ import type { Page, Request } from '@playwright/test'; +import type { ReplayContainer } from '@sentry/replay/build/npm/types/types'; import type { Event, EventEnvelopeHeaders } from '@sentry/types'; const envelopeUrlRegex = /\.sentry\.io\/api\/\d+\/envelope\//; @@ -8,7 +9,13 @@ const envelopeRequestParser = (request: Request | null): Event => { const envelope = request?.postData() || ''; // Third row of the envelop is the event payload. - return envelope.split('\n').map(line => JSON.parse(line))[2]; + return envelope.split('\n').map(line => { + try { + return JSON.parse(line); + } catch (error) { + return line; + } + })[2]; }; export const envelopeHeaderRequestParser = (request: Request | null): EventEnvelopeHeaders => { @@ -46,24 +53,34 @@ async function getSentryEvents(page: Page, url?: string): Promise> return eventsHandle.jsonValue(); } +/** + * This returns the replay container (assuming it exists). + * Note that due to how this works with playwright, this is a POJO copy of replay. + * This means that we cannot access any methods on it, and also not mutate it in any way. + */ +export async function getReplaySnapshot(page: Page): Promise { + const replayIntegration = await page.evaluate<{ _replay: ReplayContainer }>('window.Replay'); + return replayIntegration._replay; +} + /** * Waits until a number of requests matching urlRgx at the given URL arrive. * If the timout option is configured, this function will abort waiting, even if it hasn't reveived the configured * amount of requests, and returns all the events recieved up to that point in time. */ -async function getMultipleRequests( +async function getMultipleRequests( page: Page, count: number, urlRgx: RegExp, - requestParser: (req: Request) => Event, + requestParser: (req: Request) => T, options?: { url?: string; timeout?: number; }, -): Promise { - const requests: Promise = new Promise((resolve, reject) => { +): Promise { + const requests: Promise = new Promise((resolve, reject) => { let reqCount = count; - const requestData: Event[] = []; + const requestData: T[] = []; let timeoutId: NodeJS.Timeout | undefined = undefined; function requestHandler(request: Request): void { @@ -115,7 +132,7 @@ async function getMultipleSentryEnvelopeRequests( ): Promise { // TODO: This is not currently checking the type of envelope, just casting for now. // We can update this to include optional type-guarding when we have types for Envelope. - return getMultipleRequests(page, count, envelopeUrlRegex, requestParser, options) as Promise; + return getMultipleRequests(page, count, envelopeUrlRegex, requestParser, options) as Promise; } /** From 24cbc4cb82686409b87fb0382a46e8ad193b25d4 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Thu, 12 Jan 2023 15:33:28 +0100 Subject: [PATCH 051/113] test: Adjust long task integration test ui span check (#6754) Make the check in `should capture long task` integration test check for any number of ui spans instead of just one. In some situations in CI, there can be multiple long tasks instead of one (due to environmental conditions), so assert just to make sure they exist (and they are being recorded). --- .../suites/tracing/browsertracing/long-tasks-enabled/test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/integration-tests/suites/tracing/browsertracing/long-tasks-enabled/test.ts b/packages/integration-tests/suites/tracing/browsertracing/long-tasks-enabled/test.ts index a91e68c8d1bf..9ee877e39268 100644 --- a/packages/integration-tests/suites/tracing/browsertracing/long-tasks-enabled/test.ts +++ b/packages/integration-tests/suites/tracing/browsertracing/long-tasks-enabled/test.ts @@ -18,7 +18,7 @@ sentryTest('should capture long task.', async ({ browserName, getLocalTestPath, const eventData = await getFirstSentryEnvelopeRequest(page, url); const uiSpans = eventData.spans?.filter(({ op }) => op?.startsWith('ui')); - expect(uiSpans?.length).toBe(1); + expect(uiSpans?.length).toBeGreaterThan(0); const [firstUISpan] = uiSpans || []; expect(firstUISpan).toEqual( From b921631952e82476b3af61afbbcd00420198bfb7 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Thu, 12 Jan 2023 15:58:06 +0100 Subject: [PATCH 052/113] ref(replay): Pause recording when replay requests are rate-limited (#6733) Add the following rate-limiting strategy to the Replay integration: If we receive a rate limit response from the server after sending a replay event, we pause recording and flushing for the received rate limit period. After this period, we resume recording the same session, which triggers a full snapshot and hence should give us working replays. This should reduce segment-loss due to rate limiting, as we previously ignored handling rate limits specifically for replay (other than the transport's internal rate-limiting). --- .../suites/replay/captureReplay/test.ts | 2 +- packages/replay/src/replay.ts | 46 ++- .../test/integration/rateLimiting.test.ts | 301 ++++++++++++++++++ 3 files changed, 344 insertions(+), 5 deletions(-) create mode 100644 packages/replay/test/integration/rateLimiting.test.ts diff --git a/packages/integration-tests/suites/replay/captureReplay/test.ts b/packages/integration-tests/suites/replay/captureReplay/test.ts index 80a3f52201dd..51a1d900f9ed 100644 --- a/packages/integration-tests/suites/replay/captureReplay/test.ts +++ b/packages/integration-tests/suites/replay/captureReplay/test.ts @@ -23,7 +23,7 @@ sentryTest('captureReplay', async ({ getLocalTestPath, page }) => { await page.goto(url); await page.click('button'); - await page.waitForTimeout(200); + await page.waitForTimeout(300); const replayEvent = await getFirstSentryEnvelopeRequest(page, url); diff --git a/packages/replay/src/replay.ts b/packages/replay/src/replay.ts index e82908defa58..06a8ae58f64a 100644 --- a/packages/replay/src/replay.ts +++ b/packages/replay/src/replay.ts @@ -1,7 +1,8 @@ /* eslint-disable max-lines */ // TODO: We might want to split this file up import { addGlobalEventProcessor, captureException, getCurrentHub, setContext } from '@sentry/core'; import type { Breadcrumb, ReplayEvent, ReplayRecordingMode, TransportMakeRequestResponse } from '@sentry/types'; -import { addInstrumentationHandler, logger } from '@sentry/utils'; +import type { RateLimits } from '@sentry/utils'; +import { addInstrumentationHandler, disabledUntil, isRateLimited, logger, updateRateLimits } from '@sentry/utils'; import { EventType, record } from 'rrweb'; import { @@ -128,6 +129,11 @@ export class ReplayContainer implements ReplayContainerInterface { initialUrl: '', }; + /** + * A RateLimits object holding the rate-limit durations in case a sent replay event was rate-limited. + */ + private _rateLimits: RateLimits = {}; + public constructor({ options, recordingOptions, @@ -988,7 +994,15 @@ export class ReplayContainer implements ReplayContainerInterface { const envelope = createReplayEnvelope(replayEvent, recordingData, dsn, client.getOptions().tunnel); try { - return await transport.send(envelope); + const response = await transport.send(envelope); + // TODO (v8): we can remove this guard once transport.send's type signature doesn't include void anymore + if (response) { + this._rateLimits = updateRateLimits(this._rateLimits, response); + if (isRateLimited(this._rateLimits, 'replay')) { + this._handleRateLimit(); + } + } + return response; } catch { throw new Error(UNABLE_TO_SEND_REPLAY); } @@ -1040,9 +1054,8 @@ export class ReplayContainer implements ReplayContainerInterface { throw new Error(`${UNABLE_TO_SEND_REPLAY} - max retries exceeded`); } - this._retryCount = this._retryCount + 1; // will retry in intervals of 5, 10, 30 - this._retryInterval = this._retryCount * this._retryInterval; + this._retryInterval = ++this._retryCount * this._retryInterval; return await new Promise((resolve, reject) => { setTimeout(async () => { @@ -1069,4 +1082,29 @@ export class ReplayContainer implements ReplayContainerInterface { saveSession(this.session); } } + + /** + * Pauses the replay and resumes it after the rate-limit duration is over. + */ + private _handleRateLimit(): void { + // in case recording is already paused, we don't need to do anything, as we might have already paused because of a + // rate limit + if (this.isPaused()) { + return; + } + + const rateLimitEnd = disabledUntil(this._rateLimits, 'replay'); + const rateLimitDuration = rateLimitEnd - Date.now(); + + if (rateLimitDuration > 0) { + __DEBUG_BUILD__ && logger.warn('[Replay]', `Rate limit hit, pausing replay for ${rateLimitDuration}ms`); + this.pause(); + this._debouncedFlush && this._debouncedFlush.cancel(); + + setTimeout(() => { + __DEBUG_BUILD__ && logger.info('[Replay]', 'Resuming replay after rate limit'); + this.resume(); + }, rateLimitDuration); + } + } } diff --git a/packages/replay/test/integration/rateLimiting.test.ts b/packages/replay/test/integration/rateLimiting.test.ts new file mode 100644 index 000000000000..1270d7c9d780 --- /dev/null +++ b/packages/replay/test/integration/rateLimiting.test.ts @@ -0,0 +1,301 @@ +import { getCurrentHub } from '@sentry/core'; +import type { Transport, TransportMakeRequestResponse } from '@sentry/types'; + +import { DEFAULT_FLUSH_MIN_DELAY, SESSION_IDLE_DURATION } from '../../src/constants'; +import type { ReplayContainer } from '../../src/replay'; +import { BASE_TIMESTAMP, mockSdk } from '../index'; +import { mockRrweb } from '../mocks/mockRrweb'; +import { clearSession } from '../utils/clearSession'; +import { useFakeTimers } from '../utils/use-fake-timers'; + +useFakeTimers(); + +async function advanceTimers(time: number) { + jest.advanceTimersByTime(time); + await new Promise(process.nextTick); +} + +type MockTransportSend = jest.MockedFunction; +type MockSendReplayRequest = jest.MockedFunction; + +describe('Integration | rate-limiting behaviour', () => { + let replay: ReplayContainer; + let mockTransportSend: MockTransportSend; + let mockSendReplayRequest: MockSendReplayRequest; + const { record: mockRecord } = mockRrweb(); + + beforeAll(async () => { + jest.setSystemTime(new Date(BASE_TIMESTAMP)); + + ({ replay } = await mockSdk({ + replayOptions: { + stickySession: false, + }, + })); + + // @ts-ignore private API + jest.spyOn(replay, '_sendReplayRequest'); + + jest.runAllTimers(); + mockTransportSend = getCurrentHub()?.getClient()?.getTransport()?.send as MockTransportSend; + mockSendReplayRequest = replay['_sendReplayRequest'] as MockSendReplayRequest; + }); + + beforeEach(() => { + jest.setSystemTime(new Date(BASE_TIMESTAMP)); + mockRecord.takeFullSnapshot.mockClear(); + mockTransportSend.mockClear(); + + // Create a new session and clear mocks because a segment (from initial + // checkout) will have already been uploaded by the time the tests run + clearSession(replay); + replay['_loadSession']({ expiry: 0 }); + + mockSendReplayRequest.mockClear(); + + replay['_rateLimits'] = {}; + }); + + afterEach(async () => { + jest.runAllTimers(); + await new Promise(process.nextTick); + jest.setSystemTime(new Date(BASE_TIMESTAMP)); + clearSession(replay); + jest.clearAllMocks(); + replay['_loadSession']({ expiry: SESSION_IDLE_DURATION }); + }); + + afterAll(() => { + replay && replay.stop(); + }); + + it.each([ + { + statusCode: 429, + headers: { + 'x-sentry-rate-limits': '30', + 'retry-after': null, + }, + }, + { + statusCode: 429, + headers: { + 'x-sentry-rate-limits': '30:replay', + 'retry-after': null, + }, + }, + { + statusCode: 429, + headers: { + 'x-sentry-rate-limits': null, + 'retry-after': '30', + }, + }, + ] as TransportMakeRequestResponse[])( + 'pauses recording and flushing a rate limit is hit and resumes both after the rate limit duration is over', + async rateLimitResponse => { + expect(replay.session?.segmentId).toBe(0); + jest.spyOn(replay, 'pause'); + jest.spyOn(replay, 'resume'); + // @ts-ignore private API + jest.spyOn(replay, '_handleRateLimit'); + // @ts-ignore private API + jest.spyOn(replay, '_sendReplay'); + + const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 2 }; + + mockTransportSend.mockImplementationOnce(() => { + return Promise.resolve(rateLimitResponse); + }); + + mockRecord._emitter(TEST_EVENT); + + // T = base + 5 + await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); + + expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); + expect(mockTransportSend).toHaveBeenCalledTimes(1); + expect(replay).toHaveLastSentReplay({ events: JSON.stringify([TEST_EVENT]) }); + + expect(replay['_handleRateLimit']).toHaveBeenCalledTimes(1); + // resume() was called once before we even started + expect(replay.resume).not.toHaveBeenCalled(); + expect(replay.pause).toHaveBeenCalledTimes(1); + + // No user activity to trigger an update + expect(replay.session?.lastActivity).toBe(BASE_TIMESTAMP); + expect(replay.session?.segmentId).toBe(1); + + // let's simulate the rate-limit time of inactivity (30secs) and check that we don't do anything in the meantime + const TEST_EVENT2 = { data: {}, timestamp: BASE_TIMESTAMP + DEFAULT_FLUSH_MIN_DELAY, type: 3 }; + for (let i = 0; i < 5; i++) { + const ev = { + ...TEST_EVENT2, + timestamp: BASE_TIMESTAMP + DEFAULT_FLUSH_MIN_DELAY * (i + 1), + }; + mockRecord._emitter(ev); + await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); + expect(replay.isPaused()).toBe(true); + expect(replay['_sendReplay']).toHaveBeenCalledTimes(1); + expect(mockTransportSend).toHaveBeenCalledTimes(1); + } + + // T = base + 35 + await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); + + // now, recording should resume and first, we expect a checkout event to be sent, as resume() + // should trigger a full snapshot + expect(replay.resume).toHaveBeenCalledTimes(1); + expect(replay.isPaused()).toBe(false); + + expect(replay['_sendReplay']).toHaveBeenCalledTimes(2); + expect(replay).toHaveLastSentReplay({ + events: JSON.stringify([ + { data: { isCheckout: true }, timestamp: BASE_TIMESTAMP + DEFAULT_FLUSH_MIN_DELAY * 7, type: 2 }, + ]), + }); + + // and let's also emit a new event and check that it is recorded + const TEST_EVENT3 = { + data: {}, + timestamp: BASE_TIMESTAMP + 7 * DEFAULT_FLUSH_MIN_DELAY, + type: 3, + }; + mockRecord._emitter(TEST_EVENT3); + + // T = base + 40 + await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); + expect(replay['_sendReplay']).toHaveBeenCalledTimes(3); + expect(replay).toHaveLastSentReplay({ events: JSON.stringify([TEST_EVENT3]) }); + + // nothing should happen afterwards + // T = base + 60 + await advanceTimers(20_000); + expect(replay['_sendReplay']).toHaveBeenCalledTimes(3); + expect(replay).toHaveLastSentReplay({ events: JSON.stringify([TEST_EVENT3]) }); + + // events array should be empty + expect(replay.eventBuffer?.pendingLength).toBe(0); + }, + ); + + it('handles rate-limits from a plain 429 response without any retry time', async () => { + expect(replay.session?.segmentId).toBe(0); + jest.spyOn(replay, 'pause'); + jest.spyOn(replay, 'resume'); + // @ts-ignore private API + jest.spyOn(replay, '_handleRateLimit'); + // @ts-ignore private API + jest.spyOn(replay, '_sendReplay'); + + const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 2 }; + + mockTransportSend.mockImplementationOnce(() => { + return Promise.resolve({ statusCode: 429 }); + }); + + mockRecord._emitter(TEST_EVENT); + + // T = base + 5 + await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); + + expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); + expect(mockTransportSend).toHaveBeenCalledTimes(1); + expect(replay).toHaveLastSentReplay({ events: JSON.stringify([TEST_EVENT]) }); + + expect(replay['_handleRateLimit']).toHaveBeenCalledTimes(1); + // resume() was called once before we even started + expect(replay.resume).not.toHaveBeenCalled(); + expect(replay.pause).toHaveBeenCalledTimes(1); + + // No user activity to trigger an update + expect(replay.session?.lastActivity).toBe(BASE_TIMESTAMP); + expect(replay.session?.segmentId).toBe(1); + + // let's simulate the rate-limit time of inactivity (60secs) and check that we don't do anything in the meantime + // 60secs are the default we fall back to in the plain 429 case in updateRateLimits() + const TEST_EVENT2 = { data: {}, timestamp: BASE_TIMESTAMP + DEFAULT_FLUSH_MIN_DELAY, type: 3 }; + for (let i = 0; i < 11; i++) { + const ev = { + ...TEST_EVENT2, + timestamp: BASE_TIMESTAMP + DEFAULT_FLUSH_MIN_DELAY * (i + 1), + }; + mockRecord._emitter(ev); + await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); + expect(replay.isPaused()).toBe(true); + expect(replay['_sendReplay']).toHaveBeenCalledTimes(1); + expect(mockTransportSend).toHaveBeenCalledTimes(1); + } + + // T = base + 60 + await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); + + // now, recording should resume and first, we expect a checkout event to be sent, as resume() + // should trigger a full snapshot + expect(replay.resume).toHaveBeenCalledTimes(1); + expect(replay.isPaused()).toBe(false); + + expect(replay['_sendReplay']).toHaveBeenCalledTimes(2); + expect(replay).toHaveLastSentReplay({ + events: JSON.stringify([ + { data: { isCheckout: true }, timestamp: BASE_TIMESTAMP + DEFAULT_FLUSH_MIN_DELAY * 13, type: 2 }, + ]), + }); + + // and let's also emit a new event and check that it is recorded + const TEST_EVENT3 = { + data: {}, + timestamp: BASE_TIMESTAMP + 7 * DEFAULT_FLUSH_MIN_DELAY, + type: 3, + }; + mockRecord._emitter(TEST_EVENT3); + + // T = base + 65 + await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); + expect(replay['_sendReplay']).toHaveBeenCalledTimes(3); + expect(replay).toHaveLastSentReplay({ events: JSON.stringify([TEST_EVENT3]) }); + + // nothing should happen afterwards + // T = base + 85 + await advanceTimers(20_000); + expect(replay['_sendReplay']).toHaveBeenCalledTimes(3); + expect(replay).toHaveLastSentReplay({ events: JSON.stringify([TEST_EVENT3]) }); + + // events array should be empty + expect(replay.eventBuffer?.pendingLength).toBe(0); + }); + + it("doesn't do anything, if a rate limit is hit and recording is already paused", async () => { + let paused = false; + expect(replay.session?.segmentId).toBe(0); + jest.spyOn(replay, 'isPaused').mockImplementation(() => { + return paused; + }); + jest.spyOn(replay, 'pause'); + jest.spyOn(replay, 'resume'); + // @ts-ignore private API + jest.spyOn(replay, '_handleRateLimit'); + + const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 2 }; + + mockTransportSend.mockImplementationOnce(() => { + return Promise.resolve({ statusCode: 429 }); + }); + + mockRecord._emitter(TEST_EVENT); + paused = true; + + // T = base + 5 + await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); + + expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); + expect(mockTransportSend).toHaveBeenCalledTimes(1); + + expect(replay).toHaveLastSentReplay({ events: JSON.stringify([TEST_EVENT]) }); + + expect(replay['_handleRateLimit']).toHaveBeenCalledTimes(1); + expect(replay.resume).not.toHaveBeenCalled(); + expect(replay.isPaused).toHaveBeenCalledTimes(2); + expect(replay.pause).not.toHaveBeenCalled(); + }); +}); From 92b937385e138afd1aa1d0508aa668132dbfe63b Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Thu, 12 Jan 2023 16:30:13 +0100 Subject: [PATCH 053/113] feat(nextjs): Add Edge Runtime SDK (#6752) --- packages/nextjs/rollup.npm.config.js | 2 +- packages/nextjs/src/edge/edgeclient.ts | 69 +++++++++ packages/nextjs/src/edge/eventbuilder.ts | 130 ++++++++++++++++ packages/nextjs/src/edge/index.ts | 148 +++++++++++++++++++ packages/nextjs/src/edge/transport.ts | 38 +++++ packages/nextjs/src/index.types.ts | 19 ++- packages/nextjs/test/edge/edgeclient.test.ts | 57 +++++++ packages/nextjs/test/edge/transport.test.ts | 110 ++++++++++++++ 8 files changed, 562 insertions(+), 11 deletions(-) create mode 100644 packages/nextjs/src/edge/edgeclient.ts create mode 100644 packages/nextjs/src/edge/eventbuilder.ts create mode 100644 packages/nextjs/src/edge/index.ts create mode 100644 packages/nextjs/src/edge/transport.ts create mode 100644 packages/nextjs/test/edge/edgeclient.test.ts create mode 100644 packages/nextjs/test/edge/transport.test.ts diff --git a/packages/nextjs/rollup.npm.config.js b/packages/nextjs/rollup.npm.config.js index 2329f9eafed1..db1fa9c1fde1 100644 --- a/packages/nextjs/rollup.npm.config.js +++ b/packages/nextjs/rollup.npm.config.js @@ -5,7 +5,7 @@ export default [ makeBaseNPMConfig({ // We need to include `instrumentServer.ts` separately because it's only conditionally required, and so rollup // doesn't automatically include it when calculating the module dependency tree. - entrypoints: ['src/index.ts', 'src/client/index.ts', 'src/config/webpack.ts'], + entrypoints: ['src/index.ts', 'src/client/index.ts', 'src/edge/index.ts', 'src/config/webpack.ts'], // prevent this internal nextjs code from ending up in our built package (this doesn't happen automatially because // the name doesn't match an SDK dependency) diff --git a/packages/nextjs/src/edge/edgeclient.ts b/packages/nextjs/src/edge/edgeclient.ts new file mode 100644 index 000000000000..a5b38d651aed --- /dev/null +++ b/packages/nextjs/src/edge/edgeclient.ts @@ -0,0 +1,69 @@ +import type { Scope } from '@sentry/core'; +import { BaseClient, SDK_VERSION } from '@sentry/core'; +import type { ClientOptions, Event, EventHint, Severity, SeverityLevel } from '@sentry/types'; + +import { eventFromMessage, eventFromUnknownInput } from './eventbuilder'; +import type { EdgeTransportOptions } from './transport'; + +export type EdgeClientOptions = ClientOptions; + +/** + * The Sentry Edge SDK Client. + */ +export class EdgeClient extends BaseClient { + /** + * Creates a new Edge SDK instance. + * @param options Configuration options for this SDK. + */ + public constructor(options: EdgeClientOptions) { + options._metadata = options._metadata || {}; + options._metadata.sdk = options._metadata.sdk || { + name: 'sentry.javascript.nextjs', + packages: [ + { + name: 'npm:@sentry/nextjs', + version: SDK_VERSION, + }, + ], + version: SDK_VERSION, + }; + + super(options); + } + + /** + * @inheritDoc + */ + public eventFromException(exception: unknown, hint?: EventHint): PromiseLike { + return Promise.resolve(eventFromUnknownInput(this._options.stackParser, exception, hint)); + } + + /** + * @inheritDoc + */ + public eventFromMessage( + message: string, + // eslint-disable-next-line deprecation/deprecation + level: Severity | SeverityLevel = 'info', + hint?: EventHint, + ): PromiseLike { + return Promise.resolve( + eventFromMessage(this._options.stackParser, message, level, hint, this._options.attachStacktrace), + ); + } + + /** + * @inheritDoc + */ + protected _prepareEvent(event: Event, hint: EventHint, scope?: Scope): PromiseLike { + event.platform = event.platform || 'edge'; + event.contexts = { + ...event.contexts, + runtime: event.contexts?.runtime || { + name: 'edge', + }, + }; + event.server_name = event.server_name || process.env.SENTRY_NAME; + return super._prepareEvent(event, hint, scope); + } +} diff --git a/packages/nextjs/src/edge/eventbuilder.ts b/packages/nextjs/src/edge/eventbuilder.ts new file mode 100644 index 000000000000..4e483fce3ff7 --- /dev/null +++ b/packages/nextjs/src/edge/eventbuilder.ts @@ -0,0 +1,130 @@ +import { getCurrentHub } from '@sentry/core'; +import type { + Event, + EventHint, + Exception, + Mechanism, + Severity, + SeverityLevel, + StackFrame, + StackParser, +} from '@sentry/types'; +import { + addExceptionMechanism, + addExceptionTypeValue, + extractExceptionKeysForMessage, + isError, + isPlainObject, + normalizeToSize, +} from '@sentry/utils'; + +/** + * Extracts stack frames from the error.stack string + */ +export function parseStackFrames(stackParser: StackParser, error: Error): StackFrame[] { + return stackParser(error.stack || '', 1); +} + +/** + * Extracts stack frames from the error and builds a Sentry Exception + */ +export function exceptionFromError(stackParser: StackParser, error: Error): Exception { + const exception: Exception = { + type: error.name || error.constructor.name, + value: error.message, + }; + + const frames = parseStackFrames(stackParser, error); + if (frames.length) { + exception.stacktrace = { frames }; + } + + return exception; +} + +/** + * Builds and Event from a Exception + * @hidden + */ +export function eventFromUnknownInput(stackParser: StackParser, exception: unknown, hint?: EventHint): Event { + let ex: unknown = exception; + const providedMechanism: Mechanism | undefined = + hint && hint.data && (hint.data as { mechanism: Mechanism }).mechanism; + const mechanism: Mechanism = providedMechanism || { + handled: true, + type: 'generic', + }; + + if (!isError(exception)) { + if (isPlainObject(exception)) { + // This will allow us to group events based on top-level keys + // which is much better than creating new group when any key/value change + const message = `Non-Error exception captured with keys: ${extractExceptionKeysForMessage(exception)}`; + + const hub = getCurrentHub(); + const client = hub.getClient(); + const normalizeDepth = client && client.getOptions().normalizeDepth; + hub.configureScope(scope => { + scope.setExtra('__serialized__', normalizeToSize(exception, normalizeDepth)); + }); + + ex = (hint && hint.syntheticException) || new Error(message); + (ex as Error).message = message; + } else { + // This handles when someone does: `throw "something awesome";` + // We use synthesized Error here so we can extract a (rough) stack trace. + ex = (hint && hint.syntheticException) || new Error(exception as string); + (ex as Error).message = exception as string; + } + mechanism.synthetic = true; + } + + const event = { + exception: { + values: [exceptionFromError(stackParser, ex as Error)], + }, + }; + + addExceptionTypeValue(event, undefined, undefined); + addExceptionMechanism(event, mechanism); + + return { + ...event, + event_id: hint && hint.event_id, + }; +} + +/** + * Builds and Event from a Message + * @hidden + */ +export function eventFromMessage( + stackParser: StackParser, + message: string, + // eslint-disable-next-line deprecation/deprecation + level: Severity | SeverityLevel = 'info', + hint?: EventHint, + attachStacktrace?: boolean, +): Event { + const event: Event = { + event_id: hint && hint.event_id, + level, + message, + }; + + if (attachStacktrace && hint && hint.syntheticException) { + const frames = parseStackFrames(stackParser, hint.syntheticException); + if (frames.length) { + event.exception = { + values: [ + { + value: message, + stacktrace: { frames }, + }, + ], + }; + } + } + + return event; +} diff --git a/packages/nextjs/src/edge/index.ts b/packages/nextjs/src/edge/index.ts new file mode 100644 index 000000000000..42b48094d966 --- /dev/null +++ b/packages/nextjs/src/edge/index.ts @@ -0,0 +1,148 @@ +import '@sentry/tracing'; // Allow people to call tracing API methods without explicitly importing the tracing package. + +import { getCurrentHub, getIntegrationsToSetup, initAndBind, Integrations as CoreIntegrations } from '@sentry/core'; +import type { Options } from '@sentry/types'; +import { + createStackParser, + GLOBAL_OBJ, + logger, + nodeStackLineParser, + stackParserFromStackParserOptions, +} from '@sentry/utils'; + +import { EdgeClient } from './edgeclient'; +import { makeEdgeTransport } from './transport'; + +const nodeStackParser = createStackParser(nodeStackLineParser()); + +export const defaultIntegrations = [new CoreIntegrations.InboundFilters(), new CoreIntegrations.FunctionToString()]; + +export type EdgeOptions = Options; + +/** Inits the Sentry NextJS SDK on the Edge Runtime. */ +export function init(options: EdgeOptions = {}): void { + 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; + } + } + + if (options.environment === undefined && process.env.SENTRY_ENVIRONMENT) { + options.environment = process.env.SENTRY_ENVIRONMENT; + } + + if (options.autoSessionTracking === undefined && options.dsn !== undefined) { + options.autoSessionTracking = true; + } + + if (options.instrumenter === undefined) { + options.instrumenter = 'sentry'; + } + + const clientOptions = { + ...options, + stackParser: stackParserFromStackParserOptions(options.stackParser || nodeStackParser), + integrations: getIntegrationsToSetup(options), + transport: options.transport || makeEdgeTransport, + }; + + initAndBind(EdgeClient, clientOptions); + + // TODO?: Sessiontracking +} + +/** + * 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 || + // Netlify - https://docs.netlify.com/configure-builds/environment-variables/#build-metadata + process.env.COMMIT_REF || + // 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 + ); +} + +/** + * Call `close()` on the current client, if there is one. See {@link Client.close}. + * + * @param timeout Maximum time in ms the client should wait to flush its event queue before shutting down. Omitting this + * parameter will cause the client to wait until all events are sent before disabling itself. + * @returns A promise which resolves to `true` if the queue successfully drains before the timeout, or `false` if it + * doesn't (or if there's no client defined). + */ +export async function close(timeout?: number): Promise { + const client = getCurrentHub().getClient(); + if (client) { + return client.close(timeout); + } + __DEBUG_BUILD__ && logger.warn('Cannot flush events and disable SDK. No client defined.'); + return Promise.resolve(false); +} + +/** + * Call `flush()` on the current client, if there is one. See {@link Client.flush}. + * + * @param timeout Maximum time in ms the client should wait to flush its event queue. Omitting this parameter will cause + * the client to wait until all events are sent before resolving the promise. + * @returns A promise which resolves to `true` if the queue successfully drains before the timeout, or `false` if it + * doesn't (or if there's no client defined). + */ +export async function flush(timeout?: number): Promise { + const client = getCurrentHub().getClient(); + if (client) { + return client.flush(timeout); + } + __DEBUG_BUILD__ && logger.warn('Cannot flush events. No client defined.'); + return Promise.resolve(false); +} + +/** + * This is the getter for lastEventId. + * + * @returns The last event id of a captured event. + */ +export function lastEventId(): string | undefined { + return getCurrentHub().lastEventId(); +} + +export * from '@sentry/core'; diff --git a/packages/nextjs/src/edge/transport.ts b/packages/nextjs/src/edge/transport.ts new file mode 100644 index 000000000000..3fc4b8e101c3 --- /dev/null +++ b/packages/nextjs/src/edge/transport.ts @@ -0,0 +1,38 @@ +import { createTransport } from '@sentry/core'; +import type { BaseTransportOptions, Transport, TransportMakeRequestResponse, TransportRequest } from '@sentry/types'; + +export interface EdgeTransportOptions extends BaseTransportOptions { + /** Fetch API init parameters. Used by the FetchTransport */ + fetchOptions?: RequestInit; + /** Custom headers for the transport. Used by the XHRTransport and FetchTransport */ + headers?: { [key: string]: string }; +} + +/** + * Creates a Transport that uses the Edge Runtimes native fetch API to send events to Sentry. + */ +export function makeEdgeTransport(options: EdgeTransportOptions): Transport { + function makeRequest(request: TransportRequest): PromiseLike { + const requestOptions: RequestInit = { + body: request.body, + method: 'POST', + referrerPolicy: 'origin', + headers: options.headers, + ...options.fetchOptions, + }; + + try { + return fetch(options.url, requestOptions).then(response => ({ + 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 Promise.reject(e); + } + } + + return createTransport(options, makeRequest); +} diff --git a/packages/nextjs/src/index.types.ts b/packages/nextjs/src/index.types.ts index 5df30dffd580..fcce6708a293 100644 --- a/packages/nextjs/src/index.types.ts +++ b/packages/nextjs/src/index.types.ts @@ -5,28 +5,27 @@ export * from './config'; export * from './client'; export * from './server'; +export * from './edge'; import type { Integration, Options, StackParser } from '@sentry/types'; import type { BrowserOptions } from './client'; import * as clientSdk from './client'; +import type { EdgeOptions } from './edge'; +import * as edgeSdk from './edge'; import type { NodeOptions } from './server'; import * as serverSdk from './server'; /** Initializes Sentry Next.js SDK */ -export declare function init(options: Options | BrowserOptions | NodeOptions): void; +export declare function init(options: Options | BrowserOptions | NodeOptions | EdgeOptions): void; // We export a merged Integrations object so that users can (at least typing-wise) use all integrations everywhere. -export const Integrations = { ...clientSdk.Integrations, ...serverSdk.Integrations }; +export const Integrations = { ...clientSdk.Integrations, ...serverSdk.Integrations, ...edgeSdk.Integrations }; export declare const defaultIntegrations: Integration[]; export declare const defaultStackParser: StackParser; -// This variable is not a runtime variable but just a type to tell typescript that the methods below can either come -// from the client SDK or from the server SDK. TypeScript is smart enough to understand that these resolve to the same -// methods from `@sentry/core`. -declare const runtime: 'client' | 'server'; - -export const close = runtime === 'client' ? clientSdk.close : serverSdk.close; -export const flush = runtime === 'client' ? clientSdk.flush : serverSdk.flush; -export const lastEventId = runtime === 'client' ? clientSdk.lastEventId : serverSdk.lastEventId; +export declare function close(timeout?: number | undefined): PromiseLike; +export declare function flush(timeout?: number | undefined): PromiseLike; +export declare function lastEventId(): string | undefined; +export declare function getSentryRelease(fallback?: string): string | undefined; diff --git a/packages/nextjs/test/edge/edgeclient.test.ts b/packages/nextjs/test/edge/edgeclient.test.ts new file mode 100644 index 000000000000..cba4a751c71e --- /dev/null +++ b/packages/nextjs/test/edge/edgeclient.test.ts @@ -0,0 +1,57 @@ +import { createTransport } from '@sentry/core'; +import type { Event, EventHint } from '@sentry/types'; + +import type { EdgeClientOptions } from '../../src/edge/edgeclient'; +import { EdgeClient } from '../../src/edge/edgeclient'; + +const PUBLIC_DSN = 'https://username@domain/123'; + +function getDefaultEdgeClientOptions(options: Partial = {}): EdgeClientOptions { + return { + integrations: [], + transport: () => createTransport({ recordDroppedEvent: () => undefined }, _ => Promise.resolve({})), + stackParser: () => [], + instrumenter: 'sentry', + ...options, + }; +} + +describe('NodeClient', () => { + describe('_prepareEvent', () => { + test('adds platform to event', () => { + const options = getDefaultEdgeClientOptions({ dsn: PUBLIC_DSN }); + const client = new EdgeClient(options); + + const event: Event = {}; + const hint: EventHint = {}; + (client as any)._prepareEvent(event, hint); + + expect(event.platform).toEqual('edge'); + }); + + test('adds runtime context to event', () => { + const options = getDefaultEdgeClientOptions({ dsn: PUBLIC_DSN }); + const client = new EdgeClient(options); + + const event: Event = {}; + const hint: EventHint = {}; + (client as any)._prepareEvent(event, hint); + + expect(event.contexts?.runtime).toEqual({ + name: 'edge', + }); + }); + + test("doesn't clobber existing runtime data", () => { + const options = getDefaultEdgeClientOptions({ dsn: PUBLIC_DSN }); + const client = new EdgeClient(options); + + const event: Event = { contexts: { runtime: { name: 'foo', version: '1.2.3' } } }; + const hint: EventHint = {}; + (client as any)._prepareEvent(event, hint); + + expect(event.contexts?.runtime).toEqual({ name: 'foo', version: '1.2.3' }); + expect(event.contexts?.runtime).not.toEqual({ name: 'edge' }); + }); + }); +}); diff --git a/packages/nextjs/test/edge/transport.test.ts b/packages/nextjs/test/edge/transport.test.ts new file mode 100644 index 000000000000..abb925184685 --- /dev/null +++ b/packages/nextjs/test/edge/transport.test.ts @@ -0,0 +1,110 @@ +import type { EventEnvelope, EventItem } from '@sentry/types'; +import { createEnvelope, serializeEnvelope } from '@sentry/utils'; +import { TextEncoder } from 'util'; + +import type { EdgeTransportOptions } from '../../src/edge/transport'; +import { makeEdgeTransport } from '../../src/edge/transport'; + +const DEFAULT_EDGE_TRANSPORT_OPTIONS: EdgeTransportOptions = { + 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-ignore fetch is not on global +const oldFetch = global.fetch; +// @ts-ignore fetch is not on global +global.fetch = mockFetch; + +afterAll(() => { + // @ts-ignore 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); + 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); + + 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); + expect(mockFetch).toHaveBeenLastCalledWith(DEFAULT_EDGE_TRANSPORT_OPTIONS.url, { + body: serializeEnvelope(ERROR_ENVELOPE, new TextEncoder()), + method: 'POST', + ...REQUEST_OPTIONS, + }); + }); +}); From 9e3390db2bf9d00e8e6c95935ab6e57713329987 Mon Sep 17 00:00:00 2001 From: Richard Roggenkemper <46740234+roggenkemper@users.noreply.github.com> Date: Thu, 12 Jan 2023 07:43:01 -0800 Subject: [PATCH 054/113] feat(nextjs): Add optional options argument to `withSentryConfig` as an alternative to the `sentry` property (#6721) --- packages/nextjs/src/config/withSentryConfig.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/nextjs/src/config/withSentryConfig.ts b/packages/nextjs/src/config/withSentryConfig.ts index 5c16b82286c7..f10f801e1702 100644 --- a/packages/nextjs/src/config/withSentryConfig.ts +++ b/packages/nextjs/src/config/withSentryConfig.ts @@ -6,6 +6,7 @@ import type { NextConfigObject, NextConfigObjectWithSentry, SentryWebpackPluginOptions, + UserSentryOptions, } from './types'; /** @@ -13,19 +14,21 @@ import type { * * @param exportedUserNextConfig The existing config to be exported prior to adding Sentry * @param userSentryWebpackPluginOptions Configuration for SentryWebpackPlugin + * @param sentryOptions Optional additional options to add as alternative to `sentry` property of config * @returns The modified config to be exported */ export function withSentryConfig( exportedUserNextConfig: ExportedNextConfig = {}, userSentryWebpackPluginOptions: Partial = {}, + sentryOptions?: UserSentryOptions, ): NextConfigFunction | NextConfigObject { return function (phase: string, defaults: { defaultConfig: NextConfigObject }): NextConfigObject { - if (typeof exportedUserNextConfig === 'function') { - const userNextConfigObject = exportedUserNextConfig(phase, defaults); - return getFinalConfigObject(phase, userNextConfigObject, userSentryWebpackPluginOptions); - } else { - return getFinalConfigObject(phase, exportedUserNextConfig, userSentryWebpackPluginOptions); - } + const userNextConfigObject = + typeof exportedUserNextConfig === 'function' ? exportedUserNextConfig(phase, defaults) : exportedUserNextConfig; + // Inserts additional `sentry` options into the existing config, allows for backwards compatability + // in case nothing is passed into the optional `sentryOptions` argument + userNextConfigObject.sentry = { ...userNextConfigObject.sentry, ...sentryOptions }; + return getFinalConfigObject(phase, userNextConfigObject, userSentryWebpackPluginOptions); }; } From f22366d949c686272be0347308f3f5a4ec525d9d Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Thu, 12 Jan 2023 17:14:08 +0100 Subject: [PATCH 055/113] feat(nextjs): Use Edge SDK for Edge bundles (#6753) --- packages/nextjs/package.json | 1 - packages/nextjs/src/config/loaders/index.ts | 1 + .../config/loaders/sdkMultiplexerLoader.ts | 24 ++++++ packages/nextjs/src/config/webpack.ts | 73 ++++++++++--------- packages/nextjs/src/index.ts | 2 + packages/nextjs/test/config/fixtures.ts | 9 ++- packages/nextjs/test/config/mocks.ts | 8 +- .../webpack/constructWebpackConfig.test.ts | 10 ++- 8 files changed, 82 insertions(+), 46 deletions(-) create mode 100644 packages/nextjs/src/config/loaders/sdkMultiplexerLoader.ts diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index b6faa2670c4e..1b5ebf5da4c1 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -11,7 +11,6 @@ }, "main": "build/cjs/index.js", "module": "build/esm/index.js", - "browser": "build/esm/client/index.js", "types": "build/types/index.types.d.ts", "publishConfig": { "access": "public" diff --git a/packages/nextjs/src/config/loaders/index.ts b/packages/nextjs/src/config/loaders/index.ts index 322567c1495b..27620e004f39 100644 --- a/packages/nextjs/src/config/loaders/index.ts +++ b/packages/nextjs/src/config/loaders/index.ts @@ -1,3 +1,4 @@ export { default as valueInjectionLoader } from './valueInjectionLoader'; export { default as prefixLoader } from './prefixLoader'; export { default as wrappingLoader } from './wrappingLoader'; +export { default as sdkMultiplexerLoader } from './sdkMultiplexerLoader'; diff --git a/packages/nextjs/src/config/loaders/sdkMultiplexerLoader.ts b/packages/nextjs/src/config/loaders/sdkMultiplexerLoader.ts new file mode 100644 index 000000000000..9241def48a80 --- /dev/null +++ b/packages/nextjs/src/config/loaders/sdkMultiplexerLoader.ts @@ -0,0 +1,24 @@ +import type { LoaderThis } from './types'; + +type LoaderOptions = { + importTarget: string; +}; + +/** + * This loader allows us to multiplex SDKs depending on what is passed to the `importTarget` loader option. + * If this loader encounters a file that contains the string "__SENTRY_SDK_MULTIPLEXER__" it will replace it's entire + * content with an "export all"-statement that points to `importTarget`. + * + * In our case we use this to multiplex different SDKs depending on whether we're bundling browser code, server code, + * or edge-runtime code. + */ +export default function sdkMultiplexerLoader(this: LoaderThis, userCode: string): string { + if (!userCode.includes('__SENTRY_SDK_MULTIPLEXER__')) { + return userCode; + } + + // We know one or the other will be defined, depending on the version of webpack being used + const { importTarget } = 'getOptions' in this ? this.getOptions() : this.query; + + return `export * from "${importTarget}";`; +} diff --git a/packages/nextjs/src/config/webpack.ts b/packages/nextjs/src/config/webpack.ts index 35c017e732e4..b5a8d07db2fd 100644 --- a/packages/nextjs/src/config/webpack.ts +++ b/packages/nextjs/src/config/webpack.ts @@ -85,12 +85,17 @@ export function constructWebpackConfigFunction( // Add a loader which will inject code that sets global values addValueInjectionLoader(newConfig, userNextConfig, userSentryOptions); - if (buildContext.nextRuntime === 'edge') { - // eslint-disable-next-line no-console - console.warn( - '[@sentry/nextjs] You are using edge functions or middleware. Please note that Sentry does not yet support error monitoring for these features.', - ); - } + newConfig.module.rules.push({ + test: /node_modules\/@sentry\/nextjs/, + use: [ + { + loader: path.resolve(__dirname, 'loaders/sdkMultiplexerLoader.js'), + options: { + importTarget: buildContext.nextRuntime === 'edge' ? './edge' : './client', + }, + }, + ], + }); if (isServer) { if (userSentryOptions.autoInstrumentServerFunctions !== false) { @@ -301,28 +306,25 @@ async function addSentryToEntryProperty( // we know is that it won't have gotten *simpler* in form, so we only need to worry about the object and function // options. See https://webpack.js.org/configuration/entry-context/#entry. - const { isServer, dir: projectDir, dev: isDev } = buildContext; + const { isServer, dir: projectDir, dev: isDev, nextRuntime } = buildContext; const newEntryProperty = typeof currentEntryProperty === 'function' ? await currentEntryProperty() : { ...currentEntryProperty }; // `sentry.server.config.js` or `sentry.client.config.js` (or their TS equivalents) - const userConfigFile = isServer ? getUserConfigFile(projectDir, 'server') : getUserConfigFile(projectDir, 'client'); + const userConfigFile = + nextRuntime === 'edge' + ? getUserConfigFile(projectDir, 'edge') + : isServer + ? getUserConfigFile(projectDir, 'server') + : getUserConfigFile(projectDir, 'client'); // we need to turn the filename into a path so webpack can find it - const filesToInject = [`./${userConfigFile}`]; + const filesToInject = userConfigFile ? [`./${userConfigFile}`] : []; // inject into all entry points which might contain user's code for (const entryPointName in newEntryProperty) { - if ( - shouldAddSentryToEntryPoint( - entryPointName, - isServer, - userSentryOptions.excludeServerRoutes, - isDev, - buildContext.nextRuntime === 'edge', - ) - ) { + if (shouldAddSentryToEntryPoint(entryPointName, isServer, userSentryOptions.excludeServerRoutes, isDev)) { addFilesToExistingEntryPoint(newEntryProperty, entryPointName, filesToInject); } else { if ( @@ -345,10 +347,11 @@ async function addSentryToEntryProperty( * TypeScript or JavaScript file. * * @param projectDir The root directory of the project, where the file should be located - * @param platform Either "server" or "client", so that we know which file to look for - * @returns The name of the relevant file. If no file is found, this method throws an error. + * @param platform Either "server", "client" or "edge", so that we know which file to look for + * @returns The name of the relevant file. If the server or client file is not found, this method throws an error. The + * edge file is optional, if it is not found this function will return `undefined`. */ -export function getUserConfigFile(projectDir: string, platform: 'server' | 'client'): string { +export function getUserConfigFile(projectDir: string, platform: 'server' | 'client' | 'edge'): string | undefined { const possibilities = [`sentry.${platform}.config.ts`, `sentry.${platform}.config.js`]; for (const filename of possibilities) { @@ -357,7 +360,16 @@ export function getUserConfigFile(projectDir: string, platform: 'server' | 'clie } } - throw new Error(`Cannot find '${possibilities[0]}' or '${possibilities[1]}' in '${projectDir}'.`); + // Edge config file is optional + if (platform === 'edge') { + // eslint-disable-next-line no-console + console.warn( + '[@sentry/nextjs] You are using Next.js features that run on the Edge Runtime. Please add a "sentry.edge.config.js" or a "sentry.edge.config.ts" file to your project root in which you initialize the Sentry SDK with "Sentry.init()".', + ); + return; + } else { + throw new Error(`Cannot find '${possibilities[0]}' or '${possibilities[1]}' in '${projectDir}'.`); + } } /** @@ -449,11 +461,9 @@ function shouldAddSentryToEntryPoint( isServer: boolean, excludeServerRoutes: Array = [], isDev: boolean, - isEdgeRuntime: boolean, ): boolean { - // We don't support the Edge runtime yet - if (isEdgeRuntime) { - return false; + if (entryPointName === 'middleware') { + return true; } // On the server side, by default we inject the `Sentry.init()` code into every page (with a few exceptions). @@ -479,9 +489,6 @@ function shouldAddSentryToEntryPoint( // versions.) entryPointRoute === '/_app' || entryPointRoute === '/_document' || - // While middleware was in beta, it could be anywhere (at any level) in the `pages` directory, and would be called - // `_middleware.js`. Until the SDK runs successfully in the lambda edge environment, we have to exclude these. - entryPointName.includes('_middleware') || // Newer versions of nextjs are starting to introduce things outside the `pages/` folder (middleware, an `app/` // directory, etc), but until those features are stable and we know how we want to support them, the safest bet is // not to inject anywhere but inside `pages/`. @@ -552,13 +559,7 @@ export function getWebpackPluginOptions( stripPrefix: ['webpack://_N_E/'], urlPrefix, entries: (entryPointName: string) => - shouldAddSentryToEntryPoint( - entryPointName, - isServer, - userSentryOptions.excludeServerRoutes, - isDev, - buildContext.nextRuntime === 'edge', - ), + shouldAddSentryToEntryPoint(entryPointName, isServer, userSentryOptions.excludeServerRoutes, isDev), release: getSentryRelease(buildId), dryRun: isDev, }); diff --git a/packages/nextjs/src/index.ts b/packages/nextjs/src/index.ts index 133b6ecf1da0..4133c06089d5 100644 --- a/packages/nextjs/src/index.ts +++ b/packages/nextjs/src/index.ts @@ -1,2 +1,4 @@ export * from './config'; export * from './server'; + +// __SENTRY_SDK_MULTIPLEXER__ diff --git a/packages/nextjs/test/config/fixtures.ts b/packages/nextjs/test/config/fixtures.ts index 8edf88a8caf9..f747edbc2be9 100644 --- a/packages/nextjs/test/config/fixtures.ts +++ b/packages/nextjs/test/config/fixtures.ts @@ -9,6 +9,7 @@ import type { export const SERVER_SDK_CONFIG_FILE = 'sentry.server.config.js'; export const CLIENT_SDK_CONFIG_FILE = 'sentry.client.config.js'; +export const EDGE_SDK_CONFIG_FILE = 'sentry.edge.config.js'; /** Mock next config object */ export const userNextConfig: NextConfigObject = { @@ -43,7 +44,7 @@ export const serverWebpackConfig: WebpackConfigObject = { 'pages/_error': 'private-next-pages/_error.js', 'pages/_app': 'private-next-pages/_app.js', 'pages/sniffTour': ['./node_modules/smellOVision/index.js', 'private-next-pages/sniffTour.js'], - 'pages/api/_middleware': 'private-next-pages/api/_middleware.js', + middleware: 'private-next-pages/middleware.js', 'pages/api/simulator/dogStats/[name]': { import: 'private-next-pages/api/simulator/dogStats/[name].js' }, 'pages/simulator/leaderboard': { import: ['./node_modules/dogPoints/converter.js', 'private-next-pages/simulator/leaderboard.js'], @@ -84,7 +85,7 @@ export const clientWebpackConfig: WebpackConfigObject = { * @returns A mock build context for the given target */ export function getBuildContext( - buildTarget: 'server' | 'client', + buildTarget: 'server' | 'client' | 'edge', materializedNextConfig: ExportedNextConfig, webpackVersion: string = '5.4.15', ): BuildContext { @@ -101,9 +102,11 @@ export function getBuildContext( webpack: { version: webpackVersion }, defaultLoaders: true, totalPages: 2, - isServer: buildTarget === 'server', + isServer: buildTarget === 'server' || buildTarget === 'edge', + nextRuntime: ({ server: 'nodejs', client: undefined, edge: 'edge' } as const)[buildTarget], }; } export const serverBuildContext = getBuildContext('server', exportedNextConfig); export const clientBuildContext = getBuildContext('client', exportedNextConfig); +export const edgeBuildContext = getBuildContext('edge', exportedNextConfig); diff --git a/packages/nextjs/test/config/mocks.ts b/packages/nextjs/test/config/mocks.ts index 581b7d2bbbd1..ddf4ce4d1553 100644 --- a/packages/nextjs/test/config/mocks.ts +++ b/packages/nextjs/test/config/mocks.ts @@ -6,7 +6,7 @@ import * as os from 'os'; import * as path from 'path'; import * as rimraf from 'rimraf'; -import { CLIENT_SDK_CONFIG_FILE, SERVER_SDK_CONFIG_FILE } from './fixtures'; +import { CLIENT_SDK_CONFIG_FILE, EDGE_SDK_CONFIG_FILE, SERVER_SDK_CONFIG_FILE } from './fixtures'; // We use `fs.existsSync()` in `getUserConfigFile()`. When we're not testing `getUserConfigFile()` specifically, all we // need is for it to give us any valid answer, so make it always find what it's looking for. Since this is a core node @@ -14,7 +14,11 @@ import { CLIENT_SDK_CONFIG_FILE, SERVER_SDK_CONFIG_FILE } from './fixtures'; // function also lets us restore the original when we do want to test `getUserConfigFile()`. export const realExistsSync = jest.requireActual('fs').existsSync; export const mockExistsSync = (path: fs.PathLike): ReturnType => { - if ((path as string).endsWith(SERVER_SDK_CONFIG_FILE) || (path as string).endsWith(CLIENT_SDK_CONFIG_FILE)) { + if ( + (path as string).endsWith(SERVER_SDK_CONFIG_FILE) || + (path as string).endsWith(CLIENT_SDK_CONFIG_FILE) || + (path as string).endsWith(EDGE_SDK_CONFIG_FILE) + ) { return true; } diff --git a/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts b/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts index 206050d56d38..bee971f104e6 100644 --- a/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts +++ b/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts @@ -6,6 +6,8 @@ import { CLIENT_SDK_CONFIG_FILE, clientBuildContext, clientWebpackConfig, + EDGE_SDK_CONFIG_FILE, + edgeBuildContext, exportedNextConfig, SERVER_SDK_CONFIG_FILE, serverBuildContext, @@ -87,6 +89,7 @@ describe('constructWebpackConfigFunction()', () => { describe('webpack `entry` property config', () => { const serverConfigFilePath = `./${SERVER_SDK_CONFIG_FILE}`; const clientConfigFilePath = `./${CLIENT_SDK_CONFIG_FILE}`; + const edgeConfigFilePath = `./${EDGE_SDK_CONFIG_FILE}`; it('handles various entrypoint shapes', async () => { const finalWebpackConfig = await materializeFinalWebpackConfig({ @@ -207,17 +210,16 @@ describe('constructWebpackConfigFunction()', () => { ); }); - it('does not inject user config file into API middleware', async () => { + it('injects user config file into API middleware', async () => { const finalWebpackConfig = await materializeFinalWebpackConfig({ exportedNextConfig, incomingWebpackConfig: serverWebpackConfig, - incomingWebpackBuildContext: serverBuildContext, + incomingWebpackBuildContext: edgeBuildContext, }); expect(finalWebpackConfig.entry).toEqual( expect.objectContaining({ - // no injected file - 'pages/api/_middleware': 'private-next-pages/api/_middleware.js', + middleware: [edgeConfigFilePath, 'private-next-pages/middleware.js'], }), ); }); From 93b8ec545794585afd50ac906b178da0156a45b7 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 13 Jan 2023 08:30:22 +0100 Subject: [PATCH 056/113] ref(replay): Extract `sendReplay` functionality into functions (#6751) --- packages/replay/jest.setup.ts | 8 +- packages/replay/src/constants.ts | 3 + packages/replay/src/replay.ts | 229 ++---------------- packages/replay/src/types.ts | 10 +- ...cordingData.ts => prepareRecordingData.ts} | 18 +- packages/replay/src/util/sendReplay.ts | 61 +++++ packages/replay/src/util/sendReplayRequest.ts | 138 +++++++++++ .../test/integration/errorSampleRate.test.ts | 16 +- .../replay/test/integration/events.test.ts | 10 +- .../replay/test/integration/flush.test.ts | 17 +- .../test/integration/rateLimiting.test.ts | 51 ++-- .../test/integration/sendReplayEvent.test.ts | 53 ++-- .../replay/test/integration/session.test.ts | 6 +- packages/replay/test/integration/stop.test.ts | 2 +- 14 files changed, 305 insertions(+), 317 deletions(-) rename packages/replay/src/util/{createRecordingData.ts => prepareRecordingData.ts} (59%) create mode 100644 packages/replay/src/util/sendReplay.ts create mode 100644 packages/replay/src/util/sendReplayRequest.ts diff --git a/packages/replay/jest.setup.ts b/packages/replay/jest.setup.ts index db17db23f761..26f9ea346a0d 100644 --- a/packages/replay/jest.setup.ts +++ b/packages/replay/jest.setup.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { getCurrentHub } from '@sentry/core'; -import type { ReplayRecordingData,Transport } from '@sentry/types'; +import type { ReplayRecordingData, Transport } from '@sentry/types'; import type { ReplayContainer, Session } from './src/types'; @@ -34,7 +34,7 @@ type SentReplayExpected = { replayEventPayload?: ReplayEventPayload; recordingHeader?: RecordingHeader; recordingPayloadHeader?: RecordingPayloadHeader; - events?: ReplayRecordingData; + recordingData?: ReplayRecordingData; }; // eslint-disable-next-line @typescript-eslint/explicit-function-return-type @@ -79,7 +79,7 @@ function checkCallForSentReplay( const [[replayEventHeader, replayEventPayload], [recordingHeader, recordingPayload] = []] = envelopeItems; // @ts-ignore recordingPayload is always a string in our tests - const [recordingPayloadHeader, events] = recordingPayload?.split('\n') || []; + const [recordingPayloadHeader, recordingData] = recordingPayload?.split('\n') || []; const actualObj: Required = { // @ts-ignore Custom envelope @@ -91,7 +91,7 @@ function checkCallForSentReplay( // @ts-ignore Custom envelope recordingHeader: recordingHeader, recordingPayloadHeader: recordingPayloadHeader && JSON.parse(recordingPayloadHeader), - events, + recordingData, }; const isObjectContaining = expected && 'sample' in expected && 'inverse' in expected; diff --git a/packages/replay/src/constants.ts b/packages/replay/src/constants.ts index b0176293e13c..187d729cb6f5 100644 --- a/packages/replay/src/constants.ts +++ b/packages/replay/src/constants.ts @@ -33,3 +33,6 @@ export const MASK_ALL_TEXT_SELECTOR = 'body *:not(style), body *:not(script)'; export const DEFAULT_FLUSH_MIN_DELAY = 5_000; export const DEFAULT_FLUSH_MAX_DELAY = 15_000; export const INITIAL_FLUSH_DELAY = 5_000; + +export const RETRY_BASE_INTERVAL = 5000; +export const RETRY_MAX_COUNT = 3; diff --git a/packages/replay/src/replay.ts b/packages/replay/src/replay.ts index 06a8ae58f64a..241d330dd412 100644 --- a/packages/replay/src/replay.ts +++ b/packages/replay/src/replay.ts @@ -1,18 +1,11 @@ /* eslint-disable max-lines */ // TODO: We might want to split this file up -import { addGlobalEventProcessor, captureException, getCurrentHub, setContext } from '@sentry/core'; -import type { Breadcrumb, ReplayEvent, ReplayRecordingMode, TransportMakeRequestResponse } from '@sentry/types'; +import { addGlobalEventProcessor, captureException, getCurrentHub } from '@sentry/core'; +import type { Breadcrumb, ReplayRecordingMode } from '@sentry/types'; import type { RateLimits } from '@sentry/utils'; -import { addInstrumentationHandler, disabledUntil, isRateLimited, logger, updateRateLimits } from '@sentry/utils'; +import { addInstrumentationHandler, disabledUntil, logger } from '@sentry/utils'; import { EventType, record } from 'rrweb'; -import { - MAX_SESSION_LIFE, - REPLAY_EVENT_NAME, - SESSION_IDLE_DURATION, - UNABLE_TO_SEND_REPLAY, - VISIBILITY_CHANGE_TIMEOUT, - WINDOW, -} from './constants'; +import { MAX_SESSION_LIFE, SESSION_IDLE_DURATION, VISIBILITY_CHANGE_TIMEOUT, WINDOW } from './constants'; import { breadcrumbHandler } from './coreHandlers/breadcrumbHandler'; import { handleFetchSpanListener } from './coreHandlers/handleFetch'; import { handleGlobalEventListener } from './coreHandlers/handleGlobalEvent'; @@ -34,7 +27,6 @@ import type { RecordingOptions, ReplayContainer as ReplayContainerInterface, ReplayPluginOptions, - SendReplay, Session, } from './types'; import { addEvent } from './util/addEvent'; @@ -42,20 +34,12 @@ import { addMemoryEntry } from './util/addMemoryEntry'; import { createBreadcrumb } from './util/createBreadcrumb'; import { createPerformanceEntries } from './util/createPerformanceEntries'; import { createPerformanceSpans } from './util/createPerformanceSpans'; -import { createRecordingData } from './util/createRecordingData'; -import { createReplayEnvelope } from './util/createReplayEnvelope'; import { debounce } from './util/debounce'; import { isExpired } from './util/isExpired'; import { isSessionExpired } from './util/isSessionExpired'; import { overwriteRecordDroppedEvent, restoreRecordDroppedEvent } from './util/monkeyPatchRecordDroppedEvent'; -import { prepareReplayEvent } from './util/prepareReplayEvent'; - -/** - * Returns true to return control to calling function, otherwise continue with normal batching - */ - -const BASE_RETRY_INTERVAL = 5000; -const MAX_RETRY_COUNT = 3; +import { sendReplay } from './util/sendReplay'; +import { RateLimitError } from './util/sendReplayRequest'; /** * The main replay container class, which holds all the state and methods for recording and sending replays. @@ -86,9 +70,6 @@ export class ReplayContainer implements ReplayContainerInterface { private _performanceObserver: PerformanceObserver | null = null; - private _retryCount: number = 0; - private _retryInterval: number = BASE_RETRY_INTERVAL; - private _debouncedFlush: ReturnType; private _flushLock: Promise | null = null; @@ -129,11 +110,6 @@ export class ReplayContainer implements ReplayContainerInterface { initialUrl: '', }; - /** - * A RateLimits object holding the rate-limit durations in case a sent replay event was rate-limited. - */ - private _rateLimits: RateLimits = {}; - public constructor({ options, recordingOptions, @@ -837,14 +813,20 @@ export class ReplayContainer implements ReplayContainerInterface { const segmentId = this.session.segmentId++; this._maybeSaveSession(); - await this._sendReplay({ + await sendReplay({ replayId, - events: recordingData, + recordingData, segmentId, includeReplayStartTimestamp: segmentId === 0, eventContext, + session: this.session, + options: this.getOptions(), + timestamp: new Date().getTime(), }); } catch (err) { + if (err instanceof RateLimitError) { + this._handleRateLimit(err.rateLimits); + } this._handleException(err); } } @@ -897,185 +879,6 @@ export class ReplayContainer implements ReplayContainerInterface { } }; - /** - * Send replay attachment using `fetch()` - */ - private async _sendReplayRequest({ - events, - replayId, - segmentId: segment_id, - includeReplayStartTimestamp, - eventContext, - timestamp = new Date().getTime(), - }: SendReplay): Promise { - const recordingData = createRecordingData({ - events, - headers: { - segment_id, - }, - }); - - const { urls, errorIds, traceIds, initialTimestamp } = eventContext; - - const hub = getCurrentHub(); - const client = hub.getClient(); - const scope = hub.getScope(); - const transport = client && client.getTransport(); - const dsn = client?.getDsn(); - - if (!client || !scope || !transport || !dsn || !this.session || !this.session.sampled) { - return; - } - - const baseEvent: ReplayEvent = { - // @ts-ignore private api - type: REPLAY_EVENT_NAME, - ...(includeReplayStartTimestamp ? { replay_start_timestamp: initialTimestamp / 1000 } : {}), - timestamp: timestamp / 1000, - error_ids: errorIds, - trace_ids: traceIds, - urls, - replay_id: replayId, - segment_id, - replay_type: this.session.sampled, - }; - - const replayEvent = await prepareReplayEvent({ scope, client, replayId, event: baseEvent }); - - if (!replayEvent) { - // Taken from baseclient's `_processEvent` method, where this is handled for errors/transactions - client.recordDroppedEvent('event_processor', 'replay_event', baseEvent); - __DEBUG_BUILD__ && logger.log('An event processor returned `null`, will not send event.'); - return; - } - - replayEvent.tags = { - ...replayEvent.tags, - sessionSampleRate: this._options.sessionSampleRate, - errorSampleRate: this._options.errorSampleRate, - }; - - /* - For reference, the fully built event looks something like this: - { - "type": "replay_event", - "timestamp": 1670837008.634, - "error_ids": [ - "errorId" - ], - "trace_ids": [ - "traceId" - ], - "urls": [ - "https://example.com" - ], - "replay_id": "eventId", - "segment_id": 3, - "replay_type": "error", - "platform": "javascript", - "event_id": "eventId", - "environment": "production", - "sdk": { - "integrations": [ - "BrowserTracing", - "Replay" - ], - "name": "sentry.javascript.browser", - "version": "7.25.0" - }, - "sdkProcessingMetadata": {}, - "tags": { - "sessionSampleRate": 1, - "errorSampleRate": 0, - } - } - */ - - const envelope = createReplayEnvelope(replayEvent, recordingData, dsn, client.getOptions().tunnel); - - try { - const response = await transport.send(envelope); - // TODO (v8): we can remove this guard once transport.send's type signature doesn't include void anymore - if (response) { - this._rateLimits = updateRateLimits(this._rateLimits, response); - if (isRateLimited(this._rateLimits, 'replay')) { - this._handleRateLimit(); - } - } - return response; - } catch { - throw new Error(UNABLE_TO_SEND_REPLAY); - } - } - - /** - * Reset the counter of retries for sending replays. - */ - private _resetRetries(): void { - this._retryCount = 0; - this._retryInterval = BASE_RETRY_INTERVAL; - } - - /** - * Finalize and send the current replay event to Sentry - */ - private async _sendReplay({ - replayId, - events, - segmentId, - includeReplayStartTimestamp, - eventContext, - }: SendReplay): Promise { - // short circuit if there's no events to upload (this shouldn't happen as _runFlush makes this check) - if (!events.length) { - return; - } - - try { - await this._sendReplayRequest({ - events, - replayId, - segmentId, - includeReplayStartTimestamp, - eventContext, - }); - this._resetRetries(); - return true; - } catch (err) { - // Capture error for every failed replay - setContext('Replays', { - _retryCount: this._retryCount, - }); - this._handleException(err); - - // If an error happened here, it's likely that uploading the attachment - // failed, we'll can retry with the same events payload - if (this._retryCount >= MAX_RETRY_COUNT) { - throw new Error(`${UNABLE_TO_SEND_REPLAY} - max retries exceeded`); - } - - // will retry in intervals of 5, 10, 30 - this._retryInterval = ++this._retryCount * this._retryInterval; - - return await new Promise((resolve, reject) => { - setTimeout(async () => { - try { - await this._sendReplay({ - replayId, - events, - segmentId, - includeReplayStartTimestamp, - eventContext, - }); - resolve(true); - } catch (err) { - reject(err); - } - }, this._retryInterval); - }); - } - } - /** Save the session, if it is sticky */ private _maybeSaveSession(): void { if (this.session && this._options.stickySession) { @@ -1086,14 +889,14 @@ export class ReplayContainer implements ReplayContainerInterface { /** * Pauses the replay and resumes it after the rate-limit duration is over. */ - private _handleRateLimit(): void { + private _handleRateLimit(rateLimits: RateLimits): void { // in case recording is already paused, we don't need to do anything, as we might have already paused because of a // rate limit if (this.isPaused()) { return; } - const rateLimitEnd = disabledUntil(this._rateLimits, 'replay'); + const rateLimitEnd = disabledUntil(rateLimits, 'replay'); const rateLimitDuration = rateLimitEnd - Date.now(); if (rateLimitDuration > 0) { diff --git a/packages/replay/src/types.ts b/packages/replay/src/types.ts index 21a528575380..5d97766d6d00 100644 --- a/packages/replay/src/types.ts +++ b/packages/replay/src/types.ts @@ -5,17 +5,17 @@ import type { eventWithTime, recordOptions } from './types/rrweb'; export type RecordingEvent = eventWithTime; export type RecordingOptions = recordOptions; -export type RecordedEvents = Uint8Array | string; - export type AllPerformanceEntry = PerformancePaintTiming | PerformanceResourceTiming | PerformanceNavigationTiming; -export interface SendReplay { - events: RecordedEvents; +export interface SendReplayData { + recordingData: ReplayRecordingData; replayId: string; segmentId: number; includeReplayStartTimestamp: boolean; eventContext: PopEventContext; - timestamp?: number; + timestamp: number; + session: Session; + options: ReplayPluginOptions; } export type InstrumentationTypeBreadcrumb = 'dom' | 'scope'; diff --git a/packages/replay/src/util/createRecordingData.ts b/packages/replay/src/util/prepareRecordingData.ts similarity index 59% rename from packages/replay/src/util/createRecordingData.ts rename to packages/replay/src/util/prepareRecordingData.ts index a0d9835c1094..2ee3391d6f42 100644 --- a/packages/replay/src/util/createRecordingData.ts +++ b/packages/replay/src/util/prepareRecordingData.ts @@ -1,15 +1,13 @@ import type { ReplayRecordingData } from '@sentry/types'; -import type { RecordedEvents } from '../types'; - /** - * Create the recording data ready to be sent. + * Prepare the recording data ready to be sent. */ -export function createRecordingData({ - events, +export function prepareRecordingData({ + recordingData, headers, }: { - events: RecordedEvents; + recordingData: ReplayRecordingData; headers: Record; }): ReplayRecordingData { let payloadWithSequence; @@ -18,16 +16,16 @@ export function createRecordingData({ const replayHeaders = `${JSON.stringify(headers)} `; - if (typeof events === 'string') { - payloadWithSequence = `${replayHeaders}${events}`; + if (typeof recordingData === 'string') { + payloadWithSequence = `${replayHeaders}${recordingData}`; } else { const enc = new TextEncoder(); // XXX: newline is needed to separate sequence id from events const sequence = enc.encode(replayHeaders); // Merge the two Uint8Arrays - payloadWithSequence = new Uint8Array(sequence.length + events.length); + payloadWithSequence = new Uint8Array(sequence.length + recordingData.length); payloadWithSequence.set(sequence); - payloadWithSequence.set(events, sequence.length); + payloadWithSequence.set(recordingData, sequence.length); } return payloadWithSequence; diff --git a/packages/replay/src/util/sendReplay.ts b/packages/replay/src/util/sendReplay.ts new file mode 100644 index 000000000000..aa2e4649dda7 --- /dev/null +++ b/packages/replay/src/util/sendReplay.ts @@ -0,0 +1,61 @@ +import { captureException, setContext } from '@sentry/core'; + +import { RETRY_BASE_INTERVAL, RETRY_MAX_COUNT, UNABLE_TO_SEND_REPLAY } from '../constants'; +import type { SendReplayData } from '../types'; +import { RateLimitError, sendReplayRequest } from './sendReplayRequest'; + +/** + * Finalize and send the current replay event to Sentry + */ +export async function sendReplay( + replayData: SendReplayData, + retryConfig = { + count: 0, + interval: RETRY_BASE_INTERVAL, + }, +): Promise { + const { recordingData, options } = replayData; + + // short circuit if there's no events to upload (this shouldn't happen as _runFlush makes this check) + if (!recordingData.length) { + return; + } + + try { + await sendReplayRequest(replayData); + return true; + } catch (err) { + if (err instanceof RateLimitError) { + throw err; + } + + // Capture error for every failed replay + setContext('Replays', { + _retryCount: retryConfig.count, + }); + + if (__DEBUG_BUILD__ && options._experiments && options._experiments.captureExceptions) { + captureException(err); + } + + // If an error happened here, it's likely that uploading the attachment + // failed, we'll can retry with the same events payload + if (retryConfig.count >= RETRY_MAX_COUNT) { + throw new Error(`${UNABLE_TO_SEND_REPLAY} - max retries exceeded`); + } + + // will retry in intervals of 5, 10, 30 + retryConfig.interval *= ++retryConfig.count; + + return await new Promise((resolve, reject) => { + setTimeout(async () => { + try { + await sendReplay(replayData, retryConfig); + resolve(true); + } catch (err) { + reject(err); + } + }, retryConfig.interval); + }); + } +} diff --git a/packages/replay/src/util/sendReplayRequest.ts b/packages/replay/src/util/sendReplayRequest.ts new file mode 100644 index 000000000000..23df14ffcdda --- /dev/null +++ b/packages/replay/src/util/sendReplayRequest.ts @@ -0,0 +1,138 @@ +import { getCurrentHub } from '@sentry/core'; +import type { ReplayEvent, TransportMakeRequestResponse } from '@sentry/types'; +import type { RateLimits } from '@sentry/utils'; +import { isRateLimited, logger, updateRateLimits } from '@sentry/utils'; + +import { REPLAY_EVENT_NAME, UNABLE_TO_SEND_REPLAY } from '../constants'; +import type { SendReplayData } from '../types'; +import { createReplayEnvelope } from './createReplayEnvelope'; +import { prepareRecordingData } from './prepareRecordingData'; +import { prepareReplayEvent } from './prepareReplayEvent'; + +/** + * Send replay attachment using `fetch()` + */ +export async function sendReplayRequest({ + recordingData, + replayId, + segmentId: segment_id, + includeReplayStartTimestamp, + eventContext, + timestamp, + session, + options, +}: SendReplayData): Promise { + const preparedRecordingData = prepareRecordingData({ + recordingData, + headers: { + segment_id, + }, + }); + + const { urls, errorIds, traceIds, initialTimestamp } = eventContext; + + const hub = getCurrentHub(); + const client = hub.getClient(); + const scope = hub.getScope(); + const transport = client && client.getTransport(); + const dsn = client?.getDsn(); + + if (!client || !scope || !transport || !dsn || !session.sampled) { + return; + } + + const baseEvent: ReplayEvent = { + // @ts-ignore private api + type: REPLAY_EVENT_NAME, + ...(includeReplayStartTimestamp ? { replay_start_timestamp: initialTimestamp / 1000 } : {}), + timestamp: timestamp / 1000, + error_ids: errorIds, + trace_ids: traceIds, + urls, + replay_id: replayId, + segment_id, + replay_type: session.sampled, + }; + + const replayEvent = await prepareReplayEvent({ scope, client, replayId, event: baseEvent }); + + if (!replayEvent) { + // Taken from baseclient's `_processEvent` method, where this is handled for errors/transactions + client.recordDroppedEvent('event_processor', 'replay_event', baseEvent); + __DEBUG_BUILD__ && logger.log('An event processor returned `null`, will not send event.'); + return; + } + + replayEvent.tags = { + ...replayEvent.tags, + sessionSampleRate: options.sessionSampleRate, + errorSampleRate: options.errorSampleRate, + }; + + /* + For reference, the fully built event looks something like this: + { + "type": "replay_event", + "timestamp": 1670837008.634, + "error_ids": [ + "errorId" + ], + "trace_ids": [ + "traceId" + ], + "urls": [ + "https://example.com" + ], + "replay_id": "eventId", + "segment_id": 3, + "replay_type": "error", + "platform": "javascript", + "event_id": "eventId", + "environment": "production", + "sdk": { + "integrations": [ + "BrowserTracing", + "Replay" + ], + "name": "sentry.javascript.browser", + "version": "7.25.0" + }, + "sdkProcessingMetadata": {}, + "tags": { + "sessionSampleRate": 1, + "errorSampleRate": 0, + } + } + */ + + const envelope = createReplayEnvelope(replayEvent, preparedRecordingData, dsn, client.getOptions().tunnel); + + let response: void | TransportMakeRequestResponse; + + try { + response = await transport.send(envelope); + } catch { + throw new Error(UNABLE_TO_SEND_REPLAY); + } + + // TODO (v8): we can remove this guard once transport.send's type signature doesn't include void anymore + if (response) { + const rateLimits = updateRateLimits({}, response); + if (isRateLimited(rateLimits, 'replay')) { + throw new RateLimitError(rateLimits); + } + } + return response; +} + +/** + * This error indicates that we hit a rate limit API error. + */ +export class RateLimitError extends Error { + public rateLimits: RateLimits; + + public constructor(rateLimits: RateLimits) { + super('Rate limit hit'); + this.rateLimits = rateLimits; + } +} diff --git a/packages/replay/test/integration/errorSampleRate.test.ts b/packages/replay/test/integration/errorSampleRate.test.ts index 2730560093a4..2b52e6c7d96c 100644 --- a/packages/replay/test/integration/errorSampleRate.test.ts +++ b/packages/replay/test/integration/errorSampleRate.test.ts @@ -68,7 +68,7 @@ describe('Integration | errorSampleRate', () => { sessionSampleRate: 0, }), }), - events: JSON.stringify([ + recordingData: JSON.stringify([ { data: { isCheckout: true }, timestamp: BASE_TIMESTAMP, type: 2 }, TEST_EVENT, { @@ -98,7 +98,7 @@ describe('Integration | errorSampleRate', () => { sessionSampleRate: 0, }), }), - events: JSON.stringify([{ data: { isCheckout: true }, timestamp: BASE_TIMESTAMP + 5020, type: 2 }]), + recordingData: JSON.stringify([{ data: { isCheckout: true }, timestamp: BASE_TIMESTAMP + 5020, type: 2 }]), }); jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY); @@ -106,7 +106,7 @@ describe('Integration | errorSampleRate', () => { // New checkout when we call `startRecording` again after uploading segment // after an error occurs expect(replay).toHaveLastSentReplay({ - events: JSON.stringify([ + recordingData: JSON.stringify([ { data: { isCheckout: true }, timestamp: BASE_TIMESTAMP + DEFAULT_FLUSH_MIN_DELAY + 20, @@ -124,7 +124,7 @@ describe('Integration | errorSampleRate', () => { await new Promise(process.nextTick); expect(replay).toHaveLastSentReplay({ - events: JSON.stringify([ + recordingData: JSON.stringify([ { type: 5, timestamp: BASE_TIMESTAMP + 10000 + 40, @@ -304,7 +304,7 @@ describe('Integration | errorSampleRate', () => { await new Promise(process.nextTick); expect(replay).toHaveSentReplay({ - events: JSON.stringify([{ data: { isCheckout: true }, timestamp: BASE_TIMESTAMP, type: 2 }, TEST_EVENT]), + recordingData: JSON.stringify([{ data: { isCheckout: true }, timestamp: BASE_TIMESTAMP, type: 2 }, TEST_EVENT]), replayEventPayload: expect.objectContaining({ replay_start_timestamp: BASE_TIMESTAMP / 1000, // the exception happens roughly 10 seconds after BASE_TIMESTAMP @@ -360,7 +360,7 @@ describe('Integration | errorSampleRate', () => { // Make sure the old performance event is thrown out replay_start_timestamp: (BASE_TIMESTAMP + ELAPSED + 20) / 1000, }), - events: JSON.stringify([ + recordingData: JSON.stringify([ { data: { isCheckout: true }, timestamp: BASE_TIMESTAMP + ELAPSED + 20, @@ -406,12 +406,12 @@ it('sends a replay after loading the session multiple times', async () => { await new Promise(process.nextTick); expect(replay).toHaveSentReplay({ - events: JSON.stringify([{ data: { isCheckout: true }, timestamp: BASE_TIMESTAMP, type: 2 }, TEST_EVENT]), + recordingData: JSON.stringify([{ data: { isCheckout: true }, timestamp: BASE_TIMESTAMP, type: 2 }, TEST_EVENT]), }); // Latest checkout when we call `startRecording` again after uploading segment // after an error occurs (e.g. when we switch to session replay recording) expect(replay).toHaveLastSentReplay({ - events: JSON.stringify([{ data: { isCheckout: true }, timestamp: BASE_TIMESTAMP + 5020, type: 2 }]), + recordingData: JSON.stringify([{ data: { isCheckout: true }, timestamp: BASE_TIMESTAMP + 5020, type: 2 }]), }); }); diff --git a/packages/replay/test/integration/events.test.ts b/packages/replay/test/integration/events.test.ts index 9bfe34484ce6..5168426dfd79 100644 --- a/packages/replay/test/integration/events.test.ts +++ b/packages/replay/test/integration/events.test.ts @@ -23,9 +23,6 @@ describe('Integration | events', () => { let mockTransportSend: jest.SpyInstance; const prevLocation = WINDOW.location; - type MockSendReplayRequest = jest.MockedFunction; - let mockSendReplayRequest: MockSendReplayRequest; - beforeAll(async () => { jest.setSystemTime(new Date(BASE_TIMESTAMP)); jest.runAllTimers(); @@ -40,15 +37,11 @@ describe('Integration | events', () => { mockTransportSend = jest.spyOn(getCurrentHub().getClient()!.getTransport()!, 'send'); - // @ts-ignore private API - mockSendReplayRequest = jest.spyOn(replay, '_sendReplayRequest'); - // Create a new session and clear mocks because a segment (from initial // checkout) will have already been uploaded by the time the tests run clearSession(replay); replay['_loadSession']({ expiry: 0 }); mockTransportSend.mockClear(); - mockSendReplayRequest.mockClear(); }); afterEach(async () => { @@ -60,7 +53,6 @@ describe('Integration | events', () => { }); clearSession(replay); jest.clearAllMocks(); - mockSendReplayRequest.mockRestore(); mockRecord.takeFullSnapshot.mockClear(); replay.stop(); }); @@ -184,7 +176,7 @@ describe('Integration | events', () => { // Make sure the old performance event is thrown out replay_start_timestamp: BASE_TIMESTAMP / 1000, }), - events: JSON.stringify([ + recordingData: JSON.stringify([ TEST_EVENT, { type: 5, diff --git a/packages/replay/test/integration/flush.test.ts b/packages/replay/test/integration/flush.test.ts index f2760ef43371..c0c143b255e4 100644 --- a/packages/replay/test/integration/flush.test.ts +++ b/packages/replay/test/integration/flush.test.ts @@ -6,6 +6,7 @@ import type { EventBuffer } from '../../src/types'; import * as AddMemoryEntry from '../../src/util/addMemoryEntry'; import { createPerformanceEntries } from '../../src/util/createPerformanceEntries'; import { createPerformanceSpans } from '../../src/util/createPerformanceSpans'; +import * as SendReplay from '../../src/util/sendReplay'; import { BASE_TIMESTAMP, mockRrweb, mockSdk } from '../index'; import { clearSession } from '../utils/clearSession'; import { useFakeTimers } from '../utils/use-fake-timers'; @@ -17,7 +18,7 @@ async function advanceTimers(time: number) { await new Promise(process.nextTick); } -type MockSendReplay = jest.MockedFunction; +type MockSendReplay = jest.MockedFunction; type MockAddPerformanceEntries = jest.MockedFunction; type MockAddMemoryEntry = jest.SpyInstance; type MockEventBufferFinish = jest.MockedFunction; @@ -48,8 +49,7 @@ describe('Integration | flush', () => { ({ replay } = await mockSdk()); - // @ts-ignore private API - mockSendReplay = jest.spyOn(replay, '_sendReplay'); + mockSendReplay = jest.spyOn(SendReplay, 'sendReplay'); mockSendReplay.mockImplementation( jest.fn(async () => { return; @@ -175,12 +175,16 @@ describe('Integration | flush', () => { // debouncedFlush, which will call `flush` in 1 second expect(mockFlush).toHaveBeenCalledTimes(4); // sendReplay is called with replayId, events, segment + expect(mockSendReplay).toHaveBeenLastCalledWith({ - events: expect.any(String), + recordingData: expect.any(String), replayId: expect.any(String), includeReplayStartTimestamp: true, segmentId: 0, eventContext: expect.anything(), + session: expect.any(Object), + options: expect.any(Object), + timestamp: expect.any(Number), }); // Add this to test that segment ID increases @@ -223,11 +227,14 @@ describe('Integration | flush', () => { expect(mockFlush).toHaveBeenCalledTimes(5); expect(mockRunFlush).toHaveBeenCalledTimes(2); expect(mockSendReplay).toHaveBeenLastCalledWith({ - events: expect.any(String), + recordingData: expect.any(String), replayId: expect.any(String), includeReplayStartTimestamp: false, segmentId: 1, eventContext: expect.anything(), + session: expect.any(Object), + options: expect.any(Object), + timestamp: expect.any(Number), }); // Make sure there's no other calls diff --git a/packages/replay/test/integration/rateLimiting.test.ts b/packages/replay/test/integration/rateLimiting.test.ts index 1270d7c9d780..31e15638c314 100644 --- a/packages/replay/test/integration/rateLimiting.test.ts +++ b/packages/replay/test/integration/rateLimiting.test.ts @@ -3,6 +3,7 @@ import type { Transport, TransportMakeRequestResponse } from '@sentry/types'; import { DEFAULT_FLUSH_MIN_DELAY, SESSION_IDLE_DURATION } from '../../src/constants'; import type { ReplayContainer } from '../../src/replay'; +import * as SendReplayRequest from '../../src/util/sendReplayRequest'; import { BASE_TIMESTAMP, mockSdk } from '../index'; import { mockRrweb } from '../mocks/mockRrweb'; import { clearSession } from '../utils/clearSession'; @@ -16,12 +17,11 @@ async function advanceTimers(time: number) { } type MockTransportSend = jest.MockedFunction; -type MockSendReplayRequest = jest.MockedFunction; describe('Integration | rate-limiting behaviour', () => { let replay: ReplayContainer; let mockTransportSend: MockTransportSend; - let mockSendReplayRequest: MockSendReplayRequest; + let mockSendReplayRequest: jest.MockedFunction; const { record: mockRecord } = mockRrweb(); beforeAll(async () => { @@ -33,12 +33,9 @@ describe('Integration | rate-limiting behaviour', () => { }, })); - // @ts-ignore private API - jest.spyOn(replay, '_sendReplayRequest'); - jest.runAllTimers(); mockTransportSend = getCurrentHub()?.getClient()?.getTransport()?.send as MockTransportSend; - mockSendReplayRequest = replay['_sendReplayRequest'] as MockSendReplayRequest; + mockSendReplayRequest = jest.spyOn(SendReplayRequest, 'sendReplayRequest'); }); beforeEach(() => { @@ -52,8 +49,6 @@ describe('Integration | rate-limiting behaviour', () => { replay['_loadSession']({ expiry: 0 }); mockSendReplayRequest.mockClear(); - - replay['_rateLimits'] = {}; }); afterEach(async () => { @@ -92,15 +87,13 @@ describe('Integration | rate-limiting behaviour', () => { }, }, ] as TransportMakeRequestResponse[])( - 'pauses recording and flushing a rate limit is hit and resumes both after the rate limit duration is over', + 'pauses recording and flushing a rate limit is hit and resumes both after the rate limit duration is over %j', async rateLimitResponse => { expect(replay.session?.segmentId).toBe(0); jest.spyOn(replay, 'pause'); jest.spyOn(replay, 'resume'); // @ts-ignore private API jest.spyOn(replay, '_handleRateLimit'); - // @ts-ignore private API - jest.spyOn(replay, '_sendReplay'); const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 2 }; @@ -115,7 +108,7 @@ describe('Integration | rate-limiting behaviour', () => { expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); expect(mockTransportSend).toHaveBeenCalledTimes(1); - expect(replay).toHaveLastSentReplay({ events: JSON.stringify([TEST_EVENT]) }); + expect(replay).toHaveLastSentReplay({ recordingData: JSON.stringify([TEST_EVENT]) }); expect(replay['_handleRateLimit']).toHaveBeenCalledTimes(1); // resume() was called once before we even started @@ -136,7 +129,7 @@ describe('Integration | rate-limiting behaviour', () => { mockRecord._emitter(ev); await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); expect(replay.isPaused()).toBe(true); - expect(replay['_sendReplay']).toHaveBeenCalledTimes(1); + expect(mockSendReplayRequest).toHaveBeenCalledTimes(1); expect(mockTransportSend).toHaveBeenCalledTimes(1); } @@ -148,9 +141,9 @@ describe('Integration | rate-limiting behaviour', () => { expect(replay.resume).toHaveBeenCalledTimes(1); expect(replay.isPaused()).toBe(false); - expect(replay['_sendReplay']).toHaveBeenCalledTimes(2); + expect(mockSendReplayRequest).toHaveBeenCalledTimes(2); expect(replay).toHaveLastSentReplay({ - events: JSON.stringify([ + recordingData: JSON.stringify([ { data: { isCheckout: true }, timestamp: BASE_TIMESTAMP + DEFAULT_FLUSH_MIN_DELAY * 7, type: 2 }, ]), }); @@ -165,14 +158,14 @@ describe('Integration | rate-limiting behaviour', () => { // T = base + 40 await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); - expect(replay['_sendReplay']).toHaveBeenCalledTimes(3); - expect(replay).toHaveLastSentReplay({ events: JSON.stringify([TEST_EVENT3]) }); + expect(mockSendReplayRequest).toHaveBeenCalledTimes(3); + expect(replay).toHaveLastSentReplay({ recordingData: JSON.stringify([TEST_EVENT3]) }); // nothing should happen afterwards // T = base + 60 await advanceTimers(20_000); - expect(replay['_sendReplay']).toHaveBeenCalledTimes(3); - expect(replay).toHaveLastSentReplay({ events: JSON.stringify([TEST_EVENT3]) }); + expect(mockSendReplayRequest).toHaveBeenCalledTimes(3); + expect(replay).toHaveLastSentReplay({ recordingData: JSON.stringify([TEST_EVENT3]) }); // events array should be empty expect(replay.eventBuffer?.pendingLength).toBe(0); @@ -185,8 +178,6 @@ describe('Integration | rate-limiting behaviour', () => { jest.spyOn(replay, 'resume'); // @ts-ignore private API jest.spyOn(replay, '_handleRateLimit'); - // @ts-ignore private API - jest.spyOn(replay, '_sendReplay'); const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 2 }; @@ -201,7 +192,7 @@ describe('Integration | rate-limiting behaviour', () => { expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); expect(mockTransportSend).toHaveBeenCalledTimes(1); - expect(replay).toHaveLastSentReplay({ events: JSON.stringify([TEST_EVENT]) }); + expect(replay).toHaveLastSentReplay({ recordingData: JSON.stringify([TEST_EVENT]) }); expect(replay['_handleRateLimit']).toHaveBeenCalledTimes(1); // resume() was called once before we even started @@ -223,7 +214,7 @@ describe('Integration | rate-limiting behaviour', () => { mockRecord._emitter(ev); await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); expect(replay.isPaused()).toBe(true); - expect(replay['_sendReplay']).toHaveBeenCalledTimes(1); + expect(mockSendReplayRequest).toHaveBeenCalledTimes(1); expect(mockTransportSend).toHaveBeenCalledTimes(1); } @@ -235,9 +226,9 @@ describe('Integration | rate-limiting behaviour', () => { expect(replay.resume).toHaveBeenCalledTimes(1); expect(replay.isPaused()).toBe(false); - expect(replay['_sendReplay']).toHaveBeenCalledTimes(2); + expect(mockSendReplayRequest).toHaveBeenCalledTimes(2); expect(replay).toHaveLastSentReplay({ - events: JSON.stringify([ + recordingData: JSON.stringify([ { data: { isCheckout: true }, timestamp: BASE_TIMESTAMP + DEFAULT_FLUSH_MIN_DELAY * 13, type: 2 }, ]), }); @@ -252,14 +243,14 @@ describe('Integration | rate-limiting behaviour', () => { // T = base + 65 await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); - expect(replay['_sendReplay']).toHaveBeenCalledTimes(3); - expect(replay).toHaveLastSentReplay({ events: JSON.stringify([TEST_EVENT3]) }); + expect(mockSendReplayRequest).toHaveBeenCalledTimes(3); + expect(replay).toHaveLastSentReplay({ recordingData: JSON.stringify([TEST_EVENT3]) }); // nothing should happen afterwards // T = base + 85 await advanceTimers(20_000); - expect(replay['_sendReplay']).toHaveBeenCalledTimes(3); - expect(replay).toHaveLastSentReplay({ events: JSON.stringify([TEST_EVENT3]) }); + expect(mockSendReplayRequest).toHaveBeenCalledTimes(3); + expect(replay).toHaveLastSentReplay({ recordingData: JSON.stringify([TEST_EVENT3]) }); // events array should be empty expect(replay.eventBuffer?.pendingLength).toBe(0); @@ -291,7 +282,7 @@ describe('Integration | rate-limiting behaviour', () => { expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); expect(mockTransportSend).toHaveBeenCalledTimes(1); - expect(replay).toHaveLastSentReplay({ events: JSON.stringify([TEST_EVENT]) }); + expect(replay).toHaveLastSentReplay({ recordingData: JSON.stringify([TEST_EVENT]) }); expect(replay['_handleRateLimit']).toHaveBeenCalledTimes(1); expect(replay.resume).not.toHaveBeenCalled(); diff --git a/packages/replay/test/integration/sendReplayEvent.test.ts b/packages/replay/test/integration/sendReplayEvent.test.ts index ade380a1605a..9a4c62fa8ede 100644 --- a/packages/replay/test/integration/sendReplayEvent.test.ts +++ b/packages/replay/test/integration/sendReplayEvent.test.ts @@ -1,10 +1,11 @@ -import { getCurrentHub } from '@sentry/core'; +import * as SentryCore from '@sentry/core'; import type { Transport } from '@sentry/types'; import * as SentryUtils from '@sentry/utils'; import { DEFAULT_FLUSH_MIN_DELAY, SESSION_IDLE_DURATION, WINDOW } from '../../src/constants'; import type { ReplayContainer } from '../../src/replay'; import { addEvent } from '../../src/util/addEvent'; +import * as SendReplayRequest from '../../src/util/sendReplayRequest'; import { BASE_TIMESTAMP, mockRrweb, mockSdk } from '../index'; import { clearSession } from '../utils/clearSession'; import { useFakeTimers } from '../utils/use-fake-timers'; @@ -17,12 +18,11 @@ async function advanceTimers(time: number) { } type MockTransportSend = jest.MockedFunction; -type MockSendReplayRequest = jest.MockedFunction; describe('Integration | sendReplayEvent', () => { let replay: ReplayContainer; let mockTransportSend: MockTransportSend; - let mockSendReplayRequest: MockSendReplayRequest; + let mockSendReplayRequest: jest.SpyInstance; let domHandler: (args: any) => any; const { record: mockRecord } = mockRrweb(); @@ -37,14 +37,16 @@ describe('Integration | sendReplayEvent', () => { ({ replay } = await mockSdk({ replayOptions: { stickySession: false, + _experiments: { + captureExceptions: true, + }, }, })); - // @ts-ignore private API - mockSendReplayRequest = jest.spyOn(replay, '_sendReplayRequest'); + mockSendReplayRequest = jest.spyOn(SendReplayRequest, 'sendReplayRequest'); jest.runAllTimers(); - mockTransportSend = getCurrentHub()?.getClient()?.getTransport()?.send as MockTransportSend; + mockTransportSend = SentryCore.getCurrentHub()?.getClient()?.getTransport()?.send as MockTransportSend; }); beforeEach(() => { @@ -94,7 +96,7 @@ describe('Integration | sendReplayEvent', () => { expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); - expect(replay).toHaveLastSentReplay({ events: JSON.stringify([TEST_EVENT]) }); + expect(replay).toHaveLastSentReplay({ recordingData: JSON.stringify([TEST_EVENT]) }); // Session's last activity is not updated because we do not consider // visibilitystate as user being active @@ -134,7 +136,7 @@ describe('Integration | sendReplayEvent', () => { expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); - expect(replay).toHaveLastSentReplay({ events: JSON.stringify([TEST_EVENT]) }); + expect(replay).toHaveLastSentReplay({ recordingData: JSON.stringify([TEST_EVENT]) }); // No user activity to trigger an update expect(replay.session?.lastActivity).toBe(BASE_TIMESTAMP); @@ -158,7 +160,7 @@ describe('Integration | sendReplayEvent', () => { await new Promise(process.nextTick); expect(replay).toHaveLastSentReplay({ - events: JSON.stringify([...Array(5)].map(() => TEST_EVENT)), + recordingData: JSON.stringify([...Array(5)].map(() => TEST_EVENT)), }); // There should also not be another attempt at an upload 5 seconds after the last replay event @@ -175,7 +177,7 @@ describe('Integration | sendReplayEvent', () => { mockTransportSend.mockClear(); mockRecord._emitter(TEST_EVENT); await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); - expect(replay).toHaveLastSentReplay({ events: JSON.stringify([TEST_EVENT]) }); + expect(replay).toHaveLastSentReplay({ recordingData: JSON.stringify([TEST_EVENT]) }); }); it('uploads a replay event when WINDOW is blurred', async () => { @@ -209,7 +211,7 @@ describe('Integration | sendReplayEvent', () => { await new Promise(process.nextTick); expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); expect(replay).toHaveLastSentReplay({ - events: JSON.stringify([TEST_EVENT, hiddenBreadcrumb]), + recordingData: JSON.stringify([TEST_EVENT, hiddenBreadcrumb]), }); // Session's last activity should not be updated expect(replay.session?.lastActivity).toBe(BASE_TIMESTAMP); @@ -236,7 +238,7 @@ describe('Integration | sendReplayEvent', () => { await new Promise(process.nextTick); expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); - expect(replay).toHaveLastSentReplay({ events: JSON.stringify([TEST_EVENT]) }); + expect(replay).toHaveLastSentReplay({ recordingData: JSON.stringify([TEST_EVENT]) }); // Session's last activity is not updated because we do not consider // visibilitystate as user being active @@ -254,7 +256,7 @@ describe('Integration | sendReplayEvent', () => { expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); expect(mockTransportSend).toHaveBeenCalledTimes(1); - expect(replay).toHaveLastSentReplay({ events: JSON.stringify([TEST_EVENT]) }); + expect(replay).toHaveLastSentReplay({ recordingData: JSON.stringify([TEST_EVENT]) }); // No user activity to trigger an update expect(replay.session?.lastActivity).toBe(BASE_TIMESTAMP); @@ -278,7 +280,7 @@ describe('Integration | sendReplayEvent', () => { await new Promise(process.nextTick); expect(replay).toHaveLastSentReplay({ - events: JSON.stringify([...Array(5)].map(() => TEST_EVENT)), + recordingData: JSON.stringify([...Array(5)].map(() => TEST_EVENT)), }); // There should also not be another attempt at an upload 5 seconds after the last replay event @@ -296,7 +298,7 @@ describe('Integration | sendReplayEvent', () => { mockTransportSend.mockClear(); mockRecord._emitter(TEST_EVENT); await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); - expect(replay).toHaveLastSentReplay({ events: JSON.stringify([TEST_EVENT]) }); + expect(replay).toHaveLastSentReplay({ recordingData: JSON.stringify([TEST_EVENT]) }); }); it('uploads a dom breadcrumb 5 seconds after listener receives an event', async () => { @@ -309,7 +311,7 @@ describe('Integration | sendReplayEvent', () => { await advanceTimers(ELAPSED); expect(replay).toHaveLastSentReplay({ - events: JSON.stringify([ + recordingData: JSON.stringify([ { type: 5, timestamp: BASE_TIMESTAMP, @@ -361,13 +363,13 @@ describe('Integration | sendReplayEvent', () => { error_ids: [], replay_id: expect.any(String), replay_start_timestamp: BASE_TIMESTAMP / 1000, - // 20seconds = Add up all of the previous `advanceTimers()` - timestamp: (BASE_TIMESTAMP + 20000) / 1000 + 0.02, + // timestamp is set on first try, after 5s flush + timestamp: (BASE_TIMESTAMP + 5000) / 1000, trace_ids: [], urls: ['http://localhost/'], }), recordingPayloadHeader: { segment_id: 0 }, - events: JSON.stringify([TEST_EVENT]), + recordingData: JSON.stringify([TEST_EVENT]), }); mockTransportSend.mockClear(); @@ -383,20 +385,15 @@ describe('Integration | sendReplayEvent', () => { it('fails to upload data and hits retry max and stops', async () => { const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 3 }; - // @ts-ignore private API - const spySendReplay = jest.spyOn(replay, '_sendReplay'); + const spyHandleException = jest.spyOn(SentryCore, 'captureException'); // Suppress console.errors const mockConsole = jest.spyOn(console, 'error').mockImplementation(jest.fn()); - // @ts-ignore privaye api - Check errors - const spyHandleException = jest.spyOn(replay, '_handleException'); - expect(replay.session?.segmentId).toBe(0); - // fail the first and second requests and pass the third one - mockSendReplayRequest.mockReset(); - mockSendReplayRequest.mockImplementation(() => { + // fail all requests + mockSendReplayRequest.mockImplementation(async () => { throw new Error('Something bad happened'); }); mockRecord._emitter(TEST_EVENT); @@ -414,14 +411,12 @@ describe('Integration | sendReplayEvent', () => { await advanceTimers(30000); expect(mockSendReplayRequest).toHaveBeenCalledTimes(4); - expect(spySendReplay).toHaveBeenCalledTimes(4); mockConsole.mockReset(); // Make sure it doesn't retry again jest.runAllTimers(); expect(mockSendReplayRequest).toHaveBeenCalledTimes(4); - expect(spySendReplay).toHaveBeenCalledTimes(4); // Retries = 3 (total tries = 4 including initial attempt) // + last exception is max retries exceeded diff --git a/packages/replay/test/integration/session.test.ts b/packages/replay/test/integration/session.test.ts index c07bac150751..0a8a79dc5467 100644 --- a/packages/replay/test/integration/session.test.ts +++ b/packages/replay/test/integration/session.test.ts @@ -149,7 +149,7 @@ describe('Integration | session', () => { const breadcrumbTimestamp = newTimestamp + 20; // I don't know where this 20ms comes from expect(replay).toHaveLastSentReplay({ - events: JSON.stringify([ + recordingData: JSON.stringify([ { data: { isCheckout: true }, timestamp: newTimestamp, type: 2 }, { type: 5, @@ -309,7 +309,7 @@ describe('Integration | session', () => { expect(replay).toHaveLastSentReplay({ recordingPayloadHeader: { segment_id: 0 }, - events: JSON.stringify([ + recordingData: JSON.stringify([ { data: { isCheckout: true }, timestamp: newTimestamp, type: 2 }, { type: 5, @@ -420,7 +420,7 @@ describe('Integration | session', () => { expect(replay).toHaveLastSentReplay({ recordingPayloadHeader: { segment_id: 0 }, - events: JSON.stringify([ + recordingData: JSON.stringify([ { data: { isCheckout: true }, timestamp: newTimestamp, type: 2 }, { type: 5, diff --git a/packages/replay/test/integration/stop.test.ts b/packages/replay/test/integration/stop.test.ts index 42ccbe09ca68..55f8dafd9289 100644 --- a/packages/replay/test/integration/stop.test.ts +++ b/packages/replay/test/integration/stop.test.ts @@ -110,7 +110,7 @@ describe('Integration | stop', () => { jest.runAllTimers(); await new Promise(process.nextTick); expect(replay).toHaveLastSentReplay({ - events: JSON.stringify([ + recordingData: JSON.stringify([ // This event happens when we call `replay.start` { data: { isCheckout: true }, From 266240106a7dcac75dc7c534bb9db238af9adf34 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 13 Jan 2023 08:32:12 +0100 Subject: [PATCH 057/113] ref: Replace `string.substr` with `string.slice` (#6760) `substr` is actually deprecated (see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/substr). For our cases, `slice()` should work just as well - see this very informative answer: https://stackoverflow.com/a/31910656 --- packages/node/src/module.ts | 4 ++-- packages/utils/src/path.ts | 12 ++++++------ packages/utils/src/stacktrace.ts | 10 +++++----- packages/utils/src/string.ts | 2 +- packages/wasm/src/registry.ts | 2 +- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/node/src/module.ts b/packages/node/src/module.ts index 1e759df77506..0bb3335b635c 100644 --- a/packages/node/src/module.ts +++ b/packages/node/src/module.ts @@ -27,14 +27,14 @@ export function getModule(filename: string | undefined): string | undefined { let n = path.lastIndexOf('/node_modules/'); if (n > -1) { // /node_modules/ is 14 chars - return `${path.substr(n + 14).replace(/\//g, '.')}:${file}`; + return `${path.slice(n + 14).replace(/\//g, '.')}:${file}`; } // Let's see if it's a part of the main module // To be a part of main module, it has to share the same base n = `${path}/`.lastIndexOf(base, 0); if (n === 0) { - let moduleName = path.substr(base.length).replace(/\//g, '.'); + let moduleName = path.slice(base.length).replace(/\//g, '.'); if (moduleName) { moduleName += ':'; } diff --git a/packages/utils/src/path.ts b/packages/utils/src/path.ts index b626123505d3..46f68b1b16a5 100644 --- a/packages/utils/src/path.ts +++ b/packages/utils/src/path.ts @@ -95,8 +95,8 @@ function trim(arr: string[]): string[] { /** JSDoc */ export function relative(from: string, to: string): string { /* eslint-disable no-param-reassign */ - from = resolve(from).substr(1); - to = resolve(to).substr(1); + from = resolve(from).slice(1); + to = resolve(to).slice(1); /* eslint-enable no-param-reassign */ const fromParts = trim(from.split('/')); @@ -126,7 +126,7 @@ export function relative(from: string, to: string): string { /** JSDoc */ export function normalizePath(path: string): string { const isPathAbsolute = isAbsolute(path); - const trailingSlash = path.substr(-1) === '/'; + const trailingSlash = path.slice(-1) === '/'; // Normalize the path let normalizedPath = normalizeArray( @@ -169,7 +169,7 @@ export function dirname(path: string): string { if (dir) { // It has a dirname, strip trailing slash - dir = dir.substr(0, dir.length - 1); + dir = dir.slice(0, dir.length - 1); } return root + dir; @@ -178,8 +178,8 @@ export function dirname(path: string): string { /** JSDoc */ export function basename(path: string, ext?: string): string { let f = splitPath(path)[2]; - if (ext && f.substr(ext.length * -1) === ext) { - f = f.substr(0, f.length - ext.length); + if (ext && f.slice(ext.length * -1) === ext) { + f = f.slice(0, f.length - ext.length); } return f; } diff --git a/packages/utils/src/stacktrace.ts b/packages/utils/src/stacktrace.ts index 742a28049695..d9891c89c8de 100644 --- a/packages/utils/src/stacktrace.ts +++ b/packages/utils/src/stacktrace.ts @@ -142,12 +142,12 @@ function node(getModule?: GetModuleFn): StackLineParserFn { } if (methodStart > 0) { - object = functionName.substr(0, methodStart); - method = functionName.substr(methodStart + 1); + object = functionName.slice(0, methodStart); + method = functionName.slice(methodStart + 1); const objectEnd = object.indexOf('.Module'); if (objectEnd > 0) { - functionName = functionName.substr(objectEnd + 1); - object = object.substr(0, objectEnd); + functionName = functionName.slice(objectEnd + 1); + object = object.slice(0, objectEnd); } } typeName = undefined; @@ -168,7 +168,7 @@ function node(getModule?: GetModuleFn): StackLineParserFn { functionName = typeName ? `${typeName}.${methodName}` : methodName; } - const filename = lineMatch[2]?.startsWith('file://') ? lineMatch[2].substr(7) : lineMatch[2]; + const filename = lineMatch[2]?.startsWith('file://') ? lineMatch[2].slice(7) : lineMatch[2]; const isNative = lineMatch[5] === 'native'; const isInternal = isNative || (filename && !filename.startsWith('/') && !filename.startsWith('.') && filename.indexOf(':\\') !== 1); diff --git a/packages/utils/src/string.ts b/packages/utils/src/string.ts index 90b76b6f4621..7557d1d2af77 100644 --- a/packages/utils/src/string.ts +++ b/packages/utils/src/string.ts @@ -11,7 +11,7 @@ export function truncate(str: string, max: number = 0): string { if (typeof str !== 'string' || max === 0) { return str; } - return str.length <= max ? str : `${str.substr(0, max)}...`; + return str.length <= max ? str : `${str.slice(0, max)}...`; } /** diff --git a/packages/wasm/src/registry.ts b/packages/wasm/src/registry.ts index f000dbff7c53..f0b1069ed7e9 100644 --- a/packages/wasm/src/registry.ts +++ b/packages/wasm/src/registry.ts @@ -50,7 +50,7 @@ export function registerModule(module: WebAssembly.Module, url: string): void { code_id: buildId, code_file: url, debug_file: debugFile ? new URL(debugFile, url).href : null, - debug_id: `${buildId.padEnd(32, '0').substr(0, 32)}0`, + debug_id: `${buildId.padEnd(32, '0').slice(0, 32)}0`, }); } } From f1afb1ff3a34efd149cf6134e8c1ccf7caba7632 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Fri, 13 Jan 2023 09:16:50 +0000 Subject: [PATCH 058/113] test(nextjs): Add NextJS client-side E2E tests (#6669) --- packages/e2e-tests/README.md | 4 +- packages/e2e-tests/package.json | 1 + packages/e2e-tests/run.ts | 5 + .../create-next-app/.gitignore | 41 + .../test-applications/create-next-app/.npmrc | 2 + .../create-next-app/globals.d.ts | 4 + .../create-next-app/next-env.d.ts | 5 + .../create-next-app/next.config.js | 39 + .../create-next-app/package.json | 27 + .../create-next-app/pages/_app.tsx | 5 + .../create-next-app/pages/_document.tsx | 13 + .../create-next-app/pages/_error.tsx | 40 + .../create-next-app/pages/api/hello.ts | 10 + .../create-next-app/pages/index.tsx | 30 + .../create-next-app/pages/user/[id].tsx | 5 + .../create-next-app/playwright.config.ts | 69 ++ .../create-next-app/sentry.client.config.ts | 30 + .../create-next-app/sentry.server.config.ts | 15 + .../create-next-app/test-recipe.json | 15 + .../tests/client/behaviour.test.ts | 171 ++++ .../create-next-app/tsconfig.json | 20 + .../create-next-app/yarn.lock | 789 ++++++++++++++++++ yarn.lock | 5 + 23 files changed, 1343 insertions(+), 2 deletions(-) create mode 100644 packages/e2e-tests/test-applications/create-next-app/.gitignore create mode 100644 packages/e2e-tests/test-applications/create-next-app/.npmrc create mode 100644 packages/e2e-tests/test-applications/create-next-app/globals.d.ts create mode 100644 packages/e2e-tests/test-applications/create-next-app/next-env.d.ts create mode 100644 packages/e2e-tests/test-applications/create-next-app/next.config.js create mode 100644 packages/e2e-tests/test-applications/create-next-app/package.json create mode 100644 packages/e2e-tests/test-applications/create-next-app/pages/_app.tsx create mode 100644 packages/e2e-tests/test-applications/create-next-app/pages/_document.tsx create mode 100644 packages/e2e-tests/test-applications/create-next-app/pages/_error.tsx create mode 100644 packages/e2e-tests/test-applications/create-next-app/pages/api/hello.ts create mode 100644 packages/e2e-tests/test-applications/create-next-app/pages/index.tsx create mode 100644 packages/e2e-tests/test-applications/create-next-app/pages/user/[id].tsx create mode 100644 packages/e2e-tests/test-applications/create-next-app/playwright.config.ts create mode 100644 packages/e2e-tests/test-applications/create-next-app/sentry.client.config.ts create mode 100644 packages/e2e-tests/test-applications/create-next-app/sentry.server.config.ts create mode 100644 packages/e2e-tests/test-applications/create-next-app/test-recipe.json create mode 100644 packages/e2e-tests/test-applications/create-next-app/tests/client/behaviour.test.ts create mode 100644 packages/e2e-tests/test-applications/create-next-app/tsconfig.json create mode 100644 packages/e2e-tests/test-applications/create-next-app/yarn.lock diff --git a/packages/e2e-tests/README.md b/packages/e2e-tests/README.md index ee4cedd17fb6..51b1f118f658 100644 --- a/packages/e2e-tests/README.md +++ b/packages/e2e-tests/README.md @@ -66,7 +66,7 @@ fields: - The `buildCommand` command runs only once before any of the tests and is supposed to build the test application. If this command returns a non-zero exit code, it counts as a failed test and the test application's tests are not run. In - the example above, we use the `--pure-lockfile` flag to install depencies without modifiying the lockfile so that + the example above, we use the `--pure-lockfile` flag to install dependencies without modifiying the lockfile so that there aren't any changes in the git worktree after running the tests. - The `testCommand` command is supposed to run tests on the test application. If the configured command returns a non-zero exit code, it counts as a failed test. @@ -107,7 +107,7 @@ A standardized frontend test application has the following features: `standard-frontend-nextjs`. - A page at path `/` - Having a `` that captures an Exception when clicked. The returned - `eventId` from the `Sentry.captureException()` call must be written to `window.capturedExceptionId`. It doesn not + `eventId` from the `Sentry.captureException()` call must be written to `window.capturedExceptionId`. It does not matter what the captured error looks like. - Having an link with `id="navigation"` that navigates to `/user/5`. It doesn't have to be an `` tag, for example if a framework has another way of doing routing, the important part is that the element to click for navigation has diff --git a/packages/e2e-tests/package.json b/packages/e2e-tests/package.json index 3c70a4163955..fb5a4a1d4914 100644 --- a/packages/e2e-tests/package.json +++ b/packages/e2e-tests/package.json @@ -21,6 +21,7 @@ "devDependencies": { "@types/glob": "8.0.0", "@types/node": "^14.6.4", + "dotenv": "16.0.3", "glob": "8.0.3", "ts-node": "10.9.1", "typescript": "3.8.3", diff --git a/packages/e2e-tests/run.ts b/packages/e2e-tests/run.ts index fd1ab60f2244..2709feb3a040 100644 --- a/packages/e2e-tests/run.ts +++ b/packages/e2e-tests/run.ts @@ -1,10 +1,14 @@ /* eslint-disable max-lines */ /* eslint-disable no-console */ import * as childProcess from 'child_process'; +import * as dotenv from 'dotenv'; import * as fs from 'fs'; import * as glob from 'glob'; import * as path from 'path'; +// Load environment variables from .env file locally +dotenv.config(); + const repositoryRoot = path.resolve(__dirname, '../..'); const TEST_REGISTRY_CONTAINER_NAME = 'verdaccio-e2e-test-registry'; @@ -51,6 +55,7 @@ if (missingEnvVar) { const envVarsToInject = { REACT_APP_E2E_TEST_DSN: process.env.E2E_TEST_DSN, + NEXT_PUBLIC_E2E_TEST_DSN: process.env.E2E_TEST_DSN, }; // https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#grouping-log-lines diff --git a/packages/e2e-tests/test-applications/create-next-app/.gitignore b/packages/e2e-tests/test-applications/create-next-app/.gitignore new file mode 100644 index 000000000000..d9f766878259 --- /dev/null +++ b/packages/e2e-tests/test-applications/create-next-app/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +!*.d.ts + +# Sentry +.sentryclirc diff --git a/packages/e2e-tests/test-applications/create-next-app/.npmrc b/packages/e2e-tests/test-applications/create-next-app/.npmrc new file mode 100644 index 000000000000..c6b3ef9b3eaa --- /dev/null +++ b/packages/e2e-tests/test-applications/create-next-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/create-next-app/globals.d.ts b/packages/e2e-tests/test-applications/create-next-app/globals.d.ts new file mode 100644 index 000000000000..109dbcd55648 --- /dev/null +++ b/packages/e2e-tests/test-applications/create-next-app/globals.d.ts @@ -0,0 +1,4 @@ +interface Window { + recordedTransactions?: string[]; + capturedExceptionId?: string; +} diff --git a/packages/e2e-tests/test-applications/create-next-app/next-env.d.ts b/packages/e2e-tests/test-applications/create-next-app/next-env.d.ts new file mode 100644 index 000000000000..4f11a03dc6cc --- /dev/null +++ b/packages/e2e-tests/test-applications/create-next-app/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/packages/e2e-tests/test-applications/create-next-app/next.config.js b/packages/e2e-tests/test-applications/create-next-app/next.config.js new file mode 100644 index 000000000000..efd4c00feec6 --- /dev/null +++ b/packages/e2e-tests/test-applications/create-next-app/next.config.js @@ -0,0 +1,39 @@ +// This file sets a custom webpack configuration to use your Next.js app +// with Sentry. +// https://nextjs.org/docs/api-reference/next.config.js/introduction +// https://docs.sentry.io/platforms/javascript/guides/nextjs/ + +const { withSentryConfig } = require('@sentry/nextjs'); + +const moduleExports = { + // Your existing module.exports + + sentry: { + // Use `hidden-source-map` rather than `source-map` as the Webpack `devtool` + // for client-side builds. (This will be the default starting in + // `@sentry/nextjs` version 8.0.0.) See + // https://webpack.js.org/configuration/devtool/ and + // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#use-hidden-source-map + // for more information. + hideSourceMaps: true, + }, +}; + +const sentryWebpackPluginOptions = { + // Additional config options for the Sentry Webpack plugin. Keep in mind that + // the following options are set automatically, and overriding them is not + // recommended: + // release, url, org, project, authToken, configFile, stripPrefix, + // urlPrefix, include, ignore + + silent: true, // Suppresses all logs + // For all available options, see: + // https://github.com/getsentry/sentry-webpack-plugin#options. + + // We're not testing source map uploads at the moment. + dryRun: true, +}; + +// Make sure adding Sentry options is the last code to run before exporting, to +// ensure that your source maps include changes from all other Webpack plugins +module.exports = withSentryConfig(moduleExports, sentryWebpackPluginOptions); diff --git a/packages/e2e-tests/test-applications/create-next-app/package.json b/packages/e2e-tests/test-applications/create-next-app/package.json new file mode 100644 index 000000000000..4dc80476aee9 --- /dev/null +++ b/packages/e2e-tests/test-applications/create-next-app/package.json @@ -0,0 +1,27 @@ +{ + "name": "create-next-app", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "test": "TEST_MODE=build playwright test", + "test:dev": "TEST_MODE=dev playwright test" + }, + "dependencies": { + "@next/font": "13.0.7", + "@sentry/nextjs": "*", + "@types/node": "18.11.17", + "@types/react": "18.0.26", + "@types/react-dom": "18.0.9", + "next": "13.0.7", + "react": "18.2.0", + "react-dom": "18.2.0", + "typescript": "4.9.4" + }, + "devDependencies": { + "@playwright/test": "^1.27.1" + } +} diff --git a/packages/e2e-tests/test-applications/create-next-app/pages/_app.tsx b/packages/e2e-tests/test-applications/create-next-app/pages/_app.tsx new file mode 100644 index 000000000000..da826ed16c72 --- /dev/null +++ b/packages/e2e-tests/test-applications/create-next-app/pages/_app.tsx @@ -0,0 +1,5 @@ +import type { AppProps } from 'next/app'; + +export default function App({ Component, pageProps }: AppProps) { + return ; +} diff --git a/packages/e2e-tests/test-applications/create-next-app/pages/_document.tsx b/packages/e2e-tests/test-applications/create-next-app/pages/_document.tsx new file mode 100644 index 000000000000..e1e9cbbb75aa --- /dev/null +++ b/packages/e2e-tests/test-applications/create-next-app/pages/_document.tsx @@ -0,0 +1,13 @@ +import { Html, Head, Main, NextScript } from 'next/document'; + +export default function Document() { + return ( + + + +
+ + + + ); +} diff --git a/packages/e2e-tests/test-applications/create-next-app/pages/_error.tsx b/packages/e2e-tests/test-applications/create-next-app/pages/_error.tsx new file mode 100644 index 000000000000..031553bc20a2 --- /dev/null +++ b/packages/e2e-tests/test-applications/create-next-app/pages/_error.tsx @@ -0,0 +1,40 @@ +/** + * NOTE: This requires `@sentry/nextjs` version 7.3.0 or higher. + * + * NOTE: If using this with `next` version 12.2.0 or lower, uncomment the + * penultimate line in `CustomErrorComponent`. + * + * This page is loaded by Nextjs: + * - on the server, when data-fetching methods throw or reject + * - on the client, when `getInitialProps` throws or rejects + * - on the client, when a React lifecycle method throws or rejects, and it's + * caught by the built-in Nextjs error boundary + * + * See: + * - https://nextjs.org/docs/basic-features/data-fetching/overview + * - https://nextjs.org/docs/api-reference/data-fetching/get-initial-props + * - https://reactjs.org/docs/error-boundaries.html + */ + +import * as Sentry from '@sentry/nextjs'; +import NextErrorComponent from 'next/error'; +import { NextPageContext } from 'next'; + +const CustomErrorComponent = (props: { statusCode: any }) => { + // If you're using a Nextjs version prior to 12.2.1, uncomment this to + // compensate for https://github.com/vercel/next.js/issues/8592 + // Sentry.captureUnderscoreErrorException(props); + + return ; +}; + +CustomErrorComponent.getInitialProps = async (contextData: NextPageContext) => { + // In case this is running in a serverless function, await this in order to give Sentry + // time to send the error before the lambda exits + await Sentry.captureUnderscoreErrorException(contextData); + + // This will contain the status code of the response + return NextErrorComponent.getInitialProps(contextData); +}; + +export default CustomErrorComponent; diff --git a/packages/e2e-tests/test-applications/create-next-app/pages/api/hello.ts b/packages/e2e-tests/test-applications/create-next-app/pages/api/hello.ts new file mode 100644 index 000000000000..eb4cc6657b37 --- /dev/null +++ b/packages/e2e-tests/test-applications/create-next-app/pages/api/hello.ts @@ -0,0 +1,10 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction +import type { NextApiRequest, NextApiResponse } from 'next'; + +type Data = { + name: string; +}; + +export default function handler(req: NextApiRequest, res: NextApiResponse) { + res.status(200).json({ name: 'John Doe' }); +} diff --git a/packages/e2e-tests/test-applications/create-next-app/pages/index.tsx b/packages/e2e-tests/test-applications/create-next-app/pages/index.tsx new file mode 100644 index 000000000000..807b48f3a9d3 --- /dev/null +++ b/packages/e2e-tests/test-applications/create-next-app/pages/index.tsx @@ -0,0 +1,30 @@ +import Head from 'next/head'; +import Link from 'next/link'; +import * as Sentry from '@sentry/nextjs'; + +export default function Home() { + return ( + <> + + Create Next App + + + + +
+ { + const eventId = Sentry.captureException(new Error('I am an error!')); + window.capturedExceptionId = eventId; + }} + /> + + navigate + +
+ + ); +} diff --git a/packages/e2e-tests/test-applications/create-next-app/pages/user/[id].tsx b/packages/e2e-tests/test-applications/create-next-app/pages/user/[id].tsx new file mode 100644 index 000000000000..08f65a85273d --- /dev/null +++ b/packages/e2e-tests/test-applications/create-next-app/pages/user/[id].tsx @@ -0,0 +1,5 @@ +const User = () => { + return

I am a blank page :)

; +}; + +export default User; diff --git a/packages/e2e-tests/test-applications/create-next-app/playwright.config.ts b/packages/e2e-tests/test-applications/create-next-app/playwright.config.ts new file mode 100644 index 000000000000..6bccee48f9fc --- /dev/null +++ b/packages/e2e-tests/test-applications/create-next-app/playwright.config.ts @@ -0,0 +1,69 @@ +import type { PlaywrightTestConfig } from '@playwright/test'; +import { devices } from '@playwright/test'; + +/** + * 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: 5000, + }, + /* 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, + /* Retry on CI only */ + retries: 0, + /* Opt out of parallel tests on CI. */ + workers: 1, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'dot', + /* 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:3000', + + /* 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'], + }, + }, + // For now we only test Chrome! + // { + // name: 'firefox', + // use: { + // ...devices['Desktop Firefox'], + // }, + // }, + // { + // name: 'webkit', + // use: { + // ...devices['Desktop Safari'], + // }, + // }, + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: process.env.TEST_MODE === 'build' ? 'yarn start' : 'yarn dev', + port: 3000, + }, +}; + +export default config; diff --git a/packages/e2e-tests/test-applications/create-next-app/sentry.client.config.ts b/packages/e2e-tests/test-applications/create-next-app/sentry.client.config.ts new file mode 100644 index 000000000000..9357b9eedc41 --- /dev/null +++ b/packages/e2e-tests/test-applications/create-next-app/sentry.client.config.ts @@ -0,0 +1,30 @@ +// This file configures the initialization of Sentry on the browser. +// The config you add here will be used whenever a page is visited. +// https://docs.sentry.io/platforms/javascript/guides/nextjs/ + +import * as Sentry from '@sentry/nextjs'; + +Sentry.init({ + dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN, + // Adjust this value in production, or use tracesSampler for greater control + tracesSampleRate: 1.0, + // ... + // Note: if you want to override the automatic release value, do not set a + // `release` value here - use the environment variable `SENTRY_RELEASE`, so + // that it will also get attached to your source maps +}); + +Sentry.addGlobalEventProcessor(event => { + if ( + event.type === 'transaction' && + (event.contexts?.trace?.op === 'pageload' || event.contexts?.trace?.op === 'navigation') + ) { + const eventId = event.event_id; + if (eventId) { + window.recordedTransactions = window.recordedTransactions || []; + window.recordedTransactions.push(eventId); + } + } + + return event; +}); diff --git a/packages/e2e-tests/test-applications/create-next-app/sentry.server.config.ts b/packages/e2e-tests/test-applications/create-next-app/sentry.server.config.ts new file mode 100644 index 000000000000..dfce34424f98 --- /dev/null +++ b/packages/e2e-tests/test-applications/create-next-app/sentry.server.config.ts @@ -0,0 +1,15 @@ +// This file configures the initialization of Sentry on the server. +// The config you add here will be used whenever the server handles a request. +// https://docs.sentry.io/platforms/javascript/guides/nextjs/ + +import * as Sentry from '@sentry/nextjs'; + +Sentry.init({ + dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN, + // Adjust this value in production, or use tracesSampler for greater control + tracesSampleRate: 1.0, + // ... + // Note: if you want to override the automatic release value, do not set a + // `release` value here - use the environment variable `SENTRY_RELEASE`, so + // that it will also get attached to your source maps +}); diff --git a/packages/e2e-tests/test-applications/create-next-app/test-recipe.json b/packages/e2e-tests/test-applications/create-next-app/test-recipe.json new file mode 100644 index 000000000000..b41a7a4fd05d --- /dev/null +++ b/packages/e2e-tests/test-applications/create-next-app/test-recipe.json @@ -0,0 +1,15 @@ +{ + "$schema": "../../test-recipe-schema.json", + "testApplicationName": "create-next-app", + "buildCommand": "yarn install --pure-lockfile && npx playwright install && yarn build", + "tests": [ + { + "testName": "Playwright tests - Build Mode", + "testCommand": "yarn test" + }, + { + "testName": "Playwright tests - Dev Mode", + "testCommand": "yarn test:dev" + } + ] +} diff --git a/packages/e2e-tests/test-applications/create-next-app/tests/client/behaviour.test.ts b/packages/e2e-tests/test-applications/create-next-app/tests/client/behaviour.test.ts new file mode 100644 index 000000000000..a819664f35e3 --- /dev/null +++ b/packages/e2e-tests/test-applications/create-next-app/tests/client/behaviour.test.ts @@ -0,0 +1,171 @@ +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 an exception to Sentry', async ({ page, baseURL }) => { + await page.goto('/'); + + const exceptionButton = page.locator('id=exception-button'); + await exceptionButton.click(); + + const exceptionIdHandle = await page.waitForFunction(() => window.capturedExceptionId); + const exceptionEventId = await exceptionIdHandle.jsonValue(); + + console.log(`Polling for error eventId: ${exceptionEventId}`); + + await expect + .poll( + async () => { + try { + const response = await axios.get( + `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${exceptionEventId}/`, + { 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 a pageload transaction to Sentry', async ({ page }) => { + await page.goto('/'); + + const recordedTransactionsHandle = await page.waitForFunction(() => { + if (window.recordedTransactions && window.recordedTransactions?.length >= 1) { + return window.recordedTransactions; + } else { + return undefined; + } + }); + const recordedTransactionEventIds = await recordedTransactionsHandle.jsonValue(); + + if (recordedTransactionEventIds === undefined) { + throw new Error("Application didn't record any transaction event IDs."); + } + + let hadPageLoadTransaction = false; + + console.log(`Polling for transaction eventIds: ${JSON.stringify(recordedTransactionEventIds)}`); + + await Promise.all( + recordedTransactionEventIds.map(async transactionEventId => { + await expect + .poll( + async () => { + try { + const response = await axios.get( + `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${transactionEventId}/`, + { headers: { Authorization: `Bearer ${authToken}` } }, + ); + + if (response.data.contexts.trace.op === 'pageload') { + hadPageLoadTransaction = true; + } + + 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); + }), + ); + + expect(hadPageLoadTransaction).toBe(true); +}); + +test('Sends a navigation transaction to Sentry', async ({ page }) => { + await page.goto('/'); + + // Give pageload transaction time to finish + await page.waitForTimeout(4000); + + const linkElement = page.locator('id=navigation'); + await linkElement.click(); + + const recordedTransactionsHandle = await page.waitForFunction(() => { + if (window.recordedTransactions && window.recordedTransactions?.length >= 2) { + return window.recordedTransactions; + } else { + return undefined; + } + }); + const recordedTransactionEventIds = await recordedTransactionsHandle.jsonValue(); + + if (recordedTransactionEventIds === undefined) { + throw new Error("Application didn't record any transaction event IDs."); + } + + let hadPageNavigationTransaction = false; + + console.log(`Polling for transaction eventIds: ${JSON.stringify(recordedTransactionEventIds)}`); + + await Promise.all( + recordedTransactionEventIds.map(async transactionEventId => { + await expect + .poll( + async () => { + try { + const response = await axios.get( + `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${transactionEventId}/`, + { headers: { Authorization: `Bearer ${authToken}` } }, + ); + + if (response.data.contexts.trace.op === 'navigation') { + hadPageNavigationTransaction = true; + } + + 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); + }), + ); + + expect(hadPageNavigationTransaction).toBe(true); +}); diff --git a/packages/e2e-tests/test-applications/create-next-app/tsconfig.json b/packages/e2e-tests/test-applications/create-next-app/tsconfig.json new file mode 100644 index 000000000000..3ff0501fdb85 --- /dev/null +++ b/packages/e2e-tests/test-applications/create-next-app/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "next.config.js"], + "exclude": ["node_modules"] +} diff --git a/packages/e2e-tests/test-applications/create-next-app/yarn.lock b/packages/e2e-tests/test-applications/create-next-app/yarn.lock new file mode 100644 index 000000000000..24b1a68e4df3 --- /dev/null +++ b/packages/e2e-tests/test-applications/create-next-app/yarn.lock @@ -0,0 +1,789 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@next/env@13.0.7": + version "13.0.7" + resolved "https://registry.yarnpkg.com/@next/env/-/env-13.0.7.tgz#7b6ccd9006d3fb57c369e3fb62b28e15324141e9" + integrity sha512-ZBclBRB7DbkSswXgbJ+muF5RxfgmAuQKAWL8tcm86aZmoiL1ZainxQK0hMcMYdh+IYG8UObAKV2wKB5O+6P4ng== + +"@next/font@13.0.7": + version "13.0.7" + resolved "https://registry.yarnpkg.com/@next/font/-/font-13.0.7.tgz#e0046376edb0ce592d9cfddea8f4ab321eb1515a" + integrity sha512-39SzuoMI6jbrIzPs3KtXdKX03OrVp6Y7kRHcoVmOg69spiBzruPJ5x5DQSfN+OXqznbvVBNZBXnmdnSqs3qXiA== + +"@next/swc-android-arm-eabi@13.0.7": + version "13.0.7" + resolved "https://registry.yarnpkg.com/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-13.0.7.tgz#ddbf3d092d22f17238aa34072f5dcb8129d8b23e" + integrity sha512-QTEamOK/LCwBf05GZ261rULMbZEpE3TYdjHlXfznV+nXwTztzkBNFXwP67gv2wW44BROzgi/vrR9H8oP+J5jxg== + +"@next/swc-android-arm64@13.0.7": + version "13.0.7" + resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-13.0.7.tgz#96f150232eb66da377226f21a371d30389371ed5" + integrity sha512-wcy2H0Tl9ME8vKy2GnJZ7Mybwys+43F/Eh2Pvph7mSDpMbYBJ6iA0zeY62iYYXxlZhnAID3+h79FUqUEakkClw== + +"@next/swc-darwin-arm64@13.0.7": + version "13.0.7" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.0.7.tgz#34e80a22573b5321ade8417dfb814cf6e1fd9997" + integrity sha512-F/mU7csN1/J2cqXJPMgTQ6MwAbc1pJ6sp6W+X0z5JEY4IFDzxKd3wRc3pCiNF7j8xW381JlNpWxhjCctnNmfaw== + +"@next/swc-darwin-x64@13.0.7": + version "13.0.7" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.0.7.tgz#ecec57211bf54a15872bb44e5ea70c99c2efe785" + integrity sha512-636AuRQynCPnIPRVzcCk5B7OMq9XjaYam2T0HeWUCE6y7EqEO3kxiuZ4QmN81T7A6Ydb+JnivYrLelHXmgdj6A== + +"@next/swc-freebsd-x64@13.0.7": + version "13.0.7" + resolved "https://registry.yarnpkg.com/@next/swc-freebsd-x64/-/swc-freebsd-x64-13.0.7.tgz#b4a8a49c3c3d200c9d6c43193b82ee39c6eb1d59" + integrity sha512-92XAMzNgQazowZ9t7uZmHRA5VdBl/SwEdrf5UybdfRovsxB4r3+yJWEvFaqYpSEp0gwndbwLokJdpz7OwFdL3Q== + +"@next/swc-linux-arm-gnueabihf@13.0.7": + version "13.0.7" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-13.0.7.tgz#6f550d348c6ece2b25426a53c5be49a3a8fc54a3" + integrity sha512-3r1CWl5P6I5n5Yxip8EXv/Rfu2Cp6wVmIOpvmczyUR82j+bcMkwPAcUjNkG/vMCagS4xV7NElrcdGb39iFmfLg== + +"@next/swc-linux-arm64-gnu@13.0.7": + version "13.0.7" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.0.7.tgz#20bd7f25a3af0edb4d3506c005f54212eb9a855b" + integrity sha512-RXo8tt6ppiwyS6hpDw3JdAjKcdVewsefxnxk9xOH4mRhMyq9V2lQx0e24X/dRiZqkx3jnWReR2WRrUlgN1UkSQ== + +"@next/swc-linux-arm64-musl@13.0.7": + version "13.0.7" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.0.7.tgz#f421bedcf2e1ad1ad7c90af1102df83634e92b6a" + integrity sha512-RWpnW+bmfXyxyY7iARbueYDGuIF+BEp3etLeYh/RUNHb9PhOHLDgJOG8haGSykud3a6CcyBI8hEjqOhoObaDpw== + +"@next/swc-linux-x64-gnu@13.0.7": + version "13.0.7" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.0.7.tgz#76cb25d3c00041dabc02e0b3ddd10f9325eb3f60" + integrity sha512-/ygUIiMMTYnbKlFs5Ba9J5k/tNxFWy8eI1bBF8UuMTvV8QJHl/aLDiA5dwsei2kk99/cu3eay62JnJXkSk3RSQ== + +"@next/swc-linux-x64-musl@13.0.7": + version "13.0.7" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.0.7.tgz#4e49b54b3578f7c4753dd7ac9c5e683914427884" + integrity sha512-dLzr6AL77USJN0ejgx5AS8O8SbFlbYTzs0XwAWag4oQpUG2p3ARvxwQgYQ0Z+6EP0zIRZ/XfLkN/mhsyi3m4PA== + +"@next/swc-win32-arm64-msvc@13.0.7": + version "13.0.7" + resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.0.7.tgz#98f622f9d0e34746e1ec7f25ce436a809a42313d" + integrity sha512-+vFIVa82AwqFkpFClKT+n73fGxrhAZ2u1u3mDYEBdxO6c9U4Pj3S5tZFsGFK9kLT/bFvf/eeVOICSLCC7MSgJQ== + +"@next/swc-win32-ia32-msvc@13.0.7": + version "13.0.7" + resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.0.7.tgz#f27f99aeec4207be7688a417f5934ea4868dadfc" + integrity sha512-RNLXIhp+assD39dQY9oHhDxw+/qSJRARKhOFsHfOtf8yEfCHqcKkn3X/L+ih60ntaEqK294y1WkMk6ylotsxwA== + +"@next/swc-win32-x64-msvc@13.0.7": + version "13.0.7" + resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.0.7.tgz#7aaa6cee723cde844e891e895e5561a60d9fa7f3" + integrity sha512-kvdnlLcrnEq72ZP0lqe2Z5NqvB9N5uSCvtXJ0PhKvNncWWd0fEG9Ec9erXgwCmVlM2ytw41k9/uuQ+SVw4Pihw== + +"@playwright/test@^1.27.1": + version "1.29.1" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.29.1.tgz#f2ed4dc143b9c7825a7ad2703b2f1ac4354e1145" + integrity sha512-iQxk2DX5U9wOGV3+/Jh9OHPsw5H3mleUL2S4BgQuwtlAfK3PnKvn38m4Rg9zIViGHVW24opSm99HQm/UFLEy6w== + dependencies: + "@types/node" "*" + playwright-core "1.29.1" + +"@rollup/plugin-sucrase@4.0.4": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@rollup/plugin-sucrase/-/plugin-sucrase-4.0.4.tgz#0a3b3d97cdc239ec3399f5a10711f751e9f95d98" + integrity sha512-YH4J8yoJb5EVnLhAwWxYAQNh2SJOR+SdZ6XdgoKEv6Kxm33riYkM8MlMaggN87UoISP52qAFyZ5ey56wu6umGg== + dependencies: + "@rollup/pluginutils" "^4.1.1" + sucrase "^3.20.0" + +"@rollup/plugin-virtual@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@rollup/plugin-virtual/-/plugin-virtual-3.0.0.tgz#8c3f54b4ab4b267d9cd3dcbaedc58d4fd1deddca" + integrity sha512-K9KORe1myM62o0lKkNR4MmCxjwuAXsZEtIHpaILfv4kILXTOrXt/R2ha7PzMcCHPYdnkWPiBZK8ed4Zr3Ll5lQ== + +"@rollup/pluginutils@^4.1.1": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.2.1.tgz#e6c6c3aba0744edce3fb2074922d3776c0af2a6d" + integrity sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ== + dependencies: + estree-walker "^2.0.1" + picomatch "^2.2.2" + + +"@swc/helpers@0.4.14": + version "0.4.14" + resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.4.14.tgz#1352ac6d95e3617ccb7c1498ff019654f1e12a74" + integrity sha512-4C7nX/dvpzB7za4Ql9K81xK3HPxCpHMgwTZVyf+9JQ6VUbn9jjZVN7/Nkdz/Ugzs2CSjqnL/UPXroiVBVHUWUw== + dependencies: + tslib "^2.4.0" + +"@types/node@*": + version "18.11.18" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.18.tgz#8dfb97f0da23c2293e554c5a50d61ef134d7697f" + integrity sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA== + +"@types/node@18.11.17": + version "18.11.17" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.17.tgz#5c009e1d9c38f4a2a9d45c0b0c493fe6cdb4bcb5" + integrity sha512-HJSUJmni4BeDHhfzn6nF0sVmd1SMezP7/4F0Lq+aXzmp2xm9O7WXrUtHW/CHlYVtZUbByEvWidHqRtcJXGF2Ng== + +"@types/prop-types@*": + version "15.7.5" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf" + integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w== + +"@types/react-dom@18.0.9": + version "18.0.9" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.9.tgz#ffee5e4bfc2a2f8774b15496474f8e7fe8d0b504" + integrity sha512-qnVvHxASt/H7i+XG1U1xMiY5t+IHcPGUK7TDMDzom08xa7e86eCeKOiLZezwCKVxJn6NEiiy2ekgX8aQssjIKg== + dependencies: + "@types/react" "*" + +"@types/react@*", "@types/react@18.0.26": + version "18.0.26" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.26.tgz#8ad59fc01fef8eaf5c74f4ea392621749f0b7917" + integrity sha512-hCR3PJQsAIXyxhTNSiDFY//LhnMZWpNNr5etoCqx/iUfGc5gXWtQR2Phl908jVR6uPXacojQWTg4qRpkxTuGug== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + +"@types/scheduler@*": + version "0.16.2" + resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" + integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== + +agent-base@6: + version "6.0.2" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + integrity sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA== + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +any-promise@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" + integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A== + +aproba@^1.0.3: + version "1.2.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" + integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== + +are-we-there-yet@~1.1.2: + version "1.1.7" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz#b15474a932adab4ff8a50d9adfa7e4e926f21146" + integrity sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g== + dependencies: + delegates "^1.0.0" + readable-stream "^2.0.6" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +caniuse-lite@^1.0.30001406: + version "1.0.30001442" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001442.tgz#40337f1cf3be7c637b061e2f78582dc1daec0614" + integrity sha512-239m03Pqy0hwxYPYR5JwOIxRJfLTWtle9FV8zosfV5pHg+/51uD4nxcUlM8+mWWGfwKtt8lJNHnD3cWw9VZ6ow== + +chalk@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" + integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +client-only@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1" + integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== + +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + integrity sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA== + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +commander@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" + integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +console-control-strings@^1.0.0, console-control-strings@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ== + +cookie@^0.4.1: + version "0.4.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" + integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== + +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +csstype@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.1.tgz#841b532c45c758ee546a11d5bd7b7b473c8c30b9" + integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw== + +debug@4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +estree-walker@^2.0.1: + version "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== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + +gauge@~2.7.3: + version "2.7.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" + integrity sha512-14x4kjc6lkD3ltw589k0NrPD6cCNTD6CWoVUNpB85+DrtONoZn+Rug6xZU5RvSC4+TZPxA5AnBibQYAvZn41Hg== + dependencies: + aproba "^1.0.3" + console-control-strings "^1.0.0" + has-unicode "^2.0.0" + object-assign "^4.1.0" + signal-exit "^3.0.0" + string-width "^1.0.1" + strip-ansi "^3.0.1" + wide-align "^1.1.0" + +glob@7.1.6: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-unicode@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ== + +hoist-non-react-statics@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" + integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== + dependencies: + react-is "^16.7.0" + +https-proxy-agent@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" + integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== + dependencies: + agent-base "6" + debug "4" + +immediate@~3.0.5: + version "3.0.6" + resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" + integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + integrity sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw== + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +"js-tokens@^3.0.0 || ^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +lie@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e" + integrity sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw== + dependencies: + immediate "~3.0.5" + +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + +localforage@^1.8.1: + version "1.10.0" + resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.10.0.tgz#5c465dc5f62b2807c3a84c0c6a1b1b3212781dd4" + integrity sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg== + dependencies: + lie "3.1.1" + +loose-envify@^1.1.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +lru_map@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/lru_map/-/lru_map-0.3.3.tgz#b5c8351b9464cbd750335a79650a0ec0e56118dd" + integrity sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ== + +minimatch@^3.0.4: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimist@^1.2.6: + version "1.2.7" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" + integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== + +mkdirp@^0.5.5: + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +mz@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" + integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q== + dependencies: + any-promise "^1.0.0" + object-assign "^4.0.1" + thenify-all "^1.0.0" + +nanoid@^3.3.4: + version "3.3.4" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" + integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== + +next@13.0.7: + version "13.0.7" + resolved "https://registry.yarnpkg.com/next/-/next-13.0.7.tgz#f07a0cc3afefdb86fb6668048e910a2193e3c1e2" + integrity sha512-YfTifqX9vfHm+rSU/H/3xvzOHDkYuMuh4wsvTjiqj9h7qHEF7KHB66X4qrH96Po+ohdid4JY8YVGPziDwdXL0A== + dependencies: + "@next/env" "13.0.7" + "@swc/helpers" "0.4.14" + caniuse-lite "^1.0.30001406" + postcss "8.4.14" + styled-jsx "5.1.0" + optionalDependencies: + "@next/swc-android-arm-eabi" "13.0.7" + "@next/swc-android-arm64" "13.0.7" + "@next/swc-darwin-arm64" "13.0.7" + "@next/swc-darwin-x64" "13.0.7" + "@next/swc-freebsd-x64" "13.0.7" + "@next/swc-linux-arm-gnueabihf" "13.0.7" + "@next/swc-linux-arm64-gnu" "13.0.7" + "@next/swc-linux-arm64-musl" "13.0.7" + "@next/swc-linux-x64-gnu" "13.0.7" + "@next/swc-linux-x64-musl" "13.0.7" + "@next/swc-win32-arm64-msvc" "13.0.7" + "@next/swc-win32-ia32-msvc" "13.0.7" + "@next/swc-win32-x64-msvc" "13.0.7" + +node-fetch@^2.6.7: + version "2.6.7" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" + integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== + dependencies: + whatwg-url "^5.0.0" + +npmlog@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" + integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== + dependencies: + are-we-there-yet "~1.1.2" + console-control-strings "~1.1.0" + gauge "~2.7.3" + set-blocking "~2.0.0" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + integrity sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ== + +object-assign@^4.0.1, object-assign@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +picomatch@^2.2.2: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pirates@^4.0.1: + version "4.0.5" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b" + integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ== + +playwright-core@1.29.1: + version "1.29.1" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.29.1.tgz#9ec15d61c4bd2f386ddf6ce010db53a030345a47" + integrity sha512-20Ai3d+lMkWpI9YZYlxk8gxatfgax5STW8GaMozAHwigLiyiKQrdkt7gaoT9UQR8FIVDg6qVXs9IoZUQrDjIIg== + +postcss@8.4.14: + version "8.4.14" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.14.tgz#ee9274d5622b4858c1007a74d76e42e56fd21caf" + integrity sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig== + dependencies: + nanoid "^3.3.4" + picocolors "^1.0.0" + source-map-js "^1.0.2" + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +progress@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" + integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== + +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + +react-dom@18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" + integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== + dependencies: + loose-envify "^1.1.0" + scheduler "^0.23.0" + +react-is@^16.7.0: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + +react@18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" + integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== + dependencies: + loose-envify "^1.1.0" + +readable-stream@^2.0.6: + version "2.3.7" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" + integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +rollup@2.78.0: + version "2.78.0" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.78.0.tgz#00995deae70c0f712ea79ad904d5f6b033209d9e" + integrity sha512-4+YfbQC9QEVvKTanHhIAFVUFSRsezvQF8vFOJwtGfb9Bb+r014S+qryr9PSmw8x6sMnPkmFBGAvIFVQxvJxjtg== + optionalDependencies: + fsevents "~2.3.2" + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +scheduler@^0.23.0: + version "0.23.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" + integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw== + dependencies: + loose-envify "^1.1.0" + +set-blocking@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== + +signal-exit@^3.0.0: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +source-map-js@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" + integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== + +string-width@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + integrity sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw== + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +"string-width@^1.0.2 || 2 || 3 || 4": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^3.0.0, strip-ansi@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + integrity sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg== + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +styled-jsx@5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.1.0.tgz#4a5622ab9714bd3fcfaeec292aa555871f057563" + integrity sha512-/iHaRJt9U7T+5tp6TRelLnqBqiaIT0HsO0+vgyj8hK2KUk7aejFqRrumqPUlAqDwAj8IbS/1hk3IhBAAK/FCUQ== + dependencies: + client-only "0.0.1" + +sucrase@^3.20.0: + version "3.29.0" + resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.29.0.tgz#3207c5bc1b980fdae1e539df3f8a8a518236da7d" + integrity sha512-bZPAuGA5SdFHuzqIhTAqt9fvNEo9rESqXIG3oiKdF8K4UmkQxC4KlNL3lVyAErXp+mPvUqZ5l13qx6TrDIGf3A== + dependencies: + commander "^4.0.0" + glob "7.1.6" + lines-and-columns "^1.1.6" + mz "^2.7.0" + pirates "^4.0.1" + ts-interface-checker "^0.1.9" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +thenify-all@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" + integrity sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA== + dependencies: + thenify ">= 3.1.0 < 4" + +"thenify@>= 3.1.0 < 4": + version "3.3.1" + resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.1.tgz#8932e686a4066038a016dd9e2ca46add9838a95f" + integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw== + dependencies: + any-promise "^1.0.0" + +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + +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" + integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA== + +tslib@^1.9.3: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + +tslib@^2.4.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e" + integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA== + +typescript@4.9.4: + version "4.9.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.4.tgz#a2a3d2756c079abda241d75f149df9d561091e78" + integrity sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg== + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + +"webpack-sources@^2.0.0 || ^3.0.0": + version "3.2.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" + integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + +which@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wide-align@^1.1.0: + version "1.1.5" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3" + integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== + dependencies: + string-width "^1.0.2 || 2 || 3 || 4" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== diff --git a/yarn.lock b/yarn.lock index 3ced5a52cb6f..c2dbdbaef239 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10310,6 +10310,11 @@ dot-prop@^6.0.1: dependencies: is-obj "^2.0.0" +dotenv@16.0.3: + version "16.0.3" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.3.tgz#115aec42bac5053db3c456db30cc243a5a836a07" + integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ== + dotenv@~10.0.0: version "10.0.0" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81" From 6fda61af70663e4e9075188f8d04ee7b33691dc1 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Wed, 21 Dec 2022 22:16:18 +0100 Subject: [PATCH 059/113] chore: replay metrics collection - initial script setup --- .vscode/launch.json | 8 + .vscode/tasks.json | 8 +- packages/replay/metrics/.eslintrc.js | 11 + packages/replay/metrics/package.json | 21 + packages/replay/metrics/src/index.ts | 42 ++ packages/replay/metrics/tsconfig.json | 12 + packages/replay/metrics/yarn.lock | 624 ++++++++++++++++++++++++++ 7 files changed, 725 insertions(+), 1 deletion(-) create mode 100644 packages/replay/metrics/.eslintrc.js create mode 100644 packages/replay/metrics/package.json create mode 100644 packages/replay/metrics/src/index.ts create mode 100644 packages/replay/metrics/tsconfig.json create mode 100644 packages/replay/metrics/yarn.lock diff --git a/.vscode/launch.json b/.vscode/launch.json index 2fd396c8ddbf..598d4363c1f7 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -37,6 +37,14 @@ "internalConsoleOptions": "openOnSessionStart", "outputCapture": "std" }, + { + "type": "node", + "name": "Debug replay metrics script", + "request": "launch", + "cwd": "${workspaceFolder}/packages/replay/metrics/", + "program": "${workspaceFolder}/packages/replay/metrics/src/index.ts", + "preLaunchTask": "Build Replay metrics script", + }, // Run rollup using the config file which is in the currently active tab. { "name": "Debug rollup (config from open file)", diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 6e797a064c61..e68b7996f8e9 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -7,7 +7,13 @@ "type": "npm", "script": "predebug", "path": "packages/nextjs/test/integration/", - "detail": "Link the SDK (if not already linked) and build test app" + "detail": "Link the SDK (if not already linked) and build test app", + }, + { + "label": "Build Replay metrics script", + "type": "npm", + "script": "build", + "path": "packages/replay/metrics", } ] } diff --git a/packages/replay/metrics/.eslintrc.js b/packages/replay/metrics/.eslintrc.js new file mode 100644 index 000000000000..5527b97558fb --- /dev/null +++ b/packages/replay/metrics/.eslintrc.js @@ -0,0 +1,11 @@ +module.exports = { + extends: ['../.eslintrc.js'], + overrides: [ + { + files: ['*.ts'], + rules: { + 'no-console': 'off', + }, + }, + ], +}; diff --git a/packages/replay/metrics/package.json b/packages/replay/metrics/package.json new file mode 100644 index 000000000000..3ecb4819bce2 --- /dev/null +++ b/packages/replay/metrics/package.json @@ -0,0 +1,21 @@ +{ + "private": true, + "name": "metrics", + "main": "index.js", + "author": "Sentry", + "license": "MIT", + "type": "module", + "scripts": { + "build": "tsc", + "start": "node ./build/index.js", + "dev": "ts-node-esm ./src/index.ts" + }, + "dependencies": { + "@types/node": "^18.11.17", + "puppeteer": "^19.4.1", + "typescript": "^4.9.4" + }, + "devDependencies": { + "ts-node": "^10.9.1" + } +} diff --git a/packages/replay/metrics/src/index.ts b/packages/replay/metrics/src/index.ts new file mode 100644 index 000000000000..c769557e3d00 --- /dev/null +++ b/packages/replay/metrics/src/index.ts @@ -0,0 +1,42 @@ +import puppeteer from 'puppeteer'; + +class MetricsCollector { + public async run(): Promise { + const browser = await puppeteer.launch({ headless: false }); + try { + const page = await browser.newPage(); + + await page.goto('https://developers.google.com/web/'); + + // Type into search box. + await page.type('.devsite-search-field', 'Headless Chrome'); + + // Wait for suggest overlay to appear and click "show all results". + const allResultsSelector = '.devsite-suggest-all-results'; + await page.waitForSelector(allResultsSelector); + await page.click(allResultsSelector); + + // Wait for the results page to load and display the results. + const resultsSelector = '.gsc-results .gs-title'; + await page.waitForSelector(resultsSelector); + + // // Extract the results from the page. + // const links = await page.evaluate(resultsSelector => { + // return [...document.querySelectorAll(resultsSelector)].map(anchor => { + // const title = anchor.textContent.split('|')[0].trim(); + // return `${title} - ${anchor.href}`; + // }); + // }, resultsSelector); + + // // Print all the files. + // console.log(links.join('\n')); + } finally { + await browser.close(); + } + } +} + + void (async () => { + const collector = new MetricsCollector(); + await collector.run(); +})(); diff --git a/packages/replay/metrics/tsconfig.json b/packages/replay/metrics/tsconfig.json new file mode 100644 index 000000000000..9aa9fd11b24e --- /dev/null +++ b/packages/replay/metrics/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "target": "es2020", + "module": "esnext", + "outDir": "build", + "esModuleInterop": true, + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/packages/replay/metrics/yarn.lock b/packages/replay/metrics/yarn.lock new file mode 100644 index 000000000000..1b1b34da02f2 --- /dev/null +++ b/packages/replay/metrics/yarn.lock @@ -0,0 +1,624 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/code-frame@^7.0.0": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a" + integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q== + dependencies: + "@babel/highlight" "^7.18.6" + +"@babel/helper-validator-identifier@^7.18.6": + 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/highlight@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" + integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g== + dependencies: + "@babel/helper-validator-identifier" "^7.18.6" + chalk "^2.0.0" + js-tokens "^4.0.0" + +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + +"@jridgewell/resolve-uri@^3.0.3": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" + integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== + +"@jridgewell/sourcemap-codec@^1.4.10": + version "1.4.14" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" + integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== + +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@tsconfig/node10@^1.0.7": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" + integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.2": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e" + integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== + +"@types/node@*", "@types/node@^18.11.17": + version "18.11.17" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.17.tgz#5c009e1d9c38f4a2a9d45c0b0c493fe6cdb4bcb5" + integrity sha512-HJSUJmni4BeDHhfzn6nF0sVmd1SMezP7/4F0Lq+aXzmp2xm9O7WXrUtHW/CHlYVtZUbByEvWidHqRtcJXGF2Ng== + +"@types/yauzl@^2.9.1": + version "2.10.0" + resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.0.tgz#b3248295276cf8c6f153ebe6a9aba0c988cb2599" + integrity sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw== + dependencies: + "@types/node" "*" + +acorn-walk@^8.1.1: + version "8.2.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" + integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== + +acorn@^8.4.1: + version "8.8.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.1.tgz#0a3f9cbecc4ec3bea6f0a80b66ae8dd2da250b73" + integrity sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA== + +agent-base@6: + version "6.0.2" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +bl@^4.0.3: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" + integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +buffer-crc32@~0.2.3: + version "0.2.13" + resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" + integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== + +buffer@^5.2.1, buffer@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +chalk@^2.0.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chownr@^1.1.1: + version "1.1.4" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" + integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +cosmiconfig@8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.0.0.tgz#e9feae014eab580f858f8a0288f38997a7bebe97" + integrity sha512-da1EafcpH6b/TD8vDRaWV7xFINlHlF6zKsGwS1TsuVJTZRkquaS5HTMq7uq6h31619QjbsYl21gVDOm32KM1vQ== + dependencies: + import-fresh "^3.2.1" + js-yaml "^4.1.0" + parse-json "^5.0.0" + path-type "^4.0.0" + +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + +cross-fetch@3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" + integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw== + dependencies: + node-fetch "2.6.7" + +debug@4, debug@4.3.4, debug@^4.1.1: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +devtools-protocol@0.0.1068969: + version "0.0.1068969" + resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.1068969.tgz#8b9a4bc48aed1453bed08d62b07481f9abf4d6d8" + integrity sha512-ATFTrPbY1dKYhPPvpjtwWKSK2mIwGmRwX54UASn9THEuIZCe2n9k3vVuMmt6jWeL+e5QaaguEv/pMyR+JQB7VQ== + +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + +end-of-stream@^1.1.0, end-of-stream@^1.4.1: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + +extract-zip@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" + integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== + dependencies: + debug "^4.1.1" + get-stream "^5.1.0" + yauzl "^2.10.0" + optionalDependencies: + "@types/yauzl" "^2.9.1" + +fd-slicer@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" + integrity sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g== + dependencies: + pend "~1.2.0" + +fs-constants@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" + integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +get-stream@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" + integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== + dependencies: + pump "^3.0.0" + +glob@^7.1.3: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + +https-proxy-agent@5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" + integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== + dependencies: + agent-base "6" + debug "4" + +ieee754@^1.1.13: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@^2.0.3, inherits@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +json-parse-even-better-errors@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + +minimatch@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +mkdirp-classic@^0.5.2: + version "0.5.3" + resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" + integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +node-fetch@2.6.7: + version "2.6.7" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" + integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== + dependencies: + whatwg-url "^5.0.0" + +once@^1.3.0, once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parse-json@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +pend@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" + integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg== + +progress@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" + integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== + +proxy-from-env@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +puppeteer-core@19.4.1: + version "19.4.1" + resolved "https://registry.yarnpkg.com/puppeteer-core/-/puppeteer-core-19.4.1.tgz#f4875943841ebdb6fc2ad7a475add958692b0237" + integrity sha512-JHIuqtqrUAx4jGOTxXu4ilapV2jabxtVMA/e4wwFUMvtSsqK4nVBSI+Z1SKDoz7gRy/JUIc8WzmfocCa6SIZ1w== + dependencies: + cross-fetch "3.1.5" + debug "4.3.4" + devtools-protocol "0.0.1068969" + extract-zip "2.0.1" + https-proxy-agent "5.0.1" + proxy-from-env "1.1.0" + rimraf "3.0.2" + tar-fs "2.1.1" + unbzip2-stream "1.4.3" + ws "8.11.0" + +puppeteer@^19.4.1: + version "19.4.1" + resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-19.4.1.tgz#cac7d3f0084badebb8ebacbe6f4d7262e7f21818" + integrity sha512-PCnrR13B8A+VSEDXRmrNXRZbrkF1tfsI1hKSC7vs13eNS6CUD3Y4FA8SF8/VZy+Pm1kg5AggJT2Nu3HLAtGkFg== + dependencies: + cosmiconfig "8.0.0" + https-proxy-agent "5.0.1" + progress "2.0.3" + proxy-from-env "1.1.0" + puppeteer-core "19.4.1" + +readable-stream@^3.1.1, readable-stream@^3.4.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +rimraf@3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +tar-fs@2.1.1: + 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-stream@^2.1.4: + version "2.2.0" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" + integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== + dependencies: + bl "^4.0.3" + end-of-stream "^1.4.1" + fs-constants "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.1.1" + +through@^2.3.8: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== + +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + +ts-node@^10.9.1: + version "10.9.1" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b" + integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" + yn "3.1.1" + +typescript@^4.9.4: + version "4.9.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.4.tgz#a2a3d2756c079abda241d75f149df9d561091e78" + integrity sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg== + +unbzip2-stream@1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7" + integrity sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg== + dependencies: + buffer "^5.2.1" + through "^2.3.8" + +util-deprecate@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +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" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +ws@8.11.0: + version "8.11.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143" + integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg== + +yauzl@^2.10.0: + version "2.10.0" + resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" + integrity sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g== + dependencies: + buffer-crc32 "~0.2.3" + fd-slicer "~1.1.0" + +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== From b333fbe3fd4910284942504b2a4b76560b7b023c Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Thu, 22 Dec 2022 11:04:12 +0100 Subject: [PATCH 060/113] chore: replay metrics LCP collection --- packages/replay/metrics/README.md | 7 +++ packages/replay/metrics/src/index.ts | 55 +++++++++++---------- packages/replay/metrics/src/vitals/index.ts | 26 ++++++++++ packages/replay/metrics/src/vitals/lcp.ts | 38 ++++++++++++++ 4 files changed, 100 insertions(+), 26 deletions(-) create mode 100644 packages/replay/metrics/README.md create mode 100644 packages/replay/metrics/src/vitals/index.ts create mode 100644 packages/replay/metrics/src/vitals/lcp.ts diff --git a/packages/replay/metrics/README.md b/packages/replay/metrics/README.md new file mode 100644 index 000000000000..bbf8d8cddcf5 --- /dev/null +++ b/packages/replay/metrics/README.md @@ -0,0 +1,7 @@ +# Replay performance metrics + +Evaluates Replay impact on website performance by running a web app in Chromium via Puppeteer and collecting various metrics. + +## Resources + +* https://github.com/addyosmani/puppeteer-webperf diff --git a/packages/replay/metrics/src/index.ts b/packages/replay/metrics/src/index.ts index c769557e3d00..bcbac9065c78 100644 --- a/packages/replay/metrics/src/index.ts +++ b/packages/replay/metrics/src/index.ts @@ -1,42 +1,45 @@ -import puppeteer from 'puppeteer'; +import * as puppeteer from 'puppeteer'; + +import { WebVitals, WebVitalsCollector } from './vitals/index.js'; + +const cpuThrottling = 4; +const networkConditions = puppeteer.PredefinedNetworkConditions['Fast 3G']; + +class Metrics { + constructor( + public url: string, public pageMetrics: puppeteer.Metrics, + public vitals: WebVitals) {} +} class MetricsCollector { - public async run(): Promise { - const browser = await puppeteer.launch({ headless: false }); + constructor(public url: string) {} + + public async run(): Promise { + const browser = await puppeteer.launch({headless: false,}); try { const page = await browser.newPage(); - await page.goto('https://developers.google.com/web/'); - - // Type into search box. - await page.type('.devsite-search-field', 'Headless Chrome'); + const vitalsCollector = new WebVitalsCollector(page); + await vitalsCollector.setup(); - // Wait for suggest overlay to appear and click "show all results". - const allResultsSelector = '.devsite-suggest-all-results'; - await page.waitForSelector(allResultsSelector); - await page.click(allResultsSelector); + // Simulated throttling + await page.emulateNetworkConditions(networkConditions); + await page.emulateCPUThrottling(cpuThrottling); - // Wait for the results page to load and display the results. - const resultsSelector = '.gsc-results .gs-title'; - await page.waitForSelector(resultsSelector); + await page.goto(this.url, { waitUntil: 'load', timeout: 60000 }); - // // Extract the results from the page. - // const links = await page.evaluate(resultsSelector => { - // return [...document.querySelectorAll(resultsSelector)].map(anchor => { - // const title = anchor.textContent.split('|')[0].trim(); - // return `${title} - ${anchor.href}`; - // }); - // }, resultsSelector); + const pageMetrics = await page.metrics(); + const vitals = await vitalsCollector.collect(); - // // Print all the files. - // console.log(links.join('\n')); + return new Metrics(this.url, pageMetrics, vitals); } finally { await browser.close(); } } } - void (async () => { - const collector = new MetricsCollector(); - await collector.run(); +void (async () => { + const collector = new MetricsCollector('https://developers.google.com/web/'); + const metrics = await collector.run(); + console.log(metrics); })(); diff --git a/packages/replay/metrics/src/vitals/index.ts b/packages/replay/metrics/src/vitals/index.ts new file mode 100644 index 000000000000..7dbcabf7b140 --- /dev/null +++ b/packages/replay/metrics/src/vitals/index.ts @@ -0,0 +1,26 @@ +import * as puppeteer from 'puppeteer'; + +import {LCP} from './lcp.js'; + +export {WebVitals, WebVitalsCollector}; + + +class WebVitals { + constructor(public lcp: number) {} +} + +class WebVitalsCollector { + private _lcp!: LCP; + + constructor(page: puppeteer.Page) { + this._lcp = new LCP(page); + } + + public async setup(): Promise { + await this._lcp.setup(); + } + + public async collect(): Promise { + return new WebVitals(await this._lcp.collect()); + } +} diff --git a/packages/replay/metrics/src/vitals/lcp.ts b/packages/replay/metrics/src/vitals/lcp.ts new file mode 100644 index 000000000000..5c5c835dec5f --- /dev/null +++ b/packages/replay/metrics/src/vitals/lcp.ts @@ -0,0 +1,38 @@ +import * as puppeteer from 'puppeteer'; + +export {LCP}; + +class LCP { + constructor( + private _page: puppeteer.Page) {} + + public async setup(): Promise { + await this._page.evaluateOnNewDocument(calcLCP); + } + + public async collect(): Promise { + const result = await this._page.evaluate('window.largestContentfulPaint'); + return result as number; + } +} + +const calcLCP = ` +console.log('running calcLCP'); +window.largestContentfulPaint = 0; + +const observer = new PerformanceObserver((entryList) => { + const entries = entryList.getEntries(); + const lastEntry = entries[entries.length - 1]; + window.largestContentfulPaint = lastEntry.renderTime || lastEntry.loadTime; +}); + +observer.observe({ type: 'largest-contentful-paint', buffered: true }); + +document.addEventListener('visibilitychange', () => { + if (document.visibilityState === 'hidden') { + observer.takeRecords(); + observer.disconnect(); + console.log('LCP:', window.largestContentfulPaint); + } +}); +`; From a65e18cd0e727f60c6a2c4f0b9e808363c811245 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Thu, 22 Dec 2022 13:31:28 +0100 Subject: [PATCH 061/113] cls vital collection --- packages/replay/metrics/src/index.ts | 5 ++- packages/replay/metrics/src/vitals/cls.ts | 36 +++++++++++++++++++ packages/replay/metrics/src/vitals/index.ts | 21 ++++++----- packages/replay/metrics/src/vitals/lcp.ts | 40 ++++++++++----------- 4 files changed, 68 insertions(+), 34 deletions(-) create mode 100644 packages/replay/metrics/src/vitals/cls.ts diff --git a/packages/replay/metrics/src/index.ts b/packages/replay/metrics/src/index.ts index bcbac9065c78..b43ae8b05a9d 100644 --- a/packages/replay/metrics/src/index.ts +++ b/packages/replay/metrics/src/index.ts @@ -19,13 +19,12 @@ class MetricsCollector { try { const page = await browser.newPage(); - const vitalsCollector = new WebVitalsCollector(page); - await vitalsCollector.setup(); - // Simulated throttling await page.emulateNetworkConditions(networkConditions); await page.emulateCPUThrottling(cpuThrottling); + const vitalsCollector = await WebVitalsCollector.create(page); + await page.goto(this.url, { waitUntil: 'load', timeout: 60000 }); const pageMetrics = await page.metrics(); diff --git a/packages/replay/metrics/src/vitals/cls.ts b/packages/replay/metrics/src/vitals/cls.ts new file mode 100644 index 000000000000..be89902513c3 --- /dev/null +++ b/packages/replay/metrics/src/vitals/cls.ts @@ -0,0 +1,36 @@ +import * as puppeteer from 'puppeteer'; + +export {CLS}; + +class CLS { + constructor( + private _page: puppeteer.Page) {} + + public async setup(): Promise { + await this._page.evaluateOnNewDocument(`{ + window.cumulativeLayoutShiftScore = 0; + + const observer = new PerformanceObserver((list) => { + for (const entry of list.getEntries()) { + if (!entry.hadRecentInput) { + window.cumulativeLayoutShiftScore += entry.value; + } + } + }); + + observer.observe({type: 'layout-shift', buffered: true}); + + document.addEventListener('visibilitychange', () => { + if (document.visibilityState === 'hidden') { + observer.takeRecords(); + observer.disconnect(); + } + }); + }`); + } + + public async collect(): Promise { + const result = await this._page.evaluate('window.cumulativeLayoutShiftScore'); + return result as number; + } +} diff --git a/packages/replay/metrics/src/vitals/index.ts b/packages/replay/metrics/src/vitals/index.ts index 7dbcabf7b140..fe4c03082377 100644 --- a/packages/replay/metrics/src/vitals/index.ts +++ b/packages/replay/metrics/src/vitals/index.ts @@ -1,26 +1,29 @@ import * as puppeteer from 'puppeteer'; +import {CLS} from './cls.js'; import {LCP} from './lcp.js'; export {WebVitals, WebVitalsCollector}; class WebVitals { - constructor(public lcp: number) {} + constructor(public lcp: number, public cls: number) {} } class WebVitalsCollector { - private _lcp!: LCP; + private constructor(private _lcp: LCP, private _cls: CLS) {} - constructor(page: puppeteer.Page) { - this._lcp = new LCP(page); - } - - public async setup(): Promise { - await this._lcp.setup(); + public static async create(page: puppeteer.Page): Promise { + const result = new WebVitalsCollector(new LCP(page), new CLS(page)); + await result._lcp.setup(); + await result._cls.setup(); + return result; } public async collect(): Promise { - return new WebVitals(await this._lcp.collect()); + return new WebVitals( + await this._lcp.collect(), + await this._cls.collect(), + ); } } diff --git a/packages/replay/metrics/src/vitals/lcp.ts b/packages/replay/metrics/src/vitals/lcp.ts index 5c5c835dec5f..e7903f126de5 100644 --- a/packages/replay/metrics/src/vitals/lcp.ts +++ b/packages/replay/metrics/src/vitals/lcp.ts @@ -7,7 +7,24 @@ class LCP { private _page: puppeteer.Page) {} public async setup(): Promise { - await this._page.evaluateOnNewDocument(calcLCP); + await this._page.evaluateOnNewDocument(`{ + window.largestContentfulPaint = 0; + + const observer = new PerformanceObserver((list) => { + const entries = list.getEntries(); + const lastEntry = entries[entries.length - 1]; + window.largestContentfulPaint = lastEntry.renderTime || lastEntry.loadTime; + }); + + observer.observe({ type: 'largest-contentful-paint', buffered: true }); + + document.addEventListener('visibilitychange', () => { + if (document.visibilityState === 'hidden') { + observer.takeRecords(); + observer.disconnect(); + } + }); + }`); } public async collect(): Promise { @@ -15,24 +32,3 @@ class LCP { return result as number; } } - -const calcLCP = ` -console.log('running calcLCP'); -window.largestContentfulPaint = 0; - -const observer = new PerformanceObserver((entryList) => { - const entries = entryList.getEntries(); - const lastEntry = entries[entries.length - 1]; - window.largestContentfulPaint = lastEntry.renderTime || lastEntry.loadTime; -}); - -observer.observe({ type: 'largest-contentful-paint', buffered: true }); - -document.addEventListener('visibilitychange', () => { - if (document.visibilityState === 'hidden') { - observer.takeRecords(); - observer.disconnect(); - console.log('LCP:', window.largestContentfulPaint); - } -}); -`; From be39bc556dc37a45cf1641f00ab6292a83e6187f Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Thu, 22 Dec 2022 14:24:38 +0100 Subject: [PATCH 062/113] replay: fid metric --- packages/replay/metrics/src/index.ts | 2 ++ packages/replay/metrics/src/vitals/cls.ts | 9 ++++-- packages/replay/metrics/src/vitals/fid.ts | 35 +++++++++++++++++++++ packages/replay/metrics/src/vitals/index.ts | 14 ++++++--- packages/replay/metrics/src/vitals/lcp.ts | 3 +- 5 files changed, 56 insertions(+), 7 deletions(-) create mode 100644 packages/replay/metrics/src/vitals/fid.ts diff --git a/packages/replay/metrics/src/index.ts b/packages/replay/metrics/src/index.ts index b43ae8b05a9d..321f268aa647 100644 --- a/packages/replay/metrics/src/index.ts +++ b/packages/replay/metrics/src/index.ts @@ -28,6 +28,8 @@ class MetricsCollector { await page.goto(this.url, { waitUntil: 'load', timeout: 60000 }); const pageMetrics = await page.metrics(); + + // TODO FID needs some interaction to actually show a value const vitals = await vitalsCollector.collect(); return new Metrics(this.url, pageMetrics, vitals); diff --git a/packages/replay/metrics/src/vitals/cls.ts b/packages/replay/metrics/src/vitals/cls.ts index be89902513c3..96a4056e2986 100644 --- a/packages/replay/metrics/src/vitals/cls.ts +++ b/packages/replay/metrics/src/vitals/cls.ts @@ -2,18 +2,23 @@ import * as puppeteer from 'puppeteer'; export {CLS}; +// https://web.dev/cls/ class CLS { constructor( private _page: puppeteer.Page) {} public async setup(): Promise { await this._page.evaluateOnNewDocument(`{ - window.cumulativeLayoutShiftScore = 0; + window.cumulativeLayoutShiftScore = undefined; const observer = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { if (!entry.hadRecentInput) { - window.cumulativeLayoutShiftScore += entry.value; + if (window.cumulativeLayoutShiftScore === undefined) { + window.cumulativeLayoutShiftScore = entry.value; + } else { + window.cumulativeLayoutShiftScore += entry.value; + } } } }); diff --git a/packages/replay/metrics/src/vitals/fid.ts b/packages/replay/metrics/src/vitals/fid.ts new file mode 100644 index 000000000000..ca96905dccb3 --- /dev/null +++ b/packages/replay/metrics/src/vitals/fid.ts @@ -0,0 +1,35 @@ +import * as puppeteer from 'puppeteer'; + +export {FID}; + +// https://web.dev/fid/ +class FID { + constructor( + private _page: puppeteer.Page) {} + + public async setup(): Promise { + await this._page.evaluateOnNewDocument(`{ + window.firstInputDelay = undefined; + + const observer = new PerformanceObserver((entryList) => { + for (const entry of entryList.getEntries()) { + window.firstInputDelay = entry.processingStart - entry.startTime; + } + }) + + observer.observe({type: 'first-input', buffered: true}); + + document.addEventListener('visibilitychange', () => { + if (document.visibilityState === 'hidden') { + observer.takeRecords(); + observer.disconnect(); + } + }); + }`); + } + + public async collect(): Promise { + const result = await this._page.evaluate('window.firstInputDelay'); + return result as number; + } +} diff --git a/packages/replay/metrics/src/vitals/index.ts b/packages/replay/metrics/src/vitals/index.ts index fe4c03082377..e3f060085f68 100644 --- a/packages/replay/metrics/src/vitals/index.ts +++ b/packages/replay/metrics/src/vitals/index.ts @@ -1,22 +1,27 @@ import * as puppeteer from 'puppeteer'; import {CLS} from './cls.js'; +import {FID} from './fid.js'; import {LCP} from './lcp.js'; export {WebVitals, WebVitalsCollector}; class WebVitals { - constructor(public lcp: number, public cls: number) {} + constructor(public lcp: number, public cls: number, public fid: number) {} } class WebVitalsCollector { - private constructor(private _lcp: LCP, private _cls: CLS) {} + private constructor(private _lcp: LCP, private _cls: CLS, private _fid: FID) { + } - public static async create(page: puppeteer.Page): Promise { - const result = new WebVitalsCollector(new LCP(page), new CLS(page)); + public static async create(page: puppeteer.Page): + Promise { + const result = + new WebVitalsCollector(new LCP(page), new CLS(page), new FID(page)); await result._lcp.setup(); await result._cls.setup(); + await result._fid.setup(); return result; } @@ -24,6 +29,7 @@ class WebVitalsCollector { return new WebVitals( await this._lcp.collect(), await this._cls.collect(), + await this._fid.collect(), ); } } diff --git a/packages/replay/metrics/src/vitals/lcp.ts b/packages/replay/metrics/src/vitals/lcp.ts index e7903f126de5..df4e55476197 100644 --- a/packages/replay/metrics/src/vitals/lcp.ts +++ b/packages/replay/metrics/src/vitals/lcp.ts @@ -2,13 +2,14 @@ import * as puppeteer from 'puppeteer'; export {LCP}; +// https://web.dev/lcp/ class LCP { constructor( private _page: puppeteer.Page) {} public async setup(): Promise { await this._page.evaluateOnNewDocument(`{ - window.largestContentfulPaint = 0; + window.largestContentfulPaint = undefined; const observer = new PerformanceObserver((list) => { const entries = list.getEntries(); From d57e56c11c7292ba551c8e5b37e151a6f657e1fa Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Thu, 22 Dec 2022 16:22:33 +0100 Subject: [PATCH 063/113] replay metrics - cpu stats --- packages/replay/metrics/src/cpu.ts | 61 ++++++++++++++++++++++++++++ packages/replay/metrics/src/index.ts | 20 ++++++--- 2 files changed, 75 insertions(+), 6 deletions(-) create mode 100644 packages/replay/metrics/src/cpu.ts diff --git a/packages/replay/metrics/src/cpu.ts b/packages/replay/metrics/src/cpu.ts new file mode 100644 index 000000000000..4ea0c70793a8 --- /dev/null +++ b/packages/replay/metrics/src/cpu.ts @@ -0,0 +1,61 @@ +import * as puppeteer from 'puppeteer'; + +export { CpuMonitor, CpuUsageHistory, CpuSnapshot } + +class CpuUsageHistory { + constructor(public average: number, public snapshots: CpuSnapshot[]) {} +} + +class CpuSnapshot { + constructor(public timestamp: number, public usage: number) { } +} + +class MetricsDataPoint { + constructor(public timestamp: number, public activeTime: number) { }; +} + +class CpuMonitor { + public snapshots: CpuSnapshot[] = []; + public average: number = 0; + private _timer!: NodeJS.Timer; + + private constructor(private _cdp: puppeteer.CDPSession) {} + + public static async create(cdp: puppeteer.CDPSession, interval: number): Promise { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + await cdp.send('Performance.enable', { timeDomain: 'timeTicks' }) + + const monitor = new CpuMonitor(cdp); + + let { timestamp: lastTimestamp, activeTime: cumulativeActiveTime } = await monitor._collect(); + const startTime = lastTimestamp; + monitor._timer = setInterval(async () => { + const data = await monitor._collect(); + const frameDuration = data.timestamp - lastTimestamp; + let usage = frameDuration == 0 ? 0 : (data.activeTime - cumulativeActiveTime) / frameDuration; + if (usage > 1) usage = 1 + + cumulativeActiveTime = data.activeTime + monitor.snapshots.push(new CpuSnapshot(data.timestamp, usage)); + + lastTimestamp = data.timestamp + monitor.average = cumulativeActiveTime / (lastTimestamp - startTime); + }, interval) + return monitor; + } + + public stats(): CpuUsageHistory { + return new CpuUsageHistory(this.average, this.snapshots); + } + + public stop(): void { + clearInterval(this._timer); + } + + private async _collect(): Promise { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const metrics = (await this._cdp.send('Performance.getMetrics')).metrics; + const activeTime = metrics.filter(m => m.name.includes('Duration')).map(m => m.value).reduce((a, b) => a + b) + return new MetricsDataPoint(metrics.find(m => m.name === 'Timestamp')?.value || 0, activeTime); + } +} diff --git a/packages/replay/metrics/src/index.ts b/packages/replay/metrics/src/index.ts index 321f268aa647..878a7fca57cd 100644 --- a/packages/replay/metrics/src/index.ts +++ b/packages/replay/metrics/src/index.ts @@ -1,5 +1,6 @@ import * as puppeteer from 'puppeteer'; +import { CpuMonitor, CpuUsageHistory } from './cpu.js'; import { WebVitals, WebVitalsCollector } from './vitals/index.js'; const cpuThrottling = 4; @@ -7,23 +8,30 @@ const networkConditions = puppeteer.PredefinedNetworkConditions['Fast 3G']; class Metrics { constructor( - public url: string, public pageMetrics: puppeteer.Metrics, - public vitals: WebVitals) {} + public url: string, public pageMetrics: puppeteer.Metrics, + public vitals: WebVitals, public cpu: CpuUsageHistory) { } } class MetricsCollector { - constructor(public url: string) {} + constructor(public url: string) { } public async run(): Promise { - const browser = await puppeteer.launch({headless: false,}); + const disposeCallbacks : (() => Promise)[] = []; try { + const browser = await puppeteer.launch({ headless: false, }); + disposeCallbacks.push(async () => browser.close()); const page = await browser.newPage(); // Simulated throttling await page.emulateNetworkConditions(networkConditions); await page.emulateCPUThrottling(cpuThrottling); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const cdp = await page.target().createCDPSession(); + const vitalsCollector = await WebVitalsCollector.create(page); + const cpuMonitor = await CpuMonitor.create(cdp, 100); // collect 10 times per second + disposeCallbacks.push(async () => cpuMonitor.stop()); await page.goto(this.url, { waitUntil: 'load', timeout: 60000 }); @@ -32,9 +40,9 @@ class MetricsCollector { // TODO FID needs some interaction to actually show a value const vitals = await vitalsCollector.collect(); - return new Metrics(this.url, pageMetrics, vitals); + return new Metrics(this.url, pageMetrics, vitals, cpuMonitor.stats()); } finally { - await browser.close(); + disposeCallbacks.reverse().forEach((cb) => cb().catch(console.log)); } } } From cda998c1e9acb6d6163f37c927c7e6a8bc74ad44 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Thu, 22 Dec 2022 17:24:21 +0100 Subject: [PATCH 064/113] metrics memory profiler --- packages/replay/metrics/.eslintrc.js | 1 + packages/replay/metrics/src/cpu.ts | 61 --------------------- packages/replay/metrics/src/index.ts | 34 ++++++------ packages/replay/metrics/src/perf/cpu.ts | 43 +++++++++++++++ packages/replay/metrics/src/perf/memory.ts | 17 ++++++ packages/replay/metrics/src/perf/sampler.ts | 35 ++++++++++++ 6 files changed, 114 insertions(+), 77 deletions(-) delete mode 100644 packages/replay/metrics/src/cpu.ts create mode 100644 packages/replay/metrics/src/perf/cpu.ts create mode 100644 packages/replay/metrics/src/perf/memory.ts create mode 100644 packages/replay/metrics/src/perf/sampler.ts diff --git a/packages/replay/metrics/.eslintrc.js b/packages/replay/metrics/.eslintrc.js index 5527b97558fb..3c4c14a69860 100644 --- a/packages/replay/metrics/.eslintrc.js +++ b/packages/replay/metrics/.eslintrc.js @@ -5,6 +5,7 @@ module.exports = { files: ['*.ts'], rules: { 'no-console': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', }, }, ], diff --git a/packages/replay/metrics/src/cpu.ts b/packages/replay/metrics/src/cpu.ts deleted file mode 100644 index 4ea0c70793a8..000000000000 --- a/packages/replay/metrics/src/cpu.ts +++ /dev/null @@ -1,61 +0,0 @@ -import * as puppeteer from 'puppeteer'; - -export { CpuMonitor, CpuUsageHistory, CpuSnapshot } - -class CpuUsageHistory { - constructor(public average: number, public snapshots: CpuSnapshot[]) {} -} - -class CpuSnapshot { - constructor(public timestamp: number, public usage: number) { } -} - -class MetricsDataPoint { - constructor(public timestamp: number, public activeTime: number) { }; -} - -class CpuMonitor { - public snapshots: CpuSnapshot[] = []; - public average: number = 0; - private _timer!: NodeJS.Timer; - - private constructor(private _cdp: puppeteer.CDPSession) {} - - public static async create(cdp: puppeteer.CDPSession, interval: number): Promise { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - await cdp.send('Performance.enable', { timeDomain: 'timeTicks' }) - - const monitor = new CpuMonitor(cdp); - - let { timestamp: lastTimestamp, activeTime: cumulativeActiveTime } = await monitor._collect(); - const startTime = lastTimestamp; - monitor._timer = setInterval(async () => { - const data = await monitor._collect(); - const frameDuration = data.timestamp - lastTimestamp; - let usage = frameDuration == 0 ? 0 : (data.activeTime - cumulativeActiveTime) / frameDuration; - if (usage > 1) usage = 1 - - cumulativeActiveTime = data.activeTime - monitor.snapshots.push(new CpuSnapshot(data.timestamp, usage)); - - lastTimestamp = data.timestamp - monitor.average = cumulativeActiveTime / (lastTimestamp - startTime); - }, interval) - return monitor; - } - - public stats(): CpuUsageHistory { - return new CpuUsageHistory(this.average, this.snapshots); - } - - public stop(): void { - clearInterval(this._timer); - } - - private async _collect(): Promise { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - const metrics = (await this._cdp.send('Performance.getMetrics')).metrics; - const activeTime = metrics.filter(m => m.name.includes('Duration')).map(m => m.value).reduce((a, b) => a + b) - return new MetricsDataPoint(metrics.find(m => m.name === 'Timestamp')?.value || 0, activeTime); - } -} diff --git a/packages/replay/metrics/src/index.ts b/packages/replay/metrics/src/index.ts index 878a7fca57cd..f832ecf3f7eb 100644 --- a/packages/replay/metrics/src/index.ts +++ b/packages/replay/metrics/src/index.ts @@ -1,24 +1,27 @@ import * as puppeteer from 'puppeteer'; -import { CpuMonitor, CpuUsageHistory } from './cpu.js'; -import { WebVitals, WebVitalsCollector } from './vitals/index.js'; +import {CpuUsage} from './perf/cpu.js'; +import {JsHeapUsage} from './perf/memory.js'; +import {PerfMetricsSampler} from './perf/sampler.js'; +import {WebVitals, WebVitalsCollector} from './vitals/index.js'; const cpuThrottling = 4; const networkConditions = puppeteer.PredefinedNetworkConditions['Fast 3G']; class Metrics { - constructor( - public url: string, public pageMetrics: puppeteer.Metrics, - public vitals: WebVitals, public cpu: CpuUsageHistory) { } + constructor(public url: string, public vitals: WebVitals, + public cpu: CpuUsage, public memory: JsHeapUsage) {} } class MetricsCollector { - constructor(public url: string) { } + constructor(public url: string) {} public async run(): Promise { - const disposeCallbacks : (() => Promise)[] = []; + const disposeCallbacks: (() => Promise)[] = []; try { - const browser = await puppeteer.launch({ headless: false, }); + const browser = await puppeteer.launch({ + headless : false, + }); disposeCallbacks.push(async () => browser.close()); const page = await browser.newPage(); @@ -26,21 +29,20 @@ class MetricsCollector { await page.emulateNetworkConditions(networkConditions); await page.emulateCPUThrottling(cpuThrottling); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - const cdp = await page.target().createCDPSession(); + const perfSampler = await PerfMetricsSampler.create( + page, 100); // collect 10 times per second + disposeCallbacks.push(async () => perfSampler.stop()); + const cpu = new CpuUsage(perfSampler); + const jsHeap = new JsHeapUsage(perfSampler); const vitalsCollector = await WebVitalsCollector.create(page); - const cpuMonitor = await CpuMonitor.create(cdp, 100); // collect 10 times per second - disposeCallbacks.push(async () => cpuMonitor.stop()); - await page.goto(this.url, { waitUntil: 'load', timeout: 60000 }); - - const pageMetrics = await page.metrics(); + await page.goto(this.url, {waitUntil : 'load', timeout : 60000}); // TODO FID needs some interaction to actually show a value const vitals = await vitalsCollector.collect(); - return new Metrics(this.url, pageMetrics, vitals, cpuMonitor.stats()); + return new Metrics(this.url, vitals, cpu, jsHeap); } finally { disposeCallbacks.reverse().forEach((cb) => cb().catch(console.log)); } diff --git a/packages/replay/metrics/src/perf/cpu.ts b/packages/replay/metrics/src/perf/cpu.ts new file mode 100644 index 000000000000..8a7ec3dad87a --- /dev/null +++ b/packages/replay/metrics/src/perf/cpu.ts @@ -0,0 +1,43 @@ +import * as puppeteer from 'puppeteer'; + +import { PerfMetricsSampler } from './sampler'; + +export { CpuUsage, CpuSnapshot } + +class CpuSnapshot { + constructor(public timestamp: number, public usage: number) { } +} + +class MetricsDataPoint { + constructor(public timestamp: number, public activeTime: number) { }; +} + +class CpuUsage { + public snapshots: CpuSnapshot[] = []; + public average: number = 0; + private _initial?: MetricsDataPoint = undefined; + private _startTime!: number; + private _lastTimestamp!: number; + private _cumulativeActiveTime!: number; + + public constructor(sampler: PerfMetricsSampler) { + sampler.subscribe(this._collect.bind(this)); + } + + private async _collect(metrics: puppeteer.Metrics): Promise { + const data = new MetricsDataPoint (metrics.Timestamp!, metrics.TaskDuration! + metrics.TaskDuration! + metrics.LayoutDuration! + metrics.ScriptDuration!); + if (this._initial == undefined) { + this._initial = data; + this._startTime = data.timestamp; + } else { + const frameDuration = data.timestamp - this._lastTimestamp; + let usage = frameDuration == 0 ? 0 : (data.activeTime - this._cumulativeActiveTime) / frameDuration; + if (usage > 1) usage = 1 + + this.snapshots.push(new CpuSnapshot(data.timestamp, usage)); + this.average = data.activeTime / (data.timestamp - this._startTime); + } + this._lastTimestamp = data.timestamp; + this._cumulativeActiveTime = data.activeTime; + } +} diff --git a/packages/replay/metrics/src/perf/memory.ts b/packages/replay/metrics/src/perf/memory.ts new file mode 100644 index 000000000000..2e480a188f16 --- /dev/null +++ b/packages/replay/metrics/src/perf/memory.ts @@ -0,0 +1,17 @@ +import * as puppeteer from 'puppeteer'; + +import { PerfMetricsSampler } from './sampler'; + +export { JsHeapUsage } + +class JsHeapUsage { + public snapshots: number[] = []; + + public constructor(sampler: PerfMetricsSampler) { + sampler.subscribe(this._collect.bind(this)); + } + + private async _collect(metrics: puppeteer.Metrics): Promise { + this.snapshots.push(metrics.JSHeapUsedSize!); + } +} diff --git a/packages/replay/metrics/src/perf/sampler.ts b/packages/replay/metrics/src/perf/sampler.ts new file mode 100644 index 000000000000..88d2d886b8b2 --- /dev/null +++ b/packages/replay/metrics/src/perf/sampler.ts @@ -0,0 +1,35 @@ +import * as puppeteer from 'puppeteer'; + +export { PerfMetricsSampler } + +type PerfMetricsConsumer = (metrics: puppeteer.Metrics) => Promise; + +class PerfMetricsSampler { + private _consumers: PerfMetricsConsumer[] = []; + private _timer!: NodeJS.Timer; + + public static async create(page: puppeteer.Page, interval: number): Promise { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const cdp = await page.target().createCDPSession(); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + await cdp.send('Performance.enable', { timeDomain: 'timeTicks' }) + + const self = new PerfMetricsSampler(); + + self._timer = setInterval(async () => { + const metrics = await page.metrics(); + self._consumers.forEach((cb) => cb(metrics).catch(console.log)); + }, interval); + + return self; + } + + public subscribe(consumer: PerfMetricsConsumer): void { + this._consumers.push(consumer); + } + + public stop(): void { + clearInterval(this._timer); + } +} From bcac89ca81c4ce2cd76e728f03b8fdea7de1d631 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Thu, 22 Dec 2022 17:44:55 +0100 Subject: [PATCH 065/113] replay metrics - refactor scenarios --- packages/replay/metrics/src/index.ts | 19 +++++++++---------- packages/replay/metrics/src/scenarios.ts | 15 +++++++++++++++ 2 files changed, 24 insertions(+), 10 deletions(-) create mode 100644 packages/replay/metrics/src/scenarios.ts diff --git a/packages/replay/metrics/src/index.ts b/packages/replay/metrics/src/index.ts index f832ecf3f7eb..9b3f739eeee6 100644 --- a/packages/replay/metrics/src/index.ts +++ b/packages/replay/metrics/src/index.ts @@ -3,20 +3,19 @@ import * as puppeteer from 'puppeteer'; import {CpuUsage} from './perf/cpu.js'; import {JsHeapUsage} from './perf/memory.js'; import {PerfMetricsSampler} from './perf/sampler.js'; -import {WebVitals, WebVitalsCollector} from './vitals/index.js'; +import { LoadPageScenario, Scenario } from './scenarios.js'; +import { WebVitals, WebVitalsCollector } from './vitals/index.js'; const cpuThrottling = 4; const networkConditions = puppeteer.PredefinedNetworkConditions['Fast 3G']; class Metrics { - constructor(public url: string, public vitals: WebVitals, + constructor(public scenario: Scenario, public vitals: WebVitals, public cpu: CpuUsage, public memory: JsHeapUsage) {} } class MetricsCollector { - constructor(public url: string) {} - - public async run(): Promise { + public async run(scenario: Scenario): Promise { const disposeCallbacks: (() => Promise)[] = []; try { const browser = await puppeteer.launch({ @@ -37,12 +36,12 @@ class MetricsCollector { const vitalsCollector = await WebVitalsCollector.create(page); - await page.goto(this.url, {waitUntil : 'load', timeout : 60000}); + await scenario.run(browser, page); - // TODO FID needs some interaction to actually show a value + // NOTE: FID needs some interaction to actually show a value const vitals = await vitalsCollector.collect(); - return new Metrics(this.url, vitals, cpu, jsHeap); + return new Metrics(scenario, vitals, cpu, jsHeap); } finally { disposeCallbacks.reverse().forEach((cb) => cb().catch(console.log)); } @@ -50,7 +49,7 @@ class MetricsCollector { } void (async () => { - const collector = new MetricsCollector('https://developers.google.com/web/'); - const metrics = await collector.run(); + const collector = new MetricsCollector(); + const metrics = await collector.run(new LoadPageScenario('https://developers.google.com/web/')); console.log(metrics); })(); diff --git a/packages/replay/metrics/src/scenarios.ts b/packages/replay/metrics/src/scenarios.ts new file mode 100644 index 000000000000..947d9a4a6cc2 --- /dev/null +++ b/packages/replay/metrics/src/scenarios.ts @@ -0,0 +1,15 @@ +import * as puppeteer from 'puppeteer'; + +// A testing scenario we want to collect metrics for. +export interface Scenario { + run(browser: puppeteer.Browser, page: puppeteer.Page): Promise; +} + +// A simple scenario that just loads the given URL. +export class LoadPageScenario implements Scenario { + public constructor(public url: string) { } + + public async run(_: puppeteer.Browser, page: puppeteer.Page): Promise { + await page.goto(this.url, {waitUntil : 'load', timeout : 60000}); + } +} From 68300fcae9a4bac662d1feed46ce82a56702c54f Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Wed, 28 Dec 2022 10:10:45 +0100 Subject: [PATCH 066/113] chore: formatting --- packages/replay/metrics/src/index.ts | 12 ++++++------ packages/replay/metrics/src/perf/cpu.ts | 2 +- packages/replay/metrics/src/scenarios.ts | 2 +- packages/replay/metrics/src/vitals/cls.ts | 4 ++-- packages/replay/metrics/src/vitals/fid.ts | 4 ++-- packages/replay/metrics/src/vitals/index.ts | 20 ++++++++++---------- packages/replay/metrics/src/vitals/lcp.ts | 4 ++-- 7 files changed, 24 insertions(+), 24 deletions(-) diff --git a/packages/replay/metrics/src/index.ts b/packages/replay/metrics/src/index.ts index 9b3f739eeee6..dc848b8ef546 100644 --- a/packages/replay/metrics/src/index.ts +++ b/packages/replay/metrics/src/index.ts @@ -1,8 +1,8 @@ import * as puppeteer from 'puppeteer'; -import {CpuUsage} from './perf/cpu.js'; -import {JsHeapUsage} from './perf/memory.js'; -import {PerfMetricsSampler} from './perf/sampler.js'; +import { CpuUsage } from './perf/cpu.js'; +import { JsHeapUsage } from './perf/memory.js'; +import { PerfMetricsSampler } from './perf/sampler.js'; import { LoadPageScenario, Scenario } from './scenarios.js'; import { WebVitals, WebVitalsCollector } from './vitals/index.js'; @@ -11,7 +11,7 @@ const networkConditions = puppeteer.PredefinedNetworkConditions['Fast 3G']; class Metrics { constructor(public scenario: Scenario, public vitals: WebVitals, - public cpu: CpuUsage, public memory: JsHeapUsage) {} + public cpu: CpuUsage, public memory: JsHeapUsage) { } } class MetricsCollector { @@ -19,7 +19,7 @@ class MetricsCollector { const disposeCallbacks: (() => Promise)[] = []; try { const browser = await puppeteer.launch({ - headless : false, + headless: false, }); disposeCallbacks.push(async () => browser.close()); const page = await browser.newPage(); @@ -29,7 +29,7 @@ class MetricsCollector { await page.emulateCPUThrottling(cpuThrottling); const perfSampler = await PerfMetricsSampler.create( - page, 100); // collect 10 times per second + page, 100); // collect 10 times per second disposeCallbacks.push(async () => perfSampler.stop()); const cpu = new CpuUsage(perfSampler); const jsHeap = new JsHeapUsage(perfSampler); diff --git a/packages/replay/metrics/src/perf/cpu.ts b/packages/replay/metrics/src/perf/cpu.ts index 8a7ec3dad87a..dcee3940f420 100644 --- a/packages/replay/metrics/src/perf/cpu.ts +++ b/packages/replay/metrics/src/perf/cpu.ts @@ -25,7 +25,7 @@ class CpuUsage { } private async _collect(metrics: puppeteer.Metrics): Promise { - const data = new MetricsDataPoint (metrics.Timestamp!, metrics.TaskDuration! + metrics.TaskDuration! + metrics.LayoutDuration! + metrics.ScriptDuration!); + const data = new MetricsDataPoint(metrics.Timestamp!, metrics.TaskDuration! + metrics.TaskDuration! + metrics.LayoutDuration! + metrics.ScriptDuration!); if (this._initial == undefined) { this._initial = data; this._startTime = data.timestamp; diff --git a/packages/replay/metrics/src/scenarios.ts b/packages/replay/metrics/src/scenarios.ts index 947d9a4a6cc2..4945ea13d0ad 100644 --- a/packages/replay/metrics/src/scenarios.ts +++ b/packages/replay/metrics/src/scenarios.ts @@ -10,6 +10,6 @@ export class LoadPageScenario implements Scenario { public constructor(public url: string) { } public async run(_: puppeteer.Browser, page: puppeteer.Page): Promise { - await page.goto(this.url, {waitUntil : 'load', timeout : 60000}); + await page.goto(this.url, { waitUntil: 'load', timeout: 60000 }); } } diff --git a/packages/replay/metrics/src/vitals/cls.ts b/packages/replay/metrics/src/vitals/cls.ts index 96a4056e2986..57175011d52b 100644 --- a/packages/replay/metrics/src/vitals/cls.ts +++ b/packages/replay/metrics/src/vitals/cls.ts @@ -1,11 +1,11 @@ import * as puppeteer from 'puppeteer'; -export {CLS}; +export { CLS }; // https://web.dev/cls/ class CLS { constructor( - private _page: puppeteer.Page) {} + private _page: puppeteer.Page) { } public async setup(): Promise { await this._page.evaluateOnNewDocument(`{ diff --git a/packages/replay/metrics/src/vitals/fid.ts b/packages/replay/metrics/src/vitals/fid.ts index ca96905dccb3..dac573010585 100644 --- a/packages/replay/metrics/src/vitals/fid.ts +++ b/packages/replay/metrics/src/vitals/fid.ts @@ -1,11 +1,11 @@ import * as puppeteer from 'puppeteer'; -export {FID}; +export { FID }; // https://web.dev/fid/ class FID { constructor( - private _page: puppeteer.Page) {} + private _page: puppeteer.Page) { } public async setup(): Promise { await this._page.evaluateOnNewDocument(`{ diff --git a/packages/replay/metrics/src/vitals/index.ts b/packages/replay/metrics/src/vitals/index.ts index e3f060085f68..1cb2b7aa0077 100644 --- a/packages/replay/metrics/src/vitals/index.ts +++ b/packages/replay/metrics/src/vitals/index.ts @@ -1,14 +1,14 @@ import * as puppeteer from 'puppeteer'; -import {CLS} from './cls.js'; -import {FID} from './fid.js'; -import {LCP} from './lcp.js'; +import { CLS } from './cls.js'; +import { FID } from './fid.js'; +import { LCP } from './lcp.js'; -export {WebVitals, WebVitalsCollector}; +export { WebVitals, WebVitalsCollector }; class WebVitals { - constructor(public lcp: number, public cls: number, public fid: number) {} + constructor(public lcp: number, public cls: number, public fid: number) { } } class WebVitalsCollector { @@ -16,9 +16,9 @@ class WebVitalsCollector { } public static async create(page: puppeteer.Page): - Promise { + Promise { const result = - new WebVitalsCollector(new LCP(page), new CLS(page), new FID(page)); + new WebVitalsCollector(new LCP(page), new CLS(page), new FID(page)); await result._lcp.setup(); await result._cls.setup(); await result._fid.setup(); @@ -27,9 +27,9 @@ class WebVitalsCollector { public async collect(): Promise { return new WebVitals( - await this._lcp.collect(), - await this._cls.collect(), - await this._fid.collect(), + await this._lcp.collect(), + await this._cls.collect(), + await this._fid.collect(), ); } } diff --git a/packages/replay/metrics/src/vitals/lcp.ts b/packages/replay/metrics/src/vitals/lcp.ts index df4e55476197..247ce0fcd3b6 100644 --- a/packages/replay/metrics/src/vitals/lcp.ts +++ b/packages/replay/metrics/src/vitals/lcp.ts @@ -1,11 +1,11 @@ import * as puppeteer from 'puppeteer'; -export {LCP}; +export { LCP }; // https://web.dev/lcp/ class LCP { constructor( - private _page: puppeteer.Page) {} + private _page: puppeteer.Page) { } public async setup(): Promise { await this._page.evaluateOnNewDocument(`{ From 628a877d652ba09206b32f9c6a302bb41a426cf0 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Wed, 28 Dec 2022 10:59:09 +0100 Subject: [PATCH 067/113] chore: refactoring --- .vscode/launch.json | 2 +- packages/replay/metrics/package.json | 4 ++-- .../metrics/src/{index.ts => collector.ts} | 16 +++++----------- packages/replay/metrics/src/run.ts | 8 ++++++++ 4 files changed, 16 insertions(+), 14 deletions(-) rename packages/replay/metrics/src/{index.ts => collector.ts} (75%) create mode 100644 packages/replay/metrics/src/run.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index 598d4363c1f7..c840cd5c7dbb 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -42,7 +42,7 @@ "name": "Debug replay metrics script", "request": "launch", "cwd": "${workspaceFolder}/packages/replay/metrics/", - "program": "${workspaceFolder}/packages/replay/metrics/src/index.ts", + "program": "${workspaceFolder}/packages/replay/metrics/src/run.ts", "preLaunchTask": "Build Replay metrics script", }, // Run rollup using the config file which is in the currently active tab. diff --git a/packages/replay/metrics/package.json b/packages/replay/metrics/package.json index 3ecb4819bce2..0a465fd8214c 100644 --- a/packages/replay/metrics/package.json +++ b/packages/replay/metrics/package.json @@ -7,8 +7,8 @@ "type": "module", "scripts": { "build": "tsc", - "start": "node ./build/index.js", - "dev": "ts-node-esm ./src/index.ts" + "start": "node ./build/run.js", + "dev": "ts-node-esm ./src/run.ts" }, "dependencies": { "@types/node": "^18.11.17", diff --git a/packages/replay/metrics/src/index.ts b/packages/replay/metrics/src/collector.ts similarity index 75% rename from packages/replay/metrics/src/index.ts rename to packages/replay/metrics/src/collector.ts index dc848b8ef546..91db6c940270 100644 --- a/packages/replay/metrics/src/index.ts +++ b/packages/replay/metrics/src/collector.ts @@ -3,7 +3,7 @@ import * as puppeteer from 'puppeteer'; import { CpuUsage } from './perf/cpu.js'; import { JsHeapUsage } from './perf/memory.js'; import { PerfMetricsSampler } from './perf/sampler.js'; -import { LoadPageScenario, Scenario } from './scenarios.js'; +import { Scenario } from './scenarios.js'; import { WebVitals, WebVitalsCollector } from './vitals/index.js'; const cpuThrottling = 4; @@ -14,7 +14,7 @@ class Metrics { public cpu: CpuUsage, public memory: JsHeapUsage) { } } -class MetricsCollector { +export class MetricsCollector { public async run(scenario: Scenario): Promise { const disposeCallbacks: (() => Promise)[] = []; try { @@ -24,12 +24,12 @@ class MetricsCollector { disposeCallbacks.push(async () => browser.close()); const page = await browser.newPage(); - // Simulated throttling + // Simulate throttling. await page.emulateNetworkConditions(networkConditions); await page.emulateCPUThrottling(cpuThrottling); - const perfSampler = await PerfMetricsSampler.create( - page, 100); // collect 10 times per second + // Collect CPU and memory info 10 times per second. + const perfSampler = await PerfMetricsSampler.create(page, 100); disposeCallbacks.push(async () => perfSampler.stop()); const cpu = new CpuUsage(perfSampler); const jsHeap = new JsHeapUsage(perfSampler); @@ -47,9 +47,3 @@ class MetricsCollector { } } } - -void (async () => { - const collector = new MetricsCollector(); - const metrics = await collector.run(new LoadPageScenario('https://developers.google.com/web/')); - console.log(metrics); -})(); diff --git a/packages/replay/metrics/src/run.ts b/packages/replay/metrics/src/run.ts new file mode 100644 index 000000000000..786f8e685887 --- /dev/null +++ b/packages/replay/metrics/src/run.ts @@ -0,0 +1,8 @@ +import { MetricsCollector } from './collector.js'; +import { LoadPageScenario } from './scenarios.js'; + +void (async () => { + const collector = new MetricsCollector(); + const metrics = await collector.run(new LoadPageScenario('https://developers.google.com/web/')); + console.log(metrics); +})(); From 9f37006f7917261234d178f4c7064a1ea87d32dd Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Wed, 28 Dec 2022 15:58:31 +0100 Subject: [PATCH 068/113] define test cases for metrics collection --- packages/replay/metrics/src/collector.ts | 41 ++++++++++++++++++++++-- packages/replay/metrics/src/perf/cpu.ts | 1 - packages/replay/metrics/src/run.ts | 26 +++++++++++---- packages/replay/metrics/src/scenarios.ts | 14 ++++++++ 4 files changed, 71 insertions(+), 11 deletions(-) diff --git a/packages/replay/metrics/src/collector.ts b/packages/replay/metrics/src/collector.ts index 91db6c940270..932fc5f780f6 100644 --- a/packages/replay/metrics/src/collector.ts +++ b/packages/replay/metrics/src/collector.ts @@ -1,21 +1,56 @@ +import assert from 'assert'; import * as puppeteer from 'puppeteer'; import { CpuUsage } from './perf/cpu.js'; import { JsHeapUsage } from './perf/memory.js'; import { PerfMetricsSampler } from './perf/sampler.js'; -import { Scenario } from './scenarios.js'; +import { Scenario, TestCase } from './scenarios.js'; import { WebVitals, WebVitalsCollector } from './vitals/index.js'; const cpuThrottling = 4; const networkConditions = puppeteer.PredefinedNetworkConditions['Fast 3G']; -class Metrics { +export class Metrics { constructor(public scenario: Scenario, public vitals: WebVitals, public cpu: CpuUsage, public memory: JsHeapUsage) { } } export class MetricsCollector { - public async run(scenario: Scenario): Promise { + public async execute(testCase: TestCase): Promise { + console.log(`Executing test case ${testCase.name}`); + console.group(); + for (let i = 1; i <= testCase.tries; i++) { + let aResults = await this.collect('A', testCase.a, testCase.runs); + let bResults = await this.collect('B', testCase.b, testCase.runs); + if (await testCase.test(aResults, bResults)) { + console.groupEnd(); + console.log(`Test case ${testCase.name} passed on try ${i}/${testCase.tries}`); + break; + } else if (i != testCase.tries) { + console.log(`Test case ${testCase.name} failed on try ${i}/${testCase.tries}`); + } else { + console.groupEnd(); + console.error(`Test case ${testCase.name} failed`); + } + } + } + + private async collect(name: string, scenario: Scenario, runs: number): Promise { + const label = `Scenario ${name} data collection (total ${runs} runs)`; + console.time(label); + const results: Metrics[] = []; + for (let run = 0; run < runs; run++) { + let innerLabel = `Scenario ${name} data collection, run ${run}/${runs}`; + console.time(innerLabel); + results.push(await this.run(scenario)); + console.timeEnd(innerLabel); + } + console.timeEnd(label); + assert(results.length == runs); + return results; + } + + private async run(scenario: Scenario): Promise { const disposeCallbacks: (() => Promise)[] = []; try { const browser = await puppeteer.launch({ diff --git a/packages/replay/metrics/src/perf/cpu.ts b/packages/replay/metrics/src/perf/cpu.ts index dcee3940f420..196bc14aef52 100644 --- a/packages/replay/metrics/src/perf/cpu.ts +++ b/packages/replay/metrics/src/perf/cpu.ts @@ -32,7 +32,6 @@ class CpuUsage { } else { const frameDuration = data.timestamp - this._lastTimestamp; let usage = frameDuration == 0 ? 0 : (data.activeTime - this._cumulativeActiveTime) / frameDuration; - if (usage > 1) usage = 1 this.snapshots.push(new CpuSnapshot(data.timestamp, usage)); this.average = data.activeTime / (data.timestamp - this._startTime); diff --git a/packages/replay/metrics/src/run.ts b/packages/replay/metrics/src/run.ts index 786f8e685887..70087ac29827 100644 --- a/packages/replay/metrics/src/run.ts +++ b/packages/replay/metrics/src/run.ts @@ -1,8 +1,20 @@ -import { MetricsCollector } from './collector.js'; -import { LoadPageScenario } from './scenarios.js'; +import { Metrics, MetricsCollector } from './collector.js'; +import { LoadPageScenario, TestCase } from './scenarios.js'; -void (async () => { - const collector = new MetricsCollector(); - const metrics = await collector.run(new LoadPageScenario('https://developers.google.com/web/')); - console.log(metrics); -})(); +const tests: TestCase[] = [ + { + name: 'dummy', + a: new LoadPageScenario('https://developers.google.com/web/'), + b: new LoadPageScenario('https://developers.google.com/'), + runs: 1, + tries: 1, + async test(_aResults: Metrics[], _bResults: Metrics[]) { + return true; + }, + } +] + +const collector = new MetricsCollector(); +for (const testCase of tests) { + await collector.execute(testCase); +} diff --git a/packages/replay/metrics/src/scenarios.ts b/packages/replay/metrics/src/scenarios.ts index 4945ea13d0ad..9ef7c4dced97 100644 --- a/packages/replay/metrics/src/scenarios.ts +++ b/packages/replay/metrics/src/scenarios.ts @@ -1,10 +1,24 @@ import * as puppeteer from 'puppeteer'; +import { Metrics } from './collector'; // A testing scenario we want to collect metrics for. export interface Scenario { run(browser: puppeteer.Browser, page: puppeteer.Page): Promise; } +// Two scenarios that are compared to each other. +export interface TestCase { + name: string; + a: Scenario; + b: Scenario; + runs: number; + tries: number; + + // Test function that will be executed and given scenarios A and B result sets. + // Each has exactly `runs` number of items. + test(aResults: Metrics[], bResults: Metrics[]): Promise; +} + // A simple scenario that just loads the given URL. export class LoadPageScenario implements Scenario { public constructor(public url: string) { } From 2c4aa3c7c815b399528bfd96c2797002109684f2 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Thu, 29 Dec 2022 10:56:06 +0100 Subject: [PATCH 069/113] refactor: split up data collection --- .vscode/launch.json | 4 ++-- packages/replay/metrics/package.json | 3 +-- packages/replay/metrics/src/{run.ts => collect-dev.ts} | 0 packages/replay/metrics/src/collector.ts | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) rename packages/replay/metrics/src/{run.ts => collect-dev.ts} (100%) diff --git a/.vscode/launch.json b/.vscode/launch.json index c840cd5c7dbb..26f846741de2 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -39,10 +39,10 @@ }, { "type": "node", - "name": "Debug replay metrics script", + "name": "Debug replay metrics collection script", "request": "launch", "cwd": "${workspaceFolder}/packages/replay/metrics/", - "program": "${workspaceFolder}/packages/replay/metrics/src/run.ts", + "program": "${workspaceFolder}/packages/replay/metrics/src/collect-dev.ts", "preLaunchTask": "Build Replay metrics script", }, // Run rollup using the config file which is in the currently active tab. diff --git a/packages/replay/metrics/package.json b/packages/replay/metrics/package.json index 0a465fd8214c..fe5144a2a59c 100644 --- a/packages/replay/metrics/package.json +++ b/packages/replay/metrics/package.json @@ -7,8 +7,7 @@ "type": "module", "scripts": { "build": "tsc", - "start": "node ./build/run.js", - "dev": "ts-node-esm ./src/run.ts" + "collect-dev": "ts-node-esm ./src/collect-dev.ts" }, "dependencies": { "@types/node": "^18.11.17", diff --git a/packages/replay/metrics/src/run.ts b/packages/replay/metrics/src/collect-dev.ts similarity index 100% rename from packages/replay/metrics/src/run.ts rename to packages/replay/metrics/src/collect-dev.ts diff --git a/packages/replay/metrics/src/collector.ts b/packages/replay/metrics/src/collector.ts index 932fc5f780f6..385a68497cd0 100644 --- a/packages/replay/metrics/src/collector.ts +++ b/packages/replay/metrics/src/collector.ts @@ -46,7 +46,7 @@ export class MetricsCollector { console.timeEnd(innerLabel); } console.timeEnd(label); - assert(results.length == runs); + assert.equal(results.length, runs); return results; } From 945845eb2a872cbe379c835ce9896e073ceb95fb Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Thu, 29 Dec 2022 15:22:16 +0100 Subject: [PATCH 070/113] write collected metrics to file --- .vscode/launch.json | 2 +- packages/replay/metrics/.gitignore | 1 + packages/replay/metrics/configs/README.md | 4 ++ .../replay/metrics/configs/dev/collect.ts | 17 +++++ packages/replay/metrics/configs/dev/env.ts | 2 + packages/replay/metrics/package.json | 2 +- packages/replay/metrics/src/collect-dev.ts | 20 ------ packages/replay/metrics/src/collector.ts | 24 ++++--- packages/replay/metrics/src/perf/cpu.ts | 20 +++++- packages/replay/metrics/src/perf/memory.ts | 14 +++- packages/replay/metrics/src/results/result.ts | 29 ++++++++ .../replay/metrics/src/results/results-set.ts | 68 +++++++++++++++++++ packages/replay/metrics/src/vitals/index.ts | 4 ++ 13 files changed, 171 insertions(+), 36 deletions(-) create mode 100644 packages/replay/metrics/.gitignore create mode 100644 packages/replay/metrics/configs/README.md create mode 100644 packages/replay/metrics/configs/dev/collect.ts create mode 100644 packages/replay/metrics/configs/dev/env.ts delete mode 100644 packages/replay/metrics/src/collect-dev.ts create mode 100644 packages/replay/metrics/src/results/result.ts create mode 100644 packages/replay/metrics/src/results/results-set.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index 26f846741de2..1a1123c1aa23 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -42,7 +42,7 @@ "name": "Debug replay metrics collection script", "request": "launch", "cwd": "${workspaceFolder}/packages/replay/metrics/", - "program": "${workspaceFolder}/packages/replay/metrics/src/collect-dev.ts", + "program": "${workspaceFolder}/packages/replay/metrics/configs/dev/collect.ts", "preLaunchTask": "Build Replay metrics script", }, // Run rollup using the config file which is in the currently active tab. diff --git a/packages/replay/metrics/.gitignore b/packages/replay/metrics/.gitignore new file mode 100644 index 000000000000..505d701f0e12 --- /dev/null +++ b/packages/replay/metrics/.gitignore @@ -0,0 +1 @@ +out diff --git a/packages/replay/metrics/configs/README.md b/packages/replay/metrics/configs/README.md new file mode 100644 index 000000000000..d47f3ac8e145 --- /dev/null +++ b/packages/replay/metrics/configs/README.md @@ -0,0 +1,4 @@ +# Replay metrics configuration & entrypoints (scripts) + +* [dev] contains scripts launched during local development +* [ci] contains scripts launched in CI diff --git a/packages/replay/metrics/configs/dev/collect.ts b/packages/replay/metrics/configs/dev/collect.ts new file mode 100644 index 000000000000..b9633e7b174a --- /dev/null +++ b/packages/replay/metrics/configs/dev/collect.ts @@ -0,0 +1,17 @@ +import { Metrics, MetricsCollector } from '../../src/collector.js'; +import { LoadPageScenario } from '../../src/scenarios.js'; +import { latestResultFile } from './env.js'; + +const collector = new MetricsCollector(); +const result = await collector.execute({ + name: 'dummy', + a: new LoadPageScenario('https://developers.google.com/web/'), + b: new LoadPageScenario('https://developers.google.com/'), + runs: 1, + tries: 1, + async test(_aResults: Metrics[], _bResults: Metrics[]) { + return true; + }, +}); + +result.writeToFile(latestResultFile); diff --git a/packages/replay/metrics/configs/dev/env.ts b/packages/replay/metrics/configs/dev/env.ts new file mode 100644 index 000000000000..c2168763ea6e --- /dev/null +++ b/packages/replay/metrics/configs/dev/env.ts @@ -0,0 +1,2 @@ +export const outDir = 'out/results-dev'; +export const latestResultFile = 'out/latest-result.json'; diff --git a/packages/replay/metrics/package.json b/packages/replay/metrics/package.json index fe5144a2a59c..a5f5d38c44c6 100644 --- a/packages/replay/metrics/package.json +++ b/packages/replay/metrics/package.json @@ -7,7 +7,7 @@ "type": "module", "scripts": { "build": "tsc", - "collect-dev": "ts-node-esm ./src/collect-dev.ts" + "dev:collect": "ts-node-esm ./configs/dev/collect.ts" }, "dependencies": { "@types/node": "^18.11.17", diff --git a/packages/replay/metrics/src/collect-dev.ts b/packages/replay/metrics/src/collect-dev.ts deleted file mode 100644 index 70087ac29827..000000000000 --- a/packages/replay/metrics/src/collect-dev.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Metrics, MetricsCollector } from './collector.js'; -import { LoadPageScenario, TestCase } from './scenarios.js'; - -const tests: TestCase[] = [ - { - name: 'dummy', - a: new LoadPageScenario('https://developers.google.com/web/'), - b: new LoadPageScenario('https://developers.google.com/'), - runs: 1, - tries: 1, - async test(_aResults: Metrics[], _bResults: Metrics[]) { - return true; - }, - } -] - -const collector = new MetricsCollector(); -for (const testCase of tests) { - await collector.execute(testCase); -} diff --git a/packages/replay/metrics/src/collector.ts b/packages/replay/metrics/src/collector.ts index 385a68497cd0..791d65acfd94 100644 --- a/packages/replay/metrics/src/collector.ts +++ b/packages/replay/metrics/src/collector.ts @@ -1,22 +1,23 @@ import assert from 'assert'; import * as puppeteer from 'puppeteer'; -import { CpuUsage } from './perf/cpu.js'; -import { JsHeapUsage } from './perf/memory.js'; +import { CpuUsage, CpuUsageSampler } from './perf/cpu.js'; +import { JsHeapUsage, JsHeapUsageSampler } from './perf/memory.js'; import { PerfMetricsSampler } from './perf/sampler.js'; +import { Result } from './results/result.js'; import { Scenario, TestCase } from './scenarios.js'; import { WebVitals, WebVitalsCollector } from './vitals/index.js'; const cpuThrottling = 4; -const networkConditions = puppeteer.PredefinedNetworkConditions['Fast 3G']; +const networkConditions = 'Fast 3G'; export class Metrics { - constructor(public scenario: Scenario, public vitals: WebVitals, - public cpu: CpuUsage, public memory: JsHeapUsage) { } + constructor(public readonly vitals: WebVitals, public readonly cpu: CpuUsage, public readonly memory: JsHeapUsage) { } } + export class MetricsCollector { - public async execute(testCase: TestCase): Promise { + public async execute(testCase: TestCase): Promise { console.log(`Executing test case ${testCase.name}`); console.group(); for (let i = 1; i <= testCase.tries; i++) { @@ -25,7 +26,7 @@ export class MetricsCollector { if (await testCase.test(aResults, bResults)) { console.groupEnd(); console.log(`Test case ${testCase.name} passed on try ${i}/${testCase.tries}`); - break; + return new Result(testCase.name, cpuThrottling, networkConditions, aResults, bResults); } else if (i != testCase.tries) { console.log(`Test case ${testCase.name} failed on try ${i}/${testCase.tries}`); } else { @@ -33,6 +34,7 @@ export class MetricsCollector { console.error(`Test case ${testCase.name} failed`); } } + throw `Test case execution ${testCase.name} failed after ${testCase.tries} tries.`; } private async collect(name: string, scenario: Scenario, runs: number): Promise { @@ -60,14 +62,14 @@ export class MetricsCollector { const page = await browser.newPage(); // Simulate throttling. - await page.emulateNetworkConditions(networkConditions); + await page.emulateNetworkConditions(puppeteer.PredefinedNetworkConditions[networkConditions]); await page.emulateCPUThrottling(cpuThrottling); // Collect CPU and memory info 10 times per second. const perfSampler = await PerfMetricsSampler.create(page, 100); disposeCallbacks.push(async () => perfSampler.stop()); - const cpu = new CpuUsage(perfSampler); - const jsHeap = new JsHeapUsage(perfSampler); + const cpuSampler = new CpuUsageSampler(perfSampler); + const memSampler = new JsHeapUsageSampler(perfSampler); const vitalsCollector = await WebVitalsCollector.create(page); @@ -76,7 +78,7 @@ export class MetricsCollector { // NOTE: FID needs some interaction to actually show a value const vitals = await vitalsCollector.collect(); - return new Metrics(scenario, vitals, cpu, jsHeap); + return new Metrics(vitals, cpuSampler.getData(), memSampler.getData()); } finally { disposeCallbacks.reverse().forEach((cb) => cb().catch(console.log)); } diff --git a/packages/replay/metrics/src/perf/cpu.ts b/packages/replay/metrics/src/perf/cpu.ts index 196bc14aef52..741d752d2e4c 100644 --- a/packages/replay/metrics/src/perf/cpu.ts +++ b/packages/replay/metrics/src/perf/cpu.ts @@ -2,17 +2,29 @@ import * as puppeteer from 'puppeteer'; import { PerfMetricsSampler } from './sampler'; -export { CpuUsage, CpuSnapshot } +export { CpuUsageSampler, CpuUsage, CpuSnapshot } class CpuSnapshot { constructor(public timestamp: number, public usage: number) { } + + public static fromJSON(data: Partial): CpuSnapshot { + return new CpuSnapshot(data.timestamp || NaN, data.usage || NaN); + } +} + +class CpuUsage { + constructor(public snapshots: CpuSnapshot[], public average: number) { }; + + public static fromJSON(data: Partial): CpuUsage { + return new CpuUsage(data.snapshots || [], data.average || NaN); + } } class MetricsDataPoint { constructor(public timestamp: number, public activeTime: number) { }; } -class CpuUsage { +class CpuUsageSampler { public snapshots: CpuSnapshot[] = []; public average: number = 0; private _initial?: MetricsDataPoint = undefined; @@ -24,6 +36,10 @@ class CpuUsage { sampler.subscribe(this._collect.bind(this)); } + public getData(): CpuUsage { + return new CpuUsage(this.snapshots, this.average); + } + private async _collect(metrics: puppeteer.Metrics): Promise { const data = new MetricsDataPoint(metrics.Timestamp!, metrics.TaskDuration! + metrics.TaskDuration! + metrics.LayoutDuration! + metrics.ScriptDuration!); if (this._initial == undefined) { diff --git a/packages/replay/metrics/src/perf/memory.ts b/packages/replay/metrics/src/perf/memory.ts index 2e480a188f16..36baf8ae1414 100644 --- a/packages/replay/metrics/src/perf/memory.ts +++ b/packages/replay/metrics/src/perf/memory.ts @@ -2,15 +2,27 @@ import * as puppeteer from 'puppeteer'; import { PerfMetricsSampler } from './sampler'; -export { JsHeapUsage } +export { JsHeapUsageSampler, JsHeapUsage } class JsHeapUsage { + public constructor(public snapshots: number[]) { } + + public static fromJSON(data: Partial): JsHeapUsage { + return new JsHeapUsage(data.snapshots || []); + } +} + +class JsHeapUsageSampler { public snapshots: number[] = []; public constructor(sampler: PerfMetricsSampler) { sampler.subscribe(this._collect.bind(this)); } + public getData(): JsHeapUsage { + return new JsHeapUsage(this.snapshots); + } + private async _collect(metrics: puppeteer.Metrics): Promise { this.snapshots.push(metrics.JSHeapUsedSize!); } diff --git a/packages/replay/metrics/src/results/result.ts b/packages/replay/metrics/src/results/result.ts new file mode 100644 index 000000000000..bc39269dc09e --- /dev/null +++ b/packages/replay/metrics/src/results/result.ts @@ -0,0 +1,29 @@ +import * as fs from 'fs'; +import path from 'path'; + +import { Metrics } from '../collector'; + +export class Result { + constructor( + public readonly name: string, public readonly cpuThrottling: number, + public readonly networkConditions: string, + public readonly aResults: Metrics[], + public readonly bResults: Metrics[]) { } + + public writeToFile(filePath: string): void { + const dir = path.dirname(filePath); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir); + } + const json = JSON.stringify(this); + fs.writeFileSync(filePath, json); + } + + public static readFromFile(filePath: string): Result { + const json = fs.readFileSync(filePath, { encoding: 'utf-8' }); + const data = JSON.parse(json); + return new Result( + data.name || '', data.cpuThrottling || NaN, + data.networkConditions || '', data.aResults || [], data.bResults || []); + } +} diff --git a/packages/replay/metrics/src/results/results-set.ts b/packages/replay/metrics/src/results/results-set.ts new file mode 100644 index 000000000000..3faafacbdf47 --- /dev/null +++ b/packages/replay/metrics/src/results/results-set.ts @@ -0,0 +1,68 @@ +import * as fs from 'fs'; +import path from 'path'; + +const delimiter = '-'; + +export class ResultSetItem { + public constructor(public path: string) { } + + public get name(): string { + return path.basename(this.path); + } + + public get number(): number { + return parseInt(this.parts[0]); + } + + public get hash(): string { + return this.parts[1]; + } + + get parts(): string[] { + return path.basename(this.path).split(delimiter); + } +} + +/// Wraps a directory containing multiple (N--result.json) files. +/// The files are numbered from the most recently added one, to the oldest one. +export class ResultsSet { + public constructor(private directory: string) { + if (!fs.existsSync(directory)) { + fs.mkdirSync(directory, { recursive: true }); + } + } + + public count(): number { + return this.items().length; + } + + public items(): ResultSetItem[] { + return this.files().map((file) => { + return new ResultSetItem(path.join(this.directory, file.name)); + }).filter((item) => !isNaN(item.number)); + } + + files(): fs.Dirent[] { + return fs.readdirSync(this.directory, { withFileTypes: true }).filter((v) => v.isFile()) + } + + public add(file: ResultSetItem) { + console.log(`Preparing to add ${file.path} to ${this.directory}`); + + // Get the list of file sorted by the prefix number in the descending order. + const files = this.items().sort((a, b) => b.number - a.number); + + // Rename all existing files, increasing the prefix + for (const file of files) { + const parts = file.name.split(delimiter); + parts[0] = (file.number + 1).toString(); + const newPath = path.join(this.directory, parts.join(delimiter)); + console.log(`Renaming ${file.path} to ${newPath}`); + fs.renameSync(file.path, newPath); + } + + const newName = `1${delimiter}${file.hash}${delimiter}result.json`; + console.log(`Adding ${file.path} to ${this.directory} as ${newName}`); + fs.copyFileSync(file.path, path.join(this.directory, newName)); + } +} diff --git a/packages/replay/metrics/src/vitals/index.ts b/packages/replay/metrics/src/vitals/index.ts index 1cb2b7aa0077..892a248266ad 100644 --- a/packages/replay/metrics/src/vitals/index.ts +++ b/packages/replay/metrics/src/vitals/index.ts @@ -9,6 +9,10 @@ export { WebVitals, WebVitalsCollector }; class WebVitals { constructor(public lcp: number, public cls: number, public fid: number) { } + + public static fromJSON(data: Partial): WebVitals { + return new WebVitals(data.lcp || NaN, data.cls || NaN, data.fid || NaN); + } } class WebVitalsCollector { From b0ec7e6e1da0f5bad7e004d96e530068ca0596b4 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Thu, 29 Dec 2022 15:58:07 +0100 Subject: [PATCH 071/113] add metrics processing script --- .vscode/launch.json | 8 +++++++ .../replay/metrics/configs/dev/process.ts | 5 ++++ packages/replay/metrics/package.json | 4 +++- packages/replay/metrics/src/git.ts | 16 +++++++++++++ .../replay/metrics/src/results/results-set.ts | 13 +++++++---- packages/replay/metrics/tsconfig.json | 3 ++- packages/replay/metrics/yarn.lock | 23 ++++++++++++++++++- 7 files changed, 64 insertions(+), 8 deletions(-) create mode 100644 packages/replay/metrics/configs/dev/process.ts create mode 100644 packages/replay/metrics/src/git.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index 1a1123c1aa23..6092a79fb1f6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -45,6 +45,14 @@ "program": "${workspaceFolder}/packages/replay/metrics/configs/dev/collect.ts", "preLaunchTask": "Build Replay metrics script", }, + { + "type": "node", + "name": "Debug replay metrics processing script", + "request": "launch", + "cwd": "${workspaceFolder}/packages/replay/metrics/", + "program": "${workspaceFolder}/packages/replay/metrics/configs/dev/process.ts", + "preLaunchTask": "Build Replay metrics script", + }, // Run rollup using the config file which is in the currently active tab. { "name": "Debug rollup (config from open file)", diff --git a/packages/replay/metrics/configs/dev/process.ts b/packages/replay/metrics/configs/dev/process.ts new file mode 100644 index 000000000000..460b11b8e26e --- /dev/null +++ b/packages/replay/metrics/configs/dev/process.ts @@ -0,0 +1,5 @@ +import { ResultsSet } from '../../src/results/results-set.js'; +import { latestResultFile, outDir } from './env.js'; + +const resultsSet = new ResultsSet(outDir); +await resultsSet.add(latestResultFile); diff --git a/packages/replay/metrics/package.json b/packages/replay/metrics/package.json index a5f5d38c44c6..bc216b8ee00f 100644 --- a/packages/replay/metrics/package.json +++ b/packages/replay/metrics/package.json @@ -7,11 +7,13 @@ "type": "module", "scripts": { "build": "tsc", - "dev:collect": "ts-node-esm ./configs/dev/collect.ts" + "dev:collect": "ts-node-esm ./configs/dev/collect.ts", + "dev:process": "ts-node-esm ./configs/dev/process.ts" }, "dependencies": { "@types/node": "^18.11.17", "puppeteer": "^19.4.1", + "simple-git": "^3.15.1", "typescript": "^4.9.4" }, "devDependencies": { diff --git a/packages/replay/metrics/src/git.ts b/packages/replay/metrics/src/git.ts new file mode 100644 index 000000000000..88f0206a759b --- /dev/null +++ b/packages/replay/metrics/src/git.ts @@ -0,0 +1,16 @@ +import { simpleGit } from 'simple-git'; + +// A testing scenario we want to collect metrics for. +export const Git = { + get hash(): Promise { + return (async () => { + const git = simpleGit(); + let gitHash = await git.revparse('HEAD'); + let diff = await git.diff(); + if (diff.trim().length > 0) { + gitHash += '+dirty'; + } + return gitHash; + })(); + } +} diff --git a/packages/replay/metrics/src/results/results-set.ts b/packages/replay/metrics/src/results/results-set.ts index 3faafacbdf47..ca386723fb27 100644 --- a/packages/replay/metrics/src/results/results-set.ts +++ b/packages/replay/metrics/src/results/results-set.ts @@ -1,5 +1,7 @@ +import assert from 'assert'; import * as fs from 'fs'; import path from 'path'; +import { Git } from '../git.js'; const delimiter = '-'; @@ -46,8 +48,9 @@ export class ResultsSet { return fs.readdirSync(this.directory, { withFileTypes: true }).filter((v) => v.isFile()) } - public add(file: ResultSetItem) { - console.log(`Preparing to add ${file.path} to ${this.directory}`); + public async add(newFile: string): Promise { + console.log(`Preparing to add ${newFile} to ${this.directory}`); + assert(fs.existsSync(newFile)); // Get the list of file sorted by the prefix number in the descending order. const files = this.items().sort((a, b) => b.number - a.number); @@ -61,8 +64,8 @@ export class ResultsSet { fs.renameSync(file.path, newPath); } - const newName = `1${delimiter}${file.hash}${delimiter}result.json`; - console.log(`Adding ${file.path} to ${this.directory} as ${newName}`); - fs.copyFileSync(file.path, path.join(this.directory, newName)); + const newName = `1${delimiter}${await Git.hash}${delimiter}result.json`; + console.log(`Adding ${newFile} to ${this.directory} as ${newName}`); + fs.copyFileSync(newFile, path.join(this.directory, newName)); } } diff --git a/packages/replay/metrics/tsconfig.json b/packages/replay/metrics/tsconfig.json index 9aa9fd11b24e..1709316f0ea8 100644 --- a/packages/replay/metrics/tsconfig.json +++ b/packages/replay/metrics/tsconfig.json @@ -7,6 +7,7 @@ "esModuleInterop": true, }, "include": [ - "src/**/*.ts" + "src/**/*.ts", + "configs/**/*.ts" ] } diff --git a/packages/replay/metrics/yarn.lock b/packages/replay/metrics/yarn.lock index 1b1b34da02f2..9d4fa4d0e407 100644 --- a/packages/replay/metrics/yarn.lock +++ b/packages/replay/metrics/yarn.lock @@ -48,6 +48,18 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" +"@kwsites/file-exists@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@kwsites/file-exists/-/file-exists-1.1.1.tgz#ad1efcac13e1987d8dbaf235ef3be5b0d96faa99" + integrity sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw== + dependencies: + debug "^4.1.1" + +"@kwsites/promise-deferred@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz#8ace5259254426ccef57f3175bc64ed7095ed919" + integrity sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw== + "@tsconfig/node10@^1.0.7": version "1.0.9" resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" @@ -212,7 +224,7 @@ cross-fetch@3.1.5: dependencies: node-fetch "2.6.7" -debug@4, debug@4.3.4, debug@^4.1.1: +debug@4, debug@4.3.4, debug@^4.1.1, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -500,6 +512,15 @@ safe-buffer@~5.2.0: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== +simple-git@^3.15.1: + version "3.15.1" + resolved "https://registry.yarnpkg.com/simple-git/-/simple-git-3.15.1.tgz#57f595682cb0c2475d5056da078a05c8715a25ef" + integrity sha512-73MVa5984t/JP4JcQt0oZlKGr42ROYWC3BcUZfuHtT3IHKPspIvL0cZBnvPXF7LL3S/qVeVHVdYYmJ3LOTw4Rg== + dependencies: + "@kwsites/file-exists" "^1.1.1" + "@kwsites/promise-deferred" "^1.1.1" + debug "^4.3.4" + string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" From d3f93d81b5c01598d8a76d14eda195868f919187 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Thu, 29 Dec 2022 16:17:52 +0100 Subject: [PATCH 072/113] metrics: reading json result file --- packages/replay/metrics/configs/dev/process.ts | 5 +++++ packages/replay/metrics/src/collector.ts | 8 ++++++++ packages/replay/metrics/src/perf/cpu.ts | 5 ++++- packages/replay/metrics/src/results/result.ts | 10 +++++++--- 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/packages/replay/metrics/configs/dev/process.ts b/packages/replay/metrics/configs/dev/process.ts index 460b11b8e26e..a5d8a684bedb 100644 --- a/packages/replay/metrics/configs/dev/process.ts +++ b/packages/replay/metrics/configs/dev/process.ts @@ -1,5 +1,10 @@ +import { Result } from '../../src/results/result.js'; import { ResultsSet } from '../../src/results/results-set.js'; import { latestResultFile, outDir } from './env.js'; const resultsSet = new ResultsSet(outDir); + +const latestResult = Result.readFromFile(latestResultFile); +console.log(latestResult); + await resultsSet.add(latestResultFile); diff --git a/packages/replay/metrics/src/collector.ts b/packages/replay/metrics/src/collector.ts index 791d65acfd94..994a8fb2bfd2 100644 --- a/packages/replay/metrics/src/collector.ts +++ b/packages/replay/metrics/src/collector.ts @@ -13,6 +13,14 @@ const networkConditions = 'Fast 3G'; export class Metrics { constructor(public readonly vitals: WebVitals, public readonly cpu: CpuUsage, public readonly memory: JsHeapUsage) { } + + public static fromJSON(data: Partial): Metrics { + return new Metrics( + WebVitals.fromJSON(data.vitals || {}), + CpuUsage.fromJSON(data.cpu || {}), + JsHeapUsage.fromJSON(data.memory || {}), + ); + } } diff --git a/packages/replay/metrics/src/perf/cpu.ts b/packages/replay/metrics/src/perf/cpu.ts index 741d752d2e4c..760fc0990aac 100644 --- a/packages/replay/metrics/src/perf/cpu.ts +++ b/packages/replay/metrics/src/perf/cpu.ts @@ -16,7 +16,10 @@ class CpuUsage { constructor(public snapshots: CpuSnapshot[], public average: number) { }; public static fromJSON(data: Partial): CpuUsage { - return new CpuUsage(data.snapshots || [], data.average || NaN); + return new CpuUsage( + (data.snapshots || []).map(CpuSnapshot.fromJSON), + data.average || NaN, + ); } } diff --git a/packages/replay/metrics/src/results/result.ts b/packages/replay/metrics/src/results/result.ts index bc39269dc09e..6643f48c6731 100644 --- a/packages/replay/metrics/src/results/result.ts +++ b/packages/replay/metrics/src/results/result.ts @@ -1,7 +1,7 @@ import * as fs from 'fs'; import path from 'path'; -import { Metrics } from '../collector'; +import { Metrics } from '../collector.js'; export class Result { constructor( @@ -23,7 +23,11 @@ export class Result { const json = fs.readFileSync(filePath, { encoding: 'utf-8' }); const data = JSON.parse(json); return new Result( - data.name || '', data.cpuThrottling || NaN, - data.networkConditions || '', data.aResults || [], data.bResults || []); + data.name || '', + data.cpuThrottling || NaN, + data.networkConditions || '', + (data.aResults || []).map(Metrics.fromJSON), + (data.bResults || []).map(Metrics.fromJSON), + ); } } From 67a6dd8c7daba67d2c3939ddb1e292cb0ad11773 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Thu, 29 Dec 2022 17:41:06 +0100 Subject: [PATCH 073/113] replay matrics: jank test app copy --- .../replay/metrics/test-apps/jank/README.md | 4 + packages/replay/metrics/test-apps/jank/app.js | 171 ++++++++++++++++++ .../metrics/test-apps/jank/favicon-96x96.png | Bin 0 -> 8194 bytes .../replay/metrics/test-apps/jank/index.html | 43 +++++ .../metrics/test-apps/jank/logo-1024px.png | Bin 0 -> 305497 bytes .../replay/metrics/test-apps/jank/styles.css | 59 ++++++ 6 files changed, 277 insertions(+) create mode 100644 packages/replay/metrics/test-apps/jank/README.md create mode 100644 packages/replay/metrics/test-apps/jank/app.js create mode 100644 packages/replay/metrics/test-apps/jank/favicon-96x96.png create mode 100644 packages/replay/metrics/test-apps/jank/index.html create mode 100644 packages/replay/metrics/test-apps/jank/logo-1024px.png create mode 100644 packages/replay/metrics/test-apps/jank/styles.css diff --git a/packages/replay/metrics/test-apps/jank/README.md b/packages/replay/metrics/test-apps/jank/README.md new file mode 100644 index 000000000000..3e0f46b66a1e --- /dev/null +++ b/packages/replay/metrics/test-apps/jank/README.md @@ -0,0 +1,4 @@ +# Chrome DevTools Jank article sample code + +* Originally coming from [devtools-samples](https://github.com/GoogleChrome/devtools-samples/tree/4818abc9dbcdb954d0eb9b70879f4ea18756451f/jank), licensed under Apache 2.0. +* Linking article: diff --git a/packages/replay/metrics/test-apps/jank/app.js b/packages/replay/metrics/test-apps/jank/app.js new file mode 100644 index 000000000000..ab961a366315 --- /dev/null +++ b/packages/replay/metrics/test-apps/jank/app.js @@ -0,0 +1,171 @@ +/* Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ + +(function(window) { + 'use strict'; + + var app = {}, + proto = document.querySelector('.proto'), + movers, + bodySize = document.body.getBoundingClientRect(), + ballSize = proto.getBoundingClientRect(), + maxHeight = Math.floor(bodySize.height - ballSize.height), + maxWidth = 97, // 100vw - width of square (3vw) + incrementor = 10, + distance = 3, + frame, + minimum = 10, + subtract = document.querySelector('.subtract'), + add = document.querySelector('.add'); + + app.optimize = false; + app.count = minimum; + app.enableApp = true; + + app.init = function () { + if (movers) { + bodySize = document.body.getBoundingClientRect(); + for (var i = 0; i < movers.length; i++) { + document.body.removeChild(movers[i]); + } + document.body.appendChild(proto); + ballSize = proto.getBoundingClientRect(); + document.body.removeChild(proto); + maxHeight = Math.floor(bodySize.height - ballSize.height); + } + for (var i = 0; i < app.count; i++) { + var m = proto.cloneNode(); + var top = Math.floor(Math.random() * (maxHeight)); + if (top === maxHeight) { + m.classList.add('up'); + } else { + m.classList.add('down'); + } + m.style.left = (i / (app.count / maxWidth)) + 'vw'; + m.style.top = top + 'px'; + document.body.appendChild(m); + } + movers = document.querySelectorAll('.mover'); + }; + + app.update = function (timestamp) { + for (var i = 0; i < app.count; i++) { + var m = movers[i]; + if (!app.optimize) { + var pos = m.classList.contains('down') ? + m.offsetTop + distance : m.offsetTop - distance; + if (pos < 0) pos = 0; + if (pos > maxHeight) pos = maxHeight; + m.style.top = pos + 'px'; + if (m.offsetTop === 0) { + m.classList.remove('up'); + m.classList.add('down'); + } + if (m.offsetTop === maxHeight) { + m.classList.remove('down'); + m.classList.add('up'); + } + } else { + var pos = parseInt(m.style.top.slice(0, m.style.top.indexOf('px'))); + m.classList.contains('down') ? pos += distance : pos -= distance; + if (pos < 0) pos = 0; + if (pos > maxHeight) pos = maxHeight; + m.style.top = pos + 'px'; + if (pos === 0) { + m.classList.remove('up'); + m.classList.add('down'); + } + if (pos === maxHeight) { + m.classList.remove('down'); + m.classList.add('up'); + } + } + } + frame = window.requestAnimationFrame(app.update); + } + + document.querySelector('.stop').addEventListener('click', function (e) { + if (app.enableApp) { + cancelAnimationFrame(frame); + e.target.textContent = 'Start'; + app.enableApp = false; + } else { + frame = window.requestAnimationFrame(app.update); + e.target.textContent = 'Stop'; + app.enableApp = true; + } + }); + + document.querySelector('.optimize').addEventListener('click', function (e) { + if (e.target.textContent === 'Optimize') { + app.optimize = true; + e.target.textContent = 'Un-Optimize'; + } else { + app.optimize = false; + e.target.textContent = 'Optimize'; + } + }); + + add.addEventListener('click', function (e) { + cancelAnimationFrame(frame); + app.count += incrementor; + subtract.disabled = false; + app.init(); + frame = requestAnimationFrame(app.update); + }); + + subtract.addEventListener('click', function () { + cancelAnimationFrame(frame); + app.count -= incrementor; + app.init(); + frame = requestAnimationFrame(app.update); + if (app.count === minimum) { + subtract.disabled = true; + } + }); + + function debounce(func, wait, immediate) { + var timeout; + return function() { + var context = this, args = arguments; + var later = function() { + timeout = null; + if (!immediate) func.apply(context, args); + }; + var callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + if (callNow) func.apply(context, args); + }; + }; + + var onResize = debounce(function () { + if (app.enableApp) { + cancelAnimationFrame(frame); + app.init(); + frame = requestAnimationFrame(app.update); + } + }, 500); + + window.addEventListener('resize', onResize); + + add.textContent = 'Add ' + incrementor; + subtract.textContent = 'Subtract ' + incrementor; + document.body.removeChild(proto); + proto.classList.remove('.proto'); + app.init(); + window.app = app; + frame = window.requestAnimationFrame(app.update); + +})(window); diff --git a/packages/replay/metrics/test-apps/jank/favicon-96x96.png b/packages/replay/metrics/test-apps/jank/favicon-96x96.png new file mode 100644 index 0000000000000000000000000000000000000000..7f01723cee0fa55387387b753b4a4990fb072daa GIT binary patch literal 8194 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 literal 0 HcmV?d00001 diff --git a/packages/replay/metrics/test-apps/jank/index.html b/packages/replay/metrics/test-apps/jank/index.html new file mode 100644 index 000000000000..5c8449143c1b --- /dev/null +++ b/packages/replay/metrics/test-apps/jank/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + Janky Animation + + + + + + + +
+ + + diff --git a/packages/replay/metrics/test-apps/jank/logo-1024px.png b/packages/replay/metrics/test-apps/jank/logo-1024px.png new file mode 100644 index 0000000000000000000000000000000000000000..84df3e22f6b09e248701d3fbfb11a1f94e11d05b GIT binary patch literal 305497 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 PZ9rawD@)OHv8s5YNAh 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

U%l}FlcCh5-x1s# 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

-oBOEqPck-}P8`y_daoUx7Pc zf&B@9J72@Shr6Hx+)}tpd;Pmt=zNfeE`1?IKGN8Q#&h`KM|t@3O>-b~SLJb(zu(X; zZ@m3>*Y?*x`_y#Rz4=OIf~W&o&j7?j^G&P3oB&9C-v&)}^U(_el3 zu9nmEXCFG4e)JWbRdLl^GW>aBVRvl`qS|>AIdS0B7-rSXQA>$@2SDow(N7dDlRd+q zYaqrGO&rX=yBJE~I-Lagm|hI{@GDPD-~DA5Y5%~(@!M^scSY>=-}_C2+ij)bbyWy|rKymqTWvE=EP!YR zt3I;Muj4dW`c?b(+_)|A){IsTn?#dn{hi?=+p;F%%14fp1$|>$PQmBkqIBhlQqFe5 z8v}qCMXV1dG&=G}R;w1b35z3FEpmJ)7CB1p#{uNPCMqtu|L$AwnEvK-Z<>DpZM#o` zTgoPv-u=Y%kG|@X=@AdOplN}^`RDmOQdONiW^JL`mY&`Ego`-WNe0+t zVW+C?Q8Dta18T+6!q961IyitgqNut*{Ic7pcYkpAlOTWXnrZsftMI#hBXqya;~88m z$p_dOEFRod8(+K+hLfe1#7k;o86gT=G&|Ltz zlDC8%Y$l}?btYm?gBV{N1X)7wuUaXV3zceA9s6i>z@?A>x*iFlbGAm@6}KK-Kw8e zq!#s0-f*b8xjq!wyjJd0Q1n_UK~$mcTL(neoRm@VQ^RIb2bRsw4&q0)-j8q;8OBYH zYa(8x{o?QaRXs+cedx}#AJ;Aa&-w9Nrw{6R?76iQNe(ObRT*hLu9{%Y1IUSh*|>Ey zQ14mT*h5kKsNwkeZ}w5CJB-30vaD$9R(OM}3YAW6F?+BbE4YSn9`I^$Dizzok5;rk zYX9V;?8v+RwXv4N8nW>Gor;K;zGk5DY~Z^#A;8mrQrlojKmNsV8Gq4H+*%JqA(rBC#B%B-Qa|)CylR;(t9#V^r0Vb$O~W zu^kweSG}`Yh7g1(JBR2kdfVzh`#D{*-m&pZ5P$y}M;j5sVFp;_9uwngOXLH5){pY= zjD%%uk%uyRmJu8IXg~7FGxG4sI}|-9vANTP#>)UNX*a@^-f4Q#Yj@A)DUUko+qmaD0P;;L$8_(NtN$w4$MoiZ$4fC_-0F}S z{M^9kz@GV8=4e>gOUBwkvvd_#RE8{`0O(EtP6FT}Eg{G47I|Rc}oE4gn4CyWwJE*_ZF zAean5%N;^`-Goou$nSPQh*}HVi*aMLIw?eKw%dW&{*I=lSOE`T1L?=e<-X zd^i|yS0PWypJa{-=+$y?j?V$5H;b^7Q3`Q~Fi6JL&W7nAM;_@QsSWa7~${Q74tE1K5C zOU|)qxq#9CHea+-ECY7m<7Ph+O9i~|;LjjIBtVvWMLzq$4NN(3CWR@*@4xMi>F0lc z_q!mT@UUrm`lrk9Li09JT!$r8YfJJ`AAYot9Gan}6p`&Bj}3HP>*Lc;i#+2AMUU>I zybGWuHvS}FeDoC8o%>l8`8pjSLiFRG4Hr7eGx-M=zSpCRyvL<~K56nwzT`rGogCdJ zxiPGU*4Mw^2JiU5q+b;oz#CZpX7+7gf%9E~{Rx2cy#@C1r(Hh`^ZbARflnFp!5$j@ z62BxL?Xrm?ho_&`%aO%z^r0gz@{w+f8|}^||AOCa&o}NE^t-;~B2PNH^7*I*{$3Z= zq=f#*IB06$xJ(UL%4TKBB#Olff07lGi9eGY*V5L(fvGlcC!GX1p~nHjr~cuCk39TH zM||`-a0nl|gwK$I3q^kM$!Yqwr$@3q{m=~u(+~bCc>XZknnKAc#QU~zGg7>GC%dLEBkq3#)$y@ONW14v9R4Nv_SiVuA=_HvoE|;&-~wV z+rjkR|LNB0#jmM5J)FD)D<=8+gaGkLfO@R7JD;e znBlNW0vAl0f#H(I{MxQzTHkdNQx@ehe+k850USq-64wE&sy-M662vW5u0J~c%dZ5G z5OGN+cMepJ=W6_FUv*vOz&>!95TfpS z{3z71BYM4(?QmlHSHFDQ^ufFo6Zn?xJO9$=pEs-gl@DRNWt5NnHYuzsS>t8Q^N^c^ zkq5()*7qapY4SowKdtX0%h-bVV*o9oOZDD_)!#)t9Hz;0%rEl8=oL5OSDK@H$up?% z8OJDJ;v+xTH2EfO@*d?QA6)Eu+HKxhocgcR{lU9t*jAkz5kdLYX=|G#G2-KYbw6bYClhhVVE3 z;xs+r^4eH>B_lMRuK*mb7 ze9#eqZHCZpn{#A{-O@n_$UyQ?$N0))HV_UIezNdi{kPp81$f}Sr|IiHr$T2vhl*gJ z&Nq2>x=nV0&clkJQCzY_Khgni?3y%2n0c43X_99z>y4tnN#(qm8!MCue;0nyZ4X@N zk2z%=kDt)s;y3c} zp`(1X3vNjs{pct1&?WsSUy8FNkN=<7V*u~gqZqrkZ`3aW@CNDmpk806m)@-ocUbe> zq>pg(4eO&{Y526#0)~TRPZTs5{xONjmH(I-C#(&a6wvkH`SKPjz;tvM0I!8#s?R9L zb>C7iaCb$U(`(N7ZSnCSdz(0a^k)atue{;3N~anqH&dLuxe_%QY*PnWajdC;T-Dp<%N`s1E;60eT@K&-H5X2gz54iHPaoiC-hT9! z>2>dFlcjdq&BJg4%0qEohR`#@$(DVuZ+0NTIPebR@c`i;0kkSF;?=RdimA1(5KGA4 z!UbCF)|i%?GmJH1ow20l3vY&)FI-ab#2Q~((nsej<8ouPR^_p9eLspi0SAtl4WYU? zS9@U;6=*)`*5pK~_>~oXNmLTXN(MEFE#K5Zq9oyt4eZ0>W)H9?`uJG+${h>%r8$;v z^FP6&cu>pPt{Mv*nDSAf6X&&WKRtcpzrV@v2e8pGR*!$^$?5x_b+LZllo2Y{y!~3- zO4H-^OE$$S?wdY_D8>EW=Lt;&f6>Y$|7sk!T3`IJ(1FkUPD|E1U6F}?m> zzV|?e+qbX(JiW_DpHXRqai|CyjhvYM5_#xgb_VNVby9}FGPdAG8r*0HF0JYk?(le$ zXK2O}er45u60jxE*Qm#g{c$YGe_g)lBJcAbeZr6V9hEouO?oirqb-eN%q6&KR??Kk~?TeG%Y8@3-4eXeQ>K?kjLtR^W_7p1acd-q*eYn=0_cPuheytiM%f(%*FL zVcE##3u{RpegB0(Kl0HoxadQJ3m@DlANk-TkKHJbexxIA#qxG^) z=ri^AzQ>8_o1dn)A+>MRlR7BAca=;`(brl%AqL>7X0JtB1?0J z#o>m+$eL(H=?Q^#Wl2ol2jIH_95~3L&Z(0x)eYR)XkV+V|GWO|9}~-e{olB=p{_#1 z*RAHO34kX_UBb*SY6uyKs3%OT$(nzOq1o_0eUDiMnQKBxF*DIta)*;Nl6g^lefy;R z*a3n6htE7YecmH=7T}oeT^~3-ebe)Ao&H4cs_>*r0UaOj8V7SQpL`NaT$R zrKhG``$?C<9Ux8$JUjK?X=hr=-utT8anCm3pQwCzyEr%C$Z1vcxaMkhBsC_y7OHsq zWr;lFb}s&-ku@<2qLU7Ijs?BqZPzf)T{sVu_-*10#(kW*s5-fTH~MbPfq>+Vb7yON zGliAjHu%HN_VgI3gamS2lpaa>BMiP&IHXwkJkJ~-yc&9n+xmb=>xpPdhdJ#Xfpo+^jR#W|6e>ojLzV8+JoS$9&{QZ-YG z*a#J07`TsuW&%i3%dCxJ)|VgQ_^;*rf8kb{1Zz(lS9~`8ttYKu-fW$V@S()%m&ij` zo{sJjf<^9XxC`*$LW2*D8TvHx8{3%Q$d7pV{*ktO=%uEoj&bCjyyHK4kMchMV0${| zn{6=4W3x#g+_|D-p5c?{x;%mj?J-B$^|;hIG&=Y{de5Yn>qfF2-Syp9+j-ri_7&K^ z0>}8=?#j3T06+jqL_t&-+TQsdxv#*c3heqJ$mGBOZTQ~*(g*pHJYNQ(!7cHL67n=4A&io}mtLCX z$+*mJ)*I?MvB1F%2lFiB7jE)m+5)a9KA1?`llK& z_U5jrWa9mWOh-PE+YYLhbP8#$ZLBi_`06A}6S=PRsXJa*!(?0~OZ~wZTjUVKgwVM< zk%}MKe5VBdq-O{<08s@j9%Wk)&k*GicNS{Z>TS!x9$Cg97jB2*`%H?&@#(AoVxUJkLV=8^KP2na&0{#gxRj`nNPfM`u4wEuYgZl_;4Dm7|={W zRLyFf>?^g^*++dRfFPg_`O=4_y)>a~>b_Rxz8O>gXNg?jXDXi zOWQvNuuHY}*t1cA_H}(P8_5TKzzlTNhpg1EVI7xkqa*T>YV`a zed}?fyyutt&Zkq3e5Z03{j)z;KOv<$?b2?#^U_S~|5gNCzKU*eZ0Ztm;e0E^)DT%YX4- zZkqn+UB^EO@b#a6(e$-XI#vGVlc2UhdBsbp$K!=81Yjsyn1L4O8 z;thc^P{52)eg6->!GM5KV*lnPw@-YA?9%v*`i3VrAI;E1>)eD-dhs85_)#BuC@wp< z5qpRXAxm)KXW*8dF|U|Ko;<@xKg#`U08M%MZ~|c6IO1o_Km2GDdF++b4MWszrU}3v@G120W!zW}~I<9$% zYfjQ)cyc3qZ#zxS>ME=_>>Z@SJ@)Do)3<-|@o)R*%KyLorQ7u8cD-av@0Ym$rR8^y zqIfmYBK3;Tz!;`dEieqW8tPAKQ3Svawbjm7`K!+1(%-;x|^)Q<*?d_-CRn~6bI znc_3C!MPus_^}5j_exh2Um9bxf5v7>KQ<$bVe;5Gc?|}c`uJ-x==XW!6uzwO!aVzr zBQ}LG836dm<=+imTfnkk0m&77WNpwARv#IYd75#ZJsV=QcTC_~eDVWBfx!8s0W3bX zt%2DcN;UJ<^_tCprS_#ub$k0Qx1E^2|HXIiP5^!d<#7+wTR0p@+gx$|w}k+iR*Z;o z*8Xat}yO@7%f*&kNgRWG&-86>L4<+)@QIBnJAKXaCyhi=V2S4V|34r8xiEakD zu=}`Mm&cfI;*5EZwxd3Fp~-LLBM%+(M@|bZ?%b@`#Y~>bV9Yo2&{03y2N$|jfAoFL zQ}1101o-F&ZvN$r`wrkjjHZ_ZdHNDJ>>A0;5J751Yo{Im+<2U{kFK? zj(%FcD54J?zqg~{&ANj~*7k&8XLq~b=p-Xz~U-+tNx?aBwuxtCa zFS@X<*vat5TTV=R8GqdkAf68=ANc1R*5All<%s=|T;T9J%J)QpqkS#D>d%pOzP5$) z#06IoOV;4EgIO+HE?>771Iocp4HuhsRxnxOJbZ@pxAj~tS0Hz3*Ij=w{q%3ICIk4y zAEl#lfZtDqSBW-2!D>zInjuq0?=N;Dxi*q{YA#b2*Hn7yNuRp_Jk>`N8<{9d8vnwWycg+1kE=QI)!~OWtG@j-U-QcW9UWv%4neL? zYEWlsWRsYy`db0I<><5>o8c;J>qsCc2qb87e40VzzWpmWl*KWvTg7(%BxpIfFij>> z_O(6Gx+a0Olt7m}in42Ensa2VJjYLSDDl4YL&m-fpj7rHkKWd``P=BF_immoAUjO1 z(&YbgJ)&erx(|Hp#Pq-Z=uOi*kNHjj_VMrgn-@=)HJ{$*T2+0B$loPv#?AIpM;tHi z1Y3VIl2~52%1r{xY8zm`YLb;wqGkvI&b1YCGRB~yMCO=j`A6svq_V0VubJWj0_ri@Zp1rJT&!7-9wl3!T;gA zg4m8;sPFX0JZL-Y9<{H)*{A@=*!#Axzq467h_>qqIkq3fTKneaP`(c5OTjI{zAZhtMs4XBm4n3+9`{aw^0x|- z1BI2O_T`r5Ebe)(K0foBS+#ZN$6k3b-O?tr80CZBlN?sYuRJwfd0DKJ$##^t{r_)2dE4|6{aoAo zKBmtH{epx&DX&X)5`g*RlGa$OCQNj(&p2>`er{XX`xbwVUr?{s+Dq&B zUgUP&^?IZGjnnikFSz;GzYcKaWfx5U z{Hxn@D&)p~<$TmvV(!Cx00OkJkQXNc+%yn+<``7lhCY$t0yhZ&!0=+T=fAvrc%4vNw6A!APM%zL7$|q%)}a|^Q1G--9(-wQ-JoXZAI0}}^ciD~ zbJT3dC(rHBhsWcW0X9QUe9|`MpL{pr%G=>}pX*r1)Om@<(`Xm@`J%};@i+0og>D+p zSm)Snq94`;khi~o6Y8-32@gBW@40;i&SeF17;!FR?qlsM;0iqc>fH+Q`gh;CFYa~G z`0^Or_yv(Km{A^nNgux>F7lBEH`3An$j8sNcEN{^b|XK=8~OP8r9YgeKYd@g?ddZ= zcO zQ0-GCmKV+lwEB5cB|VWUM)@;u`vz4X2jpPWvS@X|9`6P;D{*#MV`R5RZ{o4Q^S8l7 zRnivFn?-U=;}4lxBx0OH^Hn$_t!q{gHLO5)&Ayz4b3tdlL^ZDRWhJntG-1_bbDnV> zdtrdYp8Y5%Q1FEdWSkJ-Z#ny4nV@YUs@u4hhP4gjI%I6fPWXhBCum}+(+#>i=~~@k zf|l*WOD4bV$8VlKrhV`@?a7ZiHGTb4+HF1UGs=E64z`20C4C9m#W+x-?`>~keT%#gv+p!}!y7_?7r*ZG^sW!`X}o>o>Lx!6n2`3}r?AFV z(uJl!!WeoQ{dN43Jfn}jhZ$xI+ULc%!qe~h(N%EBKc?@vb(E|hVtYki&Rr$rvKo{wzT#{jO;U9kJMufX}L!2Sfl z`KtUr?(~Q&cPj$7|F3KLAYP~2B;(6sY~vT+_=OUDaHBr*k&d|VBQAX6EYaYW{KRev zw`3Q5?3ZY8@k1$}`_d)!vi$BBpP0Vtvrp-uR|lQ?CiPta^Cwfb?mR83qb&gw-$zs& z{qrsC1t;qBe6*Gv+RylwO3oLo3`^l>Y_ac~Tpj2q0{uupFIPVqM(h;AbTqh8AAY1< zef%qr>l|#^VcD%{|Ew>#^fA2ruI=xjVoScyotUE*| zz4ze6e5VlOy8Irz6F`vximx2BZWBsmOx~4ZH6>lOs;XEaTVnt*7RKPfFz9Nmt4TGu z@@SKS0iBy)4(PZjwbXx+<{P99T0US?T=la(J@FM39rbs$F8;qVbIHj%nkzs9n=f0AHb_#7wwoYFL3 zk9?C|aJ$5R%{!*)ry6hn7n2DYTI$-SZpBfQLkXa#4qCM~*Nr+!i9HhqKN6r>Ci{uE z2|8w{rYqU(NA}#F|2=>G)UlrXze7R((a+vK{oy+loA}xO3P27d@!soo62OlGR35x_ zn_IeRJhM-$Tdy%_`w!!4&9y2`Nu;7?G-9xSF+__5ZZ549$Iu&4q;(O|W;1-s(AXBx zSYhz(c$OPsLu@Mn2)5*26H-)cF7iWXEHd+GOH#R_#~SIJaa>C(qvS@vwX#Kvta4|v z%A^%Zkfw58d?MLou2l_Ld9V#j)x3&Pvp)*aoDtiPmq=g{<5!1+7@v4*HWaUF!2TJb zC=DOZ)gB}`9GkmEGHC_QBWKrsw8pl|QJ+t?oTk^l<6!#kpT0#CyZFOuyXN-+eCb4{c!b9rje1S)xL8+`uqAD*7x z{=V+Rz`>4Q@rwXo{h5^#F`Y)@ke&K0=$un?H}XqSS{XcsxQz^&7Fl?(St46U$2^iR z*yy0WeAJ;In}6x13GdY&1Ara#3?Dk?9Qo*fi68R}t{+R^k3MwCKKipk*VQj^l56q` z9||sgXj<^$d)mRy*dD2W>K+<7es;YB;DPs^rhDiGqx-h6!1<`a{sh4JsQNzU^r)+L zE5gTh*8E)`I&9N_@b;Q~F!!=`{r3RBN>-Fg3Cv?<@4~@SieDs^} zu^;JZx8!eI`SX5l^0TACZ$p_teAk!MEwR1=mW|ujvu#)o_H)3Gn&|-l>Zr~=I31XE z2e$BS;lP<1oY9z(1O!bpR)SV2KMv5(jDu?wc$P{;g61#so+f1PhBrj>Htk*8kG_J* z3EnkE95Ybm?LN+6i)gK0fiww&)ryI{?QK>QuGttv*%++8J>f%kTCk)m06n z)Bp8l$A9ks`(Awe^vXABGQzm*ueLvHPwq%jvHduJCUE3I^NRuR#iXWE2GyUa@VN2m zode&reqHQ5M&e2BM*b=;NFwt#iBC|p=2L9>tDimec5K?!WWQ|er)^&fld)remF^O`{F2r9 zKYG&%J?CFB=2*h9MO{<%N%$52)I(JO@AkMgq zr2@;csR0>bJ`f^Z^}wD7fRa@r$tM=`VfZ zjPk_p?U98K9qmSW_|OrbIHNp#?1Br8e8g>%U#Dkf|Ksn(%l7mM54&Lc!oP5V4>o

%{xLDf7Y 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

V!2CBh>Xbwx#vGzR&>pggy5>m; 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
lI28%|hF9ycar^H8e{ZTW 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�wjyWVZWzhueiS3}@y9CgxvJ#sdQ6b>TE; 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 K=3TLR^8WzivOVAc literal 0 HcmV?d00001 diff --git a/packages/replay/metrics/test-apps/jank/styles.css b/packages/replay/metrics/test-apps/jank/styles.css new file mode 100644 index 000000000000..1f340f179d0f --- /dev/null +++ b/packages/replay/metrics/test-apps/jank/styles.css @@ -0,0 +1,59 @@ +/* Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing permissions + * and limitations under the License. */ + + * { + margin: 0; + padding: 0; +} + +body { + height: 100vh; + width: 100vw; +} + +.controls { + position: fixed; + top: 2vw; + left: 2vw; + z-index: 1; +} + +.controls button { + display: block; + font-size: 1em; + padding: 1em; + margin: 1em; + background-color: beige; + color: black; +} + +.subtract:disabled { + opacity: 0.2; +} + +.mover { + height: 3vw; + position: absolute; + z-index: 0; +} + +.border { + border: 1px solid black; +} + +@media (max-width: 600px) { + .controls button { + min-width: 20vw; + } +} From c019cc66bf053f89192b65a3db04e9723173b061 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Thu, 29 Dec 2022 21:21:40 +0100 Subject: [PATCH 074/113] fix JSON serialization --- packages/replay/metrics/src/perf/cpu.ts | 28 +++++++------------ packages/replay/metrics/src/perf/memory.ts | 12 ++++---- packages/replay/metrics/src/perf/sampler.ts | 20 +++++++++++-- packages/replay/metrics/src/results/result.ts | 14 ++++++++-- packages/replay/metrics/src/vitals/index.ts | 3 +- 5 files changed, 46 insertions(+), 31 deletions(-) diff --git a/packages/replay/metrics/src/perf/cpu.ts b/packages/replay/metrics/src/perf/cpu.ts index 760fc0990aac..152d488a592b 100644 --- a/packages/replay/metrics/src/perf/cpu.ts +++ b/packages/replay/metrics/src/perf/cpu.ts @@ -1,24 +1,16 @@ import * as puppeteer from 'puppeteer'; -import { PerfMetricsSampler } from './sampler'; +import { PerfMetricsSampler, TimeBasedMap } from './sampler.js'; -export { CpuUsageSampler, CpuUsage, CpuSnapshot } - -class CpuSnapshot { - constructor(public timestamp: number, public usage: number) { } - - public static fromJSON(data: Partial): CpuSnapshot { - return new CpuSnapshot(data.timestamp || NaN, data.usage || NaN); - } -} +export { CpuUsageSampler, CpuUsage } class CpuUsage { - constructor(public snapshots: CpuSnapshot[], public average: number) { }; + constructor(public snapshots: TimeBasedMap, public average: number) { }; public static fromJSON(data: Partial): CpuUsage { return new CpuUsage( - (data.snapshots || []).map(CpuSnapshot.fromJSON), - data.average || NaN, + TimeBasedMap.fromJSON(data.snapshots || []), + data.average as number, ); } } @@ -28,8 +20,8 @@ class MetricsDataPoint { } class CpuUsageSampler { - public snapshots: CpuSnapshot[] = []; - public average: number = 0; + private _snapshots = new TimeBasedMap(); + private _average: number = 0; private _initial?: MetricsDataPoint = undefined; private _startTime!: number; private _lastTimestamp!: number; @@ -40,7 +32,7 @@ class CpuUsageSampler { } public getData(): CpuUsage { - return new CpuUsage(this.snapshots, this.average); + return new CpuUsage(this._snapshots, this._average); } private async _collect(metrics: puppeteer.Metrics): Promise { @@ -52,8 +44,8 @@ class CpuUsageSampler { const frameDuration = data.timestamp - this._lastTimestamp; let usage = frameDuration == 0 ? 0 : (data.activeTime - this._cumulativeActiveTime) / frameDuration; - this.snapshots.push(new CpuSnapshot(data.timestamp, usage)); - this.average = data.activeTime / (data.timestamp - this._startTime); + this._snapshots.set(data.timestamp, usage); + this._average = data.activeTime / (data.timestamp - this._startTime); } this._lastTimestamp = data.timestamp; this._cumulativeActiveTime = data.activeTime; diff --git a/packages/replay/metrics/src/perf/memory.ts b/packages/replay/metrics/src/perf/memory.ts index 36baf8ae1414..6c6e907c5e75 100644 --- a/packages/replay/metrics/src/perf/memory.ts +++ b/packages/replay/metrics/src/perf/memory.ts @@ -1,29 +1,29 @@ import * as puppeteer from 'puppeteer'; -import { PerfMetricsSampler } from './sampler'; +import { PerfMetricsSampler, TimeBasedMap } from './sampler.js'; export { JsHeapUsageSampler, JsHeapUsage } class JsHeapUsage { - public constructor(public snapshots: number[]) { } + public constructor(public snapshots: TimeBasedMap) { } public static fromJSON(data: Partial): JsHeapUsage { - return new JsHeapUsage(data.snapshots || []); + return new JsHeapUsage(TimeBasedMap.fromJSON(data.snapshots || [])); } } class JsHeapUsageSampler { - public snapshots: number[] = []; + private _snapshots = new TimeBasedMap(); public constructor(sampler: PerfMetricsSampler) { sampler.subscribe(this._collect.bind(this)); } public getData(): JsHeapUsage { - return new JsHeapUsage(this.snapshots); + return new JsHeapUsage(this._snapshots); } private async _collect(metrics: puppeteer.Metrics): Promise { - this.snapshots.push(metrics.JSHeapUsedSize!); + this._snapshots.set(metrics.Timestamp!, metrics.JSHeapUsedSize!); } } diff --git a/packages/replay/metrics/src/perf/sampler.ts b/packages/replay/metrics/src/perf/sampler.ts index 88d2d886b8b2..80ce931045b2 100644 --- a/packages/replay/metrics/src/perf/sampler.ts +++ b/packages/replay/metrics/src/perf/sampler.ts @@ -1,10 +1,24 @@ import * as puppeteer from 'puppeteer'; -export { PerfMetricsSampler } +export type PerfMetricsConsumer = (metrics: puppeteer.Metrics) => Promise; +export type TimestampSeconds = number; -type PerfMetricsConsumer = (metrics: puppeteer.Metrics) => Promise; +export class TimeBasedMap extends Map { + public toJSON(): any { + return Object.fromEntries(this.entries()); + } + + public static fromJSON(entries: Object): TimeBasedMap { + const result = new TimeBasedMap(); + for (const key in entries) { + const value = entries[key as keyof Object]; + result.set(parseFloat(key), value as T); + } + return result; + } +} -class PerfMetricsSampler { +export class PerfMetricsSampler { private _consumers: PerfMetricsConsumer[] = []; private _timer!: NodeJS.Timer; diff --git a/packages/replay/metrics/src/results/result.ts b/packages/replay/metrics/src/results/result.ts index 6643f48c6731..eac564dc882b 100644 --- a/packages/replay/metrics/src/results/result.ts +++ b/packages/replay/metrics/src/results/result.ts @@ -15,16 +15,26 @@ export class Result { if (!fs.existsSync(dir)) { fs.mkdirSync(dir); } - const json = JSON.stringify(this); + const json = this.serialize(); fs.writeFileSync(filePath, json); } + serialize(): string { + return JSON.stringify(this, (_: any, value: any): any => { + if (typeof value != 'undefined' && typeof value.toJSON == 'function') { + return value.toJSON(); + } else { + return value; + } + }, 2); + } + public static readFromFile(filePath: string): Result { const json = fs.readFileSync(filePath, { encoding: 'utf-8' }); const data = JSON.parse(json); return new Result( data.name || '', - data.cpuThrottling || NaN, + data.cpuThrottling as number, data.networkConditions || '', (data.aResults || []).map(Metrics.fromJSON), (data.bResults || []).map(Metrics.fromJSON), diff --git a/packages/replay/metrics/src/vitals/index.ts b/packages/replay/metrics/src/vitals/index.ts index 892a248266ad..68e08ba8877a 100644 --- a/packages/replay/metrics/src/vitals/index.ts +++ b/packages/replay/metrics/src/vitals/index.ts @@ -6,12 +6,11 @@ import { LCP } from './lcp.js'; export { WebVitals, WebVitalsCollector }; - class WebVitals { constructor(public lcp: number, public cls: number, public fid: number) { } public static fromJSON(data: Partial): WebVitals { - return new WebVitals(data.lcp || NaN, data.cls || NaN, data.fid || NaN); + return new WebVitals(data.lcp as number, data.cls as number, data.fid as number); } } From 5658c6ccbd9c1a45199a3e94c3ad24bd62b92714 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Thu, 29 Dec 2022 21:23:02 +0100 Subject: [PATCH 075/113] yank-test scenario --- packages/replay/metrics/configs/dev/collect.ts | 6 +++--- packages/replay/metrics/src/scenarios.ts | 13 +++++++++++++ packages/replay/metrics/test-apps/jank/app.js | 4 ++-- packages/replay/metrics/test-apps/jank/index.html | 2 +- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/packages/replay/metrics/configs/dev/collect.ts b/packages/replay/metrics/configs/dev/collect.ts index b9633e7b174a..2bdd4592ed61 100644 --- a/packages/replay/metrics/configs/dev/collect.ts +++ b/packages/replay/metrics/configs/dev/collect.ts @@ -1,12 +1,12 @@ import { Metrics, MetricsCollector } from '../../src/collector.js'; -import { LoadPageScenario } from '../../src/scenarios.js'; +import { JankTestScenario, LoadPageScenario } from '../../src/scenarios.js'; import { latestResultFile } from './env.js'; const collector = new MetricsCollector(); const result = await collector.execute({ name: 'dummy', - a: new LoadPageScenario('https://developers.google.com/web/'), - b: new LoadPageScenario('https://developers.google.com/'), + a: new JankTestScenario(), + b: new LoadPageScenario('https://developers.google.com/web/'), runs: 1, tries: 1, async test(_aResults: Metrics[], _bResults: Metrics[]) { diff --git a/packages/replay/metrics/src/scenarios.ts b/packages/replay/metrics/src/scenarios.ts index 9ef7c4dced97..289a28a9057d 100644 --- a/packages/replay/metrics/src/scenarios.ts +++ b/packages/replay/metrics/src/scenarios.ts @@ -1,5 +1,8 @@ +import path from 'path'; import * as puppeteer from 'puppeteer'; +import * as fs from 'fs'; import { Metrics } from './collector'; +import assert from 'assert'; // A testing scenario we want to collect metrics for. export interface Scenario { @@ -27,3 +30,13 @@ export class LoadPageScenario implements Scenario { await page.goto(this.url, { waitUntil: 'load', timeout: 60000 }); } } + +// Loads test-apps/jank/ as a page source & waits for a short time before quitting. +export class JankTestScenario implements Scenario { + public async run(_: puppeteer.Browser, page: puppeteer.Page): Promise { + const url = path.resolve('./test-apps/jank/index.html'); + assert(fs.existsSync(url)); + await page.goto(url, { waitUntil: 'load', timeout: 60000 }); + await new Promise(resolve => setTimeout(resolve, 5000)); + } +} diff --git a/packages/replay/metrics/test-apps/jank/app.js b/packages/replay/metrics/test-apps/jank/app.js index ab961a366315..fd02c57cfa0e 100644 --- a/packages/replay/metrics/test-apps/jank/app.js +++ b/packages/replay/metrics/test-apps/jank/app.js @@ -25,11 +25,11 @@ incrementor = 10, distance = 3, frame, - minimum = 10, + minimum = 100, subtract = document.querySelector('.subtract'), add = document.querySelector('.add'); - app.optimize = false; + app.optimize = true; app.count = minimum; app.enableApp = true; diff --git a/packages/replay/metrics/test-apps/jank/index.html b/packages/replay/metrics/test-apps/jank/index.html index 5c8449143c1b..7cc6426010de 100644 --- a/packages/replay/metrics/test-apps/jank/index.html +++ b/packages/replay/metrics/test-apps/jank/index.html @@ -33,7 +33,7 @@ - + From 64f446fed5a7b8ecc3b59eeb20669fc336de1501 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Fri, 30 Dec 2022 13:26:56 +0100 Subject: [PATCH 076/113] collect jank test-app metrics with sentry included --- .../replay/metrics/configs/dev/collect.ts | 6 +- packages/replay/metrics/package.json | 1 + packages/replay/metrics/src/scenarios.ts | 4 +- packages/replay/metrics/test-apps/jank/app.js | 4 +- .../replay/metrics/test-apps/jank/index.html | 2 +- .../metrics/test-apps/jank/with-sentry.html | 56 +++++++++++++++++++ 6 files changed, 66 insertions(+), 7 deletions(-) create mode 100644 packages/replay/metrics/test-apps/jank/with-sentry.html diff --git a/packages/replay/metrics/configs/dev/collect.ts b/packages/replay/metrics/configs/dev/collect.ts index 2bdd4592ed61..bec5af33ec8f 100644 --- a/packages/replay/metrics/configs/dev/collect.ts +++ b/packages/replay/metrics/configs/dev/collect.ts @@ -1,12 +1,12 @@ import { Metrics, MetricsCollector } from '../../src/collector.js'; -import { JankTestScenario, LoadPageScenario } from '../../src/scenarios.js'; +import { JankTestScenario } from '../../src/scenarios.js'; import { latestResultFile } from './env.js'; const collector = new MetricsCollector(); const result = await collector.execute({ name: 'dummy', - a: new JankTestScenario(), - b: new LoadPageScenario('https://developers.google.com/web/'), + a: new JankTestScenario(false), + b: new JankTestScenario(true), runs: 1, tries: 1, async test(_aResults: Metrics[], _bResults: Metrics[]) { diff --git a/packages/replay/metrics/package.json b/packages/replay/metrics/package.json index bc216b8ee00f..65e57f10da7d 100644 --- a/packages/replay/metrics/package.json +++ b/packages/replay/metrics/package.json @@ -7,6 +7,7 @@ "type": "module", "scripts": { "build": "tsc", + "deps": "yarn --cwd ../ build:bundle && yarn --cwd ../../tracing/ build:bundle", "dev:collect": "ts-node-esm ./configs/dev/collect.ts", "dev:process": "ts-node-esm ./configs/dev/process.ts" }, diff --git a/packages/replay/metrics/src/scenarios.ts b/packages/replay/metrics/src/scenarios.ts index 289a28a9057d..a87c5acebef3 100644 --- a/packages/replay/metrics/src/scenarios.ts +++ b/packages/replay/metrics/src/scenarios.ts @@ -33,8 +33,10 @@ export class LoadPageScenario implements Scenario { // Loads test-apps/jank/ as a page source & waits for a short time before quitting. export class JankTestScenario implements Scenario { + public constructor(private withSentry: boolean) { } + public async run(_: puppeteer.Browser, page: puppeteer.Page): Promise { - const url = path.resolve('./test-apps/jank/index.html'); + const url = path.resolve('./test-apps/jank/' + (this.withSentry ? 'with-sentry' : 'index') + '.html'); assert(fs.existsSync(url)); await page.goto(url, { waitUntil: 'load', timeout: 60000 }); await new Promise(resolve => setTimeout(resolve, 5000)); diff --git a/packages/replay/metrics/test-apps/jank/app.js b/packages/replay/metrics/test-apps/jank/app.js index fd02c57cfa0e..23660eecfd9b 100644 --- a/packages/replay/metrics/test-apps/jank/app.js +++ b/packages/replay/metrics/test-apps/jank/app.js @@ -12,7 +12,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function(window) { +document.addEventListener("DOMContentLoaded", function() { 'use strict'; var app = {}, @@ -168,4 +168,4 @@ window.app = app; frame = window.requestAnimationFrame(app.update); -})(window); +}); diff --git a/packages/replay/metrics/test-apps/jank/index.html b/packages/replay/metrics/test-apps/jank/index.html index 7cc6426010de..bcdb2ee1acb9 100644 --- a/packages/replay/metrics/test-apps/jank/index.html +++ b/packages/replay/metrics/test-apps/jank/index.html @@ -24,7 +24,7 @@ Janky Animation - + diff --git a/packages/replay/metrics/test-apps/jank/with-sentry.html b/packages/replay/metrics/test-apps/jank/with-sentry.html new file mode 100644 index 000000000000..7331eacfdd7f --- /dev/null +++ b/packages/replay/metrics/test-apps/jank/with-sentry.html @@ -0,0 +1,56 @@ + + + + + + + + + + Janky Animation + + + + + + + + + + + +

+ + + + + + + +
+ + + From 601d67af383208bfeabead72ea461ea53bbbd4df Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Fri, 30 Dec 2022 17:38:58 +0100 Subject: [PATCH 077/113] feat: analyze collected metrics --- .../replay/metrics/configs/dev/process.ts | 18 ++- packages/replay/metrics/package.json | 2 + .../replay/metrics/src/results/analyzer.ts | 104 ++++++++++++++++++ .../metrics/src/results/metrics-stats.ts | 41 +++++++ .../replay/metrics/src/results/results-set.ts | 30 ++++- packages/replay/metrics/src/{ => util}/git.ts | 4 +- packages/replay/metrics/yarn.lock | 10 ++ 7 files changed, 200 insertions(+), 9 deletions(-) create mode 100644 packages/replay/metrics/src/results/analyzer.ts create mode 100644 packages/replay/metrics/src/results/metrics-stats.ts rename packages/replay/metrics/src/{ => util}/git.ts (81%) diff --git a/packages/replay/metrics/configs/dev/process.ts b/packages/replay/metrics/configs/dev/process.ts index a5d8a684bedb..5a4aaca0dbf6 100644 --- a/packages/replay/metrics/configs/dev/process.ts +++ b/packages/replay/metrics/configs/dev/process.ts @@ -1,10 +1,22 @@ +import { AnalyzerItemMetric, ResultsAnalyzer } from '../../src/results/analyzer.js'; import { Result } from '../../src/results/result.js'; import { ResultsSet } from '../../src/results/results-set.js'; import { latestResultFile, outDir } from './env.js'; const resultsSet = new ResultsSet(outDir); - const latestResult = Result.readFromFile(latestResultFile); -console.log(latestResult); -await resultsSet.add(latestResultFile); +const analysis = ResultsAnalyzer.analyze(latestResult, resultsSet); + +const table: { [k: string]: any } = {}; +for (const item of analysis) { + const printable: { [k: string]: any } = {}; + printable.value = item.value.asString(); + if (item.other != undefined) { + printable.baseline = item.other.asString(); + } + table[AnalyzerItemMetric[item.metric]] = printable; +} +console.table(table); + +await resultsSet.add(latestResultFile, true); diff --git a/packages/replay/metrics/package.json b/packages/replay/metrics/package.json index 65e57f10da7d..15c22c39ed70 100644 --- a/packages/replay/metrics/package.json +++ b/packages/replay/metrics/package.json @@ -13,8 +13,10 @@ }, "dependencies": { "@types/node": "^18.11.17", + "filesize": "^10.0.6", "puppeteer": "^19.4.1", "simple-git": "^3.15.1", + "simple-statistics": "^7.8.0", "typescript": "^4.9.4" }, "devDependencies": { diff --git a/packages/replay/metrics/src/results/analyzer.ts b/packages/replay/metrics/src/results/analyzer.ts new file mode 100644 index 000000000000..d1147627fea5 --- /dev/null +++ b/packages/replay/metrics/src/results/analyzer.ts @@ -0,0 +1,104 @@ +import { GitHash } from '../util/git.js'; +import { Result } from './result.js'; +import { ResultsSet } from './results-set.js'; +import { MetricsStats } from './metrics-stats.js'; +import { filesize } from "filesize"; + +// Compares latest result to previous/baseline results and produces the needed +// info. +export class ResultsAnalyzer { + public static analyze(currentResult: Result, baselineResults: ResultsSet): AnalyzerItem[] { + const items = new ResultsAnalyzer(currentResult).collect(); + + const baseline = baselineResults.find( + (other) => other.cpuThrottling == currentResult.cpuThrottling && + other.name == currentResult.name && + other.networkConditions == currentResult.networkConditions); + + if (baseline != undefined) { + const baseItems = new ResultsAnalyzer(baseline[1]).collect(); + // update items with baseline results + for (const base of baseItems) { + for (const item of items) { + if (item.metric == base.metric) { + item.other = base.value; + item.otherHash = baseline[0]; + } + } + } + } + + return items; + } + + private constructor(private result: Result) { } + + private collect(): AnalyzerItem[] { + const items = new Array(); + + const aStats = new MetricsStats(this.result.aResults); + const bStats = new MetricsStats(this.result.bResults); + + const pushIfDefined = function (metric: AnalyzerItemMetric, unit: AnalyzerItemUnit, valueA?: number, valueB?: number) { + if (valueA == undefined || valueB == undefined) return; + + items.push({ + metric: metric, + value: { + unit: unit, + asDiff: () => valueB - valueA, + asRatio: () => valueB / valueA, + asString: () => { + const diff = valueB - valueA; + const prefix = diff >= 0 ? '+' : ''; + + switch (unit) { + case AnalyzerItemUnit.bytes: + return prefix + filesize(diff); + case AnalyzerItemUnit.ratio: + return prefix + (diff * 100).toFixed(2) + ' %'; + default: + return prefix + diff.toFixed(2) + ' ' + AnalyzerItemUnit[unit]; + } + } + } + }) + } + + pushIfDefined(AnalyzerItemMetric.lcp, AnalyzerItemUnit.ms, aStats.lcp, bStats.lcp); + pushIfDefined(AnalyzerItemMetric.cls, AnalyzerItemUnit.ms, aStats.cls, bStats.cls); + pushIfDefined(AnalyzerItemMetric.cpu, AnalyzerItemUnit.ratio, aStats.cpu, bStats.cpu); + pushIfDefined(AnalyzerItemMetric.memoryAvg, AnalyzerItemUnit.bytes, aStats.memoryAvg, bStats.memoryAvg); + pushIfDefined(AnalyzerItemMetric.memoryMax, AnalyzerItemUnit.bytes, aStats.memoryMax, bStats.memoryMax); + + return items.filter((item) => item.value != undefined); + } +} + +export enum AnalyzerItemUnit { + ms, + ratio, // 1.0 == 100 % + bytes, +} + +export interface AnalyzerItemValue { + unit: AnalyzerItemUnit; + asString(): string; + asDiff(): number; + asRatio(): number; // 1.0 == 100 % +} + +export enum AnalyzerItemMetric { + lcp, + cls, + cpu, + memoryAvg, + memoryMax, +} + +export interface AnalyzerItem { + metric: AnalyzerItemMetric; + value: AnalyzerItemValue; + other?: AnalyzerItemValue; + otherHash?: GitHash; +} diff --git a/packages/replay/metrics/src/results/metrics-stats.ts b/packages/replay/metrics/src/results/metrics-stats.ts new file mode 100644 index 000000000000..ec4071caab88 --- /dev/null +++ b/packages/replay/metrics/src/results/metrics-stats.ts @@ -0,0 +1,41 @@ +import { Metrics } from '../collector'; +import * as ss from 'simple-statistics' + +export type NumberProvider = (metrics: Metrics) => number; + +export class MetricsStats { + constructor(private items: Metrics[]) { } + + // See https://en.wikipedia.org/wiki/Interquartile_range#Outliers for details + public filterOutliers(dataProvider: NumberProvider): number[] { + let numbers = this.items.map(dataProvider); + // TODO implement, see https://github.com/getsentry/action-app-sdk-overhead-metrics/blob/9ce7d562ff79b317688d22bd5c0bb725cbdfdb81/src/test/kotlin/StartupTimeTest.kt#L27-L37 + return numbers; + } + + public filteredMean(dataProvider: NumberProvider): number | undefined { + const numbers = this.filterOutliers(dataProvider); + return numbers.length > 0 ? ss.mean(numbers) : undefined; + } + + public get lcp(): number | undefined { + return this.filteredMean((metrics) => metrics.vitals.lcp); + } + + public get cls(): number | undefined { + return this.filteredMean((metrics) => metrics.vitals.cls); + } + + public get cpu(): number | undefined { + return this.filteredMean((metrics) => metrics.cpu.average); + } + + public get memoryAvg(): number | undefined { + return this.filteredMean((metrics) => ss.mean(Array.from(metrics.memory.snapshots.values()))); + } + + public get memoryMax(): number | undefined { + const numbers = this.filterOutliers((metrics) => ss.max(Array.from(metrics.memory.snapshots.values()))); + return numbers.length > 0 ? ss.max(numbers) : undefined; + } +} diff --git a/packages/replay/metrics/src/results/results-set.ts b/packages/replay/metrics/src/results/results-set.ts index ca386723fb27..5597d423810d 100644 --- a/packages/replay/metrics/src/results/results-set.ts +++ b/packages/replay/metrics/src/results/results-set.ts @@ -1,7 +1,8 @@ import assert from 'assert'; import * as fs from 'fs'; import path from 'path'; -import { Git } from '../git.js'; +import { Git, GitHash } from '../util/git.js'; +import { Result } from './result.js'; const delimiter = '-'; @@ -16,7 +17,7 @@ export class ResultSetItem { return parseInt(this.parts[0]); } - public get hash(): string { + public get hash(): GitHash { return this.parts[1]; } @@ -38,23 +39,42 @@ export class ResultsSet { return this.items().length; } + public find(predicate: (value: Result) => boolean): [GitHash, Result] | undefined { + const items = this.items(); + for (let i = 0; i < items.length; i++) { + const result = Result.readFromFile(items[i].path); + if (predicate(result)) { + return [items[i].hash, result]; + } + } + return undefined; + } + public items(): ResultSetItem[] { return this.files().map((file) => { return new ResultSetItem(path.join(this.directory, file.name)); }).filter((item) => !isNaN(item.number)); } - files(): fs.Dirent[] { + private files(): fs.Dirent[] { return fs.readdirSync(this.directory, { withFileTypes: true }).filter((v) => v.isFile()) } - public async add(newFile: string): Promise { + public async add(newFile: string, onlyIfDifferent: boolean = false): Promise { console.log(`Preparing to add ${newFile} to ${this.directory}`); assert(fs.existsSync(newFile)); - // Get the list of file sorted by the prefix number in the descending order. + // Get the list of file sorted by the prefix number in the descending order (starting with the oldest files). const files = this.items().sort((a, b) => b.number - a.number); + if (onlyIfDifferent && files.length > 0) { + const latestFile = files[files.length - 1]; + if (fs.readFileSync(latestFile.path, { encoding: 'utf-8' }) == fs.readFileSync(newFile, { encoding: 'utf-8' })) { + console.log(`Skipping - it's already stored as ${latestFile.name}`); + return; + } + } + // Rename all existing files, increasing the prefix for (const file of files) { const parts = file.name.split(delimiter); diff --git a/packages/replay/metrics/src/git.ts b/packages/replay/metrics/src/util/git.ts similarity index 81% rename from packages/replay/metrics/src/git.ts rename to packages/replay/metrics/src/util/git.ts index 88f0206a759b..edcf74af5141 100644 --- a/packages/replay/metrics/src/git.ts +++ b/packages/replay/metrics/src/util/git.ts @@ -1,8 +1,10 @@ import { simpleGit } from 'simple-git'; +export type GitHash = string; + // A testing scenario we want to collect metrics for. export const Git = { - get hash(): Promise { + get hash(): Promise { return (async () => { const git = simpleGit(); let gitHash = await git.revparse('HEAD'); diff --git a/packages/replay/metrics/yarn.lock b/packages/replay/metrics/yarn.lock index 9d4fa4d0e407..bdcdfcb7104d 100644 --- a/packages/replay/metrics/yarn.lock +++ b/packages/replay/metrics/yarn.lock @@ -278,6 +278,11 @@ fd-slicer@~1.1.0: dependencies: pend "~1.2.0" +filesize@^10.0.6: + version "10.0.6" + resolved "https://registry.yarnpkg.com/filesize/-/filesize-10.0.6.tgz#5f4cd2721664cd925db3a7a5a87bbfd6ab5ebb1a" + integrity sha512-rzpOZ4C9vMFDqOa6dNpog92CoLYjD79dnjLk2TYDDtImRIyLTOzqojCb05Opd1WuiWjs+fshhCgTd8cl7y5t+g== + fs-constants@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" @@ -521,6 +526,11 @@ simple-git@^3.15.1: "@kwsites/promise-deferred" "^1.1.1" debug "^4.3.4" +simple-statistics@^7.8.0: + version "7.8.0" + resolved "https://registry.yarnpkg.com/simple-statistics/-/simple-statistics-7.8.0.tgz#1033d2d613656c7bd34f0e134fd7e69c803e6836" + integrity sha512-lTWbfJc0u6GZhBojLOrlHJMTHu6PdUjSsYLrpiH902dVBiYJyWlN/LdSoG8b5VvfG1D30gIBgarqMNeNmU5nAA== + string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" From 0df3970f0df08a8f0aea3db6ad12c0aff8d4b367 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Mon, 2 Jan 2023 13:51:06 +0100 Subject: [PATCH 078/113] metrics: ci configs, git & github utils (wip) --- packages/replay/metrics/configs/README.md | 4 +- packages/replay/metrics/configs/ci/collect.ts | 17 ++++ packages/replay/metrics/configs/ci/env.ts | 4 + packages/replay/metrics/configs/ci/process.ts | 37 ++++++++ packages/replay/metrics/package.json | 4 +- packages/replay/metrics/src/util/git.ts | 52 ++++++++++- packages/replay/metrics/src/util/github.ts | 87 +++++++++++++++++++ 7 files changed, 200 insertions(+), 5 deletions(-) create mode 100644 packages/replay/metrics/configs/ci/collect.ts create mode 100644 packages/replay/metrics/configs/ci/env.ts create mode 100644 packages/replay/metrics/configs/ci/process.ts create mode 100644 packages/replay/metrics/src/util/github.ts diff --git a/packages/replay/metrics/configs/README.md b/packages/replay/metrics/configs/README.md index d47f3ac8e145..cb9724ba4619 100644 --- a/packages/replay/metrics/configs/README.md +++ b/packages/replay/metrics/configs/README.md @@ -1,4 +1,4 @@ # Replay metrics configuration & entrypoints (scripts) -* [dev] contains scripts launched during local development -* [ci] contains scripts launched in CI +* [dev](dev) contains scripts launched during local development +* [ci](ci) contains scripts launched in CI diff --git a/packages/replay/metrics/configs/ci/collect.ts b/packages/replay/metrics/configs/ci/collect.ts new file mode 100644 index 000000000000..bec5af33ec8f --- /dev/null +++ b/packages/replay/metrics/configs/ci/collect.ts @@ -0,0 +1,17 @@ +import { Metrics, MetricsCollector } from '../../src/collector.js'; +import { JankTestScenario } from '../../src/scenarios.js'; +import { latestResultFile } from './env.js'; + +const collector = new MetricsCollector(); +const result = await collector.execute({ + name: 'dummy', + a: new JankTestScenario(false), + b: new JankTestScenario(true), + runs: 1, + tries: 1, + async test(_aResults: Metrics[], _bResults: Metrics[]) { + return true; + }, +}); + +result.writeToFile(latestResultFile); diff --git a/packages/replay/metrics/configs/ci/env.ts b/packages/replay/metrics/configs/ci/env.ts new file mode 100644 index 000000000000..3f8e48af6701 --- /dev/null +++ b/packages/replay/metrics/configs/ci/env.ts @@ -0,0 +1,4 @@ +export const previousResultsDir = 'out/previous-results'; +export const baselineResultsDir = 'out/baseline-results'; +export const latestResultFile = 'out/latest-result.json'; +export const artifactName = "sdk-metrics-replay" diff --git a/packages/replay/metrics/configs/ci/process.ts b/packages/replay/metrics/configs/ci/process.ts new file mode 100644 index 000000000000..df0c1dd80fc5 --- /dev/null +++ b/packages/replay/metrics/configs/ci/process.ts @@ -0,0 +1,37 @@ +import path from 'path'; +// import { AnalyzerItemMetric, ResultsAnalyzer } from '../../src/results/analyzer.js'; +import { Result } from '../../src/results/result.js'; +// import { ResultsSet } from '../../src/results/results-set.js'; +import { Git } from '../../src/util/git.js'; +import { GitHub } from '../../src/util/github.js'; +import { latestResultFile, previousResultsDir, baselineResultsDir, artifactName } from './env.js'; + +const latestResult = Result.readFromFile(latestResultFile); +console.debug(latestResult); + +GitHub.downloadPreviousArtifact(await Git.baseBranch, baselineResultsDir, artifactName); +GitHub.downloadPreviousArtifact(await Git.branch, previousResultsDir, artifactName); + +GitHub.writeOutput("artifactName", artifactName) +GitHub.writeOutput("artifactPath", path.resolve(previousResultsDir)); + +// const resultsSet = new ResultsSet(outDir); +// const analysis = ResultsAnalyzer.analyze(latestResult, resultsSet); + +// val prComment = PrCommentBuilder() +// prComment.addCurrentResult(latestResults) +// if (Git.baseBranch != Git.branch) { +// prComment.addAdditionalResultsSet( +// "Baseline results on branch: ${Git.baseBranch}", +// ResultsSet(baselineResultsDir) +// ) +// } +// prComment.addAdditionalResultsSet( +// "Previous results on branch: ${Git.branch}", +// ResultsSet(previousResultsDir) +// ) + +// GitHub.addOrUpdateComment(prComment); + +// Copy the latest test run results to the archived result dir. +// await resultsSet.add(latestResultFile, true); diff --git a/packages/replay/metrics/package.json b/packages/replay/metrics/package.json index 15c22c39ed70..c8f0b82eb4a0 100644 --- a/packages/replay/metrics/package.json +++ b/packages/replay/metrics/package.json @@ -9,7 +9,9 @@ "build": "tsc", "deps": "yarn --cwd ../ build:bundle && yarn --cwd ../../tracing/ build:bundle", "dev:collect": "ts-node-esm ./configs/dev/collect.ts", - "dev:process": "ts-node-esm ./configs/dev/process.ts" + "dev:process": "ts-node-esm ./configs/dev/process.ts", + "ci:collect": "ts-node-esm ./configs/ci/collect.ts", + "ci:process": "ts-node-esm ./configs/ci/process.ts" }, "dependencies": { "@types/node": "^18.11.17", diff --git a/packages/replay/metrics/src/util/git.ts b/packages/replay/metrics/src/util/git.ts index edcf74af5141..f5799904fb04 100644 --- a/packages/replay/metrics/src/util/git.ts +++ b/packages/replay/metrics/src/util/git.ts @@ -1,12 +1,60 @@ import { simpleGit } from 'simple-git'; export type GitHash = string; +const git = simpleGit(); + +async function defaultBranch(): Promise { + const remoteInfo = await git.remote(['show', 'origin']) as string; + for (let line of remoteInfo.split('\n')) { + line = line.trim(); + if (line.startsWith('HEAD branch:')) { + return line.substring('HEAD branch:'.length).trim(); + } + } + throw "Couldn't find base branch name"; +} -// A testing scenario we want to collect metrics for. export const Git = { + get repository(): Promise { + return (async () => { + if (typeof process.env.GITHUB_REPOSITORY == 'string' && process.env.GITHUB_REPOSITORY.length > 0) { + return `github.com/${process.env.GITHUB_REPOSITORY}`; + } else { + let url = await git.remote(['get-url', 'origin']) as string; + url = url.trim(); + url = url.replace(/^git@/, ''); + url = url.replace(/\.git$/, ''); + return url.replace(':', '/'); + } + })(); + }, + + get branch(): Promise { + return (async () => { + if (typeof process.env.GITHUB_HEAD_REF == 'string' && process.env.GITHUB_HEAD_REF.length > 0) { + return process.env.GITHUB_HEAD_REF; + } else if (typeof process.env.GITHUB_REF == 'string' && process.env.GITHUB_REF.startsWith('refs/heads/')) { + return process.env.GITHUB_REF.substring('refs/heads/'.length); + } else { + const branches = (await git.branchLocal()).branches; + for (const name in branches) { + if (branches[name].current) return name; + } + throw "Couldn't find current branch name"; + } + })(); + }, + + get baseBranch(): Promise { + if (typeof process.env.GITHUB_BASE_REF == 'string' && process.env.GITHUB_BASE_REF.length > 0) { + return Promise.resolve(process.env.GITHUB_BASE_REF); + } else { + return defaultBranch(); + } + }, + get hash(): Promise { return (async () => { - const git = simpleGit(); let gitHash = await git.revparse('HEAD'); let diff = await git.diff(); if (diff.trim().length > 0) { diff --git a/packages/replay/metrics/src/util/github.ts b/packages/replay/metrics/src/util/github.ts new file mode 100644 index 000000000000..86376227e781 --- /dev/null +++ b/packages/replay/metrics/src/util/github.ts @@ -0,0 +1,87 @@ +import * as fs from 'fs'; + +export const GitHub = { + writeOutput(name: string, value: any): void { + if (typeof process.env.GITHUB_OUTPUT == 'string' && process.env.GITHUB_OUTPUT.length > 0) { + fs.appendFileSync(process.env.GITHUB_OUTPUT, `${name}=${value}\n`); + } + console.log(`Output ${name}`, value); + }, + + downloadPreviousArtifact(branch: string, targetDir: string, artifactName: string): void { + fs.mkdirSync(targetDir, { recursive: true }); + + // if (workflow == null) { + // println("Skipping previous artifact '$artifactName' download for branch '$branch' - not running in CI") + // return + // } + console.log(`Trying to download previous artifact '${artifactName}' for branch '${branch}'`) + + // val run = workflow!!.listRuns() + // .firstOrNull { it.headBranch == branch && it.conclusion == GHWorkflowRun.Conclusion.SUCCESS } + // if (run == null) { + // println("Couldn't find any successful run workflow ${workflow!!.name}") + // return + // } + + // val artifact = run.listArtifacts().firstOrNull { it.name == artifactName } + // if (artifact == null) { + // println("Couldn't find any artifact matching $artifactName") + // return + // } + + // println("Downloading artifact ${artifact.archiveDownloadUrl} and extracting to $targetDir") + // artifact.download { + // val zipStream = ZipInputStream(it) + // var entry: ZipEntry? + // // while there are entries I process them + // while (true) { + // entry = zipStream.nextEntry + // if (entry == null) { + // break + // } + // if (entry.isDirectory) { + // Path.of(entry.name).createDirectories() + // } else { + // println("Extracting ${entry.name}") + // val outFile = FileOutputStream(targetDir.resolve(entry.name).toFile()) + // while (zipStream.available() > 0) { + // val c = zipStream.read() + // if (c > 0) { + // outFile.write(c) + // } else { + // break + // } + // } + // outFile.close() + // } + // } + // } + }, + + // fun addOrUpdateComment(commentBuilder: PrCommentBuilder) { + // if (pullRequest == null) { + // val file = File("out/comment.html") + // println("No PR available (not running in CI?): writing built comment to ${file.absolutePath}") + // file.writeText(commentBuilder.body) + // } else { + // val comments = pullRequest!!.comments + // // Trying to fetch `github!!.myself` throws (in CI only): Exception in thread "main" org.kohsuke.github.HttpException: + // // {"message":"Resource not accessible by integration","documentation_url":"https://docs.github.com/rest/reference/users#get-the-authenticated-user"} + // // Let's make this conditional on some env variable that's unlikely to be set. + // // Do not use "CI" because that's commonly set during local development and testing. + // val author = if (env.containsKey("GITHUB_ACTION")) "github-actions[bot]" else github!!.myself.login + // val comment = comments.firstOrNull { + // it.user.login.equals(author) && + // it.body.startsWith(commentBuilder.title, ignoreCase = true) + // } + // if (comment != null) { + // println("Updating PR comment ${comment.htmlUrl} body") + // comment.update(commentBuilder.body) + // } else { + // println("Adding new PR comment to ${pullRequest!!.htmlUrl}") + // pullRequest!!.comment(commentBuilder.body) + // } + // } + // } +} From 468f6b2846cb50007b1cde252b436ce913774a0b Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Mon, 2 Jan 2023 17:03:50 +0100 Subject: [PATCH 079/113] ci: collect sdk metrics --- .github/workflows/metrics.yml | 81 ++++++++ packages/replay/metrics/configs/ci/process.ts | 6 +- packages/replay/metrics/package.json | 3 + packages/replay/metrics/src/util/github.ts | 126 +++++++----- packages/replay/metrics/yarn.lock | 179 +++++++++++++++++- 5 files changed, 343 insertions(+), 52 deletions(-) create mode 100644 .github/workflows/metrics.yml diff --git a/.github/workflows/metrics.yml b/.github/workflows/metrics.yml new file mode 100644 index 000000000000..ebd57236a53e --- /dev/null +++ b/.github/workflows/metrics.yml @@ -0,0 +1,81 @@ +name: Collect SDK metrics +on: + push: + paths: + - .github/workflows/metrics.yml + - packages/** + - patches/** + - lerna.json + - package.json + - tsconfig.json + - yarn.lock + branches-ignore: + - deps/** + - dependabot/** + tags-ignore: ['**'] + +env: + CACHED_DEPENDENCY_PATHS: | + ${{ github.workspace }}/node_modules + ${{ github.workspace }}/packages/*/node_modules + ~/.cache/ms-playwright/ + ~/.cache/mongodb-binaries/ + +jobs: + cancel-previous-workflow: + runs-on: ubuntu-latest + steps: + - name: Cancel Previous Runs + uses: styfle/cancel-workflow-action@b173b6ec0100793626c2d9e6b90435061f4fc3e5 # pin@0.11.0 + with: + access_token: ${{ github.token }} + + replay: + name: Replay SDK metrics + runs-on: ubuntu-20.04 + + steps: + - uses: actions/checkout@v3 + + - name: Set up Node + uses: volta-cli/action@v4 + + - name: Compute dependency cache key + id: compute_lockfile_hash + # we use a hash of yarn.lock as our cache key, because if it hasn't changed, our dependencies haven't changed, + # so no need to reinstall them + run: echo "hash=${{ hashFiles('yarn.lock') }}" >> "$GITHUB_OUTPUT" + + - name: Check dependency cache + uses: actions/cache@v3 + id: cache_dependencies + with: + path: ${{ env.CACHED_DEPENDENCY_PATHS }} + key: ${{ steps.compute_lockfile_hash.outputs.hash }} + + - name: Install dependencies + if: steps.cache_dependencies.outputs.cache-hit == '' + run: yarn install --ignore-engines --frozen-lockfile + + - name: Build + run: | + yarn install --ignore-engines --frozen-lockfile + yarn deps + working-directory: packages/replay/metrics + + - name: Collect + run: yarn ci:collect + working-directory: packages/replay/metrics + + - name: Process + id: process + run: yarn ci:process + working-directory: packages/replay/metrics + env: + GITHUB_TOKEN: ${{ github.token }} + + - name: Upload results + uses: actions/upload-artifact@v3 + with: + name: ${{ steps.process.outputs.artifactName }} + path: ${{ steps.process.outputs.artifactPath }} diff --git a/packages/replay/metrics/configs/ci/process.ts b/packages/replay/metrics/configs/ci/process.ts index df0c1dd80fc5..feb27eed70b0 100644 --- a/packages/replay/metrics/configs/ci/process.ts +++ b/packages/replay/metrics/configs/ci/process.ts @@ -1,7 +1,7 @@ import path from 'path'; // import { AnalyzerItemMetric, ResultsAnalyzer } from '../../src/results/analyzer.js'; import { Result } from '../../src/results/result.js'; -// import { ResultsSet } from '../../src/results/results-set.js'; +import { ResultsSet } from '../../src/results/results-set.js'; import { Git } from '../../src/util/git.js'; import { GitHub } from '../../src/util/github.js'; import { latestResultFile, previousResultsDir, baselineResultsDir, artifactName } from './env.js'; @@ -15,7 +15,7 @@ GitHub.downloadPreviousArtifact(await Git.branch, previousResultsDir, artifactNa GitHub.writeOutput("artifactName", artifactName) GitHub.writeOutput("artifactPath", path.resolve(previousResultsDir)); -// const resultsSet = new ResultsSet(outDir); +const resultsSet = new ResultsSet(previousResultsDir); // const analysis = ResultsAnalyzer.analyze(latestResult, resultsSet); // val prComment = PrCommentBuilder() @@ -34,4 +34,4 @@ GitHub.writeOutput("artifactPath", path.resolve(previousResultsDir)); // GitHub.addOrUpdateComment(prComment); // Copy the latest test run results to the archived result dir. -// await resultsSet.add(latestResultFile, true); +await resultsSet.add(latestResultFile, true); diff --git a/packages/replay/metrics/package.json b/packages/replay/metrics/package.json index c8f0b82eb4a0..29fb646a12cf 100644 --- a/packages/replay/metrics/package.json +++ b/packages/replay/metrics/package.json @@ -14,7 +14,10 @@ "ci:process": "ts-node-esm ./configs/ci/process.ts" }, "dependencies": { + "@octokit/rest": "^19.0.5", "@types/node": "^18.11.17", + "axios": "^1.2.2", + "extract-zip": "^2.0.1", "filesize": "^10.0.6", "puppeteer": "^19.4.1", "simple-git": "^3.15.1", diff --git a/packages/replay/metrics/src/util/github.ts b/packages/replay/metrics/src/util/github.ts index 86376227e781..afe95e479c06 100644 --- a/packages/replay/metrics/src/util/github.ts +++ b/packages/replay/metrics/src/util/github.ts @@ -1,62 +1,96 @@ import * as fs from 'fs'; +import { Octokit } from "@octokit/rest"; +import { Git } from './git.js'; +import path from 'path'; +import Axios from 'axios'; +import extract from 'extract-zip'; + +const octokit = new Octokit({ + auth: process.env.GITHUB_TOKEN, + // log: console, +}); + +const [_, owner, repo] = (await Git.repository).split('/'); +const defaultArgs = { owner: owner, repo: repo } + +export function downloadFile(url: string, path: string) { + const writer = fs.createWriteStream(path); + return Axios({ + method: 'get', + url: url, + responseType: 'stream', + }).then(response => { + return new Promise((resolve, reject) => { + response.data.pipe(writer); + let error: Error; + writer.on('error', err => { + error = err; + writer.close(); + reject(err); + }); + writer.on('close', () => { + if (!error) resolve(true); + }); + }); + }); +} export const GitHub = { writeOutput(name: string, value: any): void { if (typeof process.env.GITHUB_OUTPUT == 'string' && process.env.GITHUB_OUTPUT.length > 0) { fs.appendFileSync(process.env.GITHUB_OUTPUT, `${name}=${value}\n`); } - console.log(`Output ${name}`, value); + console.log(`Output ${name} = ${value}`); }, - downloadPreviousArtifact(branch: string, targetDir: string, artifactName: string): void { - fs.mkdirSync(targetDir, { recursive: true }); + downloadPreviousArtifact(branch: string, targetDir: string, artifactName: string): Promise { + return (async () => { + fs.mkdirSync(targetDir, { recursive: true }); + + const workflow = (await octokit.actions.listRepoWorkflows(defaultArgs)) + .data.workflows.find((w) => w.name == process.env.GITHUB_WORKFLOW); + if (workflow == undefined) { + console.log(`Skipping previous artifact '${artifactName}' download for branch '${branch}' - not running in CI`); + return; + } + console.log(`Trying to download previous artifact '${artifactName}' for branch '${branch}'`); + + const workflowRuns = await octokit.actions.listWorkflowRuns({ + ...defaultArgs, + workflow_id: workflow.id, + branch: branch, + status: 'success', + }); + + if (workflowRuns.data.total_count == 0) { + console.warn(`Couldn't find any successful run for workflow '${workflow.name}'`); + return; + } + + const artifact = (await octokit.actions.listWorkflowRunArtifacts({ + ...defaultArgs, + run_id: workflowRuns.data.workflow_runs[0].id, + })).data.artifacts.find((it) => it.name == artifactName); - // if (workflow == null) { - // println("Skipping previous artifact '$artifactName' download for branch '$branch' - not running in CI") - // return - // } - console.log(`Trying to download previous artifact '${artifactName}' for branch '${branch}'`) + if (artifact == undefined) { + console.warn(`Couldn't find any artifact matching ${artifactName}`); + return; + } - // val run = workflow!!.listRuns() - // .firstOrNull { it.headBranch == branch && it.conclusion == GHWorkflowRun.Conclusion.SUCCESS } - // if (run == null) { - // println("Couldn't find any successful run workflow ${workflow!!.name}") - // return - // } + console.log(`Downloading artifact ${artifact.archive_download_url} and extracting to $targetDir`); - // val artifact = run.listArtifacts().firstOrNull { it.name == artifactName } - // if (artifact == null) { - // println("Couldn't find any artifact matching $artifactName") - // return - // } + const tempFilePath = path.resolve(targetDir, '../tmp-artifacts.zip'); + if (fs.existsSync(tempFilePath)) { + fs.unlinkSync(tempFilePath); + } - // println("Downloading artifact ${artifact.archiveDownloadUrl} and extracting to $targetDir") - // artifact.download { - // val zipStream = ZipInputStream(it) - // var entry: ZipEntry? - // // while there are entries I process them - // while (true) { - // entry = zipStream.nextEntry - // if (entry == null) { - // break - // } - // if (entry.isDirectory) { - // Path.of(entry.name).createDirectories() - // } else { - // println("Extracting ${entry.name}") - // val outFile = FileOutputStream(targetDir.resolve(entry.name).toFile()) - // while (zipStream.available() > 0) { - // val c = zipStream.read() - // if (c > 0) { - // outFile.write(c) - // } else { - // break - // } - // } - // outFile.close() - // } - // } - // } + try { + await downloadFile(artifact.archive_download_url, tempFilePath); + await extract(tempFilePath, { dir: targetDir }); + } finally { + fs.unlinkSync(tempFilePath); + } + })(); }, // fun addOrUpdateComment(commentBuilder: PrCommentBuilder) { diff --git a/packages/replay/metrics/yarn.lock b/packages/replay/metrics/yarn.lock index bdcdfcb7104d..976135fc15de 100644 --- a/packages/replay/metrics/yarn.lock +++ b/packages/replay/metrics/yarn.lock @@ -60,6 +60,107 @@ resolved "https://registry.yarnpkg.com/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz#8ace5259254426ccef57f3175bc64ed7095ed919" integrity sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw== +"@octokit/auth-token@^3.0.0": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-3.0.2.tgz#a0fc8de149fd15876e1ac78f6525c1c5ab48435f" + integrity sha512-pq7CwIMV1kmzkFTimdwjAINCXKTajZErLB4wMLYapR2nuB/Jpr66+05wOTZMSCBXP6n4DdDWT2W19Bm17vU69Q== + dependencies: + "@octokit/types" "^8.0.0" + +"@octokit/core@^4.1.0": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@octokit/core/-/core-4.1.0.tgz#b6b03a478f1716de92b3f4ec4fd64d05ba5a9251" + integrity sha512-Czz/59VefU+kKDy+ZfDwtOIYIkFjExOKf+HA92aiTZJ6EfWpFzYQWw0l54ji8bVmyhc+mGaLUbSUmXazG7z5OQ== + dependencies: + "@octokit/auth-token" "^3.0.0" + "@octokit/graphql" "^5.0.0" + "@octokit/request" "^6.0.0" + "@octokit/request-error" "^3.0.0" + "@octokit/types" "^8.0.0" + before-after-hook "^2.2.0" + universal-user-agent "^6.0.0" + +"@octokit/endpoint@^7.0.0": + version "7.0.3" + resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-7.0.3.tgz#0b96035673a9e3bedf8bab8f7335de424a2147ed" + integrity sha512-57gRlb28bwTsdNXq+O3JTQ7ERmBTuik9+LelgcLIVfYwf235VHbN9QNo4kXExtp/h8T423cR5iJThKtFYxC7Lw== + dependencies: + "@octokit/types" "^8.0.0" + is-plain-object "^5.0.0" + universal-user-agent "^6.0.0" + +"@octokit/graphql@^5.0.0": + version "5.0.4" + resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-5.0.4.tgz#519dd5c05123868276f3ae4e50ad565ed7dff8c8" + integrity sha512-amO1M5QUQgYQo09aStR/XO7KAl13xpigcy/kI8/N1PnZYSS69fgte+xA4+c2DISKqUZfsh0wwjc2FaCt99L41A== + dependencies: + "@octokit/request" "^6.0.0" + "@octokit/types" "^8.0.0" + universal-user-agent "^6.0.0" + +"@octokit/openapi-types@^14.0.0": + version "14.0.0" + resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-14.0.0.tgz#949c5019028c93f189abbc2fb42f333290f7134a" + integrity sha512-HNWisMYlR8VCnNurDU6os2ikx0s0VyEjDYHNS/h4cgb8DeOxQ0n72HyinUtdDVxJhFy3FWLGl0DJhfEWk3P5Iw== + +"@octokit/plugin-paginate-rest@^5.0.0": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-5.0.1.tgz#93d7e74f1f69d68ba554fa6b888c2a9cf1f99a83" + integrity sha512-7A+rEkS70pH36Z6JivSlR7Zqepz3KVucEFVDnSrgHXzG7WLAzYwcHZbKdfTXHwuTHbkT1vKvz7dHl1+HNf6Qyw== + dependencies: + "@octokit/types" "^8.0.0" + +"@octokit/plugin-request-log@^1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz#5e50ed7083a613816b1e4a28aeec5fb7f1462e85" + integrity sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA== + +"@octokit/plugin-rest-endpoint-methods@^6.7.0": + version "6.7.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-6.7.0.tgz#2f6f17f25b6babbc8b41d2bb0a95a8839672ce7c" + integrity sha512-orxQ0fAHA7IpYhG2flD2AygztPlGYNAdlzYz8yrD8NDgelPfOYoRPROfEyIe035PlxvbYrgkfUZIhSBKju/Cvw== + dependencies: + "@octokit/types" "^8.0.0" + deprecation "^2.3.1" + +"@octokit/request-error@^3.0.0": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-3.0.2.tgz#f74c0f163d19463b87528efe877216c41d6deb0a" + integrity sha512-WMNOFYrSaX8zXWoJg9u/pKgWPo94JXilMLb2VManNOby9EZxrQaBe/QSC4a1TzpAlpxofg2X/jMnCyZgL6y7eg== + dependencies: + "@octokit/types" "^8.0.0" + deprecation "^2.0.0" + once "^1.4.0" + +"@octokit/request@^6.0.0": + version "6.2.2" + resolved "https://registry.yarnpkg.com/@octokit/request/-/request-6.2.2.tgz#a2ba5ac22bddd5dcb3f539b618faa05115c5a255" + integrity sha512-6VDqgj0HMc2FUX2awIs+sM6OwLgwHvAi4KCK3mT2H2IKRt6oH9d0fej5LluF5mck1lRR/rFWN0YIDSYXYSylbw== + dependencies: + "@octokit/endpoint" "^7.0.0" + "@octokit/request-error" "^3.0.0" + "@octokit/types" "^8.0.0" + is-plain-object "^5.0.0" + node-fetch "^2.6.7" + universal-user-agent "^6.0.0" + +"@octokit/rest@^19.0.5": + version "19.0.5" + resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-19.0.5.tgz#4dbde8ae69b27dca04b5f1d8119d282575818f6c" + integrity sha512-+4qdrUFq2lk7Va+Qff3ofREQWGBeoTKNqlJO+FGjFP35ZahP+nBenhZiGdu8USSgmq4Ky3IJ/i4u0xbLqHaeow== + dependencies: + "@octokit/core" "^4.1.0" + "@octokit/plugin-paginate-rest" "^5.0.0" + "@octokit/plugin-request-log" "^1.0.4" + "@octokit/plugin-rest-endpoint-methods" "^6.7.0" + +"@octokit/types@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@octokit/types/-/types-8.0.0.tgz#93f0b865786c4153f0f6924da067fe0bb7426a9f" + integrity sha512-65/TPpOJP1i3K4lBJMnWqPUJ6zuOtzhtagDvydAWbEXpbFYA0oMKKyLb95NFZZP0lSh/4b6K+DQlzvYQJQQePg== + dependencies: + "@octokit/openapi-types" "^14.0.0" + "@tsconfig/node10@^1.0.7": version "1.0.9" resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" @@ -126,6 +227,20 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +axios@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.2.2.tgz#72681724c6e6a43a9fea860fc558127dbe32f9f1" + integrity sha512-bz/J4gS2S3I7mpN/YZfGFTqhXTYzRho8Ay38w2otuuDR322KzFIWm/4W2K6gIwvWaws5n+mnb7D1lN9uD+QH6Q== + dependencies: + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -136,6 +251,11 @@ base64-js@^1.3.1: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== +before-after-hook@^2.2.0: + version "2.2.3" + resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.2.3.tgz#c51e809c81a4e354084422b9b26bad88249c517c" + integrity sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ== + bl@^4.0.3: version "4.1.0" resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" @@ -197,6 +317,13 @@ color-name@1.1.3: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -231,6 +358,16 @@ debug@4, debug@4.3.4, debug@^4.1.1, debug@^4.3.4: dependencies: ms "2.1.2" +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +deprecation@^2.0.0, deprecation@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919" + integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ== + devtools-protocol@0.0.1068969: version "0.0.1068969" resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.1068969.tgz#8b9a4bc48aed1453bed08d62b07481f9abf4d6d8" @@ -260,7 +397,7 @@ escape-string-regexp@^1.0.5: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== -extract-zip@2.0.1: +extract-zip@2.0.1, extract-zip@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== @@ -283,6 +420,20 @@ filesize@^10.0.6: resolved "https://registry.yarnpkg.com/filesize/-/filesize-10.0.6.tgz#5f4cd2721664cd925db3a7a5a87bbfd6ab5ebb1a" integrity sha512-rzpOZ4C9vMFDqOa6dNpog92CoLYjD79dnjLk2TYDDtImRIyLTOzqojCb05Opd1WuiWjs+fshhCgTd8cl7y5t+g== +follow-redirects@^1.15.0: + version "1.15.2" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" + integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== + +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + fs-constants@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" @@ -356,6 +507,11 @@ is-arrayish@^0.2.1: resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== +is-plain-object@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" + integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== + js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -383,6 +539,18 @@ make-error@^1.1.1: resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + minimatch@^3.1.1: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" @@ -400,7 +568,7 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -node-fetch@2.6.7: +node-fetch@2.6.7, node-fetch@^2.6.7: version "2.6.7" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== @@ -451,7 +619,7 @@ progress@2.0.3: resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== -proxy-from-env@1.1.0: +proxy-from-env@1.1.0, proxy-from-env@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== @@ -608,6 +776,11 @@ unbzip2-stream@1.4.3: buffer "^5.2.1" through "^2.3.8" +universal-user-agent@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-6.0.0.tgz#3381f8503b251c0d9cd21bc1de939ec9df5480ee" + integrity sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w== + util-deprecate@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" From f7873b38c487f4041a102db4f89a57bd7be7c06a Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Mon, 2 Jan 2023 19:03:38 +0100 Subject: [PATCH 080/113] metrics collection timeout --- packages/replay/metrics/configs/ci/collect.ts | 2 +- packages/replay/metrics/src/collector.ts | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/replay/metrics/configs/ci/collect.ts b/packages/replay/metrics/configs/ci/collect.ts index bec5af33ec8f..7d9577e6a9d1 100644 --- a/packages/replay/metrics/configs/ci/collect.ts +++ b/packages/replay/metrics/configs/ci/collect.ts @@ -2,7 +2,7 @@ import { Metrics, MetricsCollector } from '../../src/collector.js'; import { JankTestScenario } from '../../src/scenarios.js'; import { latestResultFile } from './env.js'; -const collector = new MetricsCollector(); +const collector = new MetricsCollector({ headless: true }); const result = await collector.execute({ name: 'dummy', a: new JankTestScenario(false), diff --git a/packages/replay/metrics/src/collector.ts b/packages/replay/metrics/src/collector.ts index 994a8fb2bfd2..a33b0ca9bb93 100644 --- a/packages/replay/metrics/src/collector.ts +++ b/packages/replay/metrics/src/collector.ts @@ -23,8 +23,20 @@ export class Metrics { } } +export interface MetricsCollectorOptions { + headless: boolean; +} export class MetricsCollector { + private options: MetricsCollectorOptions; + + constructor(options: Partial) { + this.options = { + headless: false, + ...options + }; + } + public async execute(testCase: TestCase): Promise { console.log(`Executing test case ${testCase.name}`); console.group(); @@ -64,7 +76,7 @@ export class MetricsCollector { const disposeCallbacks: (() => Promise)[] = []; try { const browser = await puppeteer.launch({ - headless: false, + headless: this.options.headless, }); disposeCallbacks.push(async () => browser.close()); const page = await browser.newPage(); From 4df8bcea71b0fb4b352fd6f6eff73d8cf2c4eb34 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Mon, 2 Jan 2023 19:47:00 +0100 Subject: [PATCH 081/113] fix metrics collection on linux --- packages/replay/metrics/configs/ci/env.ts | 2 +- packages/replay/metrics/src/scenarios.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/replay/metrics/configs/ci/env.ts b/packages/replay/metrics/configs/ci/env.ts index 3f8e48af6701..e3492ced1056 100644 --- a/packages/replay/metrics/configs/ci/env.ts +++ b/packages/replay/metrics/configs/ci/env.ts @@ -1,4 +1,4 @@ export const previousResultsDir = 'out/previous-results'; export const baselineResultsDir = 'out/baseline-results'; export const latestResultFile = 'out/latest-result.json'; -export const artifactName = "sdk-metrics-replay" +export const artifactName = "replay-sdk-metrics" diff --git a/packages/replay/metrics/src/scenarios.ts b/packages/replay/metrics/src/scenarios.ts index a87c5acebef3..1a9a31e74eb7 100644 --- a/packages/replay/metrics/src/scenarios.ts +++ b/packages/replay/metrics/src/scenarios.ts @@ -36,8 +36,10 @@ export class JankTestScenario implements Scenario { public constructor(private withSentry: boolean) { } public async run(_: puppeteer.Browser, page: puppeteer.Page): Promise { - const url = path.resolve('./test-apps/jank/' + (this.withSentry ? 'with-sentry' : 'index') + '.html'); + let url = path.resolve('./test-apps/jank/' + (this.withSentry ? 'with-sentry' : 'index') + '.html'); assert(fs.existsSync(url)); + url = 'file:///' + url.replace('\\', '/'); + console.log('Navigating to ', url); await page.goto(url, { waitUntil: 'load', timeout: 60000 }); await new Promise(resolve => setTimeout(resolve, 5000)); } From e38bbcc8e9a82af9307227da7f9f7aa19feddb8c Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Mon, 2 Jan 2023 21:27:19 +0100 Subject: [PATCH 082/113] await artifacts download --- packages/replay/metrics/configs/ci/process.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/replay/metrics/configs/ci/process.ts b/packages/replay/metrics/configs/ci/process.ts index feb27eed70b0..26575ed622c1 100644 --- a/packages/replay/metrics/configs/ci/process.ts +++ b/packages/replay/metrics/configs/ci/process.ts @@ -9,8 +9,8 @@ import { latestResultFile, previousResultsDir, baselineResultsDir, artifactName const latestResult = Result.readFromFile(latestResultFile); console.debug(latestResult); -GitHub.downloadPreviousArtifact(await Git.baseBranch, baselineResultsDir, artifactName); -GitHub.downloadPreviousArtifact(await Git.branch, previousResultsDir, artifactName); +await GitHub.downloadPreviousArtifact(await Git.baseBranch, baselineResultsDir, artifactName); +await GitHub.downloadPreviousArtifact(await Git.branch, previousResultsDir, artifactName); GitHub.writeOutput("artifactName", artifactName) GitHub.writeOutput("artifactPath", path.resolve(previousResultsDir)); From 49b4131061a92d9d696131f7c29fe23845c26472 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Mon, 2 Jan 2023 23:24:36 +0100 Subject: [PATCH 083/113] fix gh authentication --- packages/replay/metrics/src/util/github.ts | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/replay/metrics/src/util/github.ts b/packages/replay/metrics/src/util/github.ts index afe95e479c06..f5ac856bbad8 100644 --- a/packages/replay/metrics/src/util/github.ts +++ b/packages/replay/metrics/src/util/github.ts @@ -13,12 +13,15 @@ const octokit = new Octokit({ const [_, owner, repo] = (await Git.repository).split('/'); const defaultArgs = { owner: owner, repo: repo } -export function downloadFile(url: string, path: string) { +export function downloadArtifact(url: string, path: string) { const writer = fs.createWriteStream(path); return Axios({ method: 'get', url: url, responseType: 'stream', + headers: { + 'Authorization': `Bearer ${process.env.GITHUB_TOKEN}` + } }).then(response => { return new Promise((resolve, reject) => { response.data.pipe(writer); @@ -50,7 +53,10 @@ export const GitHub = { const workflow = (await octokit.actions.listRepoWorkflows(defaultArgs)) .data.workflows.find((w) => w.name == process.env.GITHUB_WORKFLOW); if (workflow == undefined) { - console.log(`Skipping previous artifact '${artifactName}' download for branch '${branch}' - not running in CI`); + console.log( + `Skipping previous artifact '${artifactName}' download for branch '${branch}' - not running in CI?`, + "Environment variable GITHUB_WORKFLOW isn't set." + ); return; } console.log(`Trying to download previous artifact '${artifactName}' for branch '${branch}'`); @@ -77,7 +83,7 @@ export const GitHub = { return; } - console.log(`Downloading artifact ${artifact.archive_download_url} and extracting to $targetDir`); + console.log(`Downloading artifact ${artifact.archive_download_url} and extracting to ${targetDir}`); const tempFilePath = path.resolve(targetDir, '../tmp-artifacts.zip'); if (fs.existsSync(tempFilePath)) { @@ -85,10 +91,12 @@ export const GitHub = { } try { - await downloadFile(artifact.archive_download_url, tempFilePath); - await extract(tempFilePath, { dir: targetDir }); + await downloadArtifact(artifact.archive_download_url, tempFilePath); + await extract(tempFilePath, { dir: path.resolve(targetDir) }); } finally { - fs.unlinkSync(tempFilePath); + if (fs.existsSync(tempFilePath)) { + fs.unlinkSync(tempFilePath); + } } })(); }, From 940c072ddc01c1aac836addcee79e20e6eef5a80 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Tue, 3 Jan 2023 16:42:24 +0100 Subject: [PATCH 084/113] metrics: add PR comment --- packages/replay/metrics/configs/ci/process.ts | 39 +++--- .../replay/metrics/configs/dev/process.ts | 6 +- .../replay/metrics/src/results/analyzer.ts | 25 +++- .../replay/metrics/src/results/pr-comment.ts | 83 ++++++++++++ .../replay/metrics/src/results/results-set.ts | 4 - packages/replay/metrics/src/util/console.ts | 10 ++ packages/replay/metrics/src/util/github.ts | 118 +++++++++++++----- 7 files changed, 222 insertions(+), 63 deletions(-) create mode 100644 packages/replay/metrics/src/results/pr-comment.ts create mode 100644 packages/replay/metrics/src/util/console.ts diff --git a/packages/replay/metrics/configs/ci/process.ts b/packages/replay/metrics/configs/ci/process.ts index 26575ed622c1..b137577f2a6b 100644 --- a/packages/replay/metrics/configs/ci/process.ts +++ b/packages/replay/metrics/configs/ci/process.ts @@ -1,5 +1,6 @@ import path from 'path'; -// import { AnalyzerItemMetric, ResultsAnalyzer } from '../../src/results/analyzer.js'; +import { ResultsAnalyzer } from '../../src/results/analyzer.js'; +import { PrCommentBuilder } from '../../src/results/pr-comment.js'; import { Result } from '../../src/results/result.js'; import { ResultsSet } from '../../src/results/results-set.js'; import { Git } from '../../src/util/git.js'; @@ -15,23 +16,27 @@ await GitHub.downloadPreviousArtifact(await Git.branch, previousResultsDir, arti GitHub.writeOutput("artifactName", artifactName) GitHub.writeOutput("artifactPath", path.resolve(previousResultsDir)); -const resultsSet = new ResultsSet(previousResultsDir); -// const analysis = ResultsAnalyzer.analyze(latestResult, resultsSet); +const previousResults = new ResultsSet(previousResultsDir); -// val prComment = PrCommentBuilder() -// prComment.addCurrentResult(latestResults) -// if (Git.baseBranch != Git.branch) { -// prComment.addAdditionalResultsSet( -// "Baseline results on branch: ${Git.baseBranch}", -// ResultsSet(baselineResultsDir) -// ) -// } -// prComment.addAdditionalResultsSet( -// "Previous results on branch: ${Git.branch}", -// ResultsSet(previousResultsDir) -// ) +const prComment = new PrCommentBuilder(); +if (Git.baseBranch != Git.branch) { + const baseResults = new ResultsSet(baselineResultsDir); + prComment.addCurrentResult(await ResultsAnalyzer.analyze(latestResult, baseResults), "Baseline"); + await prComment.addAdditionalResultsSet( + `Baseline results on branch: ${Git.baseBranch}`, + // We skip the first one here because it's already included as `Baseline` column above in addCurrentResult(). + baseResults.items().slice(1, 10) + ); +} else { + prComment.addCurrentResult(await ResultsAnalyzer.analyze(latestResult, previousResults), "Previous"); +} -// GitHub.addOrUpdateComment(prComment); +await prComment.addAdditionalResultsSet( + `Previous results on branch: ${Git.branch}`, + previousResults.items().slice(0, 10) +); + +GitHub.addOrUpdateComment(prComment); // Copy the latest test run results to the archived result dir. -await resultsSet.add(latestResultFile, true); +await previousResults.add(latestResultFile, true); diff --git a/packages/replay/metrics/configs/dev/process.ts b/packages/replay/metrics/configs/dev/process.ts index 5a4aaca0dbf6..9672243cf804 100644 --- a/packages/replay/metrics/configs/dev/process.ts +++ b/packages/replay/metrics/configs/dev/process.ts @@ -6,14 +6,14 @@ import { latestResultFile, outDir } from './env.js'; const resultsSet = new ResultsSet(outDir); const latestResult = Result.readFromFile(latestResultFile); -const analysis = ResultsAnalyzer.analyze(latestResult, resultsSet); +const analysis = await ResultsAnalyzer.analyze(latestResult, resultsSet); const table: { [k: string]: any } = {}; -for (const item of analysis) { +for (const item of analysis.items) { const printable: { [k: string]: any } = {}; printable.value = item.value.asString(); if (item.other != undefined) { - printable.baseline = item.other.asString(); + printable.previous = item.other.asString(); } table[AnalyzerItemMetric[item.metric]] = printable; } diff --git a/packages/replay/metrics/src/results/analyzer.ts b/packages/replay/metrics/src/results/analyzer.ts index d1147627fea5..abe29cda6b63 100644 --- a/packages/replay/metrics/src/results/analyzer.ts +++ b/packages/replay/metrics/src/results/analyzer.ts @@ -4,17 +4,17 @@ import { ResultsSet } from './results-set.js'; import { MetricsStats } from './metrics-stats.js'; import { filesize } from "filesize"; -// Compares latest result to previous/baseline results and produces the needed -// info. +// Compares latest result to previous/baseline results and produces the needed info. export class ResultsAnalyzer { - public static analyze(currentResult: Result, baselineResults: ResultsSet): AnalyzerItem[] { + public static async analyze(currentResult: Result, baselineResults?: ResultsSet): Promise { const items = new ResultsAnalyzer(currentResult).collect(); - const baseline = baselineResults.find( + const baseline = baselineResults?.find( (other) => other.cpuThrottling == currentResult.cpuThrottling && other.name == currentResult.name && other.networkConditions == currentResult.networkConditions); + let otherHash: GitHash | undefined if (baseline != undefined) { const baseItems = new ResultsAnalyzer(baseline[1]).collect(); // update items with baseline results @@ -22,13 +22,16 @@ export class ResultsAnalyzer { for (const item of items) { if (item.metric == base.metric) { item.other = base.value; - item.otherHash = baseline[0]; + otherHash = baseline[0]; } } } } - return items; + return { + items: items, + otherHash: otherHash, + }; } private constructor(private result: Result) { } @@ -98,7 +101,17 @@ export enum AnalyzerItemMetric { export interface AnalyzerItem { metric: AnalyzerItemMetric; + + // Current (latest) result. value: AnalyzerItemValue; + + // Previous or baseline results, depending on the context. other?: AnalyzerItemValue; +} + +export interface Analysis { + items: AnalyzerItem[]; + + // Commit hash that the the previous or baseline (depending on the context) result was collected for. otherHash?: GitHash; } diff --git a/packages/replay/metrics/src/results/pr-comment.ts b/packages/replay/metrics/src/results/pr-comment.ts new file mode 100644 index 000000000000..8eff29891fd8 --- /dev/null +++ b/packages/replay/metrics/src/results/pr-comment.ts @@ -0,0 +1,83 @@ +import { Git } from "../util/git.js"; +import { Analysis, AnalyzerItemMetric, ResultsAnalyzer } from "./analyzer.js"; +import { Result } from "./result.js"; +import { ResultSetItem } from "./results-set.js"; + +export class PrCommentBuilder { + private buffer = ''; + + public get title(): string { + return '## Replay SDK metrics :rocket:'; + } + + public get body(): string { + return this.buffer; + } + + public addCurrentResult(analysis: Analysis, otherName: string): void { + // Decides whether to print the "Other" depending on it being set in the input data. + const maybeOther = function (content: () => string): string { + if (analysis.otherHash == undefined) { + return ''; + } + return content(); + } + + this.buffer += ` + ${this.title} + + + + + ${maybeOther(() => '')} + ` + + for (const item of analysis.items) { + this.buffer += ` + + + + ${maybeOther(() => '')} + ` + } + + this.buffer += ` +
 Latest diff (${Git.hash})' + otherName + ' diff (' + analysis.otherHash + ')
${AnalyzerItemMetric[item.metric]}${item.value.asString()}' + item.other!.asString() + '
`; + } + + public async addAdditionalResultsSet(name: String, resultFiles: ResultSetItem[]): Promise { + if (resultFiles.length == 0) return; + + this.buffer += ` +
+

${name}

+ `; + + // Each `resultFile` will be printed as a single row - with metrics as table columns. + for (let i = 0; i < resultFiles.length; i++) { + const resultFile = resultFiles[i]; + // Load the file and "analyse" - collect stats we want to print. + const analysis = await ResultsAnalyzer.analyze(Result.readFromFile(resultFile.path)); + + if (i == 0) { + // Add table header + this.buffer += ''; + for (const item of analysis.items) { + this.buffer += ``; + } + this.buffer += ''; + } + + // Add table row + this.buffer += ``; + for (const item of analysis.items) { + this.buffer += ``; + } + this.buffer += ''; + } + + this.buffer += ` +
Revision${AnalyzerItemMetric[item.metric]}
${resultFile.hash}${item.value.asString()}
+
`; + } +} diff --git a/packages/replay/metrics/src/results/results-set.ts b/packages/replay/metrics/src/results/results-set.ts index 5597d423810d..d19a2ca121e7 100644 --- a/packages/replay/metrics/src/results/results-set.ts +++ b/packages/replay/metrics/src/results/results-set.ts @@ -35,10 +35,6 @@ export class ResultsSet { } } - public count(): number { - return this.items().length; - } - public find(predicate: (value: Result) => boolean): [GitHash, Result] | undefined { const items = this.items(); for (let i = 0; i < items.length; i++) { diff --git a/packages/replay/metrics/src/util/console.ts b/packages/replay/metrics/src/util/console.ts new file mode 100644 index 000000000000..a1cef832db72 --- /dev/null +++ b/packages/replay/metrics/src/util/console.ts @@ -0,0 +1,10 @@ + +export async function consoleGroup(code: () => Promise): Promise { + console.group(); + try { + + return await code(); + } finally { + console.groupEnd(); + } +} diff --git a/packages/replay/metrics/src/util/github.ts b/packages/replay/metrics/src/util/github.ts index f5ac856bbad8..9a393a5819af 100644 --- a/packages/replay/metrics/src/util/github.ts +++ b/packages/replay/metrics/src/util/github.ts @@ -4,6 +4,8 @@ import { Git } from './git.js'; import path from 'path'; import Axios from 'axios'; import extract from 'extract-zip'; +import { consoleGroup } from './console.js'; +import { PrCommentBuilder } from '../results/pr-comment.js'; const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN, @@ -11,9 +13,9 @@ const octokit = new Octokit({ }); const [_, owner, repo] = (await Git.repository).split('/'); -const defaultArgs = { owner: owner, repo: repo } +const defaultArgs = { owner, repo } -export function downloadArtifact(url: string, path: string) { +async function downloadArtifact(url: string, path: string): Promise { const writer = fs.createWriteStream(path); return Axios({ method: 'get', @@ -32,7 +34,7 @@ export function downloadArtifact(url: string, path: string) { reject(err); }); writer.on('close', () => { - if (!error) resolve(true); + if (!error) resolve(); }); }); }); @@ -47,11 +49,17 @@ export const GitHub = { }, downloadPreviousArtifact(branch: string, targetDir: string, artifactName: string): Promise { - return (async () => { + console.log(`Trying to download previous artifact '${artifactName}' for branch '${branch}'`); + return consoleGroup(async () => { fs.mkdirSync(targetDir, { recursive: true }); - const workflow = (await octokit.actions.listRepoWorkflows(defaultArgs)) - .data.workflows.find((w) => w.name == process.env.GITHUB_WORKFLOW); + var workflow = await (async () => { + for await (const workflows of octokit.paginate.iterator(octokit.rest.actions.listRepoWorkflows, defaultArgs)) { + let found = workflows.data.find((w) => w.name == process.env.GITHUB_WORKFLOW); + if (found) return found; + } + return undefined; + })(); if (workflow == undefined) { console.log( `Skipping previous artifact '${artifactName}' download for branch '${branch}' - not running in CI?`, @@ -59,7 +67,6 @@ export const GitHub = { ); return; } - console.log(`Trying to download previous artifact '${artifactName}' for branch '${branch}'`); const workflowRuns = await octokit.actions.listWorkflowRuns({ ...defaultArgs, @@ -98,32 +105,77 @@ export const GitHub = { fs.unlinkSync(tempFilePath); } } - })(); + }); }, - // fun addOrUpdateComment(commentBuilder: PrCommentBuilder) { - // if (pullRequest == null) { - // val file = File("out/comment.html") - // println("No PR available (not running in CI?): writing built comment to ${file.absolutePath}") - // file.writeText(commentBuilder.body) - // } else { - // val comments = pullRequest!!.comments - // // Trying to fetch `github!!.myself` throws (in CI only): Exception in thread "main" org.kohsuke.github.HttpException: - // // {"message":"Resource not accessible by integration","documentation_url":"https://docs.github.com/rest/reference/users#get-the-authenticated-user"} - // // Let's make this conditional on some env variable that's unlikely to be set. - // // Do not use "CI" because that's commonly set during local development and testing. - // val author = if (env.containsKey("GITHUB_ACTION")) "github-actions[bot]" else github!!.myself.login - // val comment = comments.firstOrNull { - // it.user.login.equals(author) && - // it.body.startsWith(commentBuilder.title, ignoreCase = true) - // } - // if (comment != null) { - // println("Updating PR comment ${comment.htmlUrl} body") - // comment.update(commentBuilder.body) - // } else { - // println("Adding new PR comment to ${pullRequest!!.htmlUrl}") - // pullRequest!!.comment(commentBuilder.body) - // } - // } - // } + async addOrUpdateComment(commentBuilder: PrCommentBuilder): Promise { + console.log('Adding/updating PR comment'); + return consoleGroup(async () => { + /* Env var GITHUB_REF is only set if a branch or tag is available for the current CI event trigger type. + The ref given is fully-formed, meaning that + * for branches the format is refs/heads/, + * for pull requests it is refs/pull//merge, + * and for tags it is refs/tags/. + For example, refs/heads/feature-branch-1. + */ + let prNumber: number | undefined; + if (typeof process.env.GITHUB_REF == 'string' && process.env.GITHUB_REF.length > 0) { + if (process.env.GITHUB_REF.startsWith("refs/pull/")) { + prNumber = parseInt(process.env.GITHUB_REF.split('/')[2]); + } else { + const pr = await octokit.rest.pulls.list({ + ...defaultArgs, + base: await Git.baseBranch, + head: await Git.branch + }); + prNumber = pr.data.at(0)?.id; + } + } + + if (prNumber == undefined) { + console.log("No PR available (not running in CI?). Printing the PR comment instead:"); + console.log(commentBuilder.body); + return; + } + + // Determine the PR comment author: + // Trying to fetch `octokit.users.getAuthenticated()` throws (in CI only): + // {"message":"Resource not accessible by integration","documentation_url":"https://docs.github.com/rest/reference/users#get-the-authenticated-user"} + // Let's make this conditional on some env variable that's unlikely to be set locally but will be set in GH Actions. + // Do not use "CI" because that's commonly set during local development and testing. + const author = typeof process.env.GITHUB_ACTION == 'string' ? 'github-actions[bot]' : (await octokit.users.getAuthenticated()).data.login; + + // Try to find an existing comment by the author and title. + var comment = await (async () => { + for await (const comments of octokit.paginate.iterator(octokit.rest.issues.listComments, { + ...defaultArgs, + issue_number: prNumber, + })) { + const found = comments.data.find((comment) => { + return comment.user?.login == author + && comment.body != undefined + && comment.body.indexOf(commentBuilder.title) >= 0; + }); + if (found) return found; + } + return undefined; + })(); + + if (comment != undefined) { + console.log(`Updating PR comment ${comment.html_url} body`) + await octokit.rest.issues.updateComment({ + ...defaultArgs, + comment_id: comment.id, + body: commentBuilder.body, + }); + } else { + console.log(`Adding new PR comment to PR ${prNumber}`) + await octokit.rest.issues.createComment({ + ...defaultArgs, + issue_number: prNumber, + body: commentBuilder.body, + }); + } + }); + } } From c8a71b767caa5bfc156c4b5f11dc0b087dd78654 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Tue, 3 Jan 2023 16:58:49 +0100 Subject: [PATCH 085/113] improve pr comment --- packages/replay/metrics/configs/ci/process.ts | 7 ++-- .../replay/metrics/src/results/pr-comment.ts | 35 +++++++++++++++---- packages/replay/metrics/src/util/github.ts | 5 +-- 3 files changed, 34 insertions(+), 13 deletions(-) diff --git a/packages/replay/metrics/configs/ci/process.ts b/packages/replay/metrics/configs/ci/process.ts index b137577f2a6b..29afda5e5294 100644 --- a/packages/replay/metrics/configs/ci/process.ts +++ b/packages/replay/metrics/configs/ci/process.ts @@ -8,7 +8,6 @@ import { GitHub } from '../../src/util/github.js'; import { latestResultFile, previousResultsDir, baselineResultsDir, artifactName } from './env.js'; const latestResult = Result.readFromFile(latestResultFile); -console.debug(latestResult); await GitHub.downloadPreviousArtifact(await Git.baseBranch, baselineResultsDir, artifactName); await GitHub.downloadPreviousArtifact(await Git.branch, previousResultsDir, artifactName); @@ -21,14 +20,14 @@ const previousResults = new ResultsSet(previousResultsDir); const prComment = new PrCommentBuilder(); if (Git.baseBranch != Git.branch) { const baseResults = new ResultsSet(baselineResultsDir); - prComment.addCurrentResult(await ResultsAnalyzer.analyze(latestResult, baseResults), "Baseline"); + await prComment.addCurrentResult(await ResultsAnalyzer.analyze(latestResult, baseResults), "Baseline"); await prComment.addAdditionalResultsSet( `Baseline results on branch: ${Git.baseBranch}`, // We skip the first one here because it's already included as `Baseline` column above in addCurrentResult(). baseResults.items().slice(1, 10) ); } else { - prComment.addCurrentResult(await ResultsAnalyzer.analyze(latestResult, previousResults), "Previous"); + await prComment.addCurrentResult(await ResultsAnalyzer.analyze(latestResult, previousResults), "Previous"); } await prComment.addAdditionalResultsSet( @@ -36,7 +35,7 @@ await prComment.addAdditionalResultsSet( previousResults.items().slice(0, 10) ); -GitHub.addOrUpdateComment(prComment); +await GitHub.addOrUpdateComment(prComment); // Copy the latest test run results to the archived result dir. await previousResults.add(latestResultFile, true); diff --git a/packages/replay/metrics/src/results/pr-comment.ts b/packages/replay/metrics/src/results/pr-comment.ts index 8eff29891fd8..462a9a4e6288 100644 --- a/packages/replay/metrics/src/results/pr-comment.ts +++ b/packages/replay/metrics/src/results/pr-comment.ts @@ -3,18 +3,39 @@ import { Analysis, AnalyzerItemMetric, ResultsAnalyzer } from "./analyzer.js"; import { Result } from "./result.js"; import { ResultSetItem } from "./results-set.js"; +function trimIndent(str: string): string { + return str.split('\n').map(s => s.trim()).join('\n'); +} + +function printableMetricName(metric: AnalyzerItemMetric): string { + switch (metric) { + case AnalyzerItemMetric.lcp: + return 'LCP'; + case AnalyzerItemMetric.cls: + return 'CLS'; + case AnalyzerItemMetric.cpu: + return 'CPU'; + case AnalyzerItemMetric.memoryAvg: + return 'JS heap avg'; + case AnalyzerItemMetric.memoryMax: + return 'JS heap max'; + default: + return AnalyzerItemMetric[metric]; + } +} + export class PrCommentBuilder { private buffer = ''; public get title(): string { - return '## Replay SDK metrics :rocket:'; + return 'Replay SDK metrics :rocket:'; } public get body(): string { - return this.buffer; + return trimIndent(this.buffer); } - public addCurrentResult(analysis: Analysis, otherName: string): void { + public async addCurrentResult(analysis: Analysis, otherName: string): Promise { // Decides whether to print the "Other" depending on it being set in the input data. const maybeOther = function (content: () => string): string { if (analysis.otherHash == undefined) { @@ -24,18 +45,18 @@ export class PrCommentBuilder { } this.buffer += ` - ${this.title} +

${this.title}

- + ${maybeOther(() => '')} ` for (const item of analysis.items) { this.buffer += ` - + ${maybeOther(() => '')} ` @@ -63,7 +84,7 @@ export class PrCommentBuilder { // Add table header this.buffer += ''; for (const item of analysis.items) { - this.buffer += ``; + this.buffer += ``; } this.buffer += ''; } diff --git a/packages/replay/metrics/src/util/github.ts b/packages/replay/metrics/src/util/github.ts index 9a393a5819af..0c292513c72b 100644 --- a/packages/replay/metrics/src/util/github.ts +++ b/packages/replay/metrics/src/util/github.ts @@ -133,8 +133,9 @@ export const GitHub = { } if (prNumber == undefined) { - console.log("No PR available (not running in CI?). Printing the PR comment instead:"); - console.log(commentBuilder.body); + const file = 'out/comment.html'; + console.log(`No PR available (not running in CI?): writing built comment to ${path.resolve(file)}`); + fs.writeFileSync(file, commentBuilder.body); return; } From 78c6a949d695165b34245ab7a0ab5754cf0e22a9 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Tue, 3 Jan 2023 19:34:08 +0100 Subject: [PATCH 086/113] metrics: switch from puppeteer to playwright --- packages/replay/metrics/package.json | 2 +- packages/replay/metrics/src/collector.ts | 36 +- packages/replay/metrics/src/perf/cpu.ts | 8 +- packages/replay/metrics/src/perf/memory.ts | 8 +- packages/replay/metrics/src/perf/sampler.ts | 34 +- packages/replay/metrics/src/scenarios.ts | 8 +- packages/replay/metrics/src/vitals/cls.ts | 6 +- packages/replay/metrics/src/vitals/fid.ts | 6 +- packages/replay/metrics/src/vitals/index.ts | 4 +- packages/replay/metrics/src/vitals/lcp.ts | 6 +- packages/replay/metrics/yarn.lock | 410 +------------------- 11 files changed, 95 insertions(+), 433 deletions(-) diff --git a/packages/replay/metrics/package.json b/packages/replay/metrics/package.json index 29fb646a12cf..79fab58811ad 100644 --- a/packages/replay/metrics/package.json +++ b/packages/replay/metrics/package.json @@ -19,7 +19,7 @@ "axios": "^1.2.2", "extract-zip": "^2.0.1", "filesize": "^10.0.6", - "puppeteer": "^19.4.1", + "playwright": "^1.29.1", "simple-git": "^3.15.1", "simple-statistics": "^7.8.0", "typescript": "^4.9.4" diff --git a/packages/replay/metrics/src/collector.ts b/packages/replay/metrics/src/collector.ts index a33b0ca9bb93..432217ce085a 100644 --- a/packages/replay/metrics/src/collector.ts +++ b/packages/replay/metrics/src/collector.ts @@ -1,5 +1,5 @@ import assert from 'assert'; -import * as puppeteer from 'puppeteer'; +import * as playwright from 'playwright'; import { CpuUsage, CpuUsageSampler } from './perf/cpu.js'; import { JsHeapUsage, JsHeapUsageSampler } from './perf/memory.js'; @@ -11,6 +11,22 @@ import { WebVitals, WebVitalsCollector } from './vitals/index.js'; const cpuThrottling = 4; const networkConditions = 'Fast 3G'; +// Same as puppeteer-core PredefinedNetworkConditions +const PredefinedNetworkConditions = Object.freeze({ + 'Slow 3G': { + download: ((500 * 1000) / 8) * 0.8, + upload: ((500 * 1000) / 8) * 0.8, + latency: 400 * 5, + connectionType: 'cellular3g', + }, + 'Fast 3G': { + download: ((1.6 * 1000 * 1000) / 8) * 0.9, + upload: ((750 * 1000) / 8) * 0.9, + latency: 150 * 3.75, + connectionType: 'cellular3g', + }, +}); + export class Metrics { constructor(public readonly vitals: WebVitals, public readonly cpu: CpuUsage, public readonly memory: JsHeapUsage) { } @@ -30,7 +46,7 @@ export interface MetricsCollectorOptions { export class MetricsCollector { private options: MetricsCollectorOptions; - constructor(options: Partial) { + constructor(options?: Partial) { this.options = { headless: false, ...options @@ -75,18 +91,26 @@ export class MetricsCollector { private async run(scenario: Scenario): Promise { const disposeCallbacks: (() => Promise)[] = []; try { - const browser = await puppeteer.launch({ + const browser = await playwright.chromium.launch({ headless: this.options.headless, + }); disposeCallbacks.push(async () => browser.close()); const page = await browser.newPage(); + const cdp = await page.context().newCDPSession(page); + // Simulate throttling. - await page.emulateNetworkConditions(puppeteer.PredefinedNetworkConditions[networkConditions]); - await page.emulateCPUThrottling(cpuThrottling); + await cdp.send('Network.emulateNetworkConditions', { + offline: false, + latency: PredefinedNetworkConditions[networkConditions].latency, + uploadThroughput: PredefinedNetworkConditions[networkConditions].upload, + downloadThroughput: PredefinedNetworkConditions[networkConditions].download, + }); + await cdp.send('Emulation.setCPUThrottlingRate', { rate: cpuThrottling }); // Collect CPU and memory info 10 times per second. - const perfSampler = await PerfMetricsSampler.create(page, 100); + const perfSampler = await PerfMetricsSampler.create(cdp, 100); disposeCallbacks.push(async () => perfSampler.stop()); const cpuSampler = new CpuUsageSampler(perfSampler); const memSampler = new JsHeapUsageSampler(perfSampler); diff --git a/packages/replay/metrics/src/perf/cpu.ts b/packages/replay/metrics/src/perf/cpu.ts index 152d488a592b..29179c9d2a46 100644 --- a/packages/replay/metrics/src/perf/cpu.ts +++ b/packages/replay/metrics/src/perf/cpu.ts @@ -1,6 +1,4 @@ -import * as puppeteer from 'puppeteer'; - -import { PerfMetricsSampler, TimeBasedMap } from './sampler.js'; +import { PerfMetrics, PerfMetricsSampler, TimeBasedMap } from './sampler.js'; export { CpuUsageSampler, CpuUsage } @@ -35,8 +33,8 @@ class CpuUsageSampler { return new CpuUsage(this._snapshots, this._average); } - private async _collect(metrics: puppeteer.Metrics): Promise { - const data = new MetricsDataPoint(metrics.Timestamp!, metrics.TaskDuration! + metrics.TaskDuration! + metrics.LayoutDuration! + metrics.ScriptDuration!); + private async _collect(metrics: PerfMetrics): Promise { + const data = new MetricsDataPoint(metrics.Timestamp, metrics.Duration); if (this._initial == undefined) { this._initial = data; this._startTime = data.timestamp; diff --git a/packages/replay/metrics/src/perf/memory.ts b/packages/replay/metrics/src/perf/memory.ts index 6c6e907c5e75..3566622ccb0e 100644 --- a/packages/replay/metrics/src/perf/memory.ts +++ b/packages/replay/metrics/src/perf/memory.ts @@ -1,6 +1,4 @@ -import * as puppeteer from 'puppeteer'; - -import { PerfMetricsSampler, TimeBasedMap } from './sampler.js'; +import { PerfMetrics, PerfMetricsSampler, TimeBasedMap } from './sampler.js'; export { JsHeapUsageSampler, JsHeapUsage } @@ -23,7 +21,7 @@ class JsHeapUsageSampler { return new JsHeapUsage(this._snapshots); } - private async _collect(metrics: puppeteer.Metrics): Promise { - this._snapshots.set(metrics.Timestamp!, metrics.JSHeapUsedSize!); + private async _collect(metrics: PerfMetrics): Promise { + this._snapshots.set(metrics.Timestamp, metrics.JSHeapUsedSize!); } } diff --git a/packages/replay/metrics/src/perf/sampler.ts b/packages/replay/metrics/src/perf/sampler.ts index 80ce931045b2..58dac3516646 100644 --- a/packages/replay/metrics/src/perf/sampler.ts +++ b/packages/replay/metrics/src/perf/sampler.ts @@ -1,6 +1,7 @@ -import * as puppeteer from 'puppeteer'; +import * as playwright from 'playwright'; +import { Protocol } from 'playwright-core/types/protocol'; -export type PerfMetricsConsumer = (metrics: puppeteer.Metrics) => Promise; +export type PerfMetricsConsumer = (metrics: PerfMetrics) => Promise; export type TimestampSeconds = number; export class TimeBasedMap extends Map { @@ -18,22 +19,39 @@ export class TimeBasedMap extends Map { } } +export class PerfMetrics { + constructor(private metrics: Protocol.Performance.Metric[]) { } + + private find(name: string): number { + return this.metrics.find((metric) => metric.name == name)!.value; + } + + public get Timestamp(): number { + return this.find('Timestamp'); + } + + public get Duration(): number { + return this.metrics.reduce((sum, metric) => metric.name.endsWith('Duration') ? sum + metric.value : sum, 0); + } + + public get JSHeapUsedSize(): number { + return this.find('JSHeapUsedSize'); + } +} + export class PerfMetricsSampler { private _consumers: PerfMetricsConsumer[] = []; private _timer!: NodeJS.Timer; - public static async create(page: puppeteer.Page, interval: number): Promise { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - const cdp = await page.target().createCDPSession(); - + public static async create(cdp: playwright.CDPSession, interval: number): Promise { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access await cdp.send('Performance.enable', { timeDomain: 'timeTicks' }) const self = new PerfMetricsSampler(); self._timer = setInterval(async () => { - const metrics = await page.metrics(); - self._consumers.forEach((cb) => cb(metrics).catch(console.log)); + const metrics = await cdp.send("Performance.getMetrics").then((v) => v.metrics); + self._consumers.forEach((cb) => cb(new PerfMetrics(metrics)).catch(console.log)); }, interval); return self; diff --git a/packages/replay/metrics/src/scenarios.ts b/packages/replay/metrics/src/scenarios.ts index 1a9a31e74eb7..844f85fb573f 100644 --- a/packages/replay/metrics/src/scenarios.ts +++ b/packages/replay/metrics/src/scenarios.ts @@ -1,12 +1,12 @@ import path from 'path'; -import * as puppeteer from 'puppeteer'; +import * as playwright from 'playwright'; import * as fs from 'fs'; import { Metrics } from './collector'; import assert from 'assert'; // A testing scenario we want to collect metrics for. export interface Scenario { - run(browser: puppeteer.Browser, page: puppeteer.Page): Promise; + run(browser: playwright.Browser, page: playwright.Page): Promise; } // Two scenarios that are compared to each other. @@ -26,7 +26,7 @@ export interface TestCase { export class LoadPageScenario implements Scenario { public constructor(public url: string) { } - public async run(_: puppeteer.Browser, page: puppeteer.Page): Promise { + public async run(_: playwright.Browser, page: playwright.Page): Promise { await page.goto(this.url, { waitUntil: 'load', timeout: 60000 }); } } @@ -35,7 +35,7 @@ export class LoadPageScenario implements Scenario { export class JankTestScenario implements Scenario { public constructor(private withSentry: boolean) { } - public async run(_: puppeteer.Browser, page: puppeteer.Page): Promise { + public async run(_: playwright.Browser, page: playwright.Page): Promise { let url = path.resolve('./test-apps/jank/' + (this.withSentry ? 'with-sentry' : 'index') + '.html'); assert(fs.existsSync(url)); url = 'file:///' + url.replace('\\', '/'); diff --git a/packages/replay/metrics/src/vitals/cls.ts b/packages/replay/metrics/src/vitals/cls.ts index 57175011d52b..213c5ddef0e5 100644 --- a/packages/replay/metrics/src/vitals/cls.ts +++ b/packages/replay/metrics/src/vitals/cls.ts @@ -1,14 +1,14 @@ -import * as puppeteer from 'puppeteer'; +import * as playwright from 'playwright'; export { CLS }; // https://web.dev/cls/ class CLS { constructor( - private _page: puppeteer.Page) { } + private _page: playwright.Page) { } public async setup(): Promise { - await this._page.evaluateOnNewDocument(`{ + await this._page.context().addInitScript(`{ window.cumulativeLayoutShiftScore = undefined; const observer = new PerformanceObserver((list) => { diff --git a/packages/replay/metrics/src/vitals/fid.ts b/packages/replay/metrics/src/vitals/fid.ts index dac573010585..550a1d40fbee 100644 --- a/packages/replay/metrics/src/vitals/fid.ts +++ b/packages/replay/metrics/src/vitals/fid.ts @@ -1,14 +1,14 @@ -import * as puppeteer from 'puppeteer'; +import * as playwright from 'playwright'; export { FID }; // https://web.dev/fid/ class FID { constructor( - private _page: puppeteer.Page) { } + private _page: playwright.Page) { } public async setup(): Promise { - await this._page.evaluateOnNewDocument(`{ + await this._page.context().addInitScript(`{ window.firstInputDelay = undefined; const observer = new PerformanceObserver((entryList) => { diff --git a/packages/replay/metrics/src/vitals/index.ts b/packages/replay/metrics/src/vitals/index.ts index 68e08ba8877a..119af6622c83 100644 --- a/packages/replay/metrics/src/vitals/index.ts +++ b/packages/replay/metrics/src/vitals/index.ts @@ -1,4 +1,4 @@ -import * as puppeteer from 'puppeteer'; +import * as playwright from 'playwright'; import { CLS } from './cls.js'; import { FID } from './fid.js'; @@ -18,7 +18,7 @@ class WebVitalsCollector { private constructor(private _lcp: LCP, private _cls: CLS, private _fid: FID) { } - public static async create(page: puppeteer.Page): + public static async create(page: playwright.Page): Promise { const result = new WebVitalsCollector(new LCP(page), new CLS(page), new FID(page)); diff --git a/packages/replay/metrics/src/vitals/lcp.ts b/packages/replay/metrics/src/vitals/lcp.ts index 247ce0fcd3b6..4bc05143ddce 100644 --- a/packages/replay/metrics/src/vitals/lcp.ts +++ b/packages/replay/metrics/src/vitals/lcp.ts @@ -1,14 +1,14 @@ -import * as puppeteer from 'puppeteer'; +import * as playwright from 'playwright'; export { LCP }; // https://web.dev/lcp/ class LCP { constructor( - private _page: puppeteer.Page) { } + private _page: playwright.Page) { } public async setup(): Promise { - await this._page.evaluateOnNewDocument(`{ + await this._page.context().addInitScript(`{ window.largestContentfulPaint = undefined; const observer = new PerformanceObserver((list) => { diff --git a/packages/replay/metrics/yarn.lock b/packages/replay/metrics/yarn.lock index 976135fc15de..754a7ff63816 100644 --- a/packages/replay/metrics/yarn.lock +++ b/packages/replay/metrics/yarn.lock @@ -2,27 +2,6 @@ # yarn lockfile v1 -"@babel/code-frame@^7.0.0": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a" - integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q== - dependencies: - "@babel/highlight" "^7.18.6" - -"@babel/helper-validator-identifier@^7.18.6": - 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/highlight@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" - integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g== - dependencies: - "@babel/helper-validator-identifier" "^7.18.6" - chalk "^2.0.0" - js-tokens "^4.0.0" - "@cspotcode/source-map-support@^0.8.0": version "0.8.1" resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" @@ -203,30 +182,11 @@ acorn@^8.4.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.1.tgz#0a3f9cbecc4ec3bea6f0a80b66ae8dd2da250b73" integrity sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA== -agent-base@6: - version "6.0.2" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" - integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== - dependencies: - debug "4" - -ansi-styles@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" - integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== - dependencies: - color-convert "^1.9.0" - arg@^4.1.0: version "4.1.3" resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== -argparse@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" - integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== - asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -241,82 +201,16 @@ axios@^1.2.2: form-data "^4.0.0" proxy-from-env "^1.1.0" -balanced-match@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== - -base64-js@^1.3.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" - integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== - before-after-hook@^2.2.0: version "2.2.3" resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.2.3.tgz#c51e809c81a4e354084422b9b26bad88249c517c" integrity sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ== -bl@^4.0.3: - version "4.1.0" - resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" - integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== - dependencies: - buffer "^5.5.0" - inherits "^2.0.4" - readable-stream "^3.4.0" - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - buffer-crc32@~0.2.3: version "0.2.13" resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== -buffer@^5.2.1, buffer@^5.5.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" - integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.1.13" - -callsites@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" - integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== - -chalk@^2.0.0: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - -chownr@^1.1.1: - version "1.1.4" - resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" - integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== - -color-convert@^1.9.0: - version "1.9.3" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== - dependencies: - color-name "1.1.3" - -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== - combined-stream@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" @@ -324,34 +218,12 @@ combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== - -cosmiconfig@8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.0.0.tgz#e9feae014eab580f858f8a0288f38997a7bebe97" - integrity sha512-da1EafcpH6b/TD8vDRaWV7xFINlHlF6zKsGwS1TsuVJTZRkquaS5HTMq7uq6h31619QjbsYl21gVDOm32KM1vQ== - dependencies: - import-fresh "^3.2.1" - js-yaml "^4.1.0" - parse-json "^5.0.0" - path-type "^4.0.0" - create-require@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== -cross-fetch@3.1.5: - version "3.1.5" - resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" - integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw== - dependencies: - node-fetch "2.6.7" - -debug@4, debug@4.3.4, debug@^4.1.1, debug@^4.3.4: +debug@^4.1.1, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -368,36 +240,19 @@ deprecation@^2.0.0, deprecation@^2.3.1: resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919" integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ== -devtools-protocol@0.0.1068969: - version "0.0.1068969" - resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.1068969.tgz#8b9a4bc48aed1453bed08d62b07481f9abf4d6d8" - integrity sha512-ATFTrPbY1dKYhPPvpjtwWKSK2mIwGmRwX54UASn9THEuIZCe2n9k3vVuMmt6jWeL+e5QaaguEv/pMyR+JQB7VQ== - diff@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== -end-of-stream@^1.1.0, end-of-stream@^1.4.1: +end-of-stream@^1.1.0: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== dependencies: once "^1.4.0" -error-ex@^1.3.1: - version "1.3.2" - resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" - integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== - dependencies: - is-arrayish "^0.2.1" - -escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== - -extract-zip@2.0.1, extract-zip@^2.0.1: +extract-zip@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== @@ -434,16 +289,6 @@ form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" -fs-constants@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" - integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== - get-stream@^5.1.0: version "5.2.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" @@ -451,89 +296,11 @@ get-stream@^5.1.0: dependencies: pump "^3.0.0" -glob@^7.1.3: - version "7.2.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" - integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.1.1" - once "^1.3.0" - path-is-absolute "^1.0.0" - -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== - -https-proxy-agent@5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" - integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== - dependencies: - agent-base "6" - debug "4" - -ieee754@^1.1.13: - version "1.2.1" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" - integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== - -import-fresh@^3.2.1: - version "3.3.0" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" - integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== - dependencies: - parent-module "^1.0.0" - resolve-from "^4.0.0" - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@^2.0.3, inherits@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -is-arrayish@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" - integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== - is-plain-object@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== -js-tokens@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" - integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== - -js-yaml@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" - integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== - dependencies: - argparse "^2.0.1" - -json-parse-even-better-errors@^2.3.0: - version "2.3.1" - resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" - integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== - -lines-and-columns@^1.1.6: - version "1.2.4" - resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" - integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== - make-error@^1.1.1: version "1.3.6" resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" @@ -551,75 +318,43 @@ mime-types@^2.1.12: dependencies: mime-db "1.52.0" -minimatch@^3.1.1: - version "3.1.2" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== - dependencies: - brace-expansion "^1.1.7" - -mkdirp-classic@^0.5.2: - version "0.5.3" - resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" - integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== - ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -node-fetch@2.6.7, node-fetch@^2.6.7: +node-fetch@^2.6.7: version "2.6.7" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== dependencies: whatwg-url "^5.0.0" -once@^1.3.0, once@^1.3.1, once@^1.4.0: +once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== dependencies: wrappy "1" -parent-module@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" - integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== - dependencies: - callsites "^3.0.0" - -parse-json@^5.0.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" - integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== - dependencies: - "@babel/code-frame" "^7.0.0" - error-ex "^1.3.1" - json-parse-even-better-errors "^2.3.0" - lines-and-columns "^1.1.6" - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== - -path-type@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" - integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== - pend@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg== -progress@2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" - integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== +playwright-core@1.29.1: + version "1.29.1" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.29.1.tgz#9ec15d61c4bd2f386ddf6ce010db53a030345a47" + integrity sha512-20Ai3d+lMkWpI9YZYlxk8gxatfgax5STW8GaMozAHwigLiyiKQrdkt7gaoT9UQR8FIVDg6qVXs9IoZUQrDjIIg== -proxy-from-env@1.1.0, proxy-from-env@^1.1.0: +playwright@^1.29.1: + version "1.29.1" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.29.1.tgz#fc04b34f42e3bfc0edadb1c45ef9bffd53c21f70" + integrity sha512-lasC+pMqsQ2uWhNurt3YK3xo0gWlMjslYUylKbHcqF/NTjwp9KStRGO7S6wwz2f52GcSnop8XUK/GymJjdzrxw== + dependencies: + playwright-core "1.29.1" + +proxy-from-env@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== @@ -632,59 +367,6 @@ pump@^3.0.0: end-of-stream "^1.1.0" once "^1.3.1" -puppeteer-core@19.4.1: - version "19.4.1" - resolved "https://registry.yarnpkg.com/puppeteer-core/-/puppeteer-core-19.4.1.tgz#f4875943841ebdb6fc2ad7a475add958692b0237" - integrity sha512-JHIuqtqrUAx4jGOTxXu4ilapV2jabxtVMA/e4wwFUMvtSsqK4nVBSI+Z1SKDoz7gRy/JUIc8WzmfocCa6SIZ1w== - dependencies: - cross-fetch "3.1.5" - debug "4.3.4" - devtools-protocol "0.0.1068969" - extract-zip "2.0.1" - https-proxy-agent "5.0.1" - proxy-from-env "1.1.0" - rimraf "3.0.2" - tar-fs "2.1.1" - unbzip2-stream "1.4.3" - ws "8.11.0" - -puppeteer@^19.4.1: - version "19.4.1" - resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-19.4.1.tgz#cac7d3f0084badebb8ebacbe6f4d7262e7f21818" - integrity sha512-PCnrR13B8A+VSEDXRmrNXRZbrkF1tfsI1hKSC7vs13eNS6CUD3Y4FA8SF8/VZy+Pm1kg5AggJT2Nu3HLAtGkFg== - dependencies: - cosmiconfig "8.0.0" - https-proxy-agent "5.0.1" - progress "2.0.3" - proxy-from-env "1.1.0" - puppeteer-core "19.4.1" - -readable-stream@^3.1.1, readable-stream@^3.4.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" - integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -resolve-from@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" - integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== - -rimraf@3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" - integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== - dependencies: - glob "^7.1.3" - -safe-buffer@~5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - simple-git@^3.15.1: version "3.15.1" resolved "https://registry.yarnpkg.com/simple-git/-/simple-git-3.15.1.tgz#57f595682cb0c2475d5056da078a05c8715a25ef" @@ -699,46 +381,6 @@ simple-statistics@^7.8.0: resolved "https://registry.yarnpkg.com/simple-statistics/-/simple-statistics-7.8.0.tgz#1033d2d613656c7bd34f0e134fd7e69c803e6836" integrity sha512-lTWbfJc0u6GZhBojLOrlHJMTHu6PdUjSsYLrpiH902dVBiYJyWlN/LdSoG8b5VvfG1D30gIBgarqMNeNmU5nAA== -string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -supports-color@^5.3.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - -tar-fs@2.1.1: - 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-stream@^2.1.4: - version "2.2.0" - resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" - integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== - dependencies: - bl "^4.0.3" - end-of-stream "^1.4.1" - fs-constants "^1.0.0" - inherits "^2.0.3" - readable-stream "^3.1.1" - -through@^2.3.8: - version "2.3.8" - resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" - integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== - tr46@~0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" @@ -768,24 +410,11 @@ typescript@^4.9.4: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.4.tgz#a2a3d2756c079abda241d75f149df9d561091e78" integrity sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg== -unbzip2-stream@1.4.3: - version "1.4.3" - resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7" - integrity sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg== - dependencies: - buffer "^5.2.1" - through "^2.3.8" - universal-user-agent@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-6.0.0.tgz#3381f8503b251c0d9cd21bc1de939ec9df5480ee" integrity sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w== -util-deprecate@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== - 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" @@ -809,11 +438,6 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== -ws@8.11.0: - version "8.11.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143" - integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg== - yauzl@^2.10.0: version "2.10.0" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" From cef616c662d0ec510cd13c78691caefe795dd423 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Tue, 3 Jan 2023 20:54:29 +0100 Subject: [PATCH 087/113] fix PR commenting --- packages/replay/metrics/src/util/github.ts | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/replay/metrics/src/util/github.ts b/packages/replay/metrics/src/util/github.ts index 0c292513c72b..d00a8d8d41bf 100644 --- a/packages/replay/metrics/src/util/github.ts +++ b/packages/replay/metrics/src/util/github.ts @@ -119,16 +119,18 @@ export const GitHub = { For example, refs/heads/feature-branch-1. */ let prNumber: number | undefined; - if (typeof process.env.GITHUB_REF == 'string' && process.env.GITHUB_REF.length > 0) { - if (process.env.GITHUB_REF.startsWith("refs/pull/")) { - prNumber = parseInt(process.env.GITHUB_REF.split('/')[2]); - } else { - const pr = await octokit.rest.pulls.list({ - ...defaultArgs, - base: await Git.baseBranch, - head: await Git.branch - }); - prNumber = pr.data.at(0)?.id; + if (typeof process.env.GITHUB_REF == 'string' && process.env.GITHUB_REF.length > 0 && process.env.GITHUB_REF.startsWith("refs/pull/")) { + prNumber = parseInt(process.env.GITHUB_REF.split('/')[2]); + console.log(`Determined PR number ${prNumber} based on GITHUB_REF environment variable: '${process.env.GITHUB_REF}'`); + } else { + const pr = await octokit.rest.pulls.list({ + ...defaultArgs, + base: await Git.baseBranch, + head: await Git.branch + }); + prNumber = pr.data.at(0)?.number; + if (prNumber != undefined) { + console.log(`Found PR number ${prNumber} based on base and head branches`); } } From 9ee39437fbd4800f52985dd5f66069e1f00be73a Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Tue, 3 Jan 2023 21:10:03 +0100 Subject: [PATCH 088/113] first self-review --- packages/replay/metrics/README.md | 2 +- packages/replay/metrics/src/perf/sampler.ts | 1 + packages/replay/metrics/src/results/metrics-stats.ts | 11 ++--------- packages/replay/metrics/src/util/console.ts | 7 +------ 4 files changed, 5 insertions(+), 16 deletions(-) diff --git a/packages/replay/metrics/README.md b/packages/replay/metrics/README.md index bbf8d8cddcf5..dcf78671fba6 100644 --- a/packages/replay/metrics/README.md +++ b/packages/replay/metrics/README.md @@ -1,6 +1,6 @@ # Replay performance metrics -Evaluates Replay impact on website performance by running a web app in Chromium via Puppeteer and collecting various metrics. +Evaluates Replay impact on website performance by running a web app in Chromium via Playwright and collecting various metrics. ## Resources diff --git a/packages/replay/metrics/src/perf/sampler.ts b/packages/replay/metrics/src/perf/sampler.ts index 58dac3516646..ed585da9a0b3 100644 --- a/packages/replay/metrics/src/perf/sampler.ts +++ b/packages/replay/metrics/src/perf/sampler.ts @@ -31,6 +31,7 @@ export class PerfMetrics { } public get Duration(): number { + // TODO check if any of `Duration` fields is maybe a sum of the others. E.g. verify the measured CPU usage manually. return this.metrics.reduce((sum, metric) => metric.name.endsWith('Duration') ? sum + metric.value : sum, 0); } diff --git a/packages/replay/metrics/src/results/metrics-stats.ts b/packages/replay/metrics/src/results/metrics-stats.ts index ec4071caab88..acec86c6973c 100644 --- a/packages/replay/metrics/src/results/metrics-stats.ts +++ b/packages/replay/metrics/src/results/metrics-stats.ts @@ -6,15 +6,8 @@ export type NumberProvider = (metrics: Metrics) => number; export class MetricsStats { constructor(private items: Metrics[]) { } - // See https://en.wikipedia.org/wiki/Interquartile_range#Outliers for details - public filterOutliers(dataProvider: NumberProvider): number[] { - let numbers = this.items.map(dataProvider); - // TODO implement, see https://github.com/getsentry/action-app-sdk-overhead-metrics/blob/9ce7d562ff79b317688d22bd5c0bb725cbdfdb81/src/test/kotlin/StartupTimeTest.kt#L27-L37 - return numbers; - } - public filteredMean(dataProvider: NumberProvider): number | undefined { - const numbers = this.filterOutliers(dataProvider); + const numbers = this.items.map(dataProvider); return numbers.length > 0 ? ss.mean(numbers) : undefined; } @@ -35,7 +28,7 @@ export class MetricsStats { } public get memoryMax(): number | undefined { - const numbers = this.filterOutliers((metrics) => ss.max(Array.from(metrics.memory.snapshots.values()))); + const numbers = this.items.map((metrics) => ss.max(Array.from(metrics.memory.snapshots.values()))); return numbers.length > 0 ? ss.max(numbers) : undefined; } } diff --git a/packages/replay/metrics/src/util/console.ts b/packages/replay/metrics/src/util/console.ts index a1cef832db72..3ceaac1c862b 100644 --- a/packages/replay/metrics/src/util/console.ts +++ b/packages/replay/metrics/src/util/console.ts @@ -1,10 +1,5 @@ export async function consoleGroup(code: () => Promise): Promise { console.group(); - try { - - return await code(); - } finally { - console.groupEnd(); - } + return code().finally(console.groupEnd); } From 6133883c049b44880852d8163e3b2bcfd7c6396e Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Tue, 3 Jan 2023 21:46:32 +0100 Subject: [PATCH 089/113] fixes --- packages/replay/metrics/configs/ci/process.ts | 12 +++++++----- packages/replay/metrics/src/results/pr-comment.ts | 4 ++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/replay/metrics/configs/ci/process.ts b/packages/replay/metrics/configs/ci/process.ts index 29afda5e5294..def9dd2491b4 100644 --- a/packages/replay/metrics/configs/ci/process.ts +++ b/packages/replay/metrics/configs/ci/process.ts @@ -8,9 +8,11 @@ import { GitHub } from '../../src/util/github.js'; import { latestResultFile, previousResultsDir, baselineResultsDir, artifactName } from './env.js'; const latestResult = Result.readFromFile(latestResultFile); +const branch = await Git.branch; +const baseBranch = await Git.baseBranch; -await GitHub.downloadPreviousArtifact(await Git.baseBranch, baselineResultsDir, artifactName); -await GitHub.downloadPreviousArtifact(await Git.branch, previousResultsDir, artifactName); +await GitHub.downloadPreviousArtifact(baseBranch, baselineResultsDir, artifactName); +await GitHub.downloadPreviousArtifact(branch, previousResultsDir, artifactName); GitHub.writeOutput("artifactName", artifactName) GitHub.writeOutput("artifactPath", path.resolve(previousResultsDir)); @@ -18,11 +20,11 @@ GitHub.writeOutput("artifactPath", path.resolve(previousResultsDir)); const previousResults = new ResultsSet(previousResultsDir); const prComment = new PrCommentBuilder(); -if (Git.baseBranch != Git.branch) { +if (baseBranch != branch) { const baseResults = new ResultsSet(baselineResultsDir); await prComment.addCurrentResult(await ResultsAnalyzer.analyze(latestResult, baseResults), "Baseline"); await prComment.addAdditionalResultsSet( - `Baseline results on branch: ${Git.baseBranch}`, + `Baseline results on branch: ${baseBranch}`, // We skip the first one here because it's already included as `Baseline` column above in addCurrentResult(). baseResults.items().slice(1, 10) ); @@ -31,7 +33,7 @@ if (Git.baseBranch != Git.branch) { } await prComment.addAdditionalResultsSet( - `Previous results on branch: ${Git.branch}`, + `Previous results on branch: ${branch}`, previousResults.items().slice(0, 10) ); diff --git a/packages/replay/metrics/src/results/pr-comment.ts b/packages/replay/metrics/src/results/pr-comment.ts index 462a9a4e6288..704992583306 100644 --- a/packages/replay/metrics/src/results/pr-comment.ts +++ b/packages/replay/metrics/src/results/pr-comment.ts @@ -49,8 +49,8 @@ export class PrCommentBuilder {
 Latest diff (${Git.hash})Latest diff (${await Git.hash})' + otherName + ' diff (' + analysis.otherHash + ')
${AnalyzerItemMetric[item.metric]}${printableMetricName(item.metric)} ${item.value.asString()}' + item.other!.asString() + '
Revision${AnalyzerItemMetric[item.metric]}${printableMetricName(item.metric)}
- - ${maybeOther(() => '')} + + ${maybeOther(() => '')} ` for (const item of analysis.items) { From d4d7f3be5879090962f63993c724ed1127db11e5 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Wed, 4 Jan 2023 11:20:29 +0100 Subject: [PATCH 090/113] chore - metrics linter issues --- .../metrics/{.eslintrc.js => .eslintrc.cjs} | 2 + packages/replay/metrics/configs/ci/env.ts | 2 +- packages/replay/metrics/configs/ci/process.ts | 11 ++--- .../replay/metrics/configs/dev/process.ts | 13 +++--- packages/replay/metrics/package.json | 1 + packages/replay/metrics/src/collector.ts | 26 ++++++------ packages/replay/metrics/src/perf/cpu.ts | 11 +++-- packages/replay/metrics/src/perf/memory.ts | 9 +++-- packages/replay/metrics/src/perf/sampler.ts | 30 +++++++------- .../replay/metrics/src/results/analyzer.ts | 25 ++++++------ .../metrics/src/results/metrics-stats.ts | 9 +++-- .../replay/metrics/src/results/pr-comment.ts | 40 +++++++++---------- packages/replay/metrics/src/results/result.ts | 37 +++++++---------- .../replay/metrics/src/results/results-set.ts | 34 ++++++++-------- packages/replay/metrics/src/scenarios.ts | 11 ++--- packages/replay/metrics/src/util/git.ts | 2 +- packages/replay/metrics/src/util/github.ts | 31 +++++++------- packages/replay/metrics/src/util/json.ts | 14 +++++++ packages/replay/metrics/yarn.lock | 2 +- 19 files changed, 166 insertions(+), 144 deletions(-) rename packages/replay/metrics/{.eslintrc.js => .eslintrc.cjs} (75%) create mode 100644 packages/replay/metrics/src/util/json.ts diff --git a/packages/replay/metrics/.eslintrc.js b/packages/replay/metrics/.eslintrc.cjs similarity index 75% rename from packages/replay/metrics/.eslintrc.js rename to packages/replay/metrics/.eslintrc.cjs index 3c4c14a69860..9f90433a8fa8 100644 --- a/packages/replay/metrics/.eslintrc.js +++ b/packages/replay/metrics/.eslintrc.cjs @@ -1,11 +1,13 @@ module.exports = { extends: ['../.eslintrc.js'], + ignorePatterns: ['test-apps'], overrides: [ { files: ['*.ts'], rules: { 'no-console': 'off', '@typescript-eslint/no-non-null-assertion': 'off', + 'import/no-unresolved': 'off', }, }, ], diff --git a/packages/replay/metrics/configs/ci/env.ts b/packages/replay/metrics/configs/ci/env.ts index e3492ced1056..c41e4bcdf6c3 100644 --- a/packages/replay/metrics/configs/ci/env.ts +++ b/packages/replay/metrics/configs/ci/env.ts @@ -1,4 +1,4 @@ export const previousResultsDir = 'out/previous-results'; export const baselineResultsDir = 'out/baseline-results'; export const latestResultFile = 'out/latest-result.json'; -export const artifactName = "replay-sdk-metrics" +export const artifactName = 'replay-sdk-metrics' diff --git a/packages/replay/metrics/configs/ci/process.ts b/packages/replay/metrics/configs/ci/process.ts index def9dd2491b4..aa6686a98237 100644 --- a/packages/replay/metrics/configs/ci/process.ts +++ b/packages/replay/metrics/configs/ci/process.ts @@ -1,11 +1,12 @@ import path from 'path'; + import { ResultsAnalyzer } from '../../src/results/analyzer.js'; import { PrCommentBuilder } from '../../src/results/pr-comment.js'; import { Result } from '../../src/results/result.js'; import { ResultsSet } from '../../src/results/results-set.js'; import { Git } from '../../src/util/git.js'; import { GitHub } from '../../src/util/github.js'; -import { latestResultFile, previousResultsDir, baselineResultsDir, artifactName } from './env.js'; +import { artifactName,baselineResultsDir, latestResultFile, previousResultsDir } from './env.js'; const latestResult = Result.readFromFile(latestResultFile); const branch = await Git.branch; @@ -14,22 +15,22 @@ const baseBranch = await Git.baseBranch; await GitHub.downloadPreviousArtifact(baseBranch, baselineResultsDir, artifactName); await GitHub.downloadPreviousArtifact(branch, previousResultsDir, artifactName); -GitHub.writeOutput("artifactName", artifactName) -GitHub.writeOutput("artifactPath", path.resolve(previousResultsDir)); +GitHub.writeOutput('artifactName', artifactName) +GitHub.writeOutput('artifactPath', path.resolve(previousResultsDir)); const previousResults = new ResultsSet(previousResultsDir); const prComment = new PrCommentBuilder(); if (baseBranch != branch) { const baseResults = new ResultsSet(baselineResultsDir); - await prComment.addCurrentResult(await ResultsAnalyzer.analyze(latestResult, baseResults), "Baseline"); + await prComment.addCurrentResult(await ResultsAnalyzer.analyze(latestResult, baseResults), 'Baseline'); await prComment.addAdditionalResultsSet( `Baseline results on branch: ${baseBranch}`, // We skip the first one here because it's already included as `Baseline` column above in addCurrentResult(). baseResults.items().slice(1, 10) ); } else { - await prComment.addCurrentResult(await ResultsAnalyzer.analyze(latestResult, previousResults), "Previous"); + await prComment.addCurrentResult(await ResultsAnalyzer.analyze(latestResult, previousResults), 'Previous'); } await prComment.addAdditionalResultsSet( diff --git a/packages/replay/metrics/configs/dev/process.ts b/packages/replay/metrics/configs/dev/process.ts index 9672243cf804..ac19291d1e48 100644 --- a/packages/replay/metrics/configs/dev/process.ts +++ b/packages/replay/metrics/configs/dev/process.ts @@ -8,14 +8,15 @@ const latestResult = Result.readFromFile(latestResultFile); const analysis = await ResultsAnalyzer.analyze(latestResult, resultsSet); +// eslint-disable-next-line @typescript-eslint/no-explicit-any const table: { [k: string]: any } = {}; for (const item of analysis.items) { - const printable: { [k: string]: any } = {}; - printable.value = item.value.asString(); - if (item.other != undefined) { - printable.previous = item.other.asString(); - } - table[AnalyzerItemMetric[item.metric]] = printable; + table[AnalyzerItemMetric[item.metric]] = { + value: item.value.asString(), + ...((item.other == undefined) ? {} : { + previous: item.other.asString() + }) + }; } console.table(table); diff --git a/packages/replay/metrics/package.json b/packages/replay/metrics/package.json index 79fab58811ad..e4163257018b 100644 --- a/packages/replay/metrics/package.json +++ b/packages/replay/metrics/package.json @@ -20,6 +20,7 @@ "extract-zip": "^2.0.1", "filesize": "^10.0.6", "playwright": "^1.29.1", + "playwright-core": "^1.29.1", "simple-git": "^3.15.1", "simple-statistics": "^7.8.0", "typescript": "^4.9.4" diff --git a/packages/replay/metrics/src/collector.ts b/packages/replay/metrics/src/collector.ts index 432217ce085a..394dd2b8c956 100644 --- a/packages/replay/metrics/src/collector.ts +++ b/packages/replay/metrics/src/collector.ts @@ -1,8 +1,8 @@ import assert from 'assert'; import * as playwright from 'playwright'; -import { CpuUsage, CpuUsageSampler } from './perf/cpu.js'; -import { JsHeapUsage, JsHeapUsageSampler } from './perf/memory.js'; +import { CpuUsage, CpuUsageSampler, CpuUsageSerialized } from './perf/cpu.js'; +import { JsHeapUsage, JsHeapUsageSampler, JsHeapUsageSerialized } from './perf/memory.js'; import { PerfMetricsSampler } from './perf/sampler.js'; import { Result } from './results/result.js'; import { Scenario, TestCase } from './scenarios.js'; @@ -30,7 +30,7 @@ const PredefinedNetworkConditions = Object.freeze({ export class Metrics { constructor(public readonly vitals: WebVitals, public readonly cpu: CpuUsage, public readonly memory: JsHeapUsage) { } - public static fromJSON(data: Partial): Metrics { + public static fromJSON(data: Partial<{ vitals: Partial, cpu: CpuUsageSerialized, memory: JsHeapUsageSerialized }>): Metrics { return new Metrics( WebVitals.fromJSON(data.vitals || {}), CpuUsage.fromJSON(data.cpu || {}), @@ -44,10 +44,10 @@ export interface MetricsCollectorOptions { } export class MetricsCollector { - private options: MetricsCollectorOptions; + private _options: MetricsCollectorOptions; constructor(options?: Partial) { - this.options = { + this._options = { headless: false, ...options }; @@ -57,8 +57,8 @@ export class MetricsCollector { console.log(`Executing test case ${testCase.name}`); console.group(); for (let i = 1; i <= testCase.tries; i++) { - let aResults = await this.collect('A', testCase.a, testCase.runs); - let bResults = await this.collect('B', testCase.b, testCase.runs); + const aResults = await this._collect('A', testCase.a, testCase.runs); + const bResults = await this._collect('B', testCase.b, testCase.runs); if (await testCase.test(aResults, bResults)) { console.groupEnd(); console.log(`Test case ${testCase.name} passed on try ${i}/${testCase.tries}`); @@ -73,26 +73,26 @@ export class MetricsCollector { throw `Test case execution ${testCase.name} failed after ${testCase.tries} tries.`; } - private async collect(name: string, scenario: Scenario, runs: number): Promise { + private async _collect(name: string, scenario: Scenario, runs: number): Promise { const label = `Scenario ${name} data collection (total ${runs} runs)`; console.time(label); const results: Metrics[] = []; for (let run = 0; run < runs; run++) { - let innerLabel = `Scenario ${name} data collection, run ${run}/${runs}`; + const innerLabel = `Scenario ${name} data collection, run ${run}/${runs}`; console.time(innerLabel); - results.push(await this.run(scenario)); + results.push(await this._run(scenario)); console.timeEnd(innerLabel); } console.timeEnd(label); - assert.equal(results.length, runs); + assert.strictEqual(results.length, runs); return results; } - private async run(scenario: Scenario): Promise { + private async _run(scenario: Scenario): Promise { const disposeCallbacks: (() => Promise)[] = []; try { const browser = await playwright.chromium.launch({ - headless: this.options.headless, + headless: this._options.headless, }); disposeCallbacks.push(async () => browser.close()); diff --git a/packages/replay/metrics/src/perf/cpu.ts b/packages/replay/metrics/src/perf/cpu.ts index 29179c9d2a46..cd64fd6038f5 100644 --- a/packages/replay/metrics/src/perf/cpu.ts +++ b/packages/replay/metrics/src/perf/cpu.ts @@ -1,13 +1,16 @@ +import { JsonObject } from '../util/json.js'; import { PerfMetrics, PerfMetricsSampler, TimeBasedMap } from './sampler.js'; export { CpuUsageSampler, CpuUsage } +export type CpuUsageSerialized = Partial<{ snapshots: JsonObject, average: number }>; + class CpuUsage { constructor(public snapshots: TimeBasedMap, public average: number) { }; - public static fromJSON(data: Partial): CpuUsage { + public static fromJSON(data: CpuUsageSerialized): CpuUsage { return new CpuUsage( - TimeBasedMap.fromJSON(data.snapshots || []), + TimeBasedMap.fromJSON(data.snapshots || {}), data.average as number, ); } @@ -18,7 +21,7 @@ class MetricsDataPoint { } class CpuUsageSampler { - private _snapshots = new TimeBasedMap(); + private _snapshots: TimeBasedMap = new TimeBasedMap(); private _average: number = 0; private _initial?: MetricsDataPoint = undefined; private _startTime!: number; @@ -40,7 +43,7 @@ class CpuUsageSampler { this._startTime = data.timestamp; } else { const frameDuration = data.timestamp - this._lastTimestamp; - let usage = frameDuration == 0 ? 0 : (data.activeTime - this._cumulativeActiveTime) / frameDuration; + const usage = frameDuration == 0 ? 0 : (data.activeTime - this._cumulativeActiveTime) / frameDuration; this._snapshots.set(data.timestamp, usage); this._average = data.activeTime / (data.timestamp - this._startTime); diff --git a/packages/replay/metrics/src/perf/memory.ts b/packages/replay/metrics/src/perf/memory.ts index 3566622ccb0e..97ad3a490e04 100644 --- a/packages/replay/metrics/src/perf/memory.ts +++ b/packages/replay/metrics/src/perf/memory.ts @@ -1,17 +1,20 @@ +import { JsonObject } from '../util/json.js'; import { PerfMetrics, PerfMetricsSampler, TimeBasedMap } from './sampler.js'; export { JsHeapUsageSampler, JsHeapUsage } +export type JsHeapUsageSerialized = Partial<{ snapshots: JsonObject }>; + class JsHeapUsage { public constructor(public snapshots: TimeBasedMap) { } - public static fromJSON(data: Partial): JsHeapUsage { - return new JsHeapUsage(TimeBasedMap.fromJSON(data.snapshots || [])); + public static fromJSON(data: JsHeapUsageSerialized): JsHeapUsage { + return new JsHeapUsage(TimeBasedMap.fromJSON(data.snapshots || {})); } } class JsHeapUsageSampler { - private _snapshots = new TimeBasedMap(); + private _snapshots: TimeBasedMap = new TimeBasedMap(); public constructor(sampler: PerfMetricsSampler) { sampler.subscribe(this._collect.bind(this)); diff --git a/packages/replay/metrics/src/perf/sampler.ts b/packages/replay/metrics/src/perf/sampler.ts index ed585da9a0b3..e50ce3dd02b4 100644 --- a/packages/replay/metrics/src/perf/sampler.ts +++ b/packages/replay/metrics/src/perf/sampler.ts @@ -1,42 +1,44 @@ import * as playwright from 'playwright'; import { Protocol } from 'playwright-core/types/protocol'; +import { JsonObject } from '../util/json'; + export type PerfMetricsConsumer = (metrics: PerfMetrics) => Promise; export type TimestampSeconds = number; export class TimeBasedMap extends Map { - public toJSON(): any { - return Object.fromEntries(this.entries()); - } - - public static fromJSON(entries: Object): TimeBasedMap { + public static fromJSON(entries: JsonObject): TimeBasedMap { const result = new TimeBasedMap(); + // eslint-disable-next-line guard-for-in for (const key in entries) { - const value = entries[key as keyof Object]; - result.set(parseFloat(key), value as T); + result.set(parseFloat(key), entries[key]); } return result; } + + public toJSON(): JsonObject { + return Object.fromEntries(this.entries()); + } } export class PerfMetrics { - constructor(private metrics: Protocol.Performance.Metric[]) { } + constructor(private _metrics: Protocol.Performance.Metric[]) { } - private find(name: string): number { - return this.metrics.find((metric) => metric.name == name)!.value; + private _find(name: string): number { + return this._metrics.find((metric) => metric.name == name)!.value; } public get Timestamp(): number { - return this.find('Timestamp'); + return this._find('Timestamp'); } public get Duration(): number { // TODO check if any of `Duration` fields is maybe a sum of the others. E.g. verify the measured CPU usage manually. - return this.metrics.reduce((sum, metric) => metric.name.endsWith('Duration') ? sum + metric.value : sum, 0); + return this._metrics.reduce((sum, metric) => metric.name.endsWith('Duration') ? sum + metric.value : sum, 0); } public get JSHeapUsedSize(): number { - return this.find('JSHeapUsedSize'); + return this._find('JSHeapUsedSize'); } } @@ -51,7 +53,7 @@ export class PerfMetricsSampler { const self = new PerfMetricsSampler(); self._timer = setInterval(async () => { - const metrics = await cdp.send("Performance.getMetrics").then((v) => v.metrics); + const metrics = await cdp.send('Performance.getMetrics').then((v) => v.metrics); self._consumers.forEach((cb) => cb(new PerfMetrics(metrics)).catch(console.log)); }, interval); diff --git a/packages/replay/metrics/src/results/analyzer.ts b/packages/replay/metrics/src/results/analyzer.ts index abe29cda6b63..edd99fa69398 100644 --- a/packages/replay/metrics/src/results/analyzer.ts +++ b/packages/replay/metrics/src/results/analyzer.ts @@ -1,13 +1,16 @@ +import { filesize } from 'filesize'; + import { GitHash } from '../util/git.js'; +import { MetricsStats } from './metrics-stats.js'; import { Result } from './result.js'; import { ResultsSet } from './results-set.js'; -import { MetricsStats } from './metrics-stats.js'; -import { filesize } from "filesize"; // Compares latest result to previous/baseline results and produces the needed info. export class ResultsAnalyzer { + private constructor(private _result: Result) { } + public static async analyze(currentResult: Result, baselineResults?: ResultsSet): Promise { - const items = new ResultsAnalyzer(currentResult).collect(); + const items = new ResultsAnalyzer(currentResult)._collect(); const baseline = baselineResults?.find( (other) => other.cpuThrottling == currentResult.cpuThrottling && @@ -16,7 +19,7 @@ export class ResultsAnalyzer { let otherHash: GitHash | undefined if (baseline != undefined) { - const baseItems = new ResultsAnalyzer(baseline[1]).collect(); + const baseItems = new ResultsAnalyzer(baseline[1])._collect(); // update items with baseline results for (const base of baseItems) { for (const item of items) { @@ -34,15 +37,13 @@ export class ResultsAnalyzer { }; } - private constructor(private result: Result) { } - - private collect(): AnalyzerItem[] { + private _collect(): AnalyzerItem[] { const items = new Array(); - const aStats = new MetricsStats(this.result.aResults); - const bStats = new MetricsStats(this.result.bResults); + const aStats = new MetricsStats(this._result.aResults); + const bStats = new MetricsStats(this._result.bResults); - const pushIfDefined = function (metric: AnalyzerItemMetric, unit: AnalyzerItemUnit, valueA?: number, valueB?: number) { + const pushIfDefined = function (metric: AnalyzerItemMetric, unit: AnalyzerItemUnit, valueA?: number, valueB?: number): void { if (valueA == undefined || valueB == undefined) return; items.push({ @@ -59,9 +60,9 @@ export class ResultsAnalyzer { case AnalyzerItemUnit.bytes: return prefix + filesize(diff); case AnalyzerItemUnit.ratio: - return prefix + (diff * 100).toFixed(2) + ' %'; + return `${prefix + (diff * 100).toFixed(2)} %`; default: - return prefix + diff.toFixed(2) + ' ' + AnalyzerItemUnit[unit]; + return `${prefix + diff.toFixed(2)} ${AnalyzerItemUnit[unit]}`; } } } diff --git a/packages/replay/metrics/src/results/metrics-stats.ts b/packages/replay/metrics/src/results/metrics-stats.ts index acec86c6973c..37bc05c26fb6 100644 --- a/packages/replay/metrics/src/results/metrics-stats.ts +++ b/packages/replay/metrics/src/results/metrics-stats.ts @@ -1,13 +1,14 @@ -import { Metrics } from '../collector'; import * as ss from 'simple-statistics' +import { Metrics } from '../collector'; + export type NumberProvider = (metrics: Metrics) => number; export class MetricsStats { - constructor(private items: Metrics[]) { } + constructor(private _items: Metrics[]) { } public filteredMean(dataProvider: NumberProvider): number | undefined { - const numbers = this.items.map(dataProvider); + const numbers = this._items.map(dataProvider); return numbers.length > 0 ? ss.mean(numbers) : undefined; } @@ -28,7 +29,7 @@ export class MetricsStats { } public get memoryMax(): number | undefined { - const numbers = this.items.map((metrics) => ss.max(Array.from(metrics.memory.snapshots.values()))); + const numbers = this._items.map((metrics) => ss.max(Array.from(metrics.memory.snapshots.values()))); return numbers.length > 0 ? ss.max(numbers) : undefined; } } diff --git a/packages/replay/metrics/src/results/pr-comment.ts b/packages/replay/metrics/src/results/pr-comment.ts index 704992583306..c9fa79e3a500 100644 --- a/packages/replay/metrics/src/results/pr-comment.ts +++ b/packages/replay/metrics/src/results/pr-comment.ts @@ -1,7 +1,7 @@ -import { Git } from "../util/git.js"; -import { Analysis, AnalyzerItemMetric, ResultsAnalyzer } from "./analyzer.js"; -import { Result } from "./result.js"; -import { ResultSetItem } from "./results-set.js"; +import { Git } from '../util/git.js'; +import { Analysis, AnalyzerItemMetric, ResultsAnalyzer } from './analyzer.js'; +import { Result } from './result.js'; +import { ResultSetItem } from './results-set.js'; function trimIndent(str: string): string { return str.split('\n').map(s => s.trim()).join('\n'); @@ -25,14 +25,14 @@ function printableMetricName(metric: AnalyzerItemMetric): string { } export class PrCommentBuilder { - private buffer = ''; + private _buffer: string = ''; public get title(): string { return 'Replay SDK metrics :rocket:'; } public get body(): string { - return trimIndent(this.buffer); + return trimIndent(this._buffer); } public async addCurrentResult(analysis: Analysis, otherName: string): Promise { @@ -44,32 +44,32 @@ export class PrCommentBuilder { return content(); } - this.buffer += ` + this._buffer += `

${this.title}

 Latest diff (${await Git.hash})' + otherName + ' diff (' + analysis.otherHash + ')This PR (${await Git.hash})' + otherName + ' (' + analysis.otherHash + ')
- ${maybeOther(() => '')} + ${maybeOther(() => ``)} ` for (const item of analysis.items) { - this.buffer += ` + this._buffer += ` - ${maybeOther(() => '')} + ${maybeOther(() => ``)} ` } - this.buffer += ` + this._buffer += `
  This PR (${await Git.hash})' + otherName + ' (' + analysis.otherHash + ')${ otherName } (${ analysis.otherHash })
${printableMetricName(item.metric)} ${item.value.asString()}' + item.other!.asString() + '${ item.other!.asString() }
`; } - public async addAdditionalResultsSet(name: String, resultFiles: ResultSetItem[]): Promise { + public async addAdditionalResultsSet(name: string, resultFiles: ResultSetItem[]): Promise { if (resultFiles.length == 0) return; - this.buffer += ` + this._buffer += `

${name}

`; @@ -82,22 +82,22 @@ export class PrCommentBuilder { if (i == 0) { // Add table header - this.buffer += ''; + this._buffer += ''; for (const item of analysis.items) { - this.buffer += ``; + this._buffer += ``; } - this.buffer += ''; + this._buffer += ''; } // Add table row - this.buffer += ``; + this._buffer += ``; for (const item of analysis.items) { - this.buffer += ``; + this._buffer += ``; } - this.buffer += ''; + this._buffer += ''; } - this.buffer += ` + this._buffer += `
Revision
Revision${printableMetricName(item.metric)}${printableMetricName(item.metric)}
${resultFile.hash}
${resultFile.hash}${item.value.asString()}${item.value.asString()}
`; } diff --git a/packages/replay/metrics/src/results/result.ts b/packages/replay/metrics/src/results/result.ts index eac564dc882b..8229642b6fea 100644 --- a/packages/replay/metrics/src/results/result.ts +++ b/packages/replay/metrics/src/results/result.ts @@ -2,6 +2,7 @@ import * as fs from 'fs'; import path from 'path'; import { Metrics } from '../collector.js'; +import { JsonObject, JsonStringify } from '../util/json.js'; export class Result { constructor( @@ -10,34 +11,24 @@ export class Result { public readonly aResults: Metrics[], public readonly bResults: Metrics[]) { } + public static readFromFile(filePath: string): Result { + const json = fs.readFileSync(filePath, { encoding: 'utf-8' }); + const data = JSON.parse(json) as JsonObject; + return new Result( + data.name as string, + data.cpuThrottling as number, + data.networkConditions as string, + (data.aResults as Partial[] || []).map(Metrics.fromJSON.bind(Metrics)), + (data.bResults as Partial[] || []).map(Metrics.fromJSON.bind(Metrics)), + ); + } + public writeToFile(filePath: string): void { const dir = path.dirname(filePath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir); } - const json = this.serialize(); + const json = JsonStringify(this); fs.writeFileSync(filePath, json); } - - serialize(): string { - return JSON.stringify(this, (_: any, value: any): any => { - if (typeof value != 'undefined' && typeof value.toJSON == 'function') { - return value.toJSON(); - } else { - return value; - } - }, 2); - } - - public static readFromFile(filePath: string): Result { - const json = fs.readFileSync(filePath, { encoding: 'utf-8' }); - const data = JSON.parse(json); - return new Result( - data.name || '', - data.cpuThrottling as number, - data.networkConditions || '', - (data.aResults || []).map(Metrics.fromJSON), - (data.bResults || []).map(Metrics.fromJSON), - ); - } } diff --git a/packages/replay/metrics/src/results/results-set.ts b/packages/replay/metrics/src/results/results-set.ts index d19a2ca121e7..f3432441deb7 100644 --- a/packages/replay/metrics/src/results/results-set.ts +++ b/packages/replay/metrics/src/results/results-set.ts @@ -1,6 +1,7 @@ import assert from 'assert'; import * as fs from 'fs'; import path from 'path'; + import { Git, GitHash } from '../util/git.js'; import { Result } from './result.js'; @@ -29,35 +30,30 @@ export class ResultSetItem { /// Wraps a directory containing multiple (N--result.json) files. /// The files are numbered from the most recently added one, to the oldest one. export class ResultsSet { - public constructor(private directory: string) { - if (!fs.existsSync(directory)) { - fs.mkdirSync(directory, { recursive: true }); + public constructor(private _directory: string) { + if (!fs.existsSync(_directory)) { + fs.mkdirSync(_directory, { recursive: true }); } } public find(predicate: (value: Result) => boolean): [GitHash, Result] | undefined { - const items = this.items(); - for (let i = 0; i < items.length; i++) { - const result = Result.readFromFile(items[i].path); + for (const item of this.items()) { + const result = Result.readFromFile(item.path); if (predicate(result)) { - return [items[i].hash, result]; + return [item.hash, result]; } } return undefined; } public items(): ResultSetItem[] { - return this.files().map((file) => { - return new ResultSetItem(path.join(this.directory, file.name)); + return this._files().map((file) => { + return new ResultSetItem(path.join(this._directory, file.name)); }).filter((item) => !isNaN(item.number)); } - private files(): fs.Dirent[] { - return fs.readdirSync(this.directory, { withFileTypes: true }).filter((v) => v.isFile()) - } - public async add(newFile: string, onlyIfDifferent: boolean = false): Promise { - console.log(`Preparing to add ${newFile} to ${this.directory}`); + console.log(`Preparing to add ${newFile} to ${this._directory}`); assert(fs.existsSync(newFile)); // Get the list of file sorted by the prefix number in the descending order (starting with the oldest files). @@ -75,13 +71,17 @@ export class ResultsSet { for (const file of files) { const parts = file.name.split(delimiter); parts[0] = (file.number + 1).toString(); - const newPath = path.join(this.directory, parts.join(delimiter)); + const newPath = path.join(this._directory, parts.join(delimiter)); console.log(`Renaming ${file.path} to ${newPath}`); fs.renameSync(file.path, newPath); } const newName = `1${delimiter}${await Git.hash}${delimiter}result.json`; - console.log(`Adding ${newFile} to ${this.directory} as ${newName}`); - fs.copyFileSync(newFile, path.join(this.directory, newName)); + console.log(`Adding ${newFile} to ${this._directory} as ${newName}`); + fs.copyFileSync(newFile, path.join(this._directory, newName)); + } + + private _files(): fs.Dirent[] { + return fs.readdirSync(this._directory, { withFileTypes: true }).filter((v) => v.isFile()) } } diff --git a/packages/replay/metrics/src/scenarios.ts b/packages/replay/metrics/src/scenarios.ts index 844f85fb573f..2141b74ec460 100644 --- a/packages/replay/metrics/src/scenarios.ts +++ b/packages/replay/metrics/src/scenarios.ts @@ -1,8 +1,9 @@ +import assert from 'assert'; +import * as fs from 'fs'; import path from 'path'; import * as playwright from 'playwright'; -import * as fs from 'fs'; + import { Metrics } from './collector'; -import assert from 'assert'; // A testing scenario we want to collect metrics for. export interface Scenario { @@ -33,12 +34,12 @@ export class LoadPageScenario implements Scenario { // Loads test-apps/jank/ as a page source & waits for a short time before quitting. export class JankTestScenario implements Scenario { - public constructor(private withSentry: boolean) { } + public constructor(private _withSentry: boolean) { } public async run(_: playwright.Browser, page: playwright.Page): Promise { - let url = path.resolve('./test-apps/jank/' + (this.withSentry ? 'with-sentry' : 'index') + '.html'); + let url = path.resolve(`./test-apps/jank/${ this._withSentry ? 'with-sentry' : 'index' }.html`); assert(fs.existsSync(url)); - url = 'file:///' + url.replace('\\', '/'); + url = `file:///${ url.replace('\\', '/')}`; console.log('Navigating to ', url); await page.goto(url, { waitUntil: 'load', timeout: 60000 }); await new Promise(resolve => setTimeout(resolve, 5000)); diff --git a/packages/replay/metrics/src/util/git.ts b/packages/replay/metrics/src/util/git.ts index f5799904fb04..9ec6acae0600 100644 --- a/packages/replay/metrics/src/util/git.ts +++ b/packages/replay/metrics/src/util/git.ts @@ -56,7 +56,7 @@ export const Git = { get hash(): Promise { return (async () => { let gitHash = await git.revparse('HEAD'); - let diff = await git.diff(); + const diff = await git.diff(); if (diff.trim().length > 0) { gitHash += '+dirty'; } diff --git a/packages/replay/metrics/src/util/github.ts b/packages/replay/metrics/src/util/github.ts index d00a8d8d41bf..3dbc3ee66692 100644 --- a/packages/replay/metrics/src/util/github.ts +++ b/packages/replay/metrics/src/util/github.ts @@ -1,23 +1,24 @@ +import { Octokit } from '@octokit/rest'; +import axios from 'axios'; +import extract from 'extract-zip'; import * as fs from 'fs'; -import { Octokit } from "@octokit/rest"; -import { Git } from './git.js'; import path from 'path'; -import Axios from 'axios'; -import extract from 'extract-zip'; -import { consoleGroup } from './console.js'; + import { PrCommentBuilder } from '../results/pr-comment.js'; +import { consoleGroup } from './console.js'; +import { Git } from './git.js'; const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN, // log: console, }); -const [_, owner, repo] = (await Git.repository).split('/'); +const [, owner, repo] = (await Git.repository).split('/'); const defaultArgs = { owner, repo } async function downloadArtifact(url: string, path: string): Promise { const writer = fs.createWriteStream(path); - return Axios({ + return axios({ method: 'get', url: url, responseType: 'stream', @@ -26,6 +27,7 @@ async function downloadArtifact(url: string, path: string): Promise { } }).then(response => { return new Promise((resolve, reject) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access response.data.pipe(writer); let error: Error; writer.on('error', err => { @@ -41,7 +43,7 @@ async function downloadArtifact(url: string, path: string): Promise { } export const GitHub = { - writeOutput(name: string, value: any): void { + writeOutput(name: string, value: string): void { if (typeof process.env.GITHUB_OUTPUT == 'string' && process.env.GITHUB_OUTPUT.length > 0) { fs.appendFileSync(process.env.GITHUB_OUTPUT, `${name}=${value}\n`); } @@ -53,9 +55,9 @@ export const GitHub = { return consoleGroup(async () => { fs.mkdirSync(targetDir, { recursive: true }); - var workflow = await (async () => { + const workflow = await (async () => { for await (const workflows of octokit.paginate.iterator(octokit.rest.actions.listRepoWorkflows, defaultArgs)) { - let found = workflows.data.find((w) => w.name == process.env.GITHUB_WORKFLOW); + const found = workflows.data.find((w) => w.name == process.env.GITHUB_WORKFLOW); if (found) return found; } return undefined; @@ -119,16 +121,15 @@ export const GitHub = { For example, refs/heads/feature-branch-1. */ let prNumber: number | undefined; - if (typeof process.env.GITHUB_REF == 'string' && process.env.GITHUB_REF.length > 0 && process.env.GITHUB_REF.startsWith("refs/pull/")) { + if (typeof process.env.GITHUB_REF == 'string' && process.env.GITHUB_REF.length > 0 && process.env.GITHUB_REF.startsWith('refs/pull/')) { prNumber = parseInt(process.env.GITHUB_REF.split('/')[2]); console.log(`Determined PR number ${prNumber} based on GITHUB_REF environment variable: '${process.env.GITHUB_REF}'`); } else { - const pr = await octokit.rest.pulls.list({ + prNumber = (await octokit.rest.pulls.list({ ...defaultArgs, base: await Git.baseBranch, head: await Git.branch - }); - prNumber = pr.data.at(0)?.number; + })).data[0].number; if (prNumber != undefined) { console.log(`Found PR number ${prNumber} based on base and head branches`); } @@ -149,7 +150,7 @@ export const GitHub = { const author = typeof process.env.GITHUB_ACTION == 'string' ? 'github-actions[bot]' : (await octokit.users.getAuthenticated()).data.login; // Try to find an existing comment by the author and title. - var comment = await (async () => { + const comment = await (async () => { for await (const comments of octokit.paginate.iterator(octokit.rest.issues.listComments, { ...defaultArgs, issue_number: prNumber, diff --git a/packages/replay/metrics/src/util/json.ts b/packages/replay/metrics/src/util/json.ts new file mode 100644 index 000000000000..095614fd115e --- /dev/null +++ b/packages/replay/metrics/src/util/json.ts @@ -0,0 +1,14 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ + +export type JsonObject = { [k: string]: T }; + +export function JsonStringify(object: T): string { + return JSON.stringify(object, (_: unknown, value: any): unknown => { + if (typeof value != 'undefined' && typeof value.toJSON == 'function') { + return value.toJSON(); + } else { + return value; + } + }, 2); +} diff --git a/packages/replay/metrics/yarn.lock b/packages/replay/metrics/yarn.lock index 754a7ff63816..3db63790ad27 100644 --- a/packages/replay/metrics/yarn.lock +++ b/packages/replay/metrics/yarn.lock @@ -342,7 +342,7 @@ pend@~1.2.0: resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg== -playwright-core@1.29.1: +playwright-core@1.29.1, playwright-core@^1.29.1: version "1.29.1" resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.29.1.tgz#9ec15d61c4bd2f386ddf6ce010db53a030345a47" integrity sha512-20Ai3d+lMkWpI9YZYlxk8gxatfgax5STW8GaMozAHwigLiyiKQrdkt7gaoT9UQR8FIVDg6qVXs9IoZUQrDjIIg== From 416ac54846d443ff78e20bc23fe3be5407dfe4cf Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Wed, 4 Jan 2023 14:55:24 +0100 Subject: [PATCH 091/113] more linter fixes --- .gitignore | 2 +- packages/replay/.eslintignore | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 17ef110da73a..d822f532c8cc 100644 --- a/.gitignore +++ b/.gitignore @@ -47,4 +47,4 @@ tmp.js # eslint .eslintcache -eslintcache/* +**/eslintcache/* diff --git a/packages/replay/.eslintignore b/packages/replay/.eslintignore index 0a749745f94c..c76c6c2d64d1 100644 --- a/packages/replay/.eslintignore +++ b/packages/replay/.eslintignore @@ -3,3 +3,4 @@ build/ demo/build/ # TODO: Check if we can re-introduce linting in demo demo +metrics From d8297ec9fa633ed2c2f42a2d66bf2ef56f69d4f6 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Wed, 4 Jan 2023 16:44:24 +0100 Subject: [PATCH 092/113] metrics: improve PR comment contents --- .../replay/metrics/configs/dev/process.ts | 4 +- .../replay/metrics/src/results/analyzer.ts | 61 ++++---- .../metrics/src/results/metrics-stats.ts | 12 +- .../replay/metrics/src/results/pr-comment.ts | 34 +++-- packages/replay/metrics/src/util/github.ts | 140 ++++++++++-------- 5 files changed, 142 insertions(+), 109 deletions(-) diff --git a/packages/replay/metrics/configs/dev/process.ts b/packages/replay/metrics/configs/dev/process.ts index ac19291d1e48..872bf03cb0c8 100644 --- a/packages/replay/metrics/configs/dev/process.ts +++ b/packages/replay/metrics/configs/dev/process.ts @@ -12,9 +12,9 @@ const analysis = await ResultsAnalyzer.analyze(latestResult, resultsSet); const table: { [k: string]: any } = {}; for (const item of analysis.items) { table[AnalyzerItemMetric[item.metric]] = { - value: item.value.asString(), + value: item.value.diff, ...((item.other == undefined) ? {} : { - previous: item.other.asString() + previous: item.other.diff }) }; } diff --git a/packages/replay/metrics/src/results/analyzer.ts b/packages/replay/metrics/src/results/analyzer.ts index edd99fa69398..6158561404b8 100644 --- a/packages/replay/metrics/src/results/analyzer.ts +++ b/packages/replay/metrics/src/results/analyzer.ts @@ -45,34 +45,13 @@ export class ResultsAnalyzer { const pushIfDefined = function (metric: AnalyzerItemMetric, unit: AnalyzerItemUnit, valueA?: number, valueB?: number): void { if (valueA == undefined || valueB == undefined) return; - - items.push({ - metric: metric, - value: { - unit: unit, - asDiff: () => valueB - valueA, - asRatio: () => valueB / valueA, - asString: () => { - const diff = valueB - valueA; - const prefix = diff >= 0 ? '+' : ''; - - switch (unit) { - case AnalyzerItemUnit.bytes: - return prefix + filesize(diff); - case AnalyzerItemUnit.ratio: - return `${prefix + (diff * 100).toFixed(2)} %`; - default: - return `${prefix + diff.toFixed(2)} ${AnalyzerItemUnit[unit]}`; - } - } - } - }) + items.push({ metric: metric, value: new AnalyzerItemNumberValue(unit, valueA, valueB) }) } pushIfDefined(AnalyzerItemMetric.lcp, AnalyzerItemUnit.ms, aStats.lcp, bStats.lcp); pushIfDefined(AnalyzerItemMetric.cls, AnalyzerItemUnit.ms, aStats.cls, bStats.cls); pushIfDefined(AnalyzerItemMetric.cpu, AnalyzerItemUnit.ratio, aStats.cpu, bStats.cpu); - pushIfDefined(AnalyzerItemMetric.memoryAvg, AnalyzerItemUnit.bytes, aStats.memoryAvg, bStats.memoryAvg); + pushIfDefined(AnalyzerItemMetric.memoryAvg, AnalyzerItemUnit.bytes, aStats.memoryMean, bStats.memoryMean); pushIfDefined(AnalyzerItemMetric.memoryMax, AnalyzerItemUnit.bytes, aStats.memoryMax, bStats.memoryMax); return items.filter((item) => item.value != undefined); @@ -86,10 +65,38 @@ export enum AnalyzerItemUnit { } export interface AnalyzerItemValue { - unit: AnalyzerItemUnit; - asString(): string; - asDiff(): number; - asRatio(): number; // 1.0 == 100 % + readonly a: string; + readonly b: string; + readonly diff: string; +} + +class AnalyzerItemNumberValue implements AnalyzerItemValue { + constructor(private _unit: AnalyzerItemUnit, private _a: number, private _b: number) { } + + public get a(): string { + return this._withUnit(this._a); + } + + public get b(): string { + return this._withUnit(this._b); + } + + public get diff(): string { + const diff = this._b - this._a; + const str = this._withUnit(diff); + return diff > 0 ? `+${str}` : str; + } + + private _withUnit(value: number): string { + switch (this._unit) { + case AnalyzerItemUnit.bytes: + return filesize(value) as string; + case AnalyzerItemUnit.ratio: + return `${(value * 100).toFixed(2)} %`; + default: + return `${value.toFixed(2)} ${AnalyzerItemUnit[this._unit]}`; + } + } } export enum AnalyzerItemMetric { diff --git a/packages/replay/metrics/src/results/metrics-stats.ts b/packages/replay/metrics/src/results/metrics-stats.ts index 37bc05c26fb6..a75154e68de8 100644 --- a/packages/replay/metrics/src/results/metrics-stats.ts +++ b/packages/replay/metrics/src/results/metrics-stats.ts @@ -7,25 +7,25 @@ export type NumberProvider = (metrics: Metrics) => number; export class MetricsStats { constructor(private _items: Metrics[]) { } - public filteredMean(dataProvider: NumberProvider): number | undefined { + public mean(dataProvider: NumberProvider): number | undefined { const numbers = this._items.map(dataProvider); return numbers.length > 0 ? ss.mean(numbers) : undefined; } public get lcp(): number | undefined { - return this.filteredMean((metrics) => metrics.vitals.lcp); + return this.mean((metrics) => metrics.vitals.lcp); } public get cls(): number | undefined { - return this.filteredMean((metrics) => metrics.vitals.cls); + return this.mean((metrics) => metrics.vitals.cls); } public get cpu(): number | undefined { - return this.filteredMean((metrics) => metrics.cpu.average); + return this.mean((metrics) => metrics.cpu.average); } - public get memoryAvg(): number | undefined { - return this.filteredMean((metrics) => ss.mean(Array.from(metrics.memory.snapshots.values()))); + public get memoryMean(): number | undefined { + return this.mean((metrics) => ss.mean(Array.from(metrics.memory.snapshots.values()))); } public get memoryMax(): number | undefined { diff --git a/packages/replay/metrics/src/results/pr-comment.ts b/packages/replay/metrics/src/results/pr-comment.ts index c9fa79e3a500..12bb7723004b 100644 --- a/packages/replay/metrics/src/results/pr-comment.ts +++ b/packages/replay/metrics/src/results/pr-comment.ts @@ -47,18 +47,34 @@ export class PrCommentBuilder { this._buffer += `

${this.title}

- - - - ${maybeOther(() => ``)} - ` + + + + + ${maybeOther(() => ``)} + + + + + + ${maybeOther(() => ` + + + `)} + + ` for (const item of analysis.items) { this._buffer += ` - - - ${maybeOther(() => ``)} + + + + + ${maybeOther(() => ` + + + `)} ` } @@ -92,7 +108,7 @@ export class PrCommentBuilder { // Add table row this._buffer += ``; for (const item of analysis.items) { - this._buffer += ``; + this._buffer += ``; } this._buffer += ''; } diff --git a/packages/replay/metrics/src/util/github.ts b/packages/replay/metrics/src/util/github.ts index 3dbc3ee66692..0345c10c9ef4 100644 --- a/packages/replay/metrics/src/util/github.ts +++ b/packages/replay/metrics/src/util/github.ts @@ -42,6 +42,73 @@ async function downloadArtifact(url: string, path: string): Promise { }); } +async function tryAddOrUpdateComment(commentBuilder: PrCommentBuilder): Promise { + /* Env var GITHUB_REF is only set if a branch or tag is available for the current CI event trigger type. + The ref given is fully-formed, meaning that + * for branches the format is refs/heads/, + * for pull requests it is refs/pull//merge, + * and for tags it is refs/tags/. + For example, refs/heads/feature-branch-1. + */ + let prNumber: number | undefined; + if (typeof process.env.GITHUB_REF == 'string' && process.env.GITHUB_REF.length > 0 && process.env.GITHUB_REF.startsWith('refs/pull/')) { + prNumber = parseInt(process.env.GITHUB_REF.split('/')[2]); + console.log(`Determined PR number ${prNumber} based on GITHUB_REF environment variable: '${process.env.GITHUB_REF}'`); + } else { + prNumber = (await octokit.rest.pulls.list({ + ...defaultArgs, + base: await Git.baseBranch, + head: await Git.branch + })).data[0].number; + if (prNumber != undefined) { + console.log(`Found PR number ${prNumber} based on base and head branches`); + } + } + + if (prNumber == undefined) return false; + + // Determine the PR comment author: + // Trying to fetch `octokit.users.getAuthenticated()` throws (in CI only): + // {"message":"Resource not accessible by integration","documentation_url":"https://docs.github.com/rest/reference/users#get-the-authenticated-user"} + // Let's make this conditional on some env variable that's unlikely to be set locally but will be set in GH Actions. + // Do not use "CI" because that's commonly set during local development and testing. + const author = typeof process.env.GITHUB_ACTION == 'string' ? 'github-actions[bot]' : (await octokit.users.getAuthenticated()).data.login; + + // Try to find an existing comment by the author and title. + const comment = await (async () => { + for await (const comments of octokit.paginate.iterator(octokit.rest.issues.listComments, { + ...defaultArgs, + issue_number: prNumber, + })) { + const found = comments.data.find((comment) => { + return comment.user?.login == author + && comment.body != undefined + && comment.body.indexOf(commentBuilder.title) >= 0; + }); + if (found) return found; + } + return undefined; + })(); + + if (comment != undefined) { + console.log(`Updating PR comment ${comment.html_url} body`) + await octokit.rest.issues.updateComment({ + ...defaultArgs, + comment_id: comment.id, + body: commentBuilder.body, + }); + } else { + console.log(`Adding new PR comment to PR ${prNumber}`) + await octokit.rest.issues.createComment({ + ...defaultArgs, + issue_number: prNumber, + body: commentBuilder.body, + }); + } + + return true; +} + export const GitHub = { writeOutput(name: string, value: string): void { if (typeof process.env.GITHUB_OUTPUT == 'string' && process.env.GITHUB_OUTPUT.length > 0) { @@ -113,72 +180,15 @@ export const GitHub = { async addOrUpdateComment(commentBuilder: PrCommentBuilder): Promise { console.log('Adding/updating PR comment'); return consoleGroup(async () => { - /* Env var GITHUB_REF is only set if a branch or tag is available for the current CI event trigger type. - The ref given is fully-formed, meaning that - * for branches the format is refs/heads/, - * for pull requests it is refs/pull//merge, - * and for tags it is refs/tags/. - For example, refs/heads/feature-branch-1. - */ - let prNumber: number | undefined; - if (typeof process.env.GITHUB_REF == 'string' && process.env.GITHUB_REF.length > 0 && process.env.GITHUB_REF.startsWith('refs/pull/')) { - prNumber = parseInt(process.env.GITHUB_REF.split('/')[2]); - console.log(`Determined PR number ${prNumber} based on GITHUB_REF environment variable: '${process.env.GITHUB_REF}'`); - } else { - prNumber = (await octokit.rest.pulls.list({ - ...defaultArgs, - base: await Git.baseBranch, - head: await Git.branch - })).data[0].number; - if (prNumber != undefined) { - console.log(`Found PR number ${prNumber} based on base and head branches`); - } - } - - if (prNumber == undefined) { - const file = 'out/comment.html'; - console.log(`No PR available (not running in CI?): writing built comment to ${path.resolve(file)}`); - fs.writeFileSync(file, commentBuilder.body); - return; - } - - // Determine the PR comment author: - // Trying to fetch `octokit.users.getAuthenticated()` throws (in CI only): - // {"message":"Resource not accessible by integration","documentation_url":"https://docs.github.com/rest/reference/users#get-the-authenticated-user"} - // Let's make this conditional on some env variable that's unlikely to be set locally but will be set in GH Actions. - // Do not use "CI" because that's commonly set during local development and testing. - const author = typeof process.env.GITHUB_ACTION == 'string' ? 'github-actions[bot]' : (await octokit.users.getAuthenticated()).data.login; - - // Try to find an existing comment by the author and title. - const comment = await (async () => { - for await (const comments of octokit.paginate.iterator(octokit.rest.issues.listComments, { - ...defaultArgs, - issue_number: prNumber, - })) { - const found = comments.data.find((comment) => { - return comment.user?.login == author - && comment.body != undefined - && comment.body.indexOf(commentBuilder.title) >= 0; - }); - if (found) return found; + let successful = false; + try { + successful = await tryAddOrUpdateComment(commentBuilder); + } finally { + if (!successful) { + const file = 'out/comment.html'; + console.log(`Writing built comment to ${path.resolve(file)}`); + fs.writeFileSync(file, commentBuilder.body); } - return undefined; - })(); - - if (comment != undefined) { - console.log(`Updating PR comment ${comment.html_url} body`) - await octokit.rest.issues.updateComment({ - ...defaultArgs, - comment_id: comment.id, - body: commentBuilder.body, - }); - } else { - console.log(`Adding new PR comment to PR ${prNumber}`) - await octokit.rest.issues.createComment({ - ...defaultArgs, - issue_number: prNumber, - body: commentBuilder.body, - }); } }); } From 639722526fded7e8a9e8363808936881ec20dae2 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Wed, 4 Jan 2023 17:15:17 +0100 Subject: [PATCH 093/113] metrics: improve comment --- packages/replay/metrics/configs/ci/process.ts | 6 +-- .../replay/metrics/src/results/pr-comment.ts | 42 +++++++++++-------- 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/packages/replay/metrics/configs/ci/process.ts b/packages/replay/metrics/configs/ci/process.ts index aa6686a98237..cf23744e4eab 100644 --- a/packages/replay/metrics/configs/ci/process.ts +++ b/packages/replay/metrics/configs/ci/process.ts @@ -6,7 +6,7 @@ import { Result } from '../../src/results/result.js'; import { ResultsSet } from '../../src/results/results-set.js'; import { Git } from '../../src/util/git.js'; import { GitHub } from '../../src/util/github.js'; -import { artifactName,baselineResultsDir, latestResultFile, previousResultsDir } from './env.js'; +import { artifactName, baselineResultsDir, latestResultFile, previousResultsDir } from './env.js'; const latestResult = Result.readFromFile(latestResultFile); const branch = await Git.branch; @@ -25,7 +25,7 @@ if (baseBranch != branch) { const baseResults = new ResultsSet(baselineResultsDir); await prComment.addCurrentResult(await ResultsAnalyzer.analyze(latestResult, baseResults), 'Baseline'); await prComment.addAdditionalResultsSet( - `Baseline results on branch: ${baseBranch}`, + `Baseline results on branch: ${baseBranch}`, // We skip the first one here because it's already included as `Baseline` column above in addCurrentResult(). baseResults.items().slice(1, 10) ); @@ -34,7 +34,7 @@ if (baseBranch != branch) { } await prComment.addAdditionalResultsSet( - `Previous results on branch: ${branch}`, + `Previous results on branch: ${branch}`, previousResults.items().slice(0, 10) ); diff --git a/packages/replay/metrics/src/results/pr-comment.ts b/packages/replay/metrics/src/results/pr-comment.ts index 12bb7723004b..2f01637d07b5 100644 --- a/packages/replay/metrics/src/results/pr-comment.ts +++ b/packages/replay/metrics/src/results/pr-comment.ts @@ -47,22 +47,28 @@ export class PrCommentBuilder { this._buffer += `

${this.title}

 This PR (${await Git.hash})${ otherName } (${ analysis.otherHash })
 This PR (${await Git.hash})${otherName} (${analysis.otherHash})
Plain+ReplayDiffPlain+ReplayDiff
${printableMetricName(item.metric)}${item.value.asString()}${ item.other!.asString() }${printableMetricName(item.metric)}${item.value.a}${item.value.b}${item.value.diff}${item.other!.a}${item.other!.b}${item.other!.diff}
${resultFile.hash}${item.value.asString()}${item.value.diff}
- - - - - ${maybeOther(() => ``)} - - - - - - ${maybeOther(() => ` - - - `)} - - ` + `; + + const headerCols = ''; + if (analysis.otherHash != undefined) { + // If "other" is defined, add an aditional row of headers. + this._buffer += ` + + + + + + + ${headerCols} + ${headerCols} + `; + } else { + this._buffer += ` + + + ${headerCols} + `; + } for (const item of analysis.items) { this._buffer += ` @@ -70,11 +76,11 @@ export class PrCommentBuilder { - + ${maybeOther(() => ` - `)} + `)} ` } From 2bfaa589e24ab7b73caf5dd30af8348ad40ee98f Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Wed, 4 Jan 2023 18:51:27 +0100 Subject: [PATCH 094/113] metrics: check stddev when collecting results --- packages/replay/metrics/configs/ci/collect.ts | 32 ++++++++-- .../replay/metrics/configs/dev/collect.ts | 2 +- packages/replay/metrics/src/collector.ts | 58 ++++++++++--------- .../replay/metrics/src/results/analyzer.ts | 10 ++-- .../metrics/src/results/metrics-stats.ts | 29 ++++------ packages/replay/metrics/src/scenarios.ts | 11 ++-- 6 files changed, 81 insertions(+), 61 deletions(-) diff --git a/packages/replay/metrics/configs/ci/collect.ts b/packages/replay/metrics/configs/ci/collect.ts index 7d9577e6a9d1..543b4f026a0b 100644 --- a/packages/replay/metrics/configs/ci/collect.ts +++ b/packages/replay/metrics/configs/ci/collect.ts @@ -1,16 +1,38 @@ import { Metrics, MetricsCollector } from '../../src/collector.js'; +import { MetricsStats, NumberProvider } from '../../src/results/metrics-stats.js'; import { JankTestScenario } from '../../src/scenarios.js'; import { latestResultFile } from './env.js'; +function checkStdDev(stats: MetricsStats, name: string, provider: NumberProvider, max: number): boolean { + const value = stats.stddev(provider); + if (value == undefined) { + console.warn(`✗ | Discarding results because StandardDeviation(${name}) is undefined`); + return false; + } else if (value > max) { + console.warn(`✗ | Discarding results because StandardDeviation(${name}) is larger than ${max}. Actual value: ${value}`); + return false; + } else { + console.log(`✓ | StandardDeviation(${name}) is ${value} (<= ${max})`) + } + return true; +} + const collector = new MetricsCollector({ headless: true }); const result = await collector.execute({ - name: 'dummy', + name: 'jank', a: new JankTestScenario(false), b: new JankTestScenario(true), - runs: 1, - tries: 1, - async test(_aResults: Metrics[], _bResults: Metrics[]) { - return true; + runs: 10, + tries: 10, + async shouldAccept(results: Metrics[]): Promise { + const stats = new MetricsStats(results); + return true + && checkStdDev(stats, 'lcp', MetricsStats.lcp, 10) + && checkStdDev(stats, 'cls', MetricsStats.cls, 10) + && checkStdDev(stats, 'cpu', MetricsStats.cpu, 10) + && checkStdDev(stats, 'memory-mean', MetricsStats.memoryMean, 10000) + && checkStdDev(stats, 'memory-max', MetricsStats.memoryMax, 10000); + ; }, }); diff --git a/packages/replay/metrics/configs/dev/collect.ts b/packages/replay/metrics/configs/dev/collect.ts index bec5af33ec8f..24e7b87a7db8 100644 --- a/packages/replay/metrics/configs/dev/collect.ts +++ b/packages/replay/metrics/configs/dev/collect.ts @@ -9,7 +9,7 @@ const result = await collector.execute({ b: new JankTestScenario(true), runs: 1, tries: 1, - async test(_aResults: Metrics[], _bResults: Metrics[]) { + async shouldAccept(_results: Metrics[]): Promise { return true; }, }); diff --git a/packages/replay/metrics/src/collector.ts b/packages/replay/metrics/src/collector.ts index 394dd2b8c956..de37851e0864 100644 --- a/packages/replay/metrics/src/collector.ts +++ b/packages/replay/metrics/src/collector.ts @@ -6,6 +6,7 @@ import { JsHeapUsage, JsHeapUsageSampler, JsHeapUsageSerialized } from './perf/m import { PerfMetricsSampler } from './perf/sampler.js'; import { Result } from './results/result.js'; import { Scenario, TestCase } from './scenarios.js'; +import { consoleGroup } from './util/console.js'; import { WebVitals, WebVitalsCollector } from './vitals/index.js'; const cpuThrottling = 4; @@ -55,37 +56,38 @@ export class MetricsCollector { public async execute(testCase: TestCase): Promise { console.log(`Executing test case ${testCase.name}`); - console.group(); - for (let i = 1; i <= testCase.tries; i++) { - const aResults = await this._collect('A', testCase.a, testCase.runs); - const bResults = await this._collect('B', testCase.b, testCase.runs); - if (await testCase.test(aResults, bResults)) { - console.groupEnd(); - console.log(`Test case ${testCase.name} passed on try ${i}/${testCase.tries}`); - return new Result(testCase.name, cpuThrottling, networkConditions, aResults, bResults); - } else if (i != testCase.tries) { - console.log(`Test case ${testCase.name} failed on try ${i}/${testCase.tries}`); - } else { - console.groupEnd(); - console.error(`Test case ${testCase.name} failed`); - } - } - throw `Test case execution ${testCase.name} failed after ${testCase.tries} tries.`; + return consoleGroup(async () => { + const aResults = await this._collect(testCase, 'A', testCase.a); + const bResults = await this._collect(testCase, 'B', testCase.b); + return new Result(testCase.name, cpuThrottling, networkConditions, aResults, bResults); + }); } - private async _collect(name: string, scenario: Scenario, runs: number): Promise { - const label = `Scenario ${name} data collection (total ${runs} runs)`; - console.time(label); - const results: Metrics[] = []; - for (let run = 0; run < runs; run++) { - const innerLabel = `Scenario ${name} data collection, run ${run}/${runs}`; - console.time(innerLabel); - results.push(await this._run(scenario)); - console.timeEnd(innerLabel); + private async _collect(testCase: TestCase, name: string, scenario: Scenario): Promise { + const label = `Scenario ${name} data collection (total ${testCase.runs} runs)`; + for (let try_ = 1; try_ <= testCase.tries; try_++) { + console.time(label); + const results: Metrics[] = []; + for (let run = 1; run <= testCase.runs; run++) { + const innerLabel = `Scenario ${name} data collection, run ${run}/${testCase.runs}`; + console.time(innerLabel); + results.push(await this._run(scenario)); + console.timeEnd(innerLabel); + } + console.timeEnd(label); + assert.strictEqual(results.length, testCase.runs); + if (await testCase.shouldAccept(results)) { + console.log(`Test case ${testCase.name}, scenario ${name} passed on try ${try_}/${testCase.tries}`); + return results; + } else if (try_ != testCase.tries) { + console.log(`Test case ${testCase.name} failed on try ${try_}/${testCase.tries}, retrying`); + } else { + throw `Test case ${testCase.name}, scenario ${name} failed after ${testCase.tries} tries.`; + } } - console.timeEnd(label); - assert.strictEqual(results.length, runs); - return results; + // Unreachable code, if configured properly: + console.assert(testCase.tries >= 1); + return []; } private async _run(scenario: Scenario): Promise { diff --git a/packages/replay/metrics/src/results/analyzer.ts b/packages/replay/metrics/src/results/analyzer.ts index 6158561404b8..d010ca0f2204 100644 --- a/packages/replay/metrics/src/results/analyzer.ts +++ b/packages/replay/metrics/src/results/analyzer.ts @@ -48,11 +48,11 @@ export class ResultsAnalyzer { items.push({ metric: metric, value: new AnalyzerItemNumberValue(unit, valueA, valueB) }) } - pushIfDefined(AnalyzerItemMetric.lcp, AnalyzerItemUnit.ms, aStats.lcp, bStats.lcp); - pushIfDefined(AnalyzerItemMetric.cls, AnalyzerItemUnit.ms, aStats.cls, bStats.cls); - pushIfDefined(AnalyzerItemMetric.cpu, AnalyzerItemUnit.ratio, aStats.cpu, bStats.cpu); - pushIfDefined(AnalyzerItemMetric.memoryAvg, AnalyzerItemUnit.bytes, aStats.memoryMean, bStats.memoryMean); - pushIfDefined(AnalyzerItemMetric.memoryMax, AnalyzerItemUnit.bytes, aStats.memoryMax, bStats.memoryMax); + pushIfDefined(AnalyzerItemMetric.lcp, AnalyzerItemUnit.ms, aStats.mean(MetricsStats.lcp), bStats.mean(MetricsStats.lcp)); + pushIfDefined(AnalyzerItemMetric.cls, AnalyzerItemUnit.ms, aStats.mean(MetricsStats.cls), bStats.mean(MetricsStats.cls)); + pushIfDefined(AnalyzerItemMetric.cpu, AnalyzerItemUnit.ratio, aStats.mean(MetricsStats.cpu), bStats.mean(MetricsStats.cpu)); + pushIfDefined(AnalyzerItemMetric.memoryAvg, AnalyzerItemUnit.bytes, aStats.mean(MetricsStats.memoryMean), bStats.mean(MetricsStats.memoryMean)); + pushIfDefined(AnalyzerItemMetric.memoryMax, AnalyzerItemUnit.bytes, aStats.max(MetricsStats.memoryMax), bStats.max(MetricsStats.memoryMax)); return items.filter((item) => item.value != undefined); } diff --git a/packages/replay/metrics/src/results/metrics-stats.ts b/packages/replay/metrics/src/results/metrics-stats.ts index a75154e68de8..6ac6db22bb7a 100644 --- a/packages/replay/metrics/src/results/metrics-stats.ts +++ b/packages/replay/metrics/src/results/metrics-stats.ts @@ -7,29 +7,24 @@ export type NumberProvider = (metrics: Metrics) => number; export class MetricsStats { constructor(private _items: Metrics[]) { } + static lcp: NumberProvider = metrics => metrics.vitals.lcp; + static cls: NumberProvider = metrics => metrics.vitals.cls; + static cpu: NumberProvider = metrics => metrics.cpu.average; + static memoryMean: NumberProvider = metrics => ss.mean(Array.from(metrics.memory.snapshots.values())); + static memoryMax: NumberProvider = metrics => ss.max(Array.from(metrics.memory.snapshots.values())); + public mean(dataProvider: NumberProvider): number | undefined { const numbers = this._items.map(dataProvider); return numbers.length > 0 ? ss.mean(numbers) : undefined; } - public get lcp(): number | undefined { - return this.mean((metrics) => metrics.vitals.lcp); - } - - public get cls(): number | undefined { - return this.mean((metrics) => metrics.vitals.cls); - } - - public get cpu(): number | undefined { - return this.mean((metrics) => metrics.cpu.average); - } - - public get memoryMean(): number | undefined { - return this.mean((metrics) => ss.mean(Array.from(metrics.memory.snapshots.values()))); + public max(dataProvider: NumberProvider): number | undefined { + const numbers = this._items.map(dataProvider); + return numbers.length > 0 ? ss.max(numbers) : undefined; } - public get memoryMax(): number | undefined { - const numbers = this._items.map((metrics) => ss.max(Array.from(metrics.memory.snapshots.values()))); - return numbers.length > 0 ? ss.max(numbers) : undefined; + public stddev(dataProvider: NumberProvider): number | undefined { + const numbers = this._items.map(dataProvider); + return numbers.length > 0 ? ss.standardDeviation(numbers) : undefined; } } diff --git a/packages/replay/metrics/src/scenarios.ts b/packages/replay/metrics/src/scenarios.ts index 2141b74ec460..47bf27294012 100644 --- a/packages/replay/metrics/src/scenarios.ts +++ b/packages/replay/metrics/src/scenarios.ts @@ -18,9 +18,10 @@ export interface TestCase { runs: number; tries: number; - // Test function that will be executed and given scenarios A and B result sets. - // Each has exactly `runs` number of items. - test(aResults: Metrics[], bResults: Metrics[]): Promise; + // Test function that will be executed and given a scenarios result set with exactly `runs` number of items. + // Should returns true if this "try" should be accepted and collected. + // If false is returned, `Collector` will retry up to `tries` number of times. + shouldAccept(results: Metrics[]): Promise; } // A simple scenario that just loads the given URL. @@ -37,9 +38,9 @@ export class JankTestScenario implements Scenario { public constructor(private _withSentry: boolean) { } public async run(_: playwright.Browser, page: playwright.Page): Promise { - let url = path.resolve(`./test-apps/jank/${ this._withSentry ? 'with-sentry' : 'index' }.html`); + let url = path.resolve(`./test-apps/jank/${this._withSentry ? 'with-sentry' : 'index'}.html`); assert(fs.existsSync(url)); - url = `file:///${ url.replace('\\', '/')}`; + url = `file:///${url.replace('\\', '/')}`; console.log('Navigating to ', url); await page.goto(url, { waitUntil: 'load', timeout: 60000 }); await new Promise(resolve => setTimeout(resolve, 5000)); From 432591e7114cf5fd24728c58f3704d1ba8dc680b Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Wed, 4 Jan 2023 20:56:26 +0100 Subject: [PATCH 095/113] metrics: filter outliers --- packages/replay/metrics/configs/ci/collect.ts | 9 ++++----- .../metrics/src/results/metrics-stats.ts | 18 +++++++++++++++--- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/packages/replay/metrics/configs/ci/collect.ts b/packages/replay/metrics/configs/ci/collect.ts index 543b4f026a0b..ce34aafcd2a9 100644 --- a/packages/replay/metrics/configs/ci/collect.ts +++ b/packages/replay/metrics/configs/ci/collect.ts @@ -27,12 +27,11 @@ const result = await collector.execute({ async shouldAccept(results: Metrics[]): Promise { const stats = new MetricsStats(results); return true - && checkStdDev(stats, 'lcp', MetricsStats.lcp, 10) - && checkStdDev(stats, 'cls', MetricsStats.cls, 10) + && checkStdDev(stats, 'lcp', MetricsStats.lcp, 30) + && checkStdDev(stats, 'cls', MetricsStats.cls, 0.1) && checkStdDev(stats, 'cpu', MetricsStats.cpu, 10) - && checkStdDev(stats, 'memory-mean', MetricsStats.memoryMean, 10000) - && checkStdDev(stats, 'memory-max', MetricsStats.memoryMax, 10000); - ; + && checkStdDev(stats, 'memory-mean', MetricsStats.memoryMean, 30 * 1024) + && checkStdDev(stats, 'memory-max', MetricsStats.memoryMax, 100 * 1024); }, }); diff --git a/packages/replay/metrics/src/results/metrics-stats.ts b/packages/replay/metrics/src/results/metrics-stats.ts index 6ac6db22bb7a..f15cf2731dfb 100644 --- a/packages/replay/metrics/src/results/metrics-stats.ts +++ b/packages/replay/metrics/src/results/metrics-stats.ts @@ -14,17 +14,29 @@ export class MetricsStats { static memoryMax: NumberProvider = metrics => ss.max(Array.from(metrics.memory.snapshots.values())); public mean(dataProvider: NumberProvider): number | undefined { - const numbers = this._items.map(dataProvider); + const numbers = this._filteredValues(dataProvider); return numbers.length > 0 ? ss.mean(numbers) : undefined; } public max(dataProvider: NumberProvider): number | undefined { - const numbers = this._items.map(dataProvider); + const numbers = this._filteredValues(dataProvider); return numbers.length > 0 ? ss.max(numbers) : undefined; } public stddev(dataProvider: NumberProvider): number | undefined { - const numbers = this._items.map(dataProvider); + const numbers = this._filteredValues(dataProvider); return numbers.length > 0 ? ss.standardDeviation(numbers) : undefined; } + + // See https://en.wikipedia.org/wiki/Interquartile_range#Outliers for details on filtering. + private _filteredValues(dataProvider: NumberProvider): number[] { + const numbers = this._items.map(dataProvider); + numbers.sort((a, b) => a - b) + + const q1 = ss.quantileSorted(numbers, 0.25); + const q3 = ss.quantileSorted(numbers, 0.75); + const iqr = q3 - q1 + + return numbers.filter(num => num >= (q1 - 1.5 * iqr) && num <= (q3 + 1.5 * iqr)) + } } From 686a313d9185b089695ba3035473ee208f3beb4c Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Thu, 5 Jan 2023 14:31:03 +0100 Subject: [PATCH 096/113] metrics: fix CPU usage collection --- packages/replay/metrics/configs/ci/collect.ts | 22 ++++++++++++++----- .../replay/metrics/configs/dev/collect.ts | 10 ++++++++- packages/replay/metrics/src/perf/sampler.ts | 22 ++++++++++++------- 3 files changed, 39 insertions(+), 15 deletions(-) diff --git a/packages/replay/metrics/configs/ci/collect.ts b/packages/replay/metrics/configs/ci/collect.ts index ce34aafcd2a9..de65e5166985 100644 --- a/packages/replay/metrics/configs/ci/collect.ts +++ b/packages/replay/metrics/configs/ci/collect.ts @@ -26,12 +26,22 @@ const result = await collector.execute({ tries: 10, async shouldAccept(results: Metrics[]): Promise { const stats = new MetricsStats(results); - return true - && checkStdDev(stats, 'lcp', MetricsStats.lcp, 30) - && checkStdDev(stats, 'cls', MetricsStats.cls, 0.1) - && checkStdDev(stats, 'cpu', MetricsStats.cpu, 10) - && checkStdDev(stats, 'memory-mean', MetricsStats.memoryMean, 30 * 1024) - && checkStdDev(stats, 'memory-max', MetricsStats.memoryMax, 100 * 1024); + if (!checkStdDev(stats, 'lcp', MetricsStats.lcp, 30) + || !checkStdDev(stats, 'cls', MetricsStats.cls, 0.1) + || !checkStdDev(stats, 'cpu', MetricsStats.cpu, 10) + || !checkStdDev(stats, 'memory-mean', MetricsStats.memoryMean, 30 * 1024) + || !checkStdDev(stats, 'memory-max', MetricsStats.memoryMax, 100 * 1024)) { + return false; + } + + const cpuUsage = stats.mean(MetricsStats.cpu)!; + if (cpuUsage > 0.9) { + console.error(`CPU usage too high to be accurate: ${(cpuUsage * 100).toFixed(2)} %.`, + 'Consider simplifying the scenario or changing the CPU throttling factor.'); + return false; + } + + return true; }, }); diff --git a/packages/replay/metrics/configs/dev/collect.ts b/packages/replay/metrics/configs/dev/collect.ts index 24e7b87a7db8..79f127b0fe27 100644 --- a/packages/replay/metrics/configs/dev/collect.ts +++ b/packages/replay/metrics/configs/dev/collect.ts @@ -1,4 +1,5 @@ import { Metrics, MetricsCollector } from '../../src/collector.js'; +import { MetricsStats } from '../../src/results/metrics-stats.js'; import { JankTestScenario } from '../../src/scenarios.js'; import { latestResultFile } from './env.js'; @@ -9,7 +10,14 @@ const result = await collector.execute({ b: new JankTestScenario(true), runs: 1, tries: 1, - async shouldAccept(_results: Metrics[]): Promise { + async shouldAccept(results: Metrics[]): Promise { + const stats = new MetricsStats(results); + const cpuUsage = stats.mean(MetricsStats.cpu)!; + if (cpuUsage > 0.9) { + console.error(`CPU usage too high to be accurate: ${(cpuUsage * 100).toFixed(2)} %.`, + 'Consider simplifying the scenario or changing the CPU throttling factor.'); + return false; + } return true; }, }); diff --git a/packages/replay/metrics/src/perf/sampler.ts b/packages/replay/metrics/src/perf/sampler.ts index e50ce3dd02b4..7b34d61293e7 100644 --- a/packages/replay/metrics/src/perf/sampler.ts +++ b/packages/replay/metrics/src/perf/sampler.ts @@ -33,8 +33,7 @@ export class PerfMetrics { } public get Duration(): number { - // TODO check if any of `Duration` fields is maybe a sum of the others. E.g. verify the measured CPU usage manually. - return this._metrics.reduce((sum, metric) => metric.name.endsWith('Duration') ? sum + metric.value : sum, 0); + return this._find('TaskDuration'); } public get JSHeapUsedSize(): number { @@ -46,16 +45,17 @@ export class PerfMetricsSampler { private _consumers: PerfMetricsConsumer[] = []; private _timer!: NodeJS.Timer; + private constructor(private _cdp: playwright.CDPSession) { } + public static async create(cdp: playwright.CDPSession, interval: number): Promise { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const self = new PerfMetricsSampler(cdp); await cdp.send('Performance.enable', { timeDomain: 'timeTicks' }) - const self = new PerfMetricsSampler(); + // collect first sample immediately + void self._collectSample(); - self._timer = setInterval(async () => { - const metrics = await cdp.send('Performance.getMetrics').then((v) => v.metrics); - self._consumers.forEach((cb) => cb(new PerfMetrics(metrics)).catch(console.log)); - }, interval); + // and set up automatic collection in the given interval + self._timer = setInterval(self._collectSample.bind(self), interval); return self; } @@ -67,4 +67,10 @@ export class PerfMetricsSampler { public stop(): void { clearInterval(this._timer); } + + private async _collectSample(): Promise { + const response = await this._cdp.send('Performance.getMetrics'); + const metrics = new PerfMetrics(response.metrics); + this._consumers.forEach(cb => cb(metrics).catch(console.error)); + } } From 743221a28886b7c329d733a0eb2ac683967e0e7f Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Thu, 5 Jan 2023 14:38:16 +0100 Subject: [PATCH 097/113] tune cpu usage --- packages/replay/metrics/configs/ci/collect.ts | 4 ++-- packages/replay/metrics/test-apps/jank/app.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/replay/metrics/configs/ci/collect.ts b/packages/replay/metrics/configs/ci/collect.ts index de65e5166985..50a60c48a59b 100644 --- a/packages/replay/metrics/configs/ci/collect.ts +++ b/packages/replay/metrics/configs/ci/collect.ts @@ -35,8 +35,8 @@ const result = await collector.execute({ } const cpuUsage = stats.mean(MetricsStats.cpu)!; - if (cpuUsage > 0.9) { - console.error(`CPU usage too high to be accurate: ${(cpuUsage * 100).toFixed(2)} %.`, + if (cpuUsage > 0.75) { + console.error(`CPU usage too high and may be inaccurate: ${(cpuUsage * 100).toFixed(2)} %.`, 'Consider simplifying the scenario or changing the CPU throttling factor.'); return false; } diff --git a/packages/replay/metrics/test-apps/jank/app.js b/packages/replay/metrics/test-apps/jank/app.js index 23660eecfd9b..390160a21f54 100644 --- a/packages/replay/metrics/test-apps/jank/app.js +++ b/packages/replay/metrics/test-apps/jank/app.js @@ -25,7 +25,7 @@ document.addEventListener("DOMContentLoaded", function() { incrementor = 10, distance = 3, frame, - minimum = 100, + minimum = 30, subtract = document.querySelector('.subtract'), add = document.querySelector('.add'); From cedef009a94797909e58109697d5962553567731 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Thu, 5 Jan 2023 15:37:29 +0100 Subject: [PATCH 098/113] perf sampler error handling --- packages/replay/metrics/src/perf/sampler.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/replay/metrics/src/perf/sampler.ts b/packages/replay/metrics/src/perf/sampler.ts index 7b34d61293e7..7929b10822d6 100644 --- a/packages/replay/metrics/src/perf/sampler.ts +++ b/packages/replay/metrics/src/perf/sampler.ts @@ -52,7 +52,7 @@ export class PerfMetricsSampler { await cdp.send('Performance.enable', { timeDomain: 'timeTicks' }) // collect first sample immediately - void self._collectSample(); + self._collectSample(); // and set up automatic collection in the given interval self._timer = setInterval(self._collectSample.bind(self), interval); @@ -68,9 +68,10 @@ export class PerfMetricsSampler { clearInterval(this._timer); } - private async _collectSample(): Promise { - const response = await this._cdp.send('Performance.getMetrics'); - const metrics = new PerfMetrics(response.metrics); - this._consumers.forEach(cb => cb(metrics).catch(console.error)); + private _collectSample(): void { + this._cdp.send('Performance.getMetrics').then(response => { + const metrics = new PerfMetrics(response.metrics); + this._consumers.forEach(cb => cb(metrics).catch(console.error)); + }, console.error); } } From 7d1b2b45a2fd0f1d6c589db0e3b285399aa2b4b1 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Thu, 5 Jan 2023 18:07:36 +0100 Subject: [PATCH 099/113] metrics collection timeout issues --- packages/replay/metrics/configs/ci/collect.ts | 11 ++- .../replay/metrics/configs/dev/process.ts | 16 +--- packages/replay/metrics/package.json | 1 + packages/replay/metrics/src/collector.ts | 80 ++++++++++--------- packages/replay/metrics/src/perf/sampler.ts | 10 ++- packages/replay/metrics/src/util/console.ts | 30 +++++++ packages/replay/metrics/test-apps/jank/app.js | 2 +- packages/replay/metrics/yarn.lock | 5 ++ 8 files changed, 100 insertions(+), 55 deletions(-) diff --git a/packages/replay/metrics/configs/ci/collect.ts b/packages/replay/metrics/configs/ci/collect.ts index 50a60c48a59b..4e64e157f56b 100644 --- a/packages/replay/metrics/configs/ci/collect.ts +++ b/packages/replay/metrics/configs/ci/collect.ts @@ -1,6 +1,7 @@ import { Metrics, MetricsCollector } from '../../src/collector.js'; import { MetricsStats, NumberProvider } from '../../src/results/metrics-stats.js'; import { JankTestScenario } from '../../src/scenarios.js'; +import { printStats } from '../../src/util/console.js'; import { latestResultFile } from './env.js'; function checkStdDev(stats: MetricsStats, name: string, provider: NumberProvider, max: number): boolean { @@ -26,17 +27,19 @@ const result = await collector.execute({ tries: 10, async shouldAccept(results: Metrics[]): Promise { const stats = new MetricsStats(results); + printStats(stats); + if (!checkStdDev(stats, 'lcp', MetricsStats.lcp, 30) || !checkStdDev(stats, 'cls', MetricsStats.cls, 0.1) || !checkStdDev(stats, 'cpu', MetricsStats.cpu, 10) - || !checkStdDev(stats, 'memory-mean', MetricsStats.memoryMean, 30 * 1024) - || !checkStdDev(stats, 'memory-max', MetricsStats.memoryMax, 100 * 1024)) { + || !checkStdDev(stats, 'memory-mean', MetricsStats.memoryMean, 1000 * 1024) + || !checkStdDev(stats, 'memory-max', MetricsStats.memoryMax, 1000 * 1024)) { return false; } const cpuUsage = stats.mean(MetricsStats.cpu)!; - if (cpuUsage > 0.75) { - console.error(`CPU usage too high and may be inaccurate: ${(cpuUsage * 100).toFixed(2)} %.`, + if (cpuUsage > 0.85) { + console.warn(`✗ | Discarding results because CPU usage is too high and may be inaccurate: ${(cpuUsage * 100).toFixed(2)} %.`, 'Consider simplifying the scenario or changing the CPU throttling factor.'); return false; } diff --git a/packages/replay/metrics/configs/dev/process.ts b/packages/replay/metrics/configs/dev/process.ts index 872bf03cb0c8..096244b5c750 100644 --- a/packages/replay/metrics/configs/dev/process.ts +++ b/packages/replay/metrics/configs/dev/process.ts @@ -1,23 +1,13 @@ -import { AnalyzerItemMetric, ResultsAnalyzer } from '../../src/results/analyzer.js'; +import { ResultsAnalyzer } from '../../src/results/analyzer.js'; import { Result } from '../../src/results/result.js'; import { ResultsSet } from '../../src/results/results-set.js'; +import { printAnalysis } from '../../src/util/console.js'; import { latestResultFile, outDir } from './env.js'; const resultsSet = new ResultsSet(outDir); const latestResult = Result.readFromFile(latestResultFile); const analysis = await ResultsAnalyzer.analyze(latestResult, resultsSet); - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const table: { [k: string]: any } = {}; -for (const item of analysis.items) { - table[AnalyzerItemMetric[item.metric]] = { - value: item.value.diff, - ...((item.other == undefined) ? {} : { - previous: item.other.diff - }) - }; -} -console.table(table); +printAnalysis(analysis); await resultsSet.add(latestResultFile, true); diff --git a/packages/replay/metrics/package.json b/packages/replay/metrics/package.json index e4163257018b..c119f1003c8f 100644 --- a/packages/replay/metrics/package.json +++ b/packages/replay/metrics/package.json @@ -19,6 +19,7 @@ "axios": "^1.2.2", "extract-zip": "^2.0.1", "filesize": "^10.0.6", + "p-timeout": "^6.0.0", "playwright": "^1.29.1", "playwright-core": "^1.29.1", "simple-git": "^3.15.1", diff --git a/packages/replay/metrics/src/collector.ts b/packages/replay/metrics/src/collector.ts index de37851e0864..9d1745f5aa79 100644 --- a/packages/replay/metrics/src/collector.ts +++ b/packages/replay/metrics/src/collector.ts @@ -1,4 +1,4 @@ -import assert from 'assert'; +import pTimeout from 'p-timeout'; import * as playwright from 'playwright'; import { CpuUsage, CpuUsageSampler, CpuUsageSerialized } from './perf/cpu.js'; @@ -71,12 +71,16 @@ export class MetricsCollector { for (let run = 1; run <= testCase.runs; run++) { const innerLabel = `Scenario ${name} data collection, run ${run}/${testCase.runs}`; console.time(innerLabel); - results.push(await this._run(scenario)); + try { + results.push(await this._run(scenario)); + } catch (e) { + console.warn(`${innerLabel} failed with ${e}`); + break; + } console.timeEnd(innerLabel); } console.timeEnd(label); - assert.strictEqual(results.length, testCase.runs); - if (await testCase.shouldAccept(results)) { + if ((results.length == testCase.runs) && await testCase.shouldAccept(results)) { console.log(`Test case ${testCase.name}, scenario ${name} passed on try ${try_}/${testCase.tries}`); return results; } else if (try_ != testCase.tries) { @@ -93,40 +97,44 @@ export class MetricsCollector { private async _run(scenario: Scenario): Promise { const disposeCallbacks: (() => Promise)[] = []; try { - const browser = await playwright.chromium.launch({ - headless: this._options.headless, - + return await pTimeout((async () => { + const browser = await playwright.chromium.launch({ + headless: this._options.headless, + }); + disposeCallbacks.push(() => browser.close()); + const page = await browser.newPage(); + disposeCallbacks.push(() => page.close()); + + const cdp = await page.context().newCDPSession(page); + + // Simulate throttling. + await cdp.send('Network.emulateNetworkConditions', { + offline: false, + latency: PredefinedNetworkConditions[networkConditions].latency, + uploadThroughput: PredefinedNetworkConditions[networkConditions].upload, + downloadThroughput: PredefinedNetworkConditions[networkConditions].download, + }); + await cdp.send('Emulation.setCPUThrottlingRate', { rate: cpuThrottling }); + + // Collect CPU and memory info 10 times per second. + const perfSampler = await PerfMetricsSampler.create(cdp, 100); + disposeCallbacks.push(async () => perfSampler.stop()); + const cpuSampler = new CpuUsageSampler(perfSampler); + const memSampler = new JsHeapUsageSampler(perfSampler); + + const vitalsCollector = await WebVitalsCollector.create(page); + await scenario.run(browser, page); + + // NOTE: FID needs some interaction to actually show a value + const vitals = await vitalsCollector.collect(); + + return new Metrics(vitals, cpuSampler.getData(), memSampler.getData()); + })(), { + milliseconds: 60 * 1000, }); - disposeCallbacks.push(async () => browser.close()); - const page = await browser.newPage(); - - const cdp = await page.context().newCDPSession(page); - - // Simulate throttling. - await cdp.send('Network.emulateNetworkConditions', { - offline: false, - latency: PredefinedNetworkConditions[networkConditions].latency, - uploadThroughput: PredefinedNetworkConditions[networkConditions].upload, - downloadThroughput: PredefinedNetworkConditions[networkConditions].download, - }); - await cdp.send('Emulation.setCPUThrottlingRate', { rate: cpuThrottling }); - - // Collect CPU and memory info 10 times per second. - const perfSampler = await PerfMetricsSampler.create(cdp, 100); - disposeCallbacks.push(async () => perfSampler.stop()); - const cpuSampler = new CpuUsageSampler(perfSampler); - const memSampler = new JsHeapUsageSampler(perfSampler); - - const vitalsCollector = await WebVitalsCollector.create(page); - - await scenario.run(browser, page); - - // NOTE: FID needs some interaction to actually show a value - const vitals = await vitalsCollector.collect(); - - return new Metrics(vitals, cpuSampler.getData(), memSampler.getData()); } finally { - disposeCallbacks.reverse().forEach((cb) => cb().catch(console.log)); + console.log('Disposing of browser and resources'); + disposeCallbacks.reverse().forEach((cb) => cb().catch(() => { /* silent */ })); } } } diff --git a/packages/replay/metrics/src/perf/sampler.ts b/packages/replay/metrics/src/perf/sampler.ts index 7929b10822d6..b4dd26013cd5 100644 --- a/packages/replay/metrics/src/perf/sampler.ts +++ b/packages/replay/metrics/src/perf/sampler.ts @@ -44,6 +44,7 @@ export class PerfMetrics { export class PerfMetricsSampler { private _consumers: PerfMetricsConsumer[] = []; private _timer!: NodeJS.Timer; + private _errorPrinted: boolean = false; private constructor(private _cdp: playwright.CDPSession) { } @@ -72,6 +73,13 @@ export class PerfMetricsSampler { this._cdp.send('Performance.getMetrics').then(response => { const metrics = new PerfMetrics(response.metrics); this._consumers.forEach(cb => cb(metrics).catch(console.error)); - }, console.error); + }, (e) => { + // This happens if the browser closed unexpectedly. No reason to try again. + if (!this._errorPrinted) { + this._errorPrinted = true; + console.log(e); + this.stop(); + } + }); } } diff --git a/packages/replay/metrics/src/util/console.ts b/packages/replay/metrics/src/util/console.ts index 3ceaac1c862b..9ebf57c54bb2 100644 --- a/packages/replay/metrics/src/util/console.ts +++ b/packages/replay/metrics/src/util/console.ts @@ -1,5 +1,35 @@ +import { filesize } from 'filesize'; + +import { Analysis, AnalyzerItemMetric } from '../results/analyzer.js'; +import { MetricsStats } from '../results/metrics-stats.js'; export async function consoleGroup(code: () => Promise): Promise { console.group(); return code().finally(console.groupEnd); } + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type PrintableTable = { [k: string]: any }; + +export function printStats(stats: MetricsStats): void { + console.table({ + lcp: `${stats.mean(MetricsStats.lcp)?.toFixed(2)} %`, + cls: `${stats.mean(MetricsStats.cls)?.toFixed(2)} %`, + cpu: `${((stats.mean(MetricsStats.cpu) || 0) * 100).toFixed(2)} %`, + memoryMean: filesize(stats.mean(MetricsStats.memoryMean)), + memoryMax: filesize(stats.max(MetricsStats.memoryMax)), + }); +} + +export function printAnalysis(analysis: Analysis): void { + const table: PrintableTable = {}; + for (const item of analysis.items) { + table[AnalyzerItemMetric[item.metric]] = { + value: item.value.diff, + ...((item.other == undefined) ? {} : { + previous: item.other.diff + }) + }; + } + console.table(table); +} diff --git a/packages/replay/metrics/test-apps/jank/app.js b/packages/replay/metrics/test-apps/jank/app.js index 390160a21f54..aa482a228bb6 100644 --- a/packages/replay/metrics/test-apps/jank/app.js +++ b/packages/replay/metrics/test-apps/jank/app.js @@ -25,7 +25,7 @@ document.addEventListener("DOMContentLoaded", function() { incrementor = 10, distance = 3, frame, - minimum = 30, + minimum = 50, subtract = document.querySelector('.subtract'), add = document.querySelector('.add'); diff --git a/packages/replay/metrics/yarn.lock b/packages/replay/metrics/yarn.lock index 3db63790ad27..b8837974c2a1 100644 --- a/packages/replay/metrics/yarn.lock +++ b/packages/replay/metrics/yarn.lock @@ -337,6 +337,11 @@ once@^1.3.1, once@^1.4.0: dependencies: wrappy "1" +p-timeout@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-6.0.0.tgz#84c210f5500da1af4c31ab2768d794e5e081dd91" + integrity sha512-5iS61MOdUMemWH9CORQRxVXTp9g5K8rPnI9uQpo97aWgsH3vVXKjkIhDi+OgIDmN3Ly9+AZ2fZV01Wut1yzfKA== + pend@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" From ec925abb8809a75c902545cbb7ac6b0b647cb7d6 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Thu, 5 Jan 2023 21:03:50 +0100 Subject: [PATCH 100/113] metrics: fail on error logs --- packages/replay/metrics/src/collector.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/replay/metrics/src/collector.ts b/packages/replay/metrics/src/collector.ts index 9d1745f5aa79..3ec628596533 100644 --- a/packages/replay/metrics/src/collector.ts +++ b/packages/replay/metrics/src/collector.ts @@ -105,6 +105,11 @@ export class MetricsCollector { const page = await browser.newPage(); disposeCallbacks.push(() => page.close()); + const errorLogs: Array = []; + await page.on('console', message => { if (message.type() === 'error') errorLogs.push(message.text()) }); + await page.on('crash', _ => { errorLogs.push('Page crashed') }); + await page.on('pageerror', error => { errorLogs.push(`${error.name}: ${error.message}`) }); + const cdp = await page.context().newCDPSession(page); // Simulate throttling. @@ -128,6 +133,10 @@ export class MetricsCollector { // NOTE: FID needs some interaction to actually show a value const vitals = await vitalsCollector.collect(); + if (errorLogs.length > 0) { + throw `Error logs in browser console:\n\t\t${errorLogs.join('\n\t\t')}`; + } + return new Metrics(vitals, cpuSampler.getData(), memSampler.getData()); })(), { milliseconds: 60 * 1000, From be830afceff503d15fd07e14e153a6e94752be94 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Thu, 5 Jan 2023 21:27:24 +0100 Subject: [PATCH 101/113] move metrics CI job to build.yaml to get the full build cache --- .github/workflows/build.yml | 44 +++++++++++++++++++ .github/workflows/metrics.yml | 81 ----------------------------------- 2 files changed, 44 insertions(+), 81 deletions(-) delete mode 100644 .github/workflows/metrics.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e2468933eeef..dc55e5ffd3bd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -815,3 +815,47 @@ jobs: if: contains(needs.*.result, 'failure') run: | echo "One of the dependent jobs have failed. You may need to re-run it." && exit 1 + + replay_metrics: + name: Replay Metrics + needs: [job_get_metadata, job_build] + runs-on: ubuntu-20.04 + timeout-minutes: 30 + steps: + - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) + uses: actions/checkout@v3 + with: + ref: ${{ env.HEAD_COMMIT }} + - name: Set up Node + uses: volta-cli/action@v4 + - name: Check dependency cache + uses: actions/cache@v3 + with: + path: ${{ env.CACHED_DEPENDENCY_PATHS }} + key: ${{ needs.job_build.outputs.dependency_cache_key }} + - name: Check build cache + uses: actions/cache@v3 + with: + path: ${{ env.CACHED_BUILD_PATHS }} + key: ${{ env.BUILD_CACHE_KEY }} + + - name: Setup + run: yarn install + working-directory: packages/replay/metrics + + - name: Collect + run: yarn ci:collect + working-directory: packages/replay/metrics + + - name: Process + id: process + run: yarn ci:process + working-directory: packages/replay/metrics + env: + GITHUB_TOKEN: ${{ github.token }} + + - name: Upload results + uses: actions/upload-artifact@v3 + with: + name: ${{ steps.process.outputs.artifactName }} + path: ${{ steps.process.outputs.artifactPath }} diff --git a/.github/workflows/metrics.yml b/.github/workflows/metrics.yml deleted file mode 100644 index ebd57236a53e..000000000000 --- a/.github/workflows/metrics.yml +++ /dev/null @@ -1,81 +0,0 @@ -name: Collect SDK metrics -on: - push: - paths: - - .github/workflows/metrics.yml - - packages/** - - patches/** - - lerna.json - - package.json - - tsconfig.json - - yarn.lock - branches-ignore: - - deps/** - - dependabot/** - tags-ignore: ['**'] - -env: - CACHED_DEPENDENCY_PATHS: | - ${{ github.workspace }}/node_modules - ${{ github.workspace }}/packages/*/node_modules - ~/.cache/ms-playwright/ - ~/.cache/mongodb-binaries/ - -jobs: - cancel-previous-workflow: - runs-on: ubuntu-latest - steps: - - name: Cancel Previous Runs - uses: styfle/cancel-workflow-action@b173b6ec0100793626c2d9e6b90435061f4fc3e5 # pin@0.11.0 - with: - access_token: ${{ github.token }} - - replay: - name: Replay SDK metrics - runs-on: ubuntu-20.04 - - steps: - - uses: actions/checkout@v3 - - - name: Set up Node - uses: volta-cli/action@v4 - - - name: Compute dependency cache key - id: compute_lockfile_hash - # we use a hash of yarn.lock as our cache key, because if it hasn't changed, our dependencies haven't changed, - # so no need to reinstall them - run: echo "hash=${{ hashFiles('yarn.lock') }}" >> "$GITHUB_OUTPUT" - - - name: Check dependency cache - uses: actions/cache@v3 - id: cache_dependencies - with: - path: ${{ env.CACHED_DEPENDENCY_PATHS }} - key: ${{ steps.compute_lockfile_hash.outputs.hash }} - - - name: Install dependencies - if: steps.cache_dependencies.outputs.cache-hit == '' - run: yarn install --ignore-engines --frozen-lockfile - - - name: Build - run: | - yarn install --ignore-engines --frozen-lockfile - yarn deps - working-directory: packages/replay/metrics - - - name: Collect - run: yarn ci:collect - working-directory: packages/replay/metrics - - - name: Process - id: process - run: yarn ci:process - working-directory: packages/replay/metrics - env: - GITHUB_TOKEN: ${{ github.token }} - - - name: Upload results - uses: actions/upload-artifact@v3 - with: - name: ${{ steps.process.outputs.artifactName }} - path: ${{ steps.process.outputs.artifactPath }} From 4d0a36c63902f5979271e897c595c71bc20f1d6a Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Thu, 5 Jan 2023 22:54:46 +0100 Subject: [PATCH 102/113] tune metrics collection --- packages/replay/metrics/configs/ci/collect.ts | 2 +- packages/replay/metrics/test-apps/jank/app.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/replay/metrics/configs/ci/collect.ts b/packages/replay/metrics/configs/ci/collect.ts index 4e64e157f56b..f2a62764f479 100644 --- a/packages/replay/metrics/configs/ci/collect.ts +++ b/packages/replay/metrics/configs/ci/collect.ts @@ -31,7 +31,7 @@ const result = await collector.execute({ if (!checkStdDev(stats, 'lcp', MetricsStats.lcp, 30) || !checkStdDev(stats, 'cls', MetricsStats.cls, 0.1) - || !checkStdDev(stats, 'cpu', MetricsStats.cpu, 10) + || !checkStdDev(stats, 'cpu', MetricsStats.cpu, 1) || !checkStdDev(stats, 'memory-mean', MetricsStats.memoryMean, 1000 * 1024) || !checkStdDev(stats, 'memory-max', MetricsStats.memoryMax, 1000 * 1024)) { return false; diff --git a/packages/replay/metrics/test-apps/jank/app.js b/packages/replay/metrics/test-apps/jank/app.js index aa482a228bb6..390160a21f54 100644 --- a/packages/replay/metrics/test-apps/jank/app.js +++ b/packages/replay/metrics/test-apps/jank/app.js @@ -25,7 +25,7 @@ document.addEventListener("DOMContentLoaded", function() { incrementor = 10, distance = 3, frame, - minimum = 50, + minimum = 30, subtract = document.querySelector('.subtract'), add = document.querySelector('.add'); From 3afa90bb06d6dfd301672c577d51e2da64dd507f Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Fri, 6 Jan 2023 08:43:28 +0100 Subject: [PATCH 103/113] ci metrics collection issues --- packages/replay/metrics/configs/ci/collect.ts | 2 +- packages/replay/metrics/configs/dev/collect.ts | 3 +++ packages/replay/metrics/src/collector.ts | 7 ++++--- packages/replay/metrics/src/util/console.ts | 4 ++-- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/replay/metrics/configs/ci/collect.ts b/packages/replay/metrics/configs/ci/collect.ts index f2a62764f479..5972ae3e0032 100644 --- a/packages/replay/metrics/configs/ci/collect.ts +++ b/packages/replay/metrics/configs/ci/collect.ts @@ -18,7 +18,7 @@ function checkStdDev(stats: MetricsStats, name: string, provider: NumberProvider return true; } -const collector = new MetricsCollector({ headless: true }); +const collector = new MetricsCollector({ headless: true, cpuThrottling: 2 }); const result = await collector.execute({ name: 'jank', a: new JankTestScenario(false), diff --git a/packages/replay/metrics/configs/dev/collect.ts b/packages/replay/metrics/configs/dev/collect.ts index 79f127b0fe27..a370fdf594f8 100644 --- a/packages/replay/metrics/configs/dev/collect.ts +++ b/packages/replay/metrics/configs/dev/collect.ts @@ -1,6 +1,7 @@ import { Metrics, MetricsCollector } from '../../src/collector.js'; import { MetricsStats } from '../../src/results/metrics-stats.js'; import { JankTestScenario } from '../../src/scenarios.js'; +import { printStats } from '../../src/util/console.js'; import { latestResultFile } from './env.js'; const collector = new MetricsCollector(); @@ -12,6 +13,8 @@ const result = await collector.execute({ tries: 1, async shouldAccept(results: Metrics[]): Promise { const stats = new MetricsStats(results); + printStats(stats); + const cpuUsage = stats.mean(MetricsStats.cpu)!; if (cpuUsage > 0.9) { console.error(`CPU usage too high to be accurate: ${(cpuUsage * 100).toFixed(2)} %.`, diff --git a/packages/replay/metrics/src/collector.ts b/packages/replay/metrics/src/collector.ts index 3ec628596533..36b0fdfb9b19 100644 --- a/packages/replay/metrics/src/collector.ts +++ b/packages/replay/metrics/src/collector.ts @@ -9,7 +9,6 @@ import { Scenario, TestCase } from './scenarios.js'; import { consoleGroup } from './util/console.js'; import { WebVitals, WebVitalsCollector } from './vitals/index.js'; -const cpuThrottling = 4; const networkConditions = 'Fast 3G'; // Same as puppeteer-core PredefinedNetworkConditions @@ -42,6 +41,7 @@ export class Metrics { export interface MetricsCollectorOptions { headless: boolean; + cpuThrottling: number; } export class MetricsCollector { @@ -50,6 +50,7 @@ export class MetricsCollector { constructor(options?: Partial) { this._options = { headless: false, + cpuThrottling: 4, ...options }; } @@ -59,7 +60,7 @@ export class MetricsCollector { return consoleGroup(async () => { const aResults = await this._collect(testCase, 'A', testCase.a); const bResults = await this._collect(testCase, 'B', testCase.b); - return new Result(testCase.name, cpuThrottling, networkConditions, aResults, bResults); + return new Result(testCase.name, this._options.cpuThrottling, networkConditions, aResults, bResults); }); } @@ -119,7 +120,7 @@ export class MetricsCollector { uploadThroughput: PredefinedNetworkConditions[networkConditions].upload, downloadThroughput: PredefinedNetworkConditions[networkConditions].download, }); - await cdp.send('Emulation.setCPUThrottlingRate', { rate: cpuThrottling }); + await cdp.send('Emulation.setCPUThrottlingRate', { rate: this._options.cpuThrottling }); // Collect CPU and memory info 10 times per second. const perfSampler = await PerfMetricsSampler.create(cdp, 100); diff --git a/packages/replay/metrics/src/util/console.ts b/packages/replay/metrics/src/util/console.ts index 9ebf57c54bb2..991216fb45ec 100644 --- a/packages/replay/metrics/src/util/console.ts +++ b/packages/replay/metrics/src/util/console.ts @@ -13,8 +13,8 @@ type PrintableTable = { [k: string]: any }; export function printStats(stats: MetricsStats): void { console.table({ - lcp: `${stats.mean(MetricsStats.lcp)?.toFixed(2)} %`, - cls: `${stats.mean(MetricsStats.cls)?.toFixed(2)} %`, + lcp: `${stats.mean(MetricsStats.lcp)?.toFixed(2)} ms`, + cls: `${stats.mean(MetricsStats.cls)?.toFixed(2)} ms`, cpu: `${((stats.mean(MetricsStats.cpu) || 0) * 100).toFixed(2)} %`, memoryMean: filesize(stats.mean(MetricsStats.memoryMean)), memoryMax: filesize(stats.max(MetricsStats.memoryMax)), From e713fab073eb36ad2035d7d039e18f3a7fd34b72 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Fri, 6 Jan 2023 10:44:37 +0100 Subject: [PATCH 104/113] tune CI metrics configs --- packages/replay/metrics/configs/ci/collect.ts | 1 + packages/replay/metrics/src/results/analyzer.ts | 6 +++--- packages/replay/metrics/src/results/pr-comment.ts | 12 ++++++++++-- packages/replay/metrics/test-apps/jank/app.js | 2 +- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/replay/metrics/configs/ci/collect.ts b/packages/replay/metrics/configs/ci/collect.ts index 5972ae3e0032..cbed78974a3e 100644 --- a/packages/replay/metrics/configs/ci/collect.ts +++ b/packages/replay/metrics/configs/ci/collect.ts @@ -39,6 +39,7 @@ const result = await collector.execute({ const cpuUsage = stats.mean(MetricsStats.cpu)!; if (cpuUsage > 0.85) { + // Note: complexity on the "JankTest" is defined by the `minimum = ...,` setting in app.js - specifying the number of animated elements. console.warn(`✗ | Discarding results because CPU usage is too high and may be inaccurate: ${(cpuUsage * 100).toFixed(2)} %.`, 'Consider simplifying the scenario or changing the CPU throttling factor.'); return false; diff --git a/packages/replay/metrics/src/results/analyzer.ts b/packages/replay/metrics/src/results/analyzer.ts index d010ca0f2204..d7cddf707ec4 100644 --- a/packages/replay/metrics/src/results/analyzer.ts +++ b/packages/replay/metrics/src/results/analyzer.ts @@ -83,16 +83,16 @@ class AnalyzerItemNumberValue implements AnalyzerItemValue { public get diff(): string { const diff = this._b - this._a; - const str = this._withUnit(diff); + const str = this._withUnit(diff, true); return diff > 0 ? `+${str}` : str; } - private _withUnit(value: number): string { + private _withUnit(value: number, isDiff: boolean = false): string { switch (this._unit) { case AnalyzerItemUnit.bytes: return filesize(value) as string; case AnalyzerItemUnit.ratio: - return `${(value * 100).toFixed(2)} %`; + return `${(value * 100).toFixed(2)} ${isDiff ? 'pp' : '%'}`; default: return `${value.toFixed(2)} ${AnalyzerItemUnit[this._unit]}`; } diff --git a/packages/replay/metrics/src/results/pr-comment.ts b/packages/replay/metrics/src/results/pr-comment.ts index 2f01637d07b5..178cd7401b0b 100644 --- a/packages/replay/metrics/src/results/pr-comment.ts +++ b/packages/replay/metrics/src/results/pr-comment.ts @@ -4,7 +4,7 @@ import { Result } from './result.js'; import { ResultSetItem } from './results-set.js'; function trimIndent(str: string): string { - return str.split('\n').map(s => s.trim()).join('\n'); + return str.trim().split('\n').map(s => s.trim()).join('\n'); } function printableMetricName(metric: AnalyzerItemMetric): string { @@ -32,7 +32,15 @@ export class PrCommentBuilder { } public get body(): string { - return trimIndent(this._buffer); + const now = new Date(); + return trimIndent(` + ${this._buffer} +
+
+ CPU usage difference is shown as percentage points.
+ Last updated: +
+ `); } public async addCurrentResult(analysis: Analysis, otherName: string): Promise { diff --git a/packages/replay/metrics/test-apps/jank/app.js b/packages/replay/metrics/test-apps/jank/app.js index 390160a21f54..546644596756 100644 --- a/packages/replay/metrics/test-apps/jank/app.js +++ b/packages/replay/metrics/test-apps/jank/app.js @@ -25,7 +25,7 @@ document.addEventListener("DOMContentLoaded", function() { incrementor = 10, distance = 3, frame, - minimum = 30, + minimum = 70, subtract = document.querySelector('.subtract'), add = document.querySelector('.add'); From 2078d631404c0fe4f59f4926acc52ad57a97d6f2 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Fri, 6 Jan 2023 12:04:53 +0100 Subject: [PATCH 105/113] more CI tuning --- packages/replay/metrics/configs/ci/collect.ts | 4 ++-- packages/replay/metrics/src/collector.ts | 3 ++- packages/replay/metrics/test-apps/jank/app.js | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/replay/metrics/configs/ci/collect.ts b/packages/replay/metrics/configs/ci/collect.ts index cbed78974a3e..49fcbb910944 100644 --- a/packages/replay/metrics/configs/ci/collect.ts +++ b/packages/replay/metrics/configs/ci/collect.ts @@ -27,9 +27,9 @@ const result = await collector.execute({ tries: 10, async shouldAccept(results: Metrics[]): Promise { const stats = new MetricsStats(results); - printStats(stats); + await printStats(stats); - if (!checkStdDev(stats, 'lcp', MetricsStats.lcp, 30) + if (!checkStdDev(stats, 'lcp', MetricsStats.lcp, 50) || !checkStdDev(stats, 'cls', MetricsStats.cls, 0.1) || !checkStdDev(stats, 'cpu', MetricsStats.cpu, 1) || !checkStdDev(stats, 'memory-mean', MetricsStats.memoryMean, 1000 * 1024) diff --git a/packages/replay/metrics/src/collector.ts b/packages/replay/metrics/src/collector.ts index 36b0fdfb9b19..1efd4afcd0be 100644 --- a/packages/replay/metrics/src/collector.ts +++ b/packages/replay/metrics/src/collector.ts @@ -77,8 +77,9 @@ export class MetricsCollector { } catch (e) { console.warn(`${innerLabel} failed with ${e}`); break; + } finally { + console.timeEnd(innerLabel); } - console.timeEnd(innerLabel); } console.timeEnd(label); if ((results.length == testCase.runs) && await testCase.shouldAccept(results)) { diff --git a/packages/replay/metrics/test-apps/jank/app.js b/packages/replay/metrics/test-apps/jank/app.js index 546644596756..a854fd00d187 100644 --- a/packages/replay/metrics/test-apps/jank/app.js +++ b/packages/replay/metrics/test-apps/jank/app.js @@ -25,7 +25,7 @@ document.addEventListener("DOMContentLoaded", function() { incrementor = 10, distance = 3, frame, - minimum = 70, + minimum = 20, subtract = document.querySelector('.subtract'), add = document.querySelector('.add'); From 0f2d1ac9d51e50a8052a864ca3451d46b3d9b504 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Fri, 6 Jan 2023 13:38:52 +0100 Subject: [PATCH 106/113] docs --- packages/replay/metrics/README.md | 4 ++++ packages/replay/metrics/configs/ci/collect.ts | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/replay/metrics/README.md b/packages/replay/metrics/README.md index dcf78671fba6..11764a62009e 100644 --- a/packages/replay/metrics/README.md +++ b/packages/replay/metrics/README.md @@ -2,6 +2,10 @@ Evaluates Replay impact on website performance by running a web app in Chromium via Playwright and collecting various metrics. +The general idea is to run a web app without Sentry Replay and then run the same app again with Replay included. +For both scenarios, we collect some metrics (CPU, memory, vitals) and later compare them and post as a comment in a PR. +Changes in the collected, compared to previous runs from the main branch, should be evaluated on case-by-case basis when preparing and reviewing the PR. + ## Resources * https://github.com/addyosmani/puppeteer-webperf diff --git a/packages/replay/metrics/configs/ci/collect.ts b/packages/replay/metrics/configs/ci/collect.ts index 49fcbb910944..aafb8d149ba6 100644 --- a/packages/replay/metrics/configs/ci/collect.ts +++ b/packages/replay/metrics/configs/ci/collect.ts @@ -21,8 +21,8 @@ function checkStdDev(stats: MetricsStats, name: string, provider: NumberProvider const collector = new MetricsCollector({ headless: true, cpuThrottling: 2 }); const result = await collector.execute({ name: 'jank', - a: new JankTestScenario(false), - b: new JankTestScenario(true), + a: new JankTestScenario(false), // No sentry + b: new JankTestScenario(true), // Sentry + Replay runs: 10, tries: 10, async shouldAccept(results: Metrics[]): Promise { From d9b8244af65cb37eb84fd08d6b264f953695bbb6 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Fri, 6 Jan 2023 13:51:53 +0100 Subject: [PATCH 107/113] show increase as ratio too --- packages/replay/metrics/src/results/analyzer.ts | 8 ++++++++ packages/replay/metrics/src/results/pr-comment.ts | 12 +++++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/replay/metrics/src/results/analyzer.ts b/packages/replay/metrics/src/results/analyzer.ts index d7cddf707ec4..e6b8ce7a68dc 100644 --- a/packages/replay/metrics/src/results/analyzer.ts +++ b/packages/replay/metrics/src/results/analyzer.ts @@ -68,6 +68,7 @@ export interface AnalyzerItemValue { readonly a: string; readonly b: string; readonly diff: string; + readonly percent: string; } class AnalyzerItemNumberValue implements AnalyzerItemValue { @@ -87,6 +88,13 @@ class AnalyzerItemNumberValue implements AnalyzerItemValue { return diff > 0 ? `+${str}` : str; } + public get percent(): string { + if (this._a == 0) return 'n/a'; + const diff = this._b / this._a * 100 - 100; + const str = `${diff.toFixed(2)} %`; + return diff > 0 ? `+${str}` : str; + } + private _withUnit(value: number, isDiff: boolean = false): string { switch (this._unit) { case AnalyzerItemUnit.bytes: diff --git a/packages/replay/metrics/src/results/pr-comment.ts b/packages/replay/metrics/src/results/pr-comment.ts index 178cd7401b0b..5406d9041650 100644 --- a/packages/replay/metrics/src/results/pr-comment.ts +++ b/packages/replay/metrics/src/results/pr-comment.ts @@ -37,7 +37,7 @@ export class PrCommentBuilder { ${this._buffer}
- CPU usage difference is shown as percentage points.
+ *) pp - percentage points - an absolute difference between two percentages.
Last updated:
`); @@ -57,14 +57,14 @@ export class PrCommentBuilder {
 This PR (${await Git.hash})${otherName} (${analysis.otherHash})
Plain+ReplayDiffPlain+ReplayDiff
Plain+ReplayDiff
 This PR (${await Git.hash})${otherName} (${analysis.otherHash})
 
${printableMetricName(item.metric)} ${item.value.a} ${item.value.b}${item.value.diff}${item.value.diff}${item.other!.a} ${item.other!.b}${item.other!.diff}${item.other!.diff}
`; - const headerCols = ''; + const headerCols = ''; if (analysis.otherHash != undefined) { // If "other" is defined, add an aditional row of headers. this._buffer += ` - - + + ${headerCols} @@ -85,10 +85,12 @@ export class PrCommentBuilder { + ${maybeOther(() => ` - `)} + + `)} ` } From b772b3e13076235b5081d94824eb601a301c1b2c Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Mon, 9 Jan 2023 14:31:37 +0100 Subject: [PATCH 108/113] fix: don't post PR comments on forks --- .github/workflows/build.yml | 3 +++ packages/replay/metrics/src/util/github.ts | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dc55e5ffd3bd..73ea96d24c1e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -851,11 +851,14 @@ jobs: id: process run: yarn ci:process working-directory: packages/replay/metrics + # Don't run on forks - the PR comment cannot be added. + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository env: GITHUB_TOKEN: ${{ github.token }} - name: Upload results uses: actions/upload-artifact@v3 + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository with: name: ${{ steps.process.outputs.artifactName }} path: ${{ steps.process.outputs.artifactPath }} diff --git a/packages/replay/metrics/src/util/github.ts b/packages/replay/metrics/src/util/github.ts index 0345c10c9ef4..01b0e9fbe991 100644 --- a/packages/replay/metrics/src/util/github.ts +++ b/packages/replay/metrics/src/util/github.ts @@ -98,7 +98,7 @@ async function tryAddOrUpdateComment(commentBuilder: PrCommentBuilder): Promise< body: commentBuilder.body, }); } else { - console.log(`Adding new PR comment to PR ${prNumber}`) + console.log(`Adding a new comment to PR ${prNumber}`) await octokit.rest.issues.createComment({ ...defaultArgs, issue_number: prNumber, From c3fcad96808bd3f827faef843e43201af1cc8c04 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Wed, 11 Jan 2023 21:06:52 +0100 Subject: [PATCH 109/113] feat: separate metrics measurements for sentry and sentry+replay --- packages/replay/metrics/configs/ci/collect.ts | 26 +++--- .../replay/metrics/configs/dev/collect.ts | 12 ++- packages/replay/metrics/src/collector.ts | 8 +- .../replay/metrics/src/results/analyzer.ts | 78 +++++++++------- .../metrics/src/results/metrics-stats.ts | 18 ++-- .../replay/metrics/src/results/pr-comment.ts | 91 +++++++++++-------- packages/replay/metrics/src/results/result.ts | 6 +- packages/replay/metrics/src/scenarios.ts | 7 +- packages/replay/metrics/src/util/console.ts | 19 ++-- .../metrics/test-apps/jank/with-replay.html | 56 ++++++++++++ .../metrics/test-apps/jank/with-sentry.html | 6 -- 11 files changed, 204 insertions(+), 123 deletions(-) create mode 100644 packages/replay/metrics/test-apps/jank/with-replay.html diff --git a/packages/replay/metrics/configs/ci/collect.ts b/packages/replay/metrics/configs/ci/collect.ts index aafb8d149ba6..67add4feb6f2 100644 --- a/packages/replay/metrics/configs/ci/collect.ts +++ b/packages/replay/metrics/configs/ci/collect.ts @@ -4,8 +4,8 @@ import { JankTestScenario } from '../../src/scenarios.js'; import { printStats } from '../../src/util/console.js'; import { latestResultFile } from './env.js'; -function checkStdDev(stats: MetricsStats, name: string, provider: NumberProvider, max: number): boolean { - const value = stats.stddev(provider); +function checkStdDev(results: Metrics[], name: string, provider: NumberProvider, max: number): boolean { + const value = MetricsStats.stddev(results, provider); if (value == undefined) { console.warn(`✗ | Discarding results because StandardDeviation(${name}) is undefined`); return false; @@ -21,23 +21,25 @@ function checkStdDev(stats: MetricsStats, name: string, provider: NumberProvider const collector = new MetricsCollector({ headless: true, cpuThrottling: 2 }); const result = await collector.execute({ name: 'jank', - a: new JankTestScenario(false), // No sentry - b: new JankTestScenario(true), // Sentry + Replay + scenarios: [ + new JankTestScenario('index.html'), + new JankTestScenario('with-sentry.html'), + new JankTestScenario('with-replay.html'), + ], runs: 10, tries: 10, async shouldAccept(results: Metrics[]): Promise { - const stats = new MetricsStats(results); - await printStats(stats); + await printStats(results); - if (!checkStdDev(stats, 'lcp', MetricsStats.lcp, 50) - || !checkStdDev(stats, 'cls', MetricsStats.cls, 0.1) - || !checkStdDev(stats, 'cpu', MetricsStats.cpu, 1) - || !checkStdDev(stats, 'memory-mean', MetricsStats.memoryMean, 1000 * 1024) - || !checkStdDev(stats, 'memory-max', MetricsStats.memoryMax, 1000 * 1024)) { + if (!checkStdDev(results, 'lcp', MetricsStats.lcp, 50) + || !checkStdDev(results, 'cls', MetricsStats.cls, 0.1) + || !checkStdDev(results, 'cpu', MetricsStats.cpu, 1) + || !checkStdDev(results, 'memory-mean', MetricsStats.memoryMean, 1000 * 1024) + || !checkStdDev(results, 'memory-max', MetricsStats.memoryMax, 1000 * 1024)) { return false; } - const cpuUsage = stats.mean(MetricsStats.cpu)!; + const cpuUsage = MetricsStats.mean(results, MetricsStats.cpu)!; if (cpuUsage > 0.85) { // Note: complexity on the "JankTest" is defined by the `minimum = ...,` setting in app.js - specifying the number of animated elements. console.warn(`✗ | Discarding results because CPU usage is too high and may be inaccurate: ${(cpuUsage * 100).toFixed(2)} %.`, diff --git a/packages/replay/metrics/configs/dev/collect.ts b/packages/replay/metrics/configs/dev/collect.ts index a370fdf594f8..a159d7a4f7b1 100644 --- a/packages/replay/metrics/configs/dev/collect.ts +++ b/packages/replay/metrics/configs/dev/collect.ts @@ -7,15 +7,17 @@ import { latestResultFile } from './env.js'; const collector = new MetricsCollector(); const result = await collector.execute({ name: 'dummy', - a: new JankTestScenario(false), - b: new JankTestScenario(true), + scenarios: [ + new JankTestScenario('index.html'), + new JankTestScenario('with-sentry.html'), + new JankTestScenario('with-replay.html'), + ], runs: 1, tries: 1, async shouldAccept(results: Metrics[]): Promise { - const stats = new MetricsStats(results); - printStats(stats); + printStats(results); - const cpuUsage = stats.mean(MetricsStats.cpu)!; + const cpuUsage = MetricsStats.mean(results, MetricsStats.cpu)!; if (cpuUsage > 0.9) { console.error(`CPU usage too high to be accurate: ${(cpuUsage * 100).toFixed(2)} %.`, 'Consider simplifying the scenario or changing the CPU throttling factor.'); diff --git a/packages/replay/metrics/src/collector.ts b/packages/replay/metrics/src/collector.ts index 1efd4afcd0be..d00fa1caf131 100644 --- a/packages/replay/metrics/src/collector.ts +++ b/packages/replay/metrics/src/collector.ts @@ -58,9 +58,11 @@ export class MetricsCollector { public async execute(testCase: TestCase): Promise { console.log(`Executing test case ${testCase.name}`); return consoleGroup(async () => { - const aResults = await this._collect(testCase, 'A', testCase.a); - const bResults = await this._collect(testCase, 'B', testCase.b); - return new Result(testCase.name, this._options.cpuThrottling, networkConditions, aResults, bResults); + const scenarioResults: Metrics[][] = []; + for (let s = 0; s < testCase.scenarios.length; s++) { + scenarioResults.push(await this._collect(testCase, s.toString(), testCase.scenarios[s])); + } + return new Result(testCase.name, this._options.cpuThrottling, networkConditions, scenarioResults); }); } diff --git a/packages/replay/metrics/src/results/analyzer.ts b/packages/replay/metrics/src/results/analyzer.ts index e6b8ce7a68dc..82da37bf3d44 100644 --- a/packages/replay/metrics/src/results/analyzer.ts +++ b/packages/replay/metrics/src/results/analyzer.ts @@ -1,7 +1,7 @@ import { filesize } from 'filesize'; import { GitHash } from '../util/git.js'; -import { MetricsStats } from './metrics-stats.js'; +import { AnalyticsFunction, MetricsStats, NumberProvider } from './metrics-stats.js'; import { Result } from './result.js'; import { ResultsSet } from './results-set.js'; @@ -24,7 +24,7 @@ export class ResultsAnalyzer { for (const base of baseItems) { for (const item of items) { if (item.metric == base.metric) { - item.other = base.value; + item.others = base.values; otherHash = baseline[0]; } } @@ -40,21 +40,26 @@ export class ResultsAnalyzer { private _collect(): AnalyzerItem[] { const items = new Array(); - const aStats = new MetricsStats(this._result.aResults); - const bStats = new MetricsStats(this._result.bResults); + const scenarioResults = this._result.scenarioResults; - const pushIfDefined = function (metric: AnalyzerItemMetric, unit: AnalyzerItemUnit, valueA?: number, valueB?: number): void { - if (valueA == undefined || valueB == undefined) return; - items.push({ metric: metric, value: new AnalyzerItemNumberValue(unit, valueA, valueB) }) + const pushIfDefined = function (metric: AnalyzerItemMetric, unit: AnalyzerItemUnit, source: NumberProvider, fn: AnalyticsFunction): void { + const values = scenarioResults.map(items => fn(items, source)); + // only push if at least one value is defined + if (values.findIndex(v => v != undefined) >= 0) { + items.push({ + metric: metric, + values: new AnalyzerItemNumberValues(unit, values) + }); + } } - pushIfDefined(AnalyzerItemMetric.lcp, AnalyzerItemUnit.ms, aStats.mean(MetricsStats.lcp), bStats.mean(MetricsStats.lcp)); - pushIfDefined(AnalyzerItemMetric.cls, AnalyzerItemUnit.ms, aStats.mean(MetricsStats.cls), bStats.mean(MetricsStats.cls)); - pushIfDefined(AnalyzerItemMetric.cpu, AnalyzerItemUnit.ratio, aStats.mean(MetricsStats.cpu), bStats.mean(MetricsStats.cpu)); - pushIfDefined(AnalyzerItemMetric.memoryAvg, AnalyzerItemUnit.bytes, aStats.mean(MetricsStats.memoryMean), bStats.mean(MetricsStats.memoryMean)); - pushIfDefined(AnalyzerItemMetric.memoryMax, AnalyzerItemUnit.bytes, aStats.max(MetricsStats.memoryMax), bStats.max(MetricsStats.memoryMax)); + pushIfDefined(AnalyzerItemMetric.lcp, AnalyzerItemUnit.ms, MetricsStats.lcp, MetricsStats.mean); + pushIfDefined(AnalyzerItemMetric.cls, AnalyzerItemUnit.ms, MetricsStats.cls, MetricsStats.mean); + pushIfDefined(AnalyzerItemMetric.cpu, AnalyzerItemUnit.ratio, MetricsStats.cpu, MetricsStats.mean); + pushIfDefined(AnalyzerItemMetric.memoryAvg, AnalyzerItemUnit.bytes, MetricsStats.memoryMean, MetricsStats.mean); + pushIfDefined(AnalyzerItemMetric.memoryMax, AnalyzerItemUnit.bytes, MetricsStats.memoryMax, MetricsStats.max); - return items.filter((item) => item.value != undefined); + return items; } } @@ -64,35 +69,42 @@ export enum AnalyzerItemUnit { bytes, } -export interface AnalyzerItemValue { - readonly a: string; - readonly b: string; - readonly diff: string; - readonly percent: string; +export interface AnalyzerItemValues { + value(index: number): string; + diff(aIndex: number, bIndex: number): string; + percent(aIndex: number, bIndex: number): string; } -class AnalyzerItemNumberValue implements AnalyzerItemValue { - constructor(private _unit: AnalyzerItemUnit, private _a: number, private _b: number) { } +const AnalyzerItemValueNotAvailable = 'n/a'; + +class AnalyzerItemNumberValues implements AnalyzerItemValues { + constructor(private _unit: AnalyzerItemUnit, private _values: (number | undefined)[]) { } - public get a(): string { - return this._withUnit(this._a); + private _has(index: number): boolean { + return index >= 0 && index < this._values.length && this._values[index] != undefined; } - public get b(): string { - return this._withUnit(this._b); + private _get(index: number): number { + return this._values[index]!; } - public get diff(): string { - const diff = this._b - this._a; + public value(index: number): string { + if (!this._has(index)) return AnalyzerItemValueNotAvailable; + return this._withUnit(this._get(index)); + } + + public diff(aIndex: number, bIndex: number): string { + if (!this._has(aIndex) || !this._has(bIndex)) return AnalyzerItemValueNotAvailable; + const diff = this._get(bIndex) - this._get(aIndex); const str = this._withUnit(diff, true); return diff > 0 ? `+${str}` : str; } - public get percent(): string { - if (this._a == 0) return 'n/a'; - const diff = this._b / this._a * 100 - 100; - const str = `${diff.toFixed(2)} %`; - return diff > 0 ? `+${str}` : str; + public percent(aIndex: number, bIndex: number): string { + if (!this._has(aIndex) || !this._has(bIndex) || this._get(aIndex) == 0.0) return AnalyzerItemValueNotAvailable; + const percent = this._get(bIndex) / this._get(aIndex) * 100 - 100; + const str = `${percent.toFixed(2)} %`; + return percent > 0 ? `+${str}` : str; } private _withUnit(value: number, isDiff: boolean = false): string { @@ -119,10 +131,10 @@ export interface AnalyzerItem { metric: AnalyzerItemMetric; // Current (latest) result. - value: AnalyzerItemValue; + values: AnalyzerItemValues; // Previous or baseline results, depending on the context. - other?: AnalyzerItemValue; + others?: AnalyzerItemValues; } export interface Analysis { diff --git a/packages/replay/metrics/src/results/metrics-stats.ts b/packages/replay/metrics/src/results/metrics-stats.ts index f15cf2731dfb..6989a4645315 100644 --- a/packages/replay/metrics/src/results/metrics-stats.ts +++ b/packages/replay/metrics/src/results/metrics-stats.ts @@ -3,34 +3,32 @@ import * as ss from 'simple-statistics' import { Metrics } from '../collector'; export type NumberProvider = (metrics: Metrics) => number; +export type AnalyticsFunction = (items: Metrics[], dataProvider: NumberProvider) => number | undefined; export class MetricsStats { - constructor(private _items: Metrics[]) { } - static lcp: NumberProvider = metrics => metrics.vitals.lcp; static cls: NumberProvider = metrics => metrics.vitals.cls; static cpu: NumberProvider = metrics => metrics.cpu.average; static memoryMean: NumberProvider = metrics => ss.mean(Array.from(metrics.memory.snapshots.values())); static memoryMax: NumberProvider = metrics => ss.max(Array.from(metrics.memory.snapshots.values())); - public mean(dataProvider: NumberProvider): number | undefined { - const numbers = this._filteredValues(dataProvider); + static mean: AnalyticsFunction = (items: Metrics[], dataProvider: NumberProvider) => { + const numbers = MetricsStats._filteredValues(items.map(dataProvider)); return numbers.length > 0 ? ss.mean(numbers) : undefined; } - public max(dataProvider: NumberProvider): number | undefined { - const numbers = this._filteredValues(dataProvider); + static max: AnalyticsFunction = (items: Metrics[], dataProvider: NumberProvider) => { + const numbers = MetricsStats._filteredValues(items.map(dataProvider)); return numbers.length > 0 ? ss.max(numbers) : undefined; } - public stddev(dataProvider: NumberProvider): number | undefined { - const numbers = this._filteredValues(dataProvider); + static stddev: AnalyticsFunction = (items: Metrics[], dataProvider: NumberProvider) => { + const numbers = MetricsStats._filteredValues(items.map(dataProvider)); return numbers.length > 0 ? ss.standardDeviation(numbers) : undefined; } // See https://en.wikipedia.org/wiki/Interquartile_range#Outliers for details on filtering. - private _filteredValues(dataProvider: NumberProvider): number[] { - const numbers = this._items.map(dataProvider); + private static _filteredValues(numbers: number[]): number[] { numbers.sort((a, b) => a - b) const q1 = ss.quantileSorted(numbers, 0.25); diff --git a/packages/replay/metrics/src/results/pr-comment.ts b/packages/replay/metrics/src/results/pr-comment.ts index 5406d9041650..618c1aa8d76e 100644 --- a/packages/replay/metrics/src/results/pr-comment.ts +++ b/packages/replay/metrics/src/results/pr-comment.ts @@ -1,5 +1,5 @@ import { Git } from '../util/git.js'; -import { Analysis, AnalyzerItemMetric, ResultsAnalyzer } from './analyzer.js'; +import { Analysis, AnalyzerItemMetric, AnalyzerItemValues, ResultsAnalyzer } from './analyzer.js'; import { Result } from './result.js'; import { ResultSetItem } from './results-set.js'; @@ -44,54 +44,70 @@ export class PrCommentBuilder { } public async addCurrentResult(analysis: Analysis, otherName: string): Promise { - // Decides whether to print the "Other" depending on it being set in the input data. + // Decides whether to print the "Other" for comparison depending on it being set in the input data. + const hasOther = analysis.otherHash != undefined; const maybeOther = function (content: () => string): string { - if (analysis.otherHash == undefined) { - return ''; - } - return content(); + return hasOther ? content() : ''; } + const currentHash = await Git.hash + + this._buffer += `

${this.title}

`; + if (!hasOther) { + this._buffer += `Latest data for: ${currentHash}`; + } this._buffer += ` -

${this.title}

-
Plain+ReplayDiffPlain+ReplayDiffRatio
 This PR (${await Git.hash})${otherName} (${analysis.otherHash})This PR (${await Git.hash})${otherName} (${analysis.otherHash})
${item.value.a} ${item.value.b} ${item.value.diff}${item.value.percent}${item.other!.a} ${item.other!.b}${item.other!.diff}${item.other!.diff}${item.other!.percent}
- `; - - const headerCols = ''; - if (analysis.otherHash != undefined) { - // If "other" is defined, add an aditional row of headers. - this._buffer += ` +
Plain+ReplayDiffRatio
+ - - + ${maybeOther(() => ``)} + + + - ${headerCols} - ${headerCols} - `; - } else { - this._buffer += ` - - - ${headerCols} + ${maybeOther(() => ``)} + + + + + + + `; + + const valueColumns = function (values: AnalyzerItemValues): string { + return ` + + + + + + + + `; } for (const item of analysis.items) { - this._buffer += ` + if (hasOther) { + this._buffer += ` - - - - - - ${maybeOther(() => ` - - - - `)} + + + + ` + } else { + this._buffer += ` + + + ${valueColumns(item.values)} + ` + } } this._buffer += ` @@ -104,7 +120,7 @@ export class PrCommentBuilder { this._buffer += `

${name}

-
 This PR (${await Git.hash})${otherName} (${analysis.otherHash}) Plain+Sentry+Replay
 RevisionValueValueDiffRatioValueDiffRatio
${values.value(0)}${values.value(1)}${values.diff(0, 1)}${values.percent(0, 1)}${values.value(2)}${values.diff(1, 2)}${values.percent(1, 2)}
${printableMetricName(item.metric)}${item.value.a}${item.value.b}${item.value.diff}${item.value.percent}${item.other!.a}${item.other!.b}${item.other!.diff}${item.other!.percent}${printableMetricName(item.metric)}This PR ${currentHash} + ${valueColumns(item.values)} +
${otherName} ${analysis.otherHash} + ${valueColumns(item.others!)}
${printableMetricName(item.metric)}
`; +
`; // Each `resultFile` will be printed as a single row - with metrics as table columns. for (let i = 0; i < resultFiles.length; i++) { @@ -124,7 +140,8 @@ export class PrCommentBuilder { // Add table row this._buffer += ``; for (const item of analysis.items) { - this._buffer += ``; + // TODO maybe find a better way of showing this. After the change to multiple scenarios, this shows diff between "With Sentry" and "With Sentry + Replay" + this._buffer += ``; } this._buffer += ''; } diff --git a/packages/replay/metrics/src/results/result.ts b/packages/replay/metrics/src/results/result.ts index 8229642b6fea..ed4ca476b039 100644 --- a/packages/replay/metrics/src/results/result.ts +++ b/packages/replay/metrics/src/results/result.ts @@ -8,8 +8,7 @@ export class Result { constructor( public readonly name: string, public readonly cpuThrottling: number, public readonly networkConditions: string, - public readonly aResults: Metrics[], - public readonly bResults: Metrics[]) { } + public readonly scenarioResults: Metrics[][]) { } public static readFromFile(filePath: string): Result { const json = fs.readFileSync(filePath, { encoding: 'utf-8' }); @@ -18,8 +17,7 @@ export class Result { data.name as string, data.cpuThrottling as number, data.networkConditions as string, - (data.aResults as Partial[] || []).map(Metrics.fromJSON.bind(Metrics)), - (data.bResults as Partial[] || []).map(Metrics.fromJSON.bind(Metrics)), + (data.scenarioResults as Partial[][] || []).map(list => list.map(Metrics.fromJSON.bind(Metrics))) ); } diff --git a/packages/replay/metrics/src/scenarios.ts b/packages/replay/metrics/src/scenarios.ts index 47bf27294012..86974272394d 100644 --- a/packages/replay/metrics/src/scenarios.ts +++ b/packages/replay/metrics/src/scenarios.ts @@ -13,8 +13,7 @@ export interface Scenario { // Two scenarios that are compared to each other. export interface TestCase { name: string; - a: Scenario; - b: Scenario; + scenarios: Scenario[]; runs: number; tries: number; @@ -35,10 +34,10 @@ export class LoadPageScenario implements Scenario { // Loads test-apps/jank/ as a page source & waits for a short time before quitting. export class JankTestScenario implements Scenario { - public constructor(private _withSentry: boolean) { } + public constructor(private _indexFile: string) { } public async run(_: playwright.Browser, page: playwright.Page): Promise { - let url = path.resolve(`./test-apps/jank/${this._withSentry ? 'with-sentry' : 'index'}.html`); + let url = path.resolve(`./test-apps/jank/${this._indexFile}`); assert(fs.existsSync(url)); url = `file:///${url.replace('\\', '/')}`; console.log('Navigating to ', url); diff --git a/packages/replay/metrics/src/util/console.ts b/packages/replay/metrics/src/util/console.ts index 991216fb45ec..098953054e87 100644 --- a/packages/replay/metrics/src/util/console.ts +++ b/packages/replay/metrics/src/util/console.ts @@ -1,4 +1,5 @@ import { filesize } from 'filesize'; +import { Metrics } from '../collector.js'; import { Analysis, AnalyzerItemMetric } from '../results/analyzer.js'; import { MetricsStats } from '../results/metrics-stats.js'; @@ -11,13 +12,13 @@ export async function consoleGroup(code: () => Promise): Promise { // eslint-disable-next-line @typescript-eslint/no-explicit-any type PrintableTable = { [k: string]: any }; -export function printStats(stats: MetricsStats): void { +export function printStats(items: Metrics[]): void { console.table({ - lcp: `${stats.mean(MetricsStats.lcp)?.toFixed(2)} ms`, - cls: `${stats.mean(MetricsStats.cls)?.toFixed(2)} ms`, - cpu: `${((stats.mean(MetricsStats.cpu) || 0) * 100).toFixed(2)} %`, - memoryMean: filesize(stats.mean(MetricsStats.memoryMean)), - memoryMax: filesize(stats.max(MetricsStats.memoryMax)), + lcp: `${MetricsStats.mean(items, MetricsStats.lcp)?.toFixed(2)} ms`, + cls: `${MetricsStats.mean(items, MetricsStats.cls)?.toFixed(2)} ms`, + cpu: `${((MetricsStats.mean(items, MetricsStats.cpu) || 0) * 100).toFixed(2)} %`, + memoryMean: filesize(MetricsStats.mean(items, MetricsStats.memoryMean)), + memoryMax: filesize(MetricsStats.max(items, MetricsStats.memoryMax)), }); } @@ -25,9 +26,9 @@ export function printAnalysis(analysis: Analysis): void { const table: PrintableTable = {}; for (const item of analysis.items) { table[AnalyzerItemMetric[item.metric]] = { - value: item.value.diff, - ...((item.other == undefined) ? {} : { - previous: item.other.diff + value: item.values.diff(0, 1), + ...((item.others == undefined) ? {} : { + previous: item.others.diff(0, 1) }) }; } diff --git a/packages/replay/metrics/test-apps/jank/with-replay.html b/packages/replay/metrics/test-apps/jank/with-replay.html new file mode 100644 index 000000000000..7331eacfdd7f --- /dev/null +++ b/packages/replay/metrics/test-apps/jank/with-replay.html @@ -0,0 +1,56 @@ + + + + + + + + + + Janky Animation + + + + + + + + + + + +
+ + + + + + + +
+ + + diff --git a/packages/replay/metrics/test-apps/jank/with-sentry.html b/packages/replay/metrics/test-apps/jank/with-sentry.html index 7331eacfdd7f..3d43051eaf5a 100644 --- a/packages/replay/metrics/test-apps/jank/with-sentry.html +++ b/packages/replay/metrics/test-apps/jank/with-sentry.html @@ -27,15 +27,9 @@ - From bedeecfaa6dd5ec150eee21576799c9707c969cf Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Thu, 12 Jan 2023 09:36:34 +0100 Subject: [PATCH 110/113] fix and improve metrics analyzer console output --- packages/replay/metrics/src/results/analyzer.ts | 10 ++++++---- packages/replay/metrics/src/results/results-set.ts | 2 +- packages/replay/metrics/src/util/console.ts | 8 ++++++-- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/replay/metrics/src/results/analyzer.ts b/packages/replay/metrics/src/results/analyzer.ts index 82da37bf3d44..bdc2ee77864e 100644 --- a/packages/replay/metrics/src/results/analyzer.ts +++ b/packages/replay/metrics/src/results/analyzer.ts @@ -1,6 +1,7 @@ import { filesize } from 'filesize'; import { GitHash } from '../util/git.js'; +import { JsonStringify } from '../util/json.js'; import { AnalyticsFunction, MetricsStats, NumberProvider } from './metrics-stats.js'; import { Result } from './result.js'; import { ResultsSet } from './results-set.js'; @@ -13,19 +14,20 @@ export class ResultsAnalyzer { const items = new ResultsAnalyzer(currentResult)._collect(); const baseline = baselineResults?.find( - (other) => other.cpuThrottling == currentResult.cpuThrottling && - other.name == currentResult.name && - other.networkConditions == currentResult.networkConditions); + (other) => other.cpuThrottling == currentResult.cpuThrottling + && other.name == currentResult.name + && other.networkConditions == currentResult.networkConditions + && JsonStringify(other) != JsonStringify(currentResult)); let otherHash: GitHash | undefined if (baseline != undefined) { + otherHash = baseline[0]; const baseItems = new ResultsAnalyzer(baseline[1])._collect(); // update items with baseline results for (const base of baseItems) { for (const item of items) { if (item.metric == base.metric) { item.others = base.values; - otherHash = baseline[0]; } } } diff --git a/packages/replay/metrics/src/results/results-set.ts b/packages/replay/metrics/src/results/results-set.ts index f3432441deb7..ee38efd8a9f9 100644 --- a/packages/replay/metrics/src/results/results-set.ts +++ b/packages/replay/metrics/src/results/results-set.ts @@ -49,7 +49,7 @@ export class ResultsSet { public items(): ResultSetItem[] { return this._files().map((file) => { return new ResultSetItem(path.join(this._directory, file.name)); - }).filter((item) => !isNaN(item.number)); + }).filter((item) => !isNaN(item.number)).sort((a, b) => a.number - b.number); } public async add(newFile: string, onlyIfDifferent: boolean = false): Promise { diff --git a/packages/replay/metrics/src/util/console.ts b/packages/replay/metrics/src/util/console.ts index 098953054e87..9af66f36eb90 100644 --- a/packages/replay/metrics/src/util/console.ts +++ b/packages/replay/metrics/src/util/console.ts @@ -26,9 +26,13 @@ export function printAnalysis(analysis: Analysis): void { const table: PrintableTable = {}; for (const item of analysis.items) { table[AnalyzerItemMetric[item.metric]] = { - value: item.values.diff(0, 1), + value: item.values.value(0), + withSentry: item.values.diff(0, 1), + withReplay: item.values.diff(1, 2), ...((item.others == undefined) ? {} : { - previous: item.others.diff(0, 1) + previous: item.others.value(0), + previousWithSentry: item.others.diff(0, 1), + previousWithReplay: item.others.diff(1, 2) }) }; } From 77fa8868408f4a0f997ec75ccab4383924bd9cef Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Fri, 13 Jan 2023 12:13:51 +0100 Subject: [PATCH 111/113] review changes --- packages/replay/metrics/README.md | 6 +++--- packages/replay/metrics/src/results/metrics-stats.ts | 12 ++++++++---- packages/replay/metrics/src/vitals/cls.ts | 2 +- packages/replay/metrics/src/vitals/fid.ts | 2 +- packages/replay/metrics/src/vitals/index.ts | 2 +- packages/replay/metrics/src/vitals/lcp.ts | 2 +- 6 files changed, 15 insertions(+), 11 deletions(-) diff --git a/packages/replay/metrics/README.md b/packages/replay/metrics/README.md index 11764a62009e..6877d491c1b2 100644 --- a/packages/replay/metrics/README.md +++ b/packages/replay/metrics/README.md @@ -2,9 +2,9 @@ Evaluates Replay impact on website performance by running a web app in Chromium via Playwright and collecting various metrics. -The general idea is to run a web app without Sentry Replay and then run the same app again with Replay included. -For both scenarios, we collect some metrics (CPU, memory, vitals) and later compare them and post as a comment in a PR. -Changes in the collected, compared to previous runs from the main branch, should be evaluated on case-by-case basis when preparing and reviewing the PR. +The general idea is to run a web app without Sentry Replay and then run the same app again with Sentry and another one with Sentry+Replay included. +For the three scenarios, we collect some metrics (CPU, memory, vitals) and later compare them and post as a comment in a PR. +Changes in the metrics, compared to previous runs from the main branch, should be evaluated on case-by-case basis when preparing and reviewing the PR. ## Resources diff --git a/packages/replay/metrics/src/results/metrics-stats.ts b/packages/replay/metrics/src/results/metrics-stats.ts index 6989a4645315..fd23d032c11d 100644 --- a/packages/replay/metrics/src/results/metrics-stats.ts +++ b/packages/replay/metrics/src/results/metrics-stats.ts @@ -2,7 +2,7 @@ import * as ss from 'simple-statistics' import { Metrics } from '../collector'; -export type NumberProvider = (metrics: Metrics) => number; +export type NumberProvider = (metrics: Metrics) => number | undefined; export type AnalyticsFunction = (items: Metrics[], dataProvider: NumberProvider) => number | undefined; export class MetricsStats { @@ -13,20 +13,24 @@ export class MetricsStats { static memoryMax: NumberProvider = metrics => ss.max(Array.from(metrics.memory.snapshots.values())); static mean: AnalyticsFunction = (items: Metrics[], dataProvider: NumberProvider) => { - const numbers = MetricsStats._filteredValues(items.map(dataProvider)); + const numbers = MetricsStats._filteredValues(MetricsStats._collect(items, dataProvider)); return numbers.length > 0 ? ss.mean(numbers) : undefined; } static max: AnalyticsFunction = (items: Metrics[], dataProvider: NumberProvider) => { - const numbers = MetricsStats._filteredValues(items.map(dataProvider)); + const numbers = MetricsStats._filteredValues(MetricsStats._collect(items, dataProvider)); return numbers.length > 0 ? ss.max(numbers) : undefined; } static stddev: AnalyticsFunction = (items: Metrics[], dataProvider: NumberProvider) => { - const numbers = MetricsStats._filteredValues(items.map(dataProvider)); + const numbers = MetricsStats._filteredValues(MetricsStats._collect(items, dataProvider)); return numbers.length > 0 ? ss.standardDeviation(numbers) : undefined; } + private static _collect(items: Metrics[], dataProvider: NumberProvider): number[] { + return items.map(dataProvider).filter(v => v != undefined && !Number.isNaN(v)) as number[]; + } + // See https://en.wikipedia.org/wiki/Interquartile_range#Outliers for details on filtering. private static _filteredValues(numbers: number[]): number[] { numbers.sort((a, b) => a - b) diff --git a/packages/replay/metrics/src/vitals/cls.ts b/packages/replay/metrics/src/vitals/cls.ts index 213c5ddef0e5..abe6d63fa58b 100644 --- a/packages/replay/metrics/src/vitals/cls.ts +++ b/packages/replay/metrics/src/vitals/cls.ts @@ -34,7 +34,7 @@ class CLS { }`); } - public async collect(): Promise { + public async collect(): Promise { const result = await this._page.evaluate('window.cumulativeLayoutShiftScore'); return result as number; } diff --git a/packages/replay/metrics/src/vitals/fid.ts b/packages/replay/metrics/src/vitals/fid.ts index 550a1d40fbee..fb6baa41537a 100644 --- a/packages/replay/metrics/src/vitals/fid.ts +++ b/packages/replay/metrics/src/vitals/fid.ts @@ -28,7 +28,7 @@ class FID { }`); } - public async collect(): Promise { + public async collect(): Promise { const result = await this._page.evaluate('window.firstInputDelay'); return result as number; } diff --git a/packages/replay/metrics/src/vitals/index.ts b/packages/replay/metrics/src/vitals/index.ts index 119af6622c83..3170a6c73cb2 100644 --- a/packages/replay/metrics/src/vitals/index.ts +++ b/packages/replay/metrics/src/vitals/index.ts @@ -7,7 +7,7 @@ import { LCP } from './lcp.js'; export { WebVitals, WebVitalsCollector }; class WebVitals { - constructor(public lcp: number, public cls: number, public fid: number) { } + constructor(public lcp: number | undefined, public cls: number | undefined, public fid: number | undefined) { } public static fromJSON(data: Partial): WebVitals { return new WebVitals(data.lcp as number, data.cls as number, data.fid as number); diff --git a/packages/replay/metrics/src/vitals/lcp.ts b/packages/replay/metrics/src/vitals/lcp.ts index 4bc05143ddce..2f817ba97297 100644 --- a/packages/replay/metrics/src/vitals/lcp.ts +++ b/packages/replay/metrics/src/vitals/lcp.ts @@ -28,7 +28,7 @@ class LCP { }`); } - public async collect(): Promise { + public async collect(): Promise { const result = await this._page.evaluate('window.largestContentfulPaint'); return result as number; } From ef05d06cc5eca91e65d0af34bda6d0c6d572f88b Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Fri, 13 Jan 2023 17:02:47 +0100 Subject: [PATCH 112/113] metrics collector: improve close/dispose error handling --- packages/replay/metrics/src/collector.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/replay/metrics/src/collector.ts b/packages/replay/metrics/src/collector.ts index d00fa1caf131..d8673a8c4021 100644 --- a/packages/replay/metrics/src/collector.ts +++ b/packages/replay/metrics/src/collector.ts @@ -147,7 +147,20 @@ export class MetricsCollector { }); } finally { console.log('Disposing of browser and resources'); - disposeCallbacks.reverse().forEach((cb) => cb().catch(() => { /* silent */ })); + disposeCallbacks.reverse(); + const errors = []; + for (const cb of disposeCallbacks) { + try { + await cb(); + } catch (e) { + errors.push(e instanceof Error ? `${e.name}: ${e.message}` : `${e}`); + } + } + if (errors.length > 0) { + console.warn(`All disposose callbacks have finished. Errors: ${errors}`); + } else { + console.warn(`All disposose callbacks have finished.`); + } } } } From 355c4dcc1b39f7feb9e1e712938938bd6ed27c52 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Fri, 13 Jan 2023 17:19:56 +0100 Subject: [PATCH 113/113] collect metrics only if `ci-overhead-measurements` label is set on a PR --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 73ea96d24c1e..c331823234af 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -821,6 +821,7 @@ jobs: needs: [job_get_metadata, job_build] runs-on: ubuntu-20.04 timeout-minutes: 30 + if: contains(github.event.pull_request.labels.*.name, 'ci-overhead-measurements') steps: - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) uses: actions/checkout@v3
${resultFile.hash}${item.value.diff}${item.values.diff(1, 2)}