From 9d98364b0f7c13a3d645b54783006be317bf4bae Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 20 Dec 2023 15:45:48 +0100 Subject: [PATCH 1/5] ref(integrations): Rewrite pluggable integrations to use functional style --- packages/core/src/integration.ts | 14 +- packages/integrations/src/captureconsole.ts | 71 +- packages/integrations/src/contextlines.ts | 98 ++- packages/integrations/src/debug.ts | 110 ++-- packages/integrations/src/dedupe.ts | 84 +-- packages/integrations/src/extraerrordata.ts | 64 +- packages/integrations/src/httpclient.ts | 620 +++++++++--------- .../integrations/src/reportingobserver.ts | 98 ++- packages/integrations/src/rewriteframes.ts | 147 ++--- packages/integrations/src/sessiontiming.ts | 76 +-- packages/integrations/src/transaction.ts | 72 +- packages/integrations/test/debug.test.ts | 8 +- packages/integrations/test/dedupe.test.ts | 1 - .../integrations/test/extraerrordata.test.ts | 20 +- .../test/reportingobserver.test.ts | 37 +- .../integrations/test/rewriteframes.test.ts | 44 +- .../integrations/test/sessiontiming.test.ts | 12 +- .../integrations/test/transaction.test.ts | 2 +- 18 files changed, 685 insertions(+), 893 deletions(-) diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts index 01a55081c04f..c70eb34c961b 100644 --- a/packages/core/src/integration.ts +++ b/packages/core/src/integration.ts @@ -176,9 +176,10 @@ export function convertIntegrationFnToClass( name: string, fn: Fn, ): IntegrationClass< - Integration & { - setupOnce: (addGlobalEventProcessor?: (callback: EventProcessor) => void, getCurrentHub?: () => Hub) => void; - } + Integration & + ReturnType & { + setupOnce: (addGlobalEventProcessor?: (callback: EventProcessor) => void, getCurrentHub?: () => Hub) => void; + } > { return Object.assign( // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -191,8 +192,9 @@ export function convertIntegrationFnToClass( }, { id: name }, ) as unknown as IntegrationClass< - Integration & { - setupOnce: (addGlobalEventProcessor?: (callback: EventProcessor) => void, getCurrentHub?: () => Hub) => void; - } + Integration & + ReturnType & { + setupOnce: (addGlobalEventProcessor?: (callback: EventProcessor) => void, getCurrentHub?: () => Hub) => void; + } >; } diff --git a/packages/integrations/src/captureconsole.ts b/packages/integrations/src/captureconsole.ts index a1792573c9b1..4f37ecb1011a 100644 --- a/packages/integrations/src/captureconsole.ts +++ b/packages/integrations/src/captureconsole.ts @@ -1,5 +1,5 @@ -import { captureException, captureMessage, getClient, withScope } from '@sentry/core'; -import type { CaptureContext, Client, EventProcessor, Hub, Integration } from '@sentry/types'; +import { captureException, captureMessage, convertIntegrationFnToClass, getClient, withScope } from '@sentry/core'; +import type { CaptureContext, IntegrationFn } from '@sentry/types'; import { CONSOLE_LEVELS, GLOBAL_OBJ, @@ -9,55 +9,36 @@ import { severityLevelFromString, } from '@sentry/utils'; -/** Send Console API calls as Sentry Events */ -export class CaptureConsole implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'CaptureConsole'; - - /** - * @inheritDoc - */ - public name: string; - - /** - * @inheritDoc - */ - private readonly _levels: readonly string[]; - - /** - * @inheritDoc - */ - public constructor(options: { levels?: string[] } = {}) { - this.name = CaptureConsole.id; - this._levels = options.levels || CONSOLE_LEVELS; - } - - /** - * @inheritDoc - */ - public setupOnce(_: (callback: EventProcessor) => void, _getCurrentHub: () => Hub): void { - // noop - } +interface CaptureConsoleOptions { + levels?: string[]; +} - /** @inheritdoc */ - public setup(client: Client): void { - if (!('console' in GLOBAL_OBJ)) { - return; - } +const INTEGRATION_NAME = 'CaptureConsole'; - const levels = this._levels; +const captureConsoleIntegration = ((options: CaptureConsoleOptions = {}) => { + const levels = options.levels || CONSOLE_LEVELS; - addConsoleInstrumentationHandler(({ args, level }) => { - if (getClient() !== client || !levels.includes(level)) { + return { + name: INTEGRATION_NAME, + setup(client) { + if (!('console' in GLOBAL_OBJ)) { return; } - consoleHandler(args, level); - }); - } -} + addConsoleInstrumentationHandler(({ args, level }) => { + if (getClient() !== client || !levels.includes(level)) { + return; + } + + consoleHandler(args, level); + }); + }, + }; +}) satisfies IntegrationFn; + +/** Send Console API calls as Sentry Events */ +// eslint-disable-next-line deprecation/deprecation +export const CaptureConsole = convertIntegrationFnToClass(INTEGRATION_NAME, captureConsoleIntegration); function consoleHandler(args: unknown[], level: string): void { const captureContext: CaptureContext = { diff --git a/packages/integrations/src/contextlines.ts b/packages/integrations/src/contextlines.ts index d716ca2fbb5f..656080ec3182 100644 --- a/packages/integrations/src/contextlines.ts +++ b/packages/integrations/src/contextlines.ts @@ -1,10 +1,13 @@ -import type { Event, Integration, StackFrame } from '@sentry/types'; +import { convertIntegrationFnToClass } from '@sentry/core'; +import type { Event, IntegrationFn, StackFrame } from '@sentry/types'; import { GLOBAL_OBJ, addContextToFrame, stripUrlQueryAndFragment } from '@sentry/utils'; const WINDOW = GLOBAL_OBJ as typeof GLOBAL_OBJ & Window; const DEFAULT_LINES_OF_CONTEXT = 7; +const INTEGRATION_NAME = 'ContextLines'; + interface ContextLinesOptions { /** * Sets the number of context lines for each frame when loading a file. @@ -15,6 +18,17 @@ interface ContextLinesOptions { frameContextLines?: number; } +const contextLinesIntegration: IntegrationFn = (options: ContextLinesOptions = {}) => { + const contextLines = options.frameContextLines != null ? options.frameContextLines : DEFAULT_LINES_OF_CONTEXT; + + return { + name: INTEGRATION_NAME, + processEvent(event) { + return addSourceContext(event, contextLines); + }, + }; +}; + /** * Collects source context lines around the lines of stackframes pointing to JS embedded in * the current page's HTML. @@ -26,73 +40,41 @@ interface ContextLinesOptions { * Use this integration if you have inline JS code in HTML pages that can't be accessed * by our backend (e.g. due to a login-protected page). */ -export class ContextLines implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'ContextLines'; - - /** - * @inheritDoc - */ - public name: string; +// eslint-disable-next-line deprecation/deprecation +export const ContextLines = convertIntegrationFnToClass(INTEGRATION_NAME, contextLinesIntegration); - public constructor(private readonly _options: ContextLinesOptions = {}) { - this.name = ContextLines.id; +/** + * Processes an event and adds context lines. + */ +function addSourceContext(event: Event, contextLines: number): Event { + const doc = WINDOW.document; + const htmlFilename = WINDOW.location && stripUrlQueryAndFragment(WINDOW.location.href); + if (!doc || !htmlFilename) { + return event; } - /** - * @inheritDoc - */ - public setupOnce(_addGlobalEventProcessor: unknown, _getCurrentHub: unknown): void { - // noop + const exceptions = event.exception && event.exception.values; + if (!exceptions || !exceptions.length) { + return event; } - /** @inheritDoc */ - public processEvent(event: Event): Event { - return this.addSourceContext(event); + const html = doc.documentElement.innerHTML; + if (!html) { + return event; } - /** - * Processes an event and adds context lines. - * - * TODO (v8): Make this internal/private - */ - public addSourceContext(event: Event): Event { - const doc = WINDOW.document; - const htmlFilename = WINDOW.location && stripUrlQueryAndFragment(WINDOW.location.href); - if (!doc || !htmlFilename) { - return event; - } - - const exceptions = event.exception && event.exception.values; - if (!exceptions || !exceptions.length) { - return event; - } + const htmlLines = ['', '', ...html.split('\n'), '']; - const html = doc.documentElement.innerHTML; - if (!html) { - return event; + exceptions.forEach(exception => { + const stacktrace = exception.stacktrace; + if (stacktrace && stacktrace.frames) { + stacktrace.frames = stacktrace.frames.map(frame => + applySourceContextToFrame(frame, htmlLines, htmlFilename, contextLines), + ); } + }); - const htmlLines = ['', '', ...html.split('\n'), '']; - - exceptions.forEach(exception => { - const stacktrace = exception.stacktrace; - if (stacktrace && stacktrace.frames) { - stacktrace.frames = stacktrace.frames.map(frame => - applySourceContextToFrame( - frame, - htmlLines, - htmlFilename, - this._options.frameContextLines != null ? this._options.frameContextLines : DEFAULT_LINES_OF_CONTEXT, - ), - ); - } - }); - - return event; - } + return event; } /** diff --git a/packages/integrations/src/debug.ts b/packages/integrations/src/debug.ts index bb8ed8924254..6edb9939269a 100644 --- a/packages/integrations/src/debug.ts +++ b/packages/integrations/src/debug.ts @@ -1,6 +1,9 @@ -import type { Client, Event, EventHint, EventProcessor, Hub, Integration } from '@sentry/types'; +import { convertIntegrationFnToClass } from '@sentry/core'; +import type { Event, EventHint, IntegrationFn } from '@sentry/types'; import { consoleSandbox } from '@sentry/utils'; +const INTEGRATION_NAME = 'Debug'; + interface DebugOptions { /** Controls whether console output created by this integration should be stringified. Default: `false` */ stringify?: boolean; @@ -8,70 +11,49 @@ interface DebugOptions { debugger?: boolean; } -/** - * Integration to debug sent Sentry events. - * This integration should not be used in production - */ -export class Debug implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'Debug'; - - /** - * @inheritDoc - */ - public name: string; - - private readonly _options: DebugOptions; - - public constructor(options?: DebugOptions) { - this.name = Debug.id; - - this._options = { - debugger: false, - stringify: false, - ...options, - }; - } - - /** - * @inheritDoc - */ - public setupOnce( - _addGlobalEventProcessor: (eventProcessor: EventProcessor) => void, - _getCurrentHub: () => Hub, - ): void { - // noop - } - - /** @inheritdoc */ - public setup(client: Client): void { - if (!client.on) { - return; - } - - client.on('beforeSendEvent', (event: Event, hint?: EventHint) => { - if (this._options.debugger) { - // eslint-disable-next-line no-debugger - debugger; +const debugIntegration = ((options: DebugOptions = {}) => { + const _options = { + debugger: false, + stringify: false, + ...options, + }; + + return { + name: INTEGRATION_NAME, + setup(client) { + if (!client.on) { + return; } - /* eslint-disable no-console */ - consoleSandbox(() => { - if (this._options.stringify) { - console.log(JSON.stringify(event, null, 2)); - if (hint && Object.keys(hint).length) { - console.log(JSON.stringify(hint, null, 2)); - } - } else { - console.log(event); - if (hint && Object.keys(hint).length) { - console.log(hint); - } + client.on('beforeSendEvent', (event: Event, hint?: EventHint) => { + if (_options.debugger) { + // eslint-disable-next-line no-debugger + debugger; } + + /* eslint-disable no-console */ + consoleSandbox(() => { + if (_options.stringify) { + console.log(JSON.stringify(event, null, 2)); + if (hint && Object.keys(hint).length) { + console.log(JSON.stringify(hint, null, 2)); + } + } else { + console.log(event); + if (hint && Object.keys(hint).length) { + console.log(hint); + } + } + }); + /* eslint-enable no-console */ }); - /* eslint-enable no-console */ - }); - } -} + }, + }; +}) satisfies IntegrationFn; + +/** + * Integration to debug sent Sentry events. + * This integration should not be used in production + */ +// eslint-disable-next-line deprecation/deprecation +export const Debug = convertIntegrationFnToClass(INTEGRATION_NAME, debugIntegration); diff --git a/packages/integrations/src/dedupe.ts b/packages/integrations/src/dedupe.ts index 464758d20dfc..1e3ae1be7626 100644 --- a/packages/integrations/src/dedupe.ts +++ b/packages/integrations/src/dedupe.ts @@ -1,59 +1,41 @@ -import type { Event, Exception, Integration, StackFrame } from '@sentry/types'; +import { convertIntegrationFnToClass } from '@sentry/core'; +import type { Event, Exception, IntegrationFn, StackFrame } from '@sentry/types'; import { logger } from '@sentry/utils'; import { DEBUG_BUILD } from './debug-build'; -/** Deduplication filter */ -export class Dedupe implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'Dedupe'; - - /** - * @inheritDoc - */ - public name: string; - - /** - * @inheritDoc - */ - private _previousEvent?: Event; - - public constructor() { - this.name = Dedupe.id; - } - - /** @inheritDoc */ - public setupOnce(_addGlobalEventProcessor: unknown, _getCurrentHub: unknown): void { - // noop - } - - /** - * @inheritDoc - */ - public processEvent(currentEvent: Event): Event | null { - // We want to ignore any non-error type events, e.g. transactions or replays - // These should never be deduped, and also not be compared against as _previousEvent. - if (currentEvent.type) { - return currentEvent; - } +const INTEGRATION_NAME = 'Dedupe'; - // Juuust in case something goes wrong - try { - if (_shouldDropEvent(currentEvent, this._previousEvent)) { - DEBUG_BUILD && logger.warn('Event dropped due to being a duplicate of previously captured event.'); - return null; +const dedupeIntegration = (() => { + let previousEvent: Event | undefined; + + return { + name: INTEGRATION_NAME, + processEvent(currentEvent) { + // We want to ignore any non-error type events, e.g. transactions or replays + // These should never be deduped, and also not be compared against as _previousEvent. + if (currentEvent.type) { + return currentEvent; } - } catch (_oO) { - return (this._previousEvent = currentEvent); - } - return (this._previousEvent = currentEvent); - } -} + // Juuust in case something goes wrong + try { + if (_shouldDropEvent(currentEvent, previousEvent)) { + DEBUG_BUILD && logger.warn('Event dropped due to being a duplicate of previously captured event.'); + return null; + } + } catch (_oO) {} // eslint-disable-line no-empty + + return (previousEvent = currentEvent); + }, + }; +}) satisfies IntegrationFn; + +/** Deduplication filter */ +// eslint-disable-next-line deprecation/deprecation +export const Dedupe = convertIntegrationFnToClass(INTEGRATION_NAME, dedupeIntegration); -/** JSDoc */ +/** only exported for tests. */ export function _shouldDropEvent(currentEvent: Event, previousEvent?: Event): boolean { if (!previousEvent) { return false; @@ -70,7 +52,6 @@ export function _shouldDropEvent(currentEvent: Event, previousEvent?: Event): bo return false; } -/** JSDoc */ function _isSameMessageEvent(currentEvent: Event, previousEvent: Event): boolean { const currentMessage = currentEvent.message; const previousMessage = previousEvent.message; @@ -100,7 +81,6 @@ function _isSameMessageEvent(currentEvent: Event, previousEvent: Event): boolean return true; } -/** JSDoc */ function _isSameExceptionEvent(currentEvent: Event, previousEvent: Event): boolean { const previousException = _getExceptionFromEvent(previousEvent); const currentException = _getExceptionFromEvent(currentEvent); @@ -124,7 +104,6 @@ function _isSameExceptionEvent(currentEvent: Event, previousEvent: Event): boole return true; } -/** JSDoc */ function _isSameStacktrace(currentEvent: Event, previousEvent: Event): boolean { let currentFrames = _getFramesFromEvent(currentEvent); let previousFrames = _getFramesFromEvent(previousEvent); @@ -165,7 +144,6 @@ function _isSameStacktrace(currentEvent: Event, previousEvent: Event): boolean { return true; } -/** JSDoc */ function _isSameFingerprint(currentEvent: Event, previousEvent: Event): boolean { let currentFingerprint = currentEvent.fingerprint; let previousFingerprint = previousEvent.fingerprint; @@ -191,12 +169,10 @@ function _isSameFingerprint(currentEvent: Event, previousEvent: Event): boolean } } -/** JSDoc */ function _getExceptionFromEvent(event: Event): Exception | undefined { return event.exception && event.exception.values && event.exception.values[0]; } -/** JSDoc */ function _getFramesFromEvent(event: Event): StackFrame[] | undefined { const exception = event.exception; diff --git a/packages/integrations/src/extraerrordata.ts b/packages/integrations/src/extraerrordata.ts index 1c1b46e58c22..9d8a00f976cf 100644 --- a/packages/integrations/src/extraerrordata.ts +++ b/packages/integrations/src/extraerrordata.ts @@ -1,61 +1,29 @@ -import type { Contexts, Event, EventHint, ExtendedError, Integration } from '@sentry/types'; +import { convertIntegrationFnToClass } from '@sentry/core'; +import type { Contexts, Event, EventHint, ExtendedError, IntegrationFn } from '@sentry/types'; import { addNonEnumerableProperty, isError, isPlainObject, logger, normalize } from '@sentry/utils'; import { DEBUG_BUILD } from './debug-build'; -/** JSDoc */ +const INTEGRATION_NAME = 'ExtraErrorData'; + interface ExtraErrorDataOptions { depth: number; } -/** Patch toString calls to return proper name for wrapped functions */ -export class ExtraErrorData implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'ExtraErrorData'; - - /** - * @inheritDoc - */ - public name: string; - - /** JSDoc */ - private readonly _options: ExtraErrorDataOptions; - - /** - * @inheritDoc - */ - public constructor(options?: Partial) { - this.name = ExtraErrorData.id; - - this._options = { - depth: 3, - ...options, - }; - } - - /** - * @inheritDoc - */ - public setupOnce(_addGlobalEventProcessor: unknown, _getCurrentHub: unknown): void { - // noop - } +const extraErrorDataIntegration = ((options: Partial = {}) => { + const depth = options.depth || 3; - /** @inheritDoc */ - public processEvent(event: Event, hint: EventHint): Event { - return this.enhanceEventWithErrorData(event, hint); - } + return { + name: INTEGRATION_NAME, + processEvent(event, hint) { + return _enhanceEventWithErrorData(event, hint, depth); + }, + }; +}) satisfies IntegrationFn; - /** - * Attaches extracted information from the Error object to extra field in the Event. - * - * TODO (v8): Drop this public function. - */ - public enhanceEventWithErrorData(event: Event, hint: EventHint = {}): Event { - return _enhanceEventWithErrorData(event, hint, this._options.depth); - } -} +/** Extract additional data for from original exceptions. */ +// eslint-disable-next-line deprecation/deprecation +export const ExtraErrorData = convertIntegrationFnToClass(INTEGRATION_NAME, extraErrorDataIntegration); function _enhanceEventWithErrorData(event: Event, hint: EventHint = {}, depth: number): Event { if (!hint.originalException || !isError(hint.originalException)) { diff --git a/packages/integrations/src/httpclient.ts b/packages/integrations/src/httpclient.ts index bcb4429b7aeb..74142487473a 100644 --- a/packages/integrations/src/httpclient.ts +++ b/packages/integrations/src/httpclient.ts @@ -1,12 +1,5 @@ -import { captureEvent, getClient, isSentryRequestUrl } from '@sentry/core'; -import type { - Client, - Event as SentryEvent, - EventProcessor, - Hub, - Integration, - SentryWrappedXMLHttpRequest, -} from '@sentry/types'; +import { captureEvent, convertIntegrationFnToClass, getClient, isSentryRequestUrl } from '@sentry/core'; +import type { Client, Event as SentryEvent, IntegrationFn, SentryWrappedXMLHttpRequest } from '@sentry/types'; import { GLOBAL_OBJ, SENTRY_XHR_DATA_KEY, @@ -22,6 +15,8 @@ import { DEBUG_BUILD } from './debug-build'; export type HttpStatusCodeRange = [number, number] | number; export type HttpRequestTarget = string | RegExp; +const INTEGRATION_NAME = 'HttpClient'; + interface HttpClientOptions { /** * HTTP status codes that should be considered failed. @@ -31,7 +26,7 @@ interface HttpClientOptions { * Example: [[500, 505], 507] * Default: [[500, 599]] */ - failedRequestStatusCodes?: HttpStatusCodeRange[]; + failedRequestStatusCodes: HttpStatusCodeRange[]; /** * Targets to track for failed requests. @@ -40,375 +35,360 @@ interface HttpClientOptions { * Example: ['http://localhost', /api\/.*\/] * Default: [/.*\/] */ - failedRequestTargets?: HttpRequestTarget[]; + 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; - - private readonly _options: HttpClientOptions; - - /** - * @inheritDoc - * - * @param options - */ - public constructor(options?: Partial) { - this.name = HttpClient.id; - this._options = { - failedRequestStatusCodes: [[500, 599]], - failedRequestTargets: [/.*/], - ...options, - }; - } - - /** - * @inheritDoc - * - * @param options - */ - public setupOnce(_: (callback: EventProcessor) => void, _getCurrentHub: () => Hub): void { - // noop - } - - /** @inheritdoc */ - public setup(client: Client): void { - this._wrapFetch(client); - this._wrapXHR(client); - } - - /** - * 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._shouldCaptureResponse(response.status, response.url)) { - const request = _getRequest(requestInfo, requestInit); - - let requestHeaders, responseHeaders, requestCookies, responseCookies; - - if (_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, - }); - - captureEvent(event); - } - } +const httpClientIntegration = ((options: Partial = {}) => { + const _options: HttpClientOptions = { + failedRequestStatusCodes: [[500, 599]], + failedRequestTargets: [/.*/], + ...options, + }; + + return { + name: INTEGRATION_NAME, + setup(client): void { + _wrapFetch(client, _options); + _wrapXHR(client, _options); + }, + }; +}) satisfies IntegrationFn; - /** - * 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._shouldCaptureResponse(xhr.status, xhr.responseURL)) { - let requestHeaders, responseCookies, responseHeaders; +/** HTTPClient integration creates events for failed client side HTTP requests. */ +// eslint-disable-next-line deprecation/deprecation +export const HttpClient = convertIntegrationFnToClass(INTEGRATION_NAME, httpClientIntegration); + +/** + * Interceptor function for fetch requests + * + * @param requestInfo The Fetch API request info + * @param response The Fetch API response + * @param requestInit The request init object + */ +function _fetchResponseHandler( + options: HttpClientOptions, + requestInfo: RequestInfo, + response: Response, + requestInit?: RequestInit, +): void { + if (_shouldCaptureResponse(options, response.status, response.url)) { + const request = _getRequest(requestInfo, requestInit); + + let requestHeaders, responseHeaders, requestCookies, responseCookies; + + if (_shouldSendDefaultPii()) { + [{ headers: requestHeaders, cookies: requestCookies }, { headers: responseHeaders, cookies: responseCookies }] = [ + { cookieHeader: 'Cookie', obj: request }, + { cookieHeader: 'Set-Cookie', obj: response }, + ].map(({ cookieHeader, obj }) => { + const headers = _extractFetchHeaders(obj.headers); + let cookies; - if (_shouldSendDefaultPii()) { try { - const cookieString = xhr.getResponseHeader('Set-Cookie') || xhr.getResponseHeader('set-cookie') || undefined; + const cookieString = headers[cookieHeader] || headers[cookieHeader.toLowerCase()] || undefined; if (cookieString) { - responseCookies = this._parseCookieString(cookieString); + cookies = _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'); + DEBUG_BUILD && logger.log(`Could not extract cookies from header ${cookieHeader}`); } - requestHeaders = headers; - } - - const event = this._createEvent({ - url: xhr.responseURL, - method, - status: xhr.status, - requestHeaders, - // Can't access request cookies from XHR - responseHeaders, - responseCookies, + return { + headers, + cookies, + }; }); - - captureEvent(event); } + + const event = _createEvent({ + url: request.url, + method: request.method, + status: response.status, + requestHeaders, + responseHeaders, + requestCookies, + responseCookies, + }); + + 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']; +/** + * Interceptor function for XHR requests + * + * @param xhr The XHR request + * @param method The HTTP method + * @param headers The HTTP headers + */ +function _xhrResponseHandler( + options: HttpClientOptions, + xhr: XMLHttpRequest, + method: string, + headers: Record, +): void { + if (_shouldCaptureResponse(options, xhr.status, xhr.responseURL)) { + let requestHeaders, responseCookies, responseHeaders; + + if (_shouldSendDefaultPii()) { + try { + const cookieString = xhr.getResponseHeader('Set-Cookie') || xhr.getResponseHeader('set-cookie') || undefined; + + if (cookieString) { + responseCookies = _parseCookieString(cookieString); + } + } catch (e) { + DEBUG_BUILD && logger.log('Could not extract cookies from response headers'); + } - if (contentLength) { - return parseInt(contentLength, 10); + try { + responseHeaders = _getXHRResponseHeaders(xhr); + } catch (e) { + DEBUG_BUILD && logger.log('Could not extract headers from response'); } + + requestHeaders = headers; } - return undefined; + const event = _createEvent({ + url: xhr.responseURL, + method, + status: xhr.status, + requestHeaders, + // Can't access request cookies from XHR + responseHeaders, + responseCookies, + }); + + captureEvent(event); } +} - /** - * 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 response size from `Content-Length` header when possible + * + * @param headers + * @returns The response size in bytes or undefined + */ +function _getResponseSizeFromHeaders(headers?: Record): number | undefined { + if (headers) { + const contentLength = headers['Content-Length'] || headers['content-length']; + + if (contentLength) { + return parseInt(contentLength, 10); + } } - /** - * 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 = {}; + return undefined; +} - headers.forEach((value, key) => { - result[key] = value; - }); +/** + * Creates an object containing cookies from the given cookie string + * + * @param cookieString The cookie string to parse + * @returns The parsed cookies + */ +function _parseCookieString(cookieString: string): Record { + return cookieString.split('; ').reduce((acc: Record, cookie: string) => { + const [key, value] = cookie.split('='); + acc[key] = value; + return acc; + }, {}); +} - return result; - } +/** + * 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 + */ +function _extractFetchHeaders(headers: Headers): Record { + const result: Record = {}; - /** - * 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(); + headers.forEach((value, key) => { + result[key] = value; + }); - if (!headers) { - return {}; - } + return result; +} - return headers.split('\r\n').reduce((acc: Record, line: string) => { - const [key, value] = line.split(': '); - acc[key] = value; - return acc; - }, {}); +/** + * 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 + */ +function _getXHRResponseHeaders(xhr: XMLHttpRequest): Record { + const headers = xhr.getAllResponseHeaders(); + + if (!headers) { + return {}; } - /** - * 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 headers.split('\r\n').reduce((acc: Record, line: string) => { + const [key, value] = line.split(': '); + acc[key] = value; + return acc; + }, {}); +} - return this._options.failedRequestTargets.some((givenRequestTarget: HttpRequestTarget) => { - if (typeof givenRequestTarget === 'string') { - return target.includes(givenRequestTarget); - } +/** + * 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 + */ +function _isInGivenRequestTargets( + failedRequestTargets: HttpClientOptions['failedRequestTargets'], + target: string, +): boolean { + return failedRequestTargets.some((givenRequestTarget: HttpRequestTarget) => { + if (typeof givenRequestTarget === 'string') { + return target.includes(givenRequestTarget); + } - return givenRequestTarget.test(target); - }); - } + 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; +/** + * 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 + */ +function _isInGivenStatusRanges( + failedRequestStatusCodes: HttpClientOptions['failedRequestStatusCodes'], + status: number, +): boolean { + return failedRequestStatusCodes.some((range: HttpStatusCodeRange) => { + if (typeof range === 'number') { + return range === status; } - return this._options.failedRequestStatusCodes.some((range: HttpStatusCodeRange) => { - if (typeof range === 'number') { - return range === status; - } + return status >= range[0] && status <= range[1]; + }); +} - return status >= range[0] && status <= range[1]; - }); +/** + * Wraps `fetch` function to capture request and response data + */ +function _wrapFetch(client: Client, options: HttpClientOptions): void { + if (!supportsNativeFetch()) { + return; } - /** - * Wraps `fetch` function to capture request and response data - */ - private _wrapFetch(client: Client): void { - if (!supportsNativeFetch()) { + addFetchInstrumentationHandler(handlerData => { + if (getClient() !== client) { return; } - addFetchInstrumentationHandler(handlerData => { - if (getClient() !== client) { - return; - } + const { response, args } = handlerData; + const [requestInfo, requestInit] = args as [RequestInfo, RequestInit | undefined]; - const { response, args } = handlerData; - const [requestInfo, requestInit] = args as [RequestInfo, RequestInit | undefined]; + if (!response) { + return; + } - if (!response) { - return; - } + _fetchResponseHandler(options, requestInfo, response as Response, requestInit); + }); +} - this._fetchResponseHandler(requestInfo, response as Response, requestInit); - }); +/** + * Wraps XMLHttpRequest to capture request and response data + */ +function _wrapXHR(client: Client, options: HttpClientOptions): void { + if (!('XMLHttpRequest' in GLOBAL_OBJ)) { + return; } - /** - * Wraps XMLHttpRequest to capture request and response data - */ - private _wrapXHR(client: Client): void { - if (!('XMLHttpRequest' in GLOBAL_OBJ)) { + addXhrInstrumentationHandler(handlerData => { + if (getClient() !== client) { return; } - addXhrInstrumentationHandler(handlerData => { - if (getClient() !== client) { - return; - } - - const xhr = handlerData.xhr as SentryWrappedXMLHttpRequest & XMLHttpRequest; + const xhr = handlerData.xhr as SentryWrappedXMLHttpRequest & XMLHttpRequest; - const sentryXhrData = xhr[SENTRY_XHR_DATA_KEY]; + const sentryXhrData = xhr[SENTRY_XHR_DATA_KEY]; - if (!sentryXhrData) { - return; - } + if (!sentryXhrData) { + return; + } - const { method, request_headers: headers } = sentryXhrData; + const { method, request_headers: headers } = sentryXhrData; - try { - this._xhrResponseHandler(xhr, method, headers); - } catch (e) { - DEBUG_BUILD && logger.warn('Error while extracting response event form XHR response', e); - } - }); - } + try { + _xhrResponseHandler(options, xhr, method, headers); + } catch (e) { + DEBUG_BUILD && logger.warn('Error while extracting response event form XHR response', e); + } + }); +} - /** - * 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) && !isSentryRequestUrl(url, getClient()) - ); - } +/** + * Checks whether to capture given response as an event + * + * @param status response status code + * @param url response url + */ +function _shouldCaptureResponse(options: HttpClientOptions, status: number, url: string): boolean { + return ( + _isInGivenStatusRanges(options.failedRequestStatusCodes, status) && + _isInGivenRequestTargets(options.failedRequestTargets, url) && + !isSentryRequestUrl(url, getClient()) + ); +} - /** - * 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), +/** + * Creates a synthetic Sentry event from given response data + * + * @param data response data + * @returns event + */ +function _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: _getResponseSizeFromHeaders(data.responseHeaders), }, - }; + }, + }; - addExceptionMechanism(event, { - type: 'http.client', - handled: false, - }); + addExceptionMechanism(event, { + type: 'http.client', + handled: false, + }); - return event; - } + return event; } function _getRequest(requestInfo: RequestInfo, requestInit?: RequestInit): Request { diff --git a/packages/integrations/src/reportingobserver.ts b/packages/integrations/src/reportingobserver.ts index dbcae7f014e2..8afb5454b7d1 100644 --- a/packages/integrations/src/reportingobserver.ts +++ b/packages/integrations/src/reportingobserver.ts @@ -1,9 +1,11 @@ -import { captureMessage, getClient, withScope } from '@sentry/core'; -import type { Client, EventProcessor, Hub, Integration } from '@sentry/types'; +import { captureMessage, convertIntegrationFnToClass, getClient, withScope } from '@sentry/core'; +import type { Client, EventProcessor, Hub, Integration, IntegrationFn } from '@sentry/types'; import { GLOBAL_OBJ, supportsReportingObserver } from '@sentry/utils'; const WINDOW = GLOBAL_OBJ as typeof GLOBAL_OBJ & Window; +const INTEGRATION_NAME = 'ReportingObserver'; + interface Report { [key: string]: unknown; type: ReportTypes; @@ -40,66 +42,16 @@ interface InterventionReportBody { columnNumber?: number; } -const SETUP_CLIENTS: Client[] = []; - -/** Reporting API integration - https://w3c.github.io/reporting/ */ -export class ReportingObserver implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'ReportingObserver'; - - /** - * @inheritDoc - */ - public readonly name: string; - - private readonly _types: ReportTypes[]; - - /** - * @inheritDoc - */ - public constructor( - options: { - types?: ReportTypes[]; - } = {}, - ) { - this.name = ReportingObserver.id; - - this._types = options.types || ['crash', 'deprecation', 'intervention']; - } - - /** - * @inheritDoc - */ - public setupOnce(_: (callback: EventProcessor) => void, _getCurrentHub: () => Hub): void { - if (!supportsReportingObserver()) { - return; - } - - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any - const observer = new (WINDOW as any).ReportingObserver(this.handler.bind(this), { - buffered: true, - types: this._types, - }); - - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - observer.observe(); - } +interface ReportingObserverOptions { + types?: ReportTypes[]; +} - /** @inheritdoc */ - public setup(client: Client): void { - if (!supportsReportingObserver()) { - return; - } +const SETUP_CLIENTS: Client[] = []; - SETUP_CLIENTS.push(client); - } +const reportingObserverIntegration = ((options: ReportingObserverOptions = {}) => { + const types = options.types || ['crash', 'deprecation', 'intervention']; - /** - * @inheritDoc - */ - public handler(reports: Report[]): void { + function handler(reports: Report[]): void { if (!SETUP_CLIENTS.includes(getClient() as Client)) { return; } @@ -138,4 +90,30 @@ export class ReportingObserver implements Integration { }); } } -} + + return { + name: INTEGRATION_NAME, + setupOnce() { + if (!supportsReportingObserver()) { + return; + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any + const observer = new (WINDOW as any).ReportingObserver(handler, { + buffered: true, + types, + }); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + observer.observe(); + }, + + setup(client): void { + SETUP_CLIENTS.push(client); + }, + }; +}) satisfies IntegrationFn; + +/** Reporting API integration - https://w3c.github.io/reporting/ */ +// eslint-disable-next-line deprecation/deprecation +export const ReportingObserver = convertIntegrationFnToClass(INTEGRATION_NAME, reportingObserverIntegration); diff --git a/packages/integrations/src/rewriteframes.ts b/packages/integrations/src/rewriteframes.ts index bcb6eeca56f2..d82bde5728c6 100644 --- a/packages/integrations/src/rewriteframes.ts +++ b/packages/integrations/src/rewriteframes.ts @@ -1,98 +1,47 @@ -import type { Event, Integration, StackFrame, Stacktrace } from '@sentry/types'; +import { convertIntegrationFnToClass } from '@sentry/core'; +import type { Event, IntegrationFn, StackFrame, Stacktrace } from '@sentry/types'; import { basename, relative } from '@sentry/utils'; type StackFrameIteratee = (frame: StackFrame) => StackFrame; -/** Rewrite event frames paths */ -export class RewriteFrames implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'RewriteFrames'; - - /** - * @inheritDoc - */ - public name: string; - - /** - * @inheritDoc - */ - private readonly _root?: string; - - /** - * @inheritDoc - */ - private readonly _prefix: string; - - /** - * @inheritDoc - */ - public constructor(options: { root?: string; prefix?: string; iteratee?: StackFrameIteratee } = {}) { - this.name = RewriteFrames.id; - - if (options.root) { - this._root = options.root; - } - this._prefix = options.prefix || 'app:///'; - if (options.iteratee) { - this._iteratee = options.iteratee; - } - } - - /** - * @inheritDoc - */ - public setupOnce(_addGlobalEventProcessor: unknown, _getCurrentHub: unknown): void { - // noop - } - - /** @inheritDoc */ - public processEvent(event: Event): Event { - return this.process(event); - } - - /** - * TODO (v8): Make this private/internal - */ - public process(originalEvent: Event): Event { - let processedEvent = originalEvent; +const INTEGRATION_NAME = 'RewriteFrames'; - if (originalEvent.exception && Array.isArray(originalEvent.exception.values)) { - processedEvent = this._processExceptionsEvent(processedEvent); - } +interface RewriteFramesOptions { + root?: string; + prefix?: string; + iteratee?: StackFrameIteratee; +} - return processedEvent; - } +const rewriteFramesIntegration = ((options: RewriteFramesOptions = {}) => { + const root = options.root; + const prefix = options.prefix || 'app:///'; - /** - * @inheritDoc - */ - private readonly _iteratee: StackFrameIteratee = (frame: StackFrame) => { - if (!frame.filename) { + const iteratee: StackFrameIteratee = + options.iteratee || + ((frame: StackFrame) => { + if (!frame.filename) { + return frame; + } + // Determine if this is a Windows frame by checking for a Windows-style prefix such as `C:\` + const isWindowsFrame = + /^[a-zA-Z]:\\/.test(frame.filename) || + // or the presence of a backslash without a forward slash (which are not allowed on Windows) + (frame.filename.includes('\\') && !frame.filename.includes('/')); + // Check if the frame filename begins with `/` + const startsWithSlash = /^\//.test(frame.filename); + if (isWindowsFrame || startsWithSlash) { + const filename = isWindowsFrame + ? frame.filename + .replace(/^[a-zA-Z]:/, '') // remove Windows-style prefix + .replace(/\\/g, '/') // replace all `\\` instances with `/` + : frame.filename; + const base = root ? relative(root, filename) : basename(filename); + frame.filename = `${prefix}${base}`; + } return frame; - } - // Determine if this is a Windows frame by checking for a Windows-style prefix such as `C:\` - const isWindowsFrame = - /^[a-zA-Z]:\\/.test(frame.filename) || - // or the presence of a backslash without a forward slash (which are not allowed on Windows) - (frame.filename.includes('\\') && !frame.filename.includes('/')); - // Check if the frame filename begins with `/` - const startsWithSlash = /^\//.test(frame.filename); - if (isWindowsFrame || startsWithSlash) { - const filename = isWindowsFrame - ? frame.filename - .replace(/^[a-zA-Z]:/, '') // remove Windows-style prefix - .replace(/\\/g, '/') // replace all `\\` instances with `/` - : frame.filename; - const base = this._root ? relative(this._root, filename) : basename(filename); - frame.filename = `${this._prefix}${base}`; - } - return frame; - }; + }); - /** JSDoc */ - private _processExceptionsEvent(event: Event): Event { + function _processExceptionsEvent(event: Event): Event { try { return { ...event, @@ -102,7 +51,7 @@ export class RewriteFrames implements Integration { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion values: event.exception!.values!.map(value => ({ ...value, - ...(value.stacktrace && { stacktrace: this._processStacktrace(value.stacktrace) }), + ...(value.stacktrace && { stacktrace: _processStacktrace(value.stacktrace) }), })), }, }; @@ -111,11 +60,27 @@ export class RewriteFrames implements Integration { } } - /** JSDoc */ - private _processStacktrace(stacktrace?: Stacktrace): Stacktrace { + function _processStacktrace(stacktrace?: Stacktrace): Stacktrace { return { ...stacktrace, - frames: stacktrace && stacktrace.frames && stacktrace.frames.map(f => this._iteratee(f)), + frames: stacktrace && stacktrace.frames && stacktrace.frames.map(f => iteratee(f)), }; } -} + + return { + name: INTEGRATION_NAME, + processEvent(originalEvent) { + let processedEvent = originalEvent; + + if (originalEvent.exception && Array.isArray(originalEvent.exception.values)) { + processedEvent = _processExceptionsEvent(processedEvent); + } + + return processedEvent; + }, + }; +}) satisfies IntegrationFn; + +/** Rewrite event frames paths */ +// eslint-disable-next-line deprecation/deprecation +export const RewriteFrames = convertIntegrationFnToClass(INTEGRATION_NAME, rewriteFramesIntegration); diff --git a/packages/integrations/src/sessiontiming.ts b/packages/integrations/src/sessiontiming.ts index 6b85b6ff9d56..4398d170a981 100644 --- a/packages/integrations/src/sessiontiming.ts +++ b/packages/integrations/src/sessiontiming.ts @@ -1,51 +1,29 @@ -import type { Event, Integration } from '@sentry/types'; +import { convertIntegrationFnToClass } from '@sentry/core'; +import type { IntegrationFn } from '@sentry/types'; + +const INTEGRATION_NAME = 'SessionTiming'; + +const sessionTimingIntegration = (() => { + const startTime = Date.now(); + + return { + name: INTEGRATION_NAME, + processEvent(event) { + const now = Date.now(); + + return { + ...event, + extra: { + ...event.extra, + ['session:start']: startTime, + ['session:duration']: now - startTime, + ['session:end']: now, + }, + }; + }, + }; +}) satisfies IntegrationFn; /** This function adds duration since Sentry was initialized till the time event was sent */ -export class SessionTiming implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'SessionTiming'; - - /** - * @inheritDoc - */ - public name: string; - - /** Exact time Client was initialized expressed in milliseconds since Unix Epoch. */ - protected readonly _startTime: number; - - public constructor() { - this.name = SessionTiming.id; - this._startTime = Date.now(); - } - - /** - * @inheritDoc - */ - public setupOnce(_addGlobalEventProcessor: unknown, _getCurrentHub: unknown): void { - // noop - } - - /** @inheritDoc */ - public processEvent(event: Event): Event { - return this.process(event); - } - - /** - * TODO (v8): make this private/internal - */ - public process(event: Event): Event { - const now = Date.now(); - - return { - ...event, - extra: { - ...event.extra, - ['session:start']: this._startTime, - ['session:duration']: now - this._startTime, - ['session:end']: now, - }, - }; - } -} +// eslint-disable-next-line deprecation/deprecation +export const SessionTiming = convertIntegrationFnToClass(INTEGRATION_NAME, sessionTimingIntegration); diff --git a/packages/integrations/src/transaction.ts b/packages/integrations/src/transaction.ts index 28bb90b0f91b..c44c94c7fe06 100644 --- a/packages/integrations/src/transaction.ts +++ b/packages/integrations/src/transaction.ts @@ -1,52 +1,32 @@ -import type { Event, Integration, StackFrame } from '@sentry/types'; +import { convertIntegrationFnToClass } from '@sentry/core'; +import type { Event, IntegrationFn, StackFrame } from '@sentry/types'; -/** Add node transaction to the event */ -export class Transaction implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'Transaction'; - - /** - * @inheritDoc - */ - public name: string; - - public constructor() { - this.name = Transaction.id; - } - - /** - * @inheritDoc - */ - public setupOnce(_addGlobalEventProcessor: unknown, _getCurrentHub: unknown): void { - // noop - } - - /** @inheritDoc */ - public processEvent(event: Event): Event { - return this.process(event); - } - - /** - * TODO (v8): Make this private/internal - */ - public process(event: Event): Event { - const frames = _getFramesFromEvent(event); - - // use for loop so we don't have to reverse whole frames array - for (let i = frames.length - 1; i >= 0; i--) { - const frame = frames[i]; - - if (frame.in_app === true) { - event.transaction = _getTransaction(frame); - break; +const INTEGRATION_NAME = 'Transaction'; + +const transactionIntegration = (() => { + return { + name: INTEGRATION_NAME, + processEvent(event) { + const frames = _getFramesFromEvent(event); + + // use for loop so we don't have to reverse whole frames array + for (let i = frames.length - 1; i >= 0; i--) { + const frame = frames[i]; + + if (frame.in_app === true) { + event.transaction = _getTransaction(frame); + break; + } } - } - return event; - } -} + return event; + }, + }; +}) satisfies IntegrationFn; + +/** Add node transaction to the event */ +// eslint-disable-next-line deprecation/deprecation +export const Transaction = convertIntegrationFnToClass(INTEGRATION_NAME, transactionIntegration); function _getFramesFromEvent(event: Event): StackFrame[] { const exception = event.exception && event.exception.values && event.exception.values[0]; diff --git a/packages/integrations/test/debug.test.ts b/packages/integrations/test/debug.test.ts index eefd9c8b9240..1cb952f26a5a 100644 --- a/packages/integrations/test/debug.test.ts +++ b/packages/integrations/test/debug.test.ts @@ -1,8 +1,12 @@ -import type { Client, Event, EventHint, Hub, Integration } from '@sentry/types'; +import type { Client, Event, EventHint, Integration } from '@sentry/types'; import { Debug } from '../src/debug'; -function testEventLogged(integration: Debug, testEvent?: Event, testEventHint?: EventHint) { +interface IntegrationWithSetup extends Integration { + setup: (client: Client) => void; +} + +function testEventLogged(integration: IntegrationWithSetup, testEvent?: Event, testEventHint?: EventHint) { const callbacks: ((event: Event, hint?: EventHint) => void)[] = []; const client: Client = { diff --git a/packages/integrations/test/dedupe.test.ts b/packages/integrations/test/dedupe.test.ts index 545aa1a83c1c..bb996fa45960 100644 --- a/packages/integrations/test/dedupe.test.ts +++ b/packages/integrations/test/dedupe.test.ts @@ -10,7 +10,6 @@ type EventWithException = SentryEvent & { type ExceptionWithStacktrace = Exception & { stacktrace: StacktraceWithFrames }; type StacktraceWithFrames = Stacktrace & { frames: StackFrame[] }; -/** JSDoc */ function clone(data: T): T { return JSON.parse(JSON.stringify(data)); } diff --git a/packages/integrations/test/extraerrordata.test.ts b/packages/integrations/test/extraerrordata.test.ts index bc7a6312a65a..166c8e66fe37 100644 --- a/packages/integrations/test/extraerrordata.test.ts +++ b/packages/integrations/test/extraerrordata.test.ts @@ -15,7 +15,7 @@ describe('ExtraErrorData()', () => { error.baz = 42; error.foo = 'bar'; - const enhancedEvent = extraErrorData.enhanceEventWithErrorData(event, { + const enhancedEvent = extraErrorData.processEvent(event, { originalException: error, }); @@ -31,7 +31,7 @@ describe('ExtraErrorData()', () => { const error = new TypeError('foo') as ExtendedError; error.cause = new SyntaxError('bar'); - const enhancedEvent = extraErrorData.enhanceEventWithErrorData(event, { + const enhancedEvent = extraErrorData.processEvent(event, { originalException: error, }); @@ -52,7 +52,7 @@ describe('ExtraErrorData()', () => { }, }; - const enhancedEvent = extraErrorData.enhanceEventWithErrorData(event, { + const enhancedEvent = extraErrorData.processEvent(event, { originalException: error, }); @@ -76,7 +76,7 @@ describe('ExtraErrorData()', () => { const error = new TypeError('foo') as ExtendedError; error.baz = 42; - const enhancedEvent = extraErrorData.enhanceEventWithErrorData(event, { + const enhancedEvent = extraErrorData.processEvent(event, { originalException: error, }); @@ -91,7 +91,7 @@ describe('ExtraErrorData()', () => { it('should return event if originalException is not an Error object', () => { const error = 'error message, not object'; - const enhancedEvent = extraErrorData.enhanceEventWithErrorData(event, { + const enhancedEvent = extraErrorData.processEvent(event, { originalException: error, }); @@ -99,13 +99,13 @@ describe('ExtraErrorData()', () => { }); it('should return event if there is no SentryEventHint', () => { - const enhancedEvent = extraErrorData.enhanceEventWithErrorData(event); + const enhancedEvent = extraErrorData.processEvent(event, {}); expect(enhancedEvent).toEqual(event); }); it('should return event if there is no originalException', () => { - const enhancedEvent = extraErrorData.enhanceEventWithErrorData(event, { + const enhancedEvent = extraErrorData.processEvent(event, { // @ts-expect-error Allow event to have extra properties notOriginalException: 'fooled you', }); @@ -124,7 +124,7 @@ describe('ExtraErrorData()', () => { }; }; - const enhancedEvent = extraErrorData.enhanceEventWithErrorData(event, { + const enhancedEvent = extraErrorData.processEvent(event, { originalException: error, }); @@ -147,7 +147,7 @@ describe('ExtraErrorData()', () => { }; }; - const enhancedEvent = extraErrorData.enhanceEventWithErrorData(event, { + const enhancedEvent = extraErrorData.processEvent(event, { originalException: error, }); @@ -167,7 +167,7 @@ describe('ExtraErrorData()', () => { }; }; - const enhancedEvent = extraErrorData.enhanceEventWithErrorData(event, { + const enhancedEvent = extraErrorData.processEvent(event, { originalException: error, }); diff --git a/packages/integrations/test/reportingobserver.test.ts b/packages/integrations/test/reportingobserver.test.ts index 6378a456c854..275e63c82ea9 100644 --- a/packages/integrations/test/reportingobserver.test.ts +++ b/packages/integrations/test/reportingobserver.test.ts @@ -128,8 +128,10 @@ describe('ReportingObserver', () => { ); // without calling setup, the integration is not registered + const handler = mockReportingObserverConstructor.mock.calls[0][0]; + expect(() => { - reportingObserverIntegration.handler([{ type: 'crash', url: 'some url' }]); + handler([{ type: 'crash', url: 'some url' }]); }).not.toThrow(); expect(captureMessage).not.toHaveBeenCalled(); @@ -142,8 +144,9 @@ describe('ReportingObserver', () => { () => mockHub, ); reportingObserverIntegration.setup(mockClient); + const handler = mockReportingObserverConstructor.mock.calls[0][0]; - reportingObserverIntegration.handler([ + handler([ { type: 'crash', url: 'some url' }, { type: 'deprecation', url: 'some url' }, ]); @@ -158,8 +161,9 @@ describe('ReportingObserver', () => { () => mockHub, ); reportingObserverIntegration.setup(mockClient); + const handler = mockReportingObserverConstructor.mock.calls[0][0]; - reportingObserverIntegration.handler([ + handler([ { type: 'crash', url: 'some url 1' }, { type: 'deprecation', url: 'some url 2' }, ]); @@ -175,11 +179,12 @@ describe('ReportingObserver', () => { () => mockHub, ); reportingObserverIntegration.setup(mockClient); + const handler = mockReportingObserverConstructor.mock.calls[0][0]; const report1 = { type: 'crash', url: 'some url 1', body: { crashId: 'id1' } } as const; const report2 = { type: 'deprecation', url: 'some url 2', body: { id: 'id2', message: 'message' } } as const; - reportingObserverIntegration.handler([report1, report2]); + handler([report1, report2]); expect(mockScope.setExtra).toHaveBeenCalledWith('body', report1.body); expect(mockScope.setExtra).toHaveBeenCalledWith('body', report2.body); @@ -192,8 +197,9 @@ describe('ReportingObserver', () => { () => mockHub, ); reportingObserverIntegration.setup(mockClient); + const handler = mockReportingObserverConstructor.mock.calls[0][0]; - reportingObserverIntegration.handler([{ type: 'crash', url: 'some url' }]); + handler([{ type: 'crash', url: 'some url' }]); expect(mockScope.setExtra).not.toHaveBeenCalledWith('body', expect.anything()); }); @@ -205,13 +211,14 @@ describe('ReportingObserver', () => { () => mockHub, ); reportingObserverIntegration.setup(mockClient); + const handler = mockReportingObserverConstructor.mock.calls[0][0]; const report = { type: 'crash', url: 'some url', body: { crashId: 'some id', reason: 'some reason' }, } as const; - reportingObserverIntegration.handler([report]); + handler([report]); expect(captureMessage).toHaveBeenCalledWith(expect.stringContaining(report.type)); expect(captureMessage).toHaveBeenCalledWith(expect.stringContaining(report.body.crashId)); @@ -225,13 +232,14 @@ describe('ReportingObserver', () => { () => mockHub, ); reportingObserverIntegration.setup(mockClient); + const handler = mockReportingObserverConstructor.mock.calls[0][0]; const report = { type: 'deprecation', url: 'some url', body: { id: 'some id', message: 'some message' }, } as const; - reportingObserverIntegration.handler([report]); + handler([report]); expect(captureMessage).toHaveBeenCalledWith(expect.stringContaining(report.type)); expect(captureMessage).toHaveBeenCalledWith(expect.stringContaining(report.body.message)); @@ -244,13 +252,14 @@ describe('ReportingObserver', () => { () => mockHub, ); reportingObserverIntegration.setup(mockClient); + const handler = mockReportingObserverConstructor.mock.calls[0][0]; const report = { type: 'intervention', url: 'some url', body: { id: 'some id', message: 'some message' }, } as const; - reportingObserverIntegration.handler([report]); + handler([report]); expect(captureMessage).toHaveBeenCalledWith(expect.stringContaining(report.type)); expect(captureMessage).toHaveBeenCalledWith(expect.stringContaining(report.body.message)); @@ -263,12 +272,13 @@ describe('ReportingObserver', () => { () => mockHub, ); reportingObserverIntegration.setup(mockClient); + const handler = mockReportingObserverConstructor.mock.calls[0][0]; const report = { type: 'intervention', url: 'some url', } as const; - reportingObserverIntegration.handler([report]); + handler([report]); expect(captureMessage).toHaveBeenCalledWith(expect.stringContaining(report.type)); expect(captureMessage).toHaveBeenCalledWith(expect.stringContaining('No details available')); @@ -281,9 +291,10 @@ describe('ReportingObserver', () => { () => mockHub, ); reportingObserverIntegration.setup(mockClient); + const handler = mockReportingObserverConstructor.mock.calls[0][0]; const report = { type: 'crash', url: 'some url', body: { crashId: '', reason: '' } } as const; - reportingObserverIntegration.handler([report]); + handler([report]); expect(captureMessage).toHaveBeenCalledWith(expect.stringContaining(report.type)); expect(captureMessage).toHaveBeenCalledWith(expect.stringContaining('No details available')); @@ -296,13 +307,14 @@ describe('ReportingObserver', () => { () => mockHub, ); reportingObserverIntegration.setup(mockClient); + const handler = mockReportingObserverConstructor.mock.calls[0][0]; const report = { type: 'deprecation', url: 'some url', body: { id: 'some id', message: '' }, } as const; - reportingObserverIntegration.handler([report]); + handler([report]); expect(captureMessage).toHaveBeenCalledWith(expect.stringContaining(report.type)); expect(captureMessage).toHaveBeenCalledWith(expect.stringContaining('No details available')); @@ -315,13 +327,14 @@ describe('ReportingObserver', () => { () => mockHub, ); reportingObserverIntegration.setup(mockClient); + const handler = mockReportingObserverConstructor.mock.calls[0][0]; const report = { type: 'intervention', url: 'some url', body: { id: 'some id', message: '' }, } as const; - reportingObserverIntegration.handler([report]); + handler([report]); expect(captureMessage).toHaveBeenCalledWith(expect.stringContaining(report.type)); expect(captureMessage).toHaveBeenCalledWith(expect.stringContaining('No details available')); diff --git a/packages/integrations/test/rewriteframes.test.ts b/packages/integrations/test/rewriteframes.test.ts index 749df9a862e1..7a65ff129aca 100644 --- a/packages/integrations/test/rewriteframes.test.ts +++ b/packages/integrations/test/rewriteframes.test.ts @@ -1,8 +1,12 @@ -import type { Event, StackFrame } from '@sentry/types'; +import type { Event, Integration, StackFrame } from '@sentry/types'; import { RewriteFrames } from '../src/rewriteframes'; -let rewriteFrames: RewriteFrames; +interface IntegrationWithProcessEvent extends Integration { + processEvent(event: Event): Event; +} + +let rewriteFrames: IntegrationWithProcessEvent; let exceptionEvent: Event; let exceptionWithoutStackTrace: Event; let windowsExceptionEvent: Event; @@ -102,7 +106,7 @@ describe('RewriteFrames', () => { }); it('transforms exceptionEvent frames', () => { - const event = rewriteFrames.process(exceptionEvent); + const event = rewriteFrames.processEvent(exceptionEvent); expect(event.exception!.values![0].stacktrace!.frames![0].filename).toEqual('app:///file1.js'); expect(event.exception!.values![0].stacktrace!.frames![1].filename).toEqual('app:///file2.js'); }); @@ -110,7 +114,7 @@ describe('RewriteFrames', () => { it('ignore exception without StackTrace', () => { // @ts-expect-error Validates that the Stacktrace does not exist before validating the test. expect(exceptionWithoutStackTrace.exception?.values[0].stacktrace).toEqual(undefined); - const event = rewriteFrames.process(exceptionWithoutStackTrace); + const event = rewriteFrames.processEvent(exceptionWithoutStackTrace); expect(event.exception!.values![0].stacktrace).toEqual(undefined); }); }); @@ -123,7 +127,7 @@ describe('RewriteFrames', () => { }); it('transforms exceptionEvent frames', () => { - const event = rewriteFrames.process(exceptionEvent); + const event = rewriteFrames.processEvent(exceptionEvent); expect(event.exception!.values![0].stacktrace!.frames![0].filename).toEqual('foobar/file1.js'); expect(event.exception!.values![0].stacktrace!.frames![1].filename).toEqual('foobar/file2.js'); }); @@ -135,25 +139,25 @@ describe('RewriteFrames', () => { }); it('transforms windowsExceptionEvent frames (C:\\)', () => { - const event = rewriteFrames.process(windowsExceptionEvent); + const event = rewriteFrames.processEvent(windowsExceptionEvent); expect(event.exception!.values![0].stacktrace!.frames![0].filename).toEqual('app:///file1.js'); expect(event.exception!.values![0].stacktrace!.frames![1].filename).toEqual('app:///file2.js'); }); it('transforms windowsExceptionEvent frames with lower-case prefix (c:\\)', () => { - const event = rewriteFrames.process(windowsLowerCaseExceptionEvent); + const event = rewriteFrames.processEvent(windowsLowerCaseExceptionEvent); expect(event.exception!.values![0].stacktrace!.frames![0].filename).toEqual('app:///file1.js'); expect(event.exception!.values![0].stacktrace!.frames![1].filename).toEqual('app:///file2.js'); }); it('transforms windowsExceptionEvent frames with no prefix', () => { - const event = rewriteFrames.process(windowsExceptionEventWithoutPrefix); + const event = rewriteFrames.processEvent(windowsExceptionEventWithoutPrefix); expect(event.exception!.values![0].stacktrace!.frames![0].filename).toEqual('app:///file1.js'); expect(event.exception!.values![0].stacktrace!.frames![1].filename).toEqual('app:///file2.js'); }); it('transforms windowsExceptionEvent frames with backslash prefix', () => { - const event = rewriteFrames.process(windowsExceptionEventWithBackslashPrefix); + const event = rewriteFrames.processEvent(windowsExceptionEventWithBackslashPrefix); expect(event.exception!.values![0].stacktrace!.frames![0].filename).toEqual('app:///file1.js'); expect(event.exception!.values![0].stacktrace!.frames![1].filename).toEqual('app:///file2.js'); }); @@ -167,31 +171,31 @@ describe('RewriteFrames', () => { }); it('transforms exceptionEvent frames', () => { - const event = rewriteFrames.process(exceptionEvent); + const event = rewriteFrames.processEvent(exceptionEvent); expect(event.exception!.values![0].stacktrace!.frames![0].filename).toEqual('app:///src/app/file1.js'); expect(event.exception!.values![0].stacktrace!.frames![1].filename).toEqual('app:///src/app/mo\\dule/file2.js'); }); it('transforms windowsExceptionEvent frames', () => { - const event = rewriteFrames.process(windowsExceptionEvent); + const event = rewriteFrames.processEvent(windowsExceptionEvent); expect(event.exception!.values![0].stacktrace!.frames![0].filename).toEqual('app:///src/app/file1.js'); expect(event.exception!.values![0].stacktrace!.frames![1].filename).toEqual('app:///src/app/file2.js'); }); it('transforms windowsExceptionEvent lower-case prefix frames', () => { - const event = rewriteFrames.process(windowsLowerCaseExceptionEvent); + const event = rewriteFrames.processEvent(windowsLowerCaseExceptionEvent); expect(event.exception!.values![0].stacktrace!.frames![0].filename).toEqual('app:///src/app/file1.js'); expect(event.exception!.values![0].stacktrace!.frames![1].filename).toEqual('app:///src/app/file2.js'); }); it('transforms windowsExceptionEvent frames with no prefix', () => { - const event = rewriteFrames.process(windowsExceptionEventWithoutPrefix); + const event = rewriteFrames.processEvent(windowsExceptionEventWithoutPrefix); expect(event.exception!.values![0].stacktrace!.frames![0].filename).toEqual('app:///src/app/file1.js'); expect(event.exception!.values![0].stacktrace!.frames![1].filename).toEqual('app:///src/app/file2.js'); }); it('transforms windowsExceptionEvent frames with backslash prefix', () => { - const event = rewriteFrames.process(windowsExceptionEventWithBackslashPrefix); + const event = rewriteFrames.processEvent(windowsExceptionEventWithBackslashPrefix); expect(event.exception!.values![0].stacktrace!.frames![0].filename).toEqual('app:///src/app/file1.js'); expect(event.exception!.values![0].stacktrace!.frames![1].filename).toEqual('app:///src/app/file2.js'); }); @@ -208,7 +212,7 @@ describe('RewriteFrames', () => { }); it('transforms exceptionEvent frames', () => { - const event = rewriteFrames.process(exceptionEvent); + const event = rewriteFrames.processEvent(exceptionEvent); expect(event.exception!.values![0].stacktrace!.frames![0].filename).toEqual('/www/src/app/file1.js'); expect(event.exception!.values![0].stacktrace!.frames![0].function).toEqual('whoops'); expect(event.exception!.values![0].stacktrace!.frames![1].filename).toEqual('/www/src/app/mo\\dule/file2.js'); @@ -219,7 +223,7 @@ describe('RewriteFrames', () => { describe('can process events that contain multiple stacktraces', () => { it('with defaults', () => { rewriteFrames = new RewriteFrames(); - const event = rewriteFrames.process(multipleStacktracesEvent); + const event = rewriteFrames.processEvent(multipleStacktracesEvent); // first stacktrace expect(event.exception!.values![0].stacktrace!.frames![0].filename).toEqual('app:///file1.js'); expect(event.exception!.values![0].stacktrace!.frames![1].filename).toEqual('app:///file2.js'); @@ -235,7 +239,7 @@ describe('RewriteFrames', () => { rewriteFrames = new RewriteFrames({ root: '/www', }); - const event = rewriteFrames.process(multipleStacktracesEvent); + const event = rewriteFrames.processEvent(multipleStacktracesEvent); // first stacktrace expect(event.exception!.values![0].stacktrace!.frames![0].filename).toEqual('app:///src/app/file1.js'); expect(event.exception!.values![0].stacktrace!.frames![1].filename).toEqual('app:///src/app/mo\\dule/file2.js'); @@ -254,7 +258,7 @@ describe('RewriteFrames', () => { function: 'whoops', }), }); - const event = rewriteFrames.process(multipleStacktracesEvent); + const event = rewriteFrames.processEvent(multipleStacktracesEvent); // first stacktrace expect(event.exception!.values![0].stacktrace!.frames![0].filename).toEqual('/www/src/app/file1.js'); expect(event.exception!.values![0].stacktrace!.frames![0].function).toEqual('whoops'); @@ -281,7 +285,7 @@ describe('RewriteFrames', () => { values: undefined, }, }; - expect(rewriteFrames.process(brokenEvent)).toEqual(brokenEvent); + expect(rewriteFrames.processEvent(brokenEvent)).toEqual(brokenEvent); }); it('no frames', () => { @@ -295,7 +299,7 @@ describe('RewriteFrames', () => { ], }, }; - expect(rewriteFrames.process(brokenEvent)).toEqual(brokenEvent); + expect(rewriteFrames.processEvent(brokenEvent)).toEqual(brokenEvent); }); }); }); diff --git a/packages/integrations/test/sessiontiming.test.ts b/packages/integrations/test/sessiontiming.test.ts index 033c9ea8a441..d1569db52095 100644 --- a/packages/integrations/test/sessiontiming.test.ts +++ b/packages/integrations/test/sessiontiming.test.ts @@ -1,18 +1,18 @@ import { SessionTiming } from '../src/sessiontiming'; -const sessionTiming: SessionTiming = new SessionTiming(); +const sessionTiming = new SessionTiming(); describe('SessionTiming', () => { it('should work as expected', () => { - const event = sessionTiming.process({ + const event = sessionTiming.processEvent({ extra: { some: 'value', }, }); - expect(typeof event.extra!['session:start']).toBe('number'); - expect(typeof event.extra!['session:duration']).toBe('number'); - expect(typeof event.extra!['session:end']).toBe('number'); - expect(event.extra!.some).toEqual('value'); + expect(typeof event.extra['session:start']).toBe('number'); + expect(typeof event.extra['session:duration']).toBe('number'); + expect(typeof event.extra['session:end']).toBe('number'); + expect((event.extra as any).some).toEqual('value'); }); }); diff --git a/packages/integrations/test/transaction.test.ts b/packages/integrations/test/transaction.test.ts index 9a87369fb234..bfc6096a519e 100644 --- a/packages/integrations/test/transaction.test.ts +++ b/packages/integrations/test/transaction.test.ts @@ -1,6 +1,6 @@ import { Transaction } from '../src/transaction'; -const transaction: Transaction = new Transaction(); +const transaction = new Transaction(); describe('Transaction', () => { describe('extracts info from module/function of the first `in_app` frame', () => { From 73a35c17da845d277dbfe937649b601843c645b4 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 21 Dec 2023 10:18:00 +0100 Subject: [PATCH 2/5] ref: fix integration class type --- packages/core/src/integration.ts | 20 +++++++++++--------- packages/core/test/lib/integration.test.ts | 17 +++++++++++++++++ 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts index c70eb34c961b..f867ad1d032a 100644 --- a/packages/core/src/integration.ts +++ b/packages/core/src/integration.ts @@ -175,15 +175,16 @@ function findIndex(arr: T[], callback: (item: T) => boolean): number { export function convertIntegrationFnToClass( name: string, fn: Fn, -): IntegrationClass< - Integration & +): { + id: string; + new (...args: Parameters): Integration & ReturnType & { setupOnce: (addGlobalEventProcessor?: (callback: EventProcessor) => void, getCurrentHub?: () => Hub) => void; - } -> { + }; +} { return Object.assign( // eslint-disable-next-line @typescript-eslint/no-explicit-any - function ConvertedIntegration(...rest: any[]) { + function ConvertedIntegration(...rest: Parameters) { return { // eslint-disable-next-line @typescript-eslint/no-empty-function setupOnce: () => {}, @@ -191,10 +192,11 @@ export function convertIntegrationFnToClass( }; }, { id: name }, - ) as unknown as IntegrationClass< - Integration & + ) as unknown as { + id: string; + new (...args: Parameters): Integration & ReturnType & { setupOnce: (addGlobalEventProcessor?: (callback: EventProcessor) => void, getCurrentHub?: () => Hub) => void; - } - >; + }; + }; } diff --git a/packages/core/test/lib/integration.test.ts b/packages/core/test/lib/integration.test.ts index 65bf30483d86..137a7dce4df3 100644 --- a/packages/core/test/lib/integration.test.ts +++ b/packages/core/test/lib/integration.test.ts @@ -670,6 +670,23 @@ describe('convertIntegrationFnToClass', () => { }); }); + it('works with options', () => { + const integrationFn = (_options: { num: number }) => ({ name: 'testName' }); + + const IntegrationClass = convertIntegrationFnToClass('testName', integrationFn); + + expect(IntegrationClass.id).toBe('testName'); + + // @ts-expect-error This should fail TS without options + new IntegrationClass(); + + const integration = new IntegrationClass({ num: 3 }); + expect(integration).toEqual({ + name: 'testName', + setupOnce: expect.any(Function), + }); + }); + it('works with integration hooks', () => { const setup = jest.fn(); const setupOnce = jest.fn(); From ad6e167805fe80d47c97b4f31ac3c1d8ca2073c9 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 21 Dec 2023 10:54:07 +0100 Subject: [PATCH 3/5] fix sveltekit test --- packages/sveltekit/test/server/utils.test.ts | 27 +++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/packages/sveltekit/test/server/utils.test.ts b/packages/sveltekit/test/server/utils.test.ts index 272dba8330ce..cad2051c2c14 100644 --- a/packages/sveltekit/test/server/utils.test.ts +++ b/packages/sveltekit/test/server/utils.test.ts @@ -1,5 +1,5 @@ import { RewriteFrames } from '@sentry/integrations'; -import type { StackFrame } from '@sentry/types'; +import type { Event, StackFrame } from '@sentry/types'; import { basename } from '@sentry/utils'; import type { GlobalWithSentryValues } from '../../src/server/utils'; @@ -80,21 +80,30 @@ describe('rewriteFramesIteratee', () => { }; const originalRewriteFrames = new RewriteFrames(); - // @ts-expect-error this property exists - const defaultIteratee = originalRewriteFrames._iteratee; - - const defaultResult = defaultIteratee({ ...frame }); - delete defaultResult.module; + const rewriteFrames = new RewriteFrames({ iteratee: rewriteFramesIteratee }); + + const event: Event = { + exception: { + values: [ + { + stacktrace: { + frames: [frame], + }, + }, + ], + }, + }; - const result = rewriteFramesIteratee({ ...frame }); + const originalResult = originalRewriteFrames.processEvent(event); + const result = rewriteFrames.processEvent(event); - expect(result).toEqual({ + expect(result.exception?.values?.[0]?.stacktrace?.frames?.[0]).toEqual({ filename: 'app:///3-ab34d22f.js', lineno: 1, colno: 1, }); - expect(result).toStrictEqual(defaultResult); + expect(result).toStrictEqual(originalResult); }); it.each([ From fb9317b335b62e0d845f5068997b12155d29f6db Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 21 Dec 2023 15:39:31 +0100 Subject: [PATCH 4/5] ref: use weakmap --- packages/integrations/src/reportingobserver.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/integrations/src/reportingobserver.ts b/packages/integrations/src/reportingobserver.ts index 8afb5454b7d1..cd8c28b54f8c 100644 --- a/packages/integrations/src/reportingobserver.ts +++ b/packages/integrations/src/reportingobserver.ts @@ -1,5 +1,5 @@ import { captureMessage, convertIntegrationFnToClass, getClient, withScope } from '@sentry/core'; -import type { Client, EventProcessor, Hub, Integration, IntegrationFn } from '@sentry/types'; +import type { Client, IntegrationFn } from '@sentry/types'; import { GLOBAL_OBJ, supportsReportingObserver } from '@sentry/utils'; const WINDOW = GLOBAL_OBJ as typeof GLOBAL_OBJ & Window; @@ -46,13 +46,13 @@ interface ReportingObserverOptions { types?: ReportTypes[]; } -const SETUP_CLIENTS: Client[] = []; +const SETUP_CLIENTS = new WeakMap(); const reportingObserverIntegration = ((options: ReportingObserverOptions = {}) => { const types = options.types || ['crash', 'deprecation', 'intervention']; function handler(reports: Report[]): void { - if (!SETUP_CLIENTS.includes(getClient() as Client)) { + if (!SETUP_CLIENTS.has(getClient() as Client)) { return; } @@ -109,7 +109,7 @@ const reportingObserverIntegration = ((options: ReportingObserverOptions = {}) = }, setup(client): void { - SETUP_CLIENTS.push(client); + SETUP_CLIENTS.set(client, true); }, }; }) satisfies IntegrationFn; From 0e1adee2df44082f91dac690571397fafefef811 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 21 Dec 2023 15:43:26 +0100 Subject: [PATCH 5/5] ref: use weakmap for deno too --- packages/deno/src/integrations/deno-cron.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/deno/src/integrations/deno-cron.ts b/packages/deno/src/integrations/deno-cron.ts index a3bbcbaf59bd..73c1bc1954fb 100644 --- a/packages/deno/src/integrations/deno-cron.ts +++ b/packages/deno/src/integrations/deno-cron.ts @@ -9,7 +9,7 @@ type CronParams = [string, string | Deno.CronSchedule, CronFn | CronOptions, Cro const INTEGRATION_NAME = 'DenoCron'; -const SETUP_CLIENTS: Client[] = []; +const SETUP_CLIENTS = new WeakMap(); const denoCronIntegration = (() => { return { @@ -37,7 +37,7 @@ const denoCronIntegration = (() => { } async function cronCalled(): Promise { - if (SETUP_CLIENTS.includes(getClient() as Client)) { + if (SETUP_CLIENTS.has(getClient() as Client)) { return; } @@ -55,7 +55,7 @@ const denoCronIntegration = (() => { }); }, setup(client) { - SETUP_CLIENTS.push(client); + SETUP_CLIENTS.set(client, true); }, }; }) satisfies IntegrationFn;