From dc7636982cdaf323c8d2bcb9974a2d67b2ea35f0 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 20 Dec 2023 09:22:52 +0100 Subject: [PATCH] ref(browser): Refactor browser integrations to functional style (#9918) Eventually we may think about merging the `LinkedErrors` integration with the core one, but for now this should be OK... --- .../browser/src/integrations/breadcrumbs.ts | 426 +++++++++--------- packages/browser/src/integrations/dedupe.ts | 81 ++-- .../src/integrations/globalhandlers.ts | 77 ++-- .../browser/src/integrations/httpcontext.ts | 77 ++-- .../browser/src/integrations/linkederrors.ts | 93 ++-- packages/browser/src/integrations/trycatch.ts | 99 ++-- 6 files changed, 386 insertions(+), 467 deletions(-) diff --git a/packages/browser/src/integrations/breadcrumbs.ts b/packages/browser/src/integrations/breadcrumbs.ts index 9c6b4cfb9764..102d1d7e500d 100644 --- a/packages/browser/src/integrations/breadcrumbs.ts +++ b/packages/browser/src/integrations/breadcrumbs.ts @@ -1,13 +1,15 @@ +import { get } from 'http'; /* eslint-disable max-lines */ -import { addBreadcrumb, getClient } from '@sentry/core'; +import { addBreadcrumb, convertIntegrationFnToClass, getClient } from '@sentry/core'; import type { + Client, Event as SentryEvent, HandlerDataConsole, HandlerDataDom, HandlerDataFetch, HandlerDataHistory, HandlerDataXhr, - Integration, + IntegrationFn, } from '@sentry/types'; import type { FetchBreadcrumbData, @@ -33,7 +35,6 @@ import { import { DEBUG_BUILD } from '../debug-build'; import { WINDOW } from '../helpers'; -/** JSDoc */ interface BreadcrumbsOptions { console: boolean; dom: @@ -51,97 +52,86 @@ interface BreadcrumbsOptions { /** maxStringLength gets capped to prevent 100 breadcrumbs exceeding 1MB event payload size */ const MAX_ALLOWED_STRING_LENGTH = 1024; +const INTEGRATION_NAME = 'Breadcrumbs'; + +const breadcrumbsIntegration: IntegrationFn = (options: Partial = {}) => { + const _options = { + console: true, + dom: true, + fetch: true, + history: true, + sentry: true, + xhr: true, + ...options, + }; + + return { + name: INTEGRATION_NAME, + setup(client) { + if (_options.console) { + addConsoleInstrumentationHandler(_getConsoleBreadcrumbHandler(client)); + } + if (_options.dom) { + addClickKeypressInstrumentationHandler(_getDomBreadcrumbHandler(client, _options.dom)); + } + if (_options.xhr) { + addXhrInstrumentationHandler(_getXhrBreadcrumbHandler(client)); + } + if (_options.fetch) { + addFetchInstrumentationHandler(_getFetchBreadcrumbHandler(client)); + } + if (_options.history) { + addHistoryInstrumentationHandler(_getHistoryBreadcrumbHandler(client)); + } + if (_options.sentry && client.on) { + client.on('beforeSendEvent', _getSentryBreadcrumbHandler(client)); + } + }, + }; +}; + /** * Default Breadcrumbs instrumentations - * TODO: Deprecated - with v6, this will be renamed to `Instrument` */ -export class Breadcrumbs implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'Breadcrumbs'; - - /** - * @inheritDoc - */ - public name: string; - - /** - * Options of the breadcrumbs integration. - */ - // This field is public, because we use it in the browser client to check if the `sentry` option is enabled. - public readonly options: Readonly; - - /** - * @inheritDoc - */ - public constructor(options?: Partial) { - this.name = Breadcrumbs.id; - this.options = { - console: true, - dom: true, - fetch: true, - history: true, - sentry: true, - xhr: true, - ...options, - }; - } - - /** - * Instrument browser built-ins w/ breadcrumb capturing - * - Console API - * - DOM API (click/typing) - * - XMLHttpRequest API - * - Fetch API - * - History API - */ - public setupOnce(): void { - if (this.options.console) { - addConsoleInstrumentationHandler(_consoleBreadcrumb); - } - if (this.options.dom) { - addClickKeypressInstrumentationHandler(_domBreadcrumb(this.options.dom)); - } - if (this.options.xhr) { - addXhrInstrumentationHandler(_xhrBreadcrumb); - } - if (this.options.fetch) { - addFetchInstrumentationHandler(_fetchBreadcrumb); - } - if (this.options.history) { - addHistoryInstrumentationHandler(_historyBreadcrumb); - } - if (this.options.sentry) { - const client = getClient(); - client && client.on && client.on('beforeSendEvent', addSentryBreadcrumb); - } - } -} +// eslint-disable-next-line deprecation/deprecation +export const Breadcrumbs = convertIntegrationFnToClass(INTEGRATION_NAME, breadcrumbsIntegration); /** * Adds a breadcrumb for Sentry events or transactions if this option is enabled. */ -function addSentryBreadcrumb(event: SentryEvent): void { - addBreadcrumb( - { - category: `sentry.${event.type === 'transaction' ? 'transaction' : 'event'}`, - event_id: event.event_id, - level: event.level, - message: getEventDescription(event), - }, - { - event, - }, - ); +function _getSentryBreadcrumbHandler(client: Client): (event: SentryEvent) => void { + return function addSentryBreadcrumb(event: SentryEvent): void { + if (getClient() !== client) { + return; + } + + addBreadcrumb( + { + category: `sentry.${event.type === 'transaction' ? 'transaction' : 'event'}`, + event_id: event.event_id, + level: event.level, + message: getEventDescription(event), + }, + { + event, + }, + ); + }; } /** * A HOC that creaes a function that creates breadcrumbs from DOM API calls. * This is a HOC so that we get access to dom options in the closure. */ -function _domBreadcrumb(dom: BreadcrumbsOptions['dom']): (handlerData: HandlerDataDom) => void { - function _innerDomBreadcrumb(handlerData: HandlerDataDom): void { +function _getDomBreadcrumbHandler( + client: Client, + dom: BreadcrumbsOptions['dom'], +): (handlerData: HandlerDataDom) => void { + return function _innerDomBreadcrumb(handlerData: HandlerDataDom): void { + if (getClient() !== client) { + return; + } + let target; let keyAttrs = typeof dom === 'object' ? dom.serializeAttribute : undefined; @@ -184,167 +174,189 @@ function _domBreadcrumb(dom: BreadcrumbsOptions['dom']): (handlerData: HandlerDa global: handlerData.global, }, ); - } - - return _innerDomBreadcrumb; + }; } /** * Creates breadcrumbs from console API calls */ -function _consoleBreadcrumb(handlerData: HandlerDataConsole): void { - const breadcrumb = { - category: 'console', - data: { - arguments: handlerData.args, - logger: 'console', - }, - level: severityLevelFromString(handlerData.level), - message: safeJoin(handlerData.args, ' '), - }; - - if (handlerData.level === 'assert') { - if (handlerData.args[0] === false) { - breadcrumb.message = `Assertion failed: ${safeJoin(handlerData.args.slice(1), ' ') || 'console.assert'}`; - breadcrumb.data.arguments = handlerData.args.slice(1); - } else { - // Don't capture a breadcrumb for passed assertions +function _getConsoleBreadcrumbHandler(client: Client): (handlerData: HandlerDataConsole) => void { + return function _consoleBreadcrumb(handlerData: HandlerDataConsole): void { + if (getClient() !== client) { return; } - } - addBreadcrumb(breadcrumb, { - input: handlerData.args, - level: handlerData.level, - }); + const breadcrumb = { + category: 'console', + data: { + arguments: handlerData.args, + logger: 'console', + }, + level: severityLevelFromString(handlerData.level), + message: safeJoin(handlerData.args, ' '), + }; + + if (handlerData.level === 'assert') { + if (handlerData.args[0] === false) { + breadcrumb.message = `Assertion failed: ${safeJoin(handlerData.args.slice(1), ' ') || 'console.assert'}`; + breadcrumb.data.arguments = handlerData.args.slice(1); + } else { + // Don't capture a breadcrumb for passed assertions + return; + } + } + + addBreadcrumb(breadcrumb, { + input: handlerData.args, + level: handlerData.level, + }); + }; } /** * Creates breadcrumbs from XHR API calls */ -function _xhrBreadcrumb(handlerData: HandlerDataXhr): void { - const { startTimestamp, endTimestamp } = handlerData; - - const sentryXhrData = handlerData.xhr[SENTRY_XHR_DATA_KEY]; - - // We only capture complete, non-sentry requests - if (!startTimestamp || !endTimestamp || !sentryXhrData) { - return; - } +function _getXhrBreadcrumbHandler(client: Client): (handlerData: HandlerDataXhr) => void { + return function _xhrBreadcrumb(handlerData: HandlerDataXhr): void { + if (getClient() !== client) { + return; + } - const { method, url, status_code, body } = sentryXhrData; + const { startTimestamp, endTimestamp } = handlerData; - const data: XhrBreadcrumbData = { - method, - url, - status_code, - }; + const sentryXhrData = handlerData.xhr[SENTRY_XHR_DATA_KEY]; - const hint: XhrBreadcrumbHint = { - xhr: handlerData.xhr, - input: body, - startTimestamp, - endTimestamp, - }; + // We only capture complete, non-sentry requests + if (!startTimestamp || !endTimestamp || !sentryXhrData) { + return; + } - addBreadcrumb( - { - category: 'xhr', - data, - type: 'http', - }, - hint, - ); -} + const { method, url, status_code, body } = sentryXhrData; -/** - * Creates breadcrumbs from fetch API calls - */ -function _fetchBreadcrumb(handlerData: HandlerDataFetch): void { - const { startTimestamp, endTimestamp } = handlerData; - - // We only capture complete fetch requests - if (!endTimestamp) { - return; - } - - if (handlerData.fetchData.url.match(/sentry_key/) && handlerData.fetchData.method === 'POST') { - // We will not create breadcrumbs for fetch requests that contain `sentry_key` (internal sentry requests) - return; - } - - if (handlerData.error) { - const data: FetchBreadcrumbData = handlerData.fetchData; - const hint: FetchBreadcrumbHint = { - data: handlerData.error, - input: handlerData.args, - startTimestamp, - endTimestamp, + const data: XhrBreadcrumbData = { + method, + url, + status_code, }; - addBreadcrumb( - { - category: 'fetch', - data, - level: 'error', - type: 'http', - }, - hint, - ); - } else { - const response = handlerData.response as Response | undefined; - const data: FetchBreadcrumbData = { - ...handlerData.fetchData, - status_code: response && response.status, - }; - const hint: FetchBreadcrumbHint = { - input: handlerData.args, - response, + const hint: XhrBreadcrumbHint = { + xhr: handlerData.xhr, + input: body, startTimestamp, endTimestamp, }; + addBreadcrumb( { - category: 'fetch', + category: 'xhr', data, type: 'http', }, hint, ); - } + }; +} + +/** + * Creates breadcrumbs from fetch API calls + */ +function _getFetchBreadcrumbHandler(client: Client): (handlerData: HandlerDataFetch) => void { + return function _fetchBreadcrumb(handlerData: HandlerDataFetch): void { + if (getClient() !== client) { + return; + } + + const { startTimestamp, endTimestamp } = handlerData; + + // We only capture complete fetch requests + if (!endTimestamp) { + return; + } + + if (handlerData.fetchData.url.match(/sentry_key/) && handlerData.fetchData.method === 'POST') { + // We will not create breadcrumbs for fetch requests that contain `sentry_key` (internal sentry requests) + return; + } + + if (handlerData.error) { + const data: FetchBreadcrumbData = handlerData.fetchData; + const hint: FetchBreadcrumbHint = { + data: handlerData.error, + input: handlerData.args, + startTimestamp, + endTimestamp, + }; + + addBreadcrumb( + { + category: 'fetch', + data, + level: 'error', + type: 'http', + }, + hint, + ); + } else { + const response = handlerData.response as Response | undefined; + const data: FetchBreadcrumbData = { + ...handlerData.fetchData, + status_code: response && response.status, + }; + const hint: FetchBreadcrumbHint = { + input: handlerData.args, + response, + startTimestamp, + endTimestamp, + }; + addBreadcrumb( + { + category: 'fetch', + data, + type: 'http', + }, + hint, + ); + } + }; } /** * Creates breadcrumbs from history API calls */ -function _historyBreadcrumb(handlerData: HandlerDataHistory): void { - let from: string | undefined = handlerData.from; - let to: string | undefined = handlerData.to; - const parsedLoc = parseUrl(WINDOW.location.href); - let parsedFrom = from ? parseUrl(from) : undefined; - const parsedTo = parseUrl(to); - - // Initial pushState doesn't provide `from` information - if (!parsedFrom || !parsedFrom.path) { - parsedFrom = parsedLoc; - } - - // Use only the path component of the URL if the URL matches the current - // document (almost all the time when using pushState) - if (parsedLoc.protocol === parsedTo.protocol && parsedLoc.host === parsedTo.host) { - to = parsedTo.relative; - } - if (parsedLoc.protocol === parsedFrom.protocol && parsedLoc.host === parsedFrom.host) { - from = parsedFrom.relative; - } - - addBreadcrumb({ - category: 'navigation', - data: { - from, - to, - }, - }); +function _getHistoryBreadcrumbHandler(client: Client): (handlerData: HandlerDataHistory) => void { + return function _historyBreadcrumb(handlerData: HandlerDataHistory): void { + if (getClient() !== client) { + return; + } + + let from: string | undefined = handlerData.from; + let to: string | undefined = handlerData.to; + const parsedLoc = parseUrl(WINDOW.location.href); + let parsedFrom = from ? parseUrl(from) : undefined; + const parsedTo = parseUrl(to); + + // Initial pushState doesn't provide `from` information + if (!parsedFrom || !parsedFrom.path) { + parsedFrom = parsedLoc; + } + + // Use only the path component of the URL if the URL matches the current + // document (almost all the time when using pushState) + if (parsedLoc.protocol === parsedTo.protocol && parsedLoc.host === parsedTo.host) { + to = parsedTo.relative; + } + if (parsedLoc.protocol === parsedFrom.protocol && parsedLoc.host === parsedFrom.host) { + from = parsedFrom.relative; + } + + addBreadcrumb({ + category: 'navigation', + data: { + from, + to, + }, + }); + }; } function _isEvent(event: unknown): event is Event { diff --git a/packages/browser/src/integrations/dedupe.ts b/packages/browser/src/integrations/dedupe.ts index 2d4b51f58767..8882f81ae701 100644 --- a/packages/browser/src/integrations/dedupe.ts +++ b/packages/browser/src/integrations/dedupe.ts @@ -1,57 +1,40 @@ -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: IntegrationFn = () => { + 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) {} // eslint-disable-line no-empty - 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); + }, + }; +}; + +/** Deduplication filter */ +// eslint-disable-next-line deprecation/deprecation +export const Dedupe = convertIntegrationFnToClass(INTEGRATION_NAME, dedupeIntegration); -/** JSDoc */ function _shouldDropEvent(currentEvent: Event, previousEvent?: Event): boolean { if (!previousEvent) { return false; @@ -68,7 +51,6 @@ function _shouldDropEvent(currentEvent: Event, previousEvent?: Event): boolean { return false; } -/** JSDoc */ function _isSameMessageEvent(currentEvent: Event, previousEvent: Event): boolean { const currentMessage = currentEvent.message; const previousMessage = previousEvent.message; @@ -98,7 +80,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); @@ -122,7 +103,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); @@ -163,7 +143,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; @@ -189,12 +168,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/browser/src/integrations/globalhandlers.ts b/packages/browser/src/integrations/globalhandlers.ts index 079ef6083212..e829b6f92845 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 { captureEvent, getClient } from '@sentry/core'; -import type { Client, Event, Integration, Primitive, StackParser } from '@sentry/types'; +import { captureEvent, convertIntegrationFnToClass, getClient } from '@sentry/core'; +import type { Client, Event, IntegrationFn, Primitive, StackParser } from '@sentry/types'; import { addGlobalErrorInstrumentationHandler, addGlobalUnhandledRejectionInstrumentationHandler, @@ -18,52 +18,38 @@ import { shouldIgnoreOnError } from '../helpers'; type GlobalHandlersIntegrationsOptionKeys = 'onerror' | 'onunhandledrejection'; -/** JSDoc */ type GlobalHandlersIntegrations = Record; -/** Global handlers */ -export class GlobalHandlers implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'GlobalHandlers'; - - /** - * @inheritDoc - */ - public name: string; - - /** JSDoc */ - private readonly _options: GlobalHandlersIntegrations; - - /** JSDoc */ - public constructor(options?: GlobalHandlersIntegrations) { - this.name = GlobalHandlers.id; - this._options = { - onerror: true, - onunhandledrejection: true, - ...options, - }; - } - /** - * @inheritDoc - */ - public setupOnce(): void { - Error.stackTraceLimit = 50; - } +const INTEGRATION_NAME = 'GlobalHandlers'; - /** @inheritdoc */ - public setup(client: Client): void { - if (this._options.onerror) { - _installGlobalOnErrorHandler(client); - globalHandlerLog('onerror'); - } - if (this._options.onunhandledrejection) { - _installGlobalOnUnhandledRejectionHandler(client); - globalHandlerLog('onunhandledrejection'); - } - } -} +const globalHandlersIntegrations: IntegrationFn = (options: Partial = {}) => { + const _options = { + onerror: true, + onunhandledrejection: true, + ...options, + }; + + return { + name: INTEGRATION_NAME, + setupOnce() { + Error.stackTraceLimit = 50; + }, + setup(client) { + if (_options.onerror) { + _installGlobalOnErrorHandler(client); + globalHandlerLog('onerror'); + } + if (_options.onunhandledrejection) { + _installGlobalOnUnhandledRejectionHandler(client); + globalHandlerLog('onunhandledrejection'); + } + }, + }; +}; + +/** Global handlers */ +// eslint-disable-next-line deprecation/deprecation +export const GlobalHandlers = convertIntegrationFnToClass(INTEGRATION_NAME, globalHandlersIntegrations); function _installGlobalOnErrorHandler(client: Client): void { addGlobalErrorInstrumentationHandler(data => { @@ -204,7 +190,6 @@ function _eventFromIncompleteOnError(msg: any, url: any, line: any, column: any) return _enhanceEventWithInitialFrame(event, url, line, column); } -/** JSDoc */ // eslint-disable-next-line @typescript-eslint/no-explicit-any function _enhanceEventWithInitialFrame(event: Event, url: any, line: any, column: any): Event { // event.exception diff --git a/packages/browser/src/integrations/httpcontext.ts b/packages/browser/src/integrations/httpcontext.ts index 34e7029e504d..2347c7cb1971 100644 --- a/packages/browser/src/integrations/httpcontext.ts +++ b/packages/browser/src/integrations/httpcontext.ts @@ -1,49 +1,36 @@ -import type { Event, Integration } from '@sentry/types'; +import { convertIntegrationFnToClass } from '@sentry/core'; +import type { IntegrationFn } from '@sentry/types'; import { WINDOW } from '../helpers'; -/** HttpContext integration collects information about HTTP request headers */ -export class HttpContext implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'HttpContext'; - - /** - * @inheritDoc - */ - public name: string; - - public constructor() { - this.name = HttpContext.id; - } - - /** - * @inheritDoc - */ - public setupOnce(): void { - // noop - } +const INTEGRATION_NAME = 'HttpContext'; + +const httpContextIntegration: IntegrationFn = () => { + return { + name: INTEGRATION_NAME, + preprocessEvent(event) { + // if none of the information we want exists, don't bother + if (!WINDOW.navigator && !WINDOW.location && !WINDOW.document) { + return; + } + + // grab as much info as exists and add it to the event + const url = (event.request && event.request.url) || (WINDOW.location && WINDOW.location.href); + const { referrer } = WINDOW.document || {}; + const { userAgent } = WINDOW.navigator || {}; + + const headers = { + ...(event.request && event.request.headers), + ...(referrer && { Referer: referrer }), + ...(userAgent && { 'User-Agent': userAgent }), + }; + const request = { ...event.request, ...(url && { url }), headers }; + + event.request = request; + }, + }; +}; - /** @inheritDoc */ - public preprocessEvent(event: Event): void { - // if none of the information we want exists, don't bother - if (!WINDOW.navigator && !WINDOW.location && !WINDOW.document) { - return; - } - - // grab as much info as exists and add it to the event - const url = (event.request && event.request.url) || (WINDOW.location && WINDOW.location.href); - const { referrer } = WINDOW.document || {}; - const { userAgent } = WINDOW.navigator || {}; - - const headers = { - ...(event.request && event.request.headers), - ...(referrer && { Referer: referrer }), - ...(userAgent && { 'User-Agent': userAgent }), - }; - const request = { ...event.request, ...(url && { url }), headers }; - - event.request = request; - } -} +/** HttpContext integration collects information about HTTP request headers */ +// eslint-disable-next-line deprecation/deprecation +export const HttpContext = convertIntegrationFnToClass(INTEGRATION_NAME, httpContextIntegration); diff --git a/packages/browser/src/integrations/linkederrors.ts b/packages/browser/src/integrations/linkederrors.ts index b07b12c98263..e74e6252a4ce 100644 --- a/packages/browser/src/integrations/linkederrors.ts +++ b/packages/browser/src/integrations/linkederrors.ts @@ -1,66 +1,41 @@ -import type { Client, Event, EventHint, Integration } from '@sentry/types'; +import { convertIntegrationFnToClass } from '@sentry/core'; +import type { IntegrationFn } from '@sentry/types'; import { applyAggregateErrorsToEvent } from '@sentry/utils'; - import { exceptionFromError } from '../eventbuilder'; -const DEFAULT_KEY = 'cause'; -const DEFAULT_LIMIT = 5; - interface LinkedErrorsOptions { - key: string; - limit: number; + key?: string; + limit?: number; } -/** Adds SDK info to an event. */ -export class LinkedErrors implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'LinkedErrors'; - - /** - * @inheritDoc - */ - public readonly name: string; - - /** - * @inheritDoc - */ - private readonly _key: LinkedErrorsOptions['key']; - - /** - * @inheritDoc - */ - private readonly _limit: LinkedErrorsOptions['limit']; - - /** - * @inheritDoc - */ - public constructor(options: Partial = {}) { - this.name = LinkedErrors.id; - this._key = options.key || DEFAULT_KEY; - this._limit = options.limit || DEFAULT_LIMIT; - } - - /** @inheritdoc */ - public setupOnce(): void { - // noop - } - - /** - * @inheritDoc - */ - public preprocessEvent(event: Event, hint: EventHint | undefined, client: Client): void { - const options = client.getOptions(); +const DEFAULT_KEY = 'cause'; +const DEFAULT_LIMIT = 5; - applyAggregateErrorsToEvent( - exceptionFromError, - options.stackParser, - options.maxValueLength, - this._key, - this._limit, - event, - hint, - ); - } -} +const INTEGRATION_NAME = 'LinkedErrors'; + +const linkedErrorsIntegration: IntegrationFn = (options: LinkedErrorsOptions = {}) => { + const limit = options.limit || DEFAULT_LIMIT; + const key = options.key || DEFAULT_KEY; + + return { + name: INTEGRATION_NAME, + preprocessEvent(event, hint, client) { + const options = client.getOptions(); + + applyAggregateErrorsToEvent( + // This differs from the LinkedErrors integration in core by using a different exceptionFromError function + exceptionFromError, + options.stackParser, + options.maxValueLength, + key, + limit, + event, + hint, + ); + }, + }; +}; + +/** Aggregrate linked errors in an event. */ +// eslint-disable-next-line deprecation/deprecation +export const LinkedErrors = convertIntegrationFnToClass(INTEGRATION_NAME, linkedErrorsIntegration); diff --git a/packages/browser/src/integrations/trycatch.ts b/packages/browser/src/integrations/trycatch.ts index 03dc1fcd2a80..e65190d02b3f 100644 --- a/packages/browser/src/integrations/trycatch.ts +++ b/packages/browser/src/integrations/trycatch.ts @@ -1,4 +1,5 @@ -import type { Integration, WrappedFunction } from '@sentry/types'; +import { convertIntegrationFnToClass } from '@sentry/core'; +import type { Client, IntegrationFn, WrappedFunction } from '@sentry/types'; import { fill, getFunctionName, getOriginalFunction } from '@sentry/utils'; import { WINDOW, wrap } from '../helpers'; @@ -37,9 +38,10 @@ const DEFAULT_EVENT_TARGET = [ 'XMLHttpRequestUpload', ]; +const INTEGRATION_NAME = 'TryCatch'; + type XMLHttpRequestProp = 'onload' | 'onerror' | 'onprogress' | 'onreadystatechange'; -/** JSDoc */ interface TryCatchOptions { setTimeout: boolean; setInterval: boolean; @@ -48,66 +50,50 @@ interface TryCatchOptions { eventTarget: boolean | string[]; } -/** Wrap timer functions and event targets to catch errors and provide better meta data */ -export class TryCatch implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'TryCatch'; - - /** - * @inheritDoc - */ - public name: string; - - /** JSDoc */ - private readonly _options: TryCatchOptions; +const tryCatchIntegration: IntegrationFn = (options: Partial = {}) => { + const _options = { + XMLHttpRequest: true, + eventTarget: true, + requestAnimationFrame: true, + setInterval: true, + setTimeout: true, + ...options, + }; - /** - * @inheritDoc - */ - public constructor(options?: Partial) { - this.name = TryCatch.id; - this._options = { - XMLHttpRequest: true, - eventTarget: true, - requestAnimationFrame: true, - setInterval: true, - setTimeout: true, - ...options, - }; - } + return { + name: INTEGRATION_NAME, + // TODO: This currently only works for the first client this is setup + // We may want to adjust this to check for client etc. + setupOnce() { + if (_options.setTimeout) { + fill(WINDOW, 'setTimeout', _wrapTimeFunction); + } - /** - * Wrap timer functions and event targets to catch errors - * and provide better metadata. - */ - public setupOnce(): void { - if (this._options.setTimeout) { - fill(WINDOW, 'setTimeout', _wrapTimeFunction); - } + if (_options.setInterval) { + fill(WINDOW, 'setInterval', _wrapTimeFunction); + } - if (this._options.setInterval) { - fill(WINDOW, 'setInterval', _wrapTimeFunction); - } + if (_options.requestAnimationFrame) { + fill(WINDOW, 'requestAnimationFrame', _wrapRAF); + } - if (this._options.requestAnimationFrame) { - fill(WINDOW, 'requestAnimationFrame', _wrapRAF); - } + if (_options.XMLHttpRequest && 'XMLHttpRequest' in WINDOW) { + fill(XMLHttpRequest.prototype, 'send', _wrapXHR); + } - if (this._options.XMLHttpRequest && 'XMLHttpRequest' in WINDOW) { - fill(XMLHttpRequest.prototype, 'send', _wrapXHR); - } + const eventTargetOption = _options.eventTarget; + if (eventTargetOption) { + const eventTarget = Array.isArray(eventTargetOption) ? eventTargetOption : DEFAULT_EVENT_TARGET; + eventTarget.forEach(_wrapEventTarget); + } + }, + }; +}; - const eventTargetOption = this._options.eventTarget; - if (eventTargetOption) { - const eventTarget = Array.isArray(eventTargetOption) ? eventTargetOption : DEFAULT_EVENT_TARGET; - eventTarget.forEach(_wrapEventTarget); - } - } -} +/** Wrap timer functions and event targets to catch errors and provide better meta data */ +// eslint-disable-next-line deprecation/deprecation +export const TryCatch = convertIntegrationFnToClass(INTEGRATION_NAME, tryCatchIntegration); -/** JSDoc */ function _wrapTimeFunction(original: () => void): () => number { // eslint-disable-next-line @typescript-eslint/no-explicit-any return function (this: any, ...args: any[]): number { @@ -123,7 +109,6 @@ function _wrapTimeFunction(original: () => void): () => number { }; } -/** JSDoc */ // eslint-disable-next-line @typescript-eslint/no-explicit-any function _wrapRAF(original: any): (callback: () => void) => any { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -144,7 +129,6 @@ function _wrapRAF(original: any): (callback: () => void) => any { }; } -/** JSDoc */ function _wrapXHR(originalSend: () => void): () => void { // eslint-disable-next-line @typescript-eslint/no-explicit-any return function (this: XMLHttpRequest, ...args: any[]): void { @@ -183,7 +167,6 @@ function _wrapXHR(originalSend: () => void): () => void { }; } -/** JSDoc */ function _wrapEventTarget(target: string): void { // eslint-disable-next-line @typescript-eslint/no-explicit-any const globalObject = WINDOW as { [key: string]: any };