diff --git a/.size-limit.js b/.size-limit.js index 599f59e07170..5d690d69768a 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -86,18 +86,21 @@ module.exports = [ name: '@sentry/browser (incl. Tracing, Replay) - ES6 CDN Bundle (minified & uncompressed)', path: 'packages/browser/build/bundles/bundle.tracing.replay.min.js', gzip: false, + brotli: false, limit: '260 KB', }, { name: '@sentry/browser (incl. Tracing) - ES6 CDN Bundle (minified & uncompressed)', path: 'packages/browser/build/bundles/bundle.tracing.min.js', gzip: false, + brotli: false, limit: '100 KB', }, { name: '@sentry/browser - ES6 CDN Bundle (minified & uncompressed)', path: 'packages/browser/build/bundles/bundle.min.js', gzip: false, + brotli: false, limit: '70 KB', }, diff --git a/CHANGELOG.md b/CHANGELOG.md index 4345f368f6fd..47895bc58fb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 7.90.0 + +- feat(replay): Change to use preset quality values (#9903) +- fix(replay): Adjust development hydration error messages (#9922) +- fix(sveltekit): Add `types` field to package.json `exports` (#9926) + ## 7.89.0 ### Important Changes diff --git a/package.json b/package.json index f8bdaced8cbe..db8a133e494b 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "yalc:publish": "lerna run yalc:publish" }, "volta": { - "node": "20.10.0", + "node": "18.17.0", "yarn": "1.22.19" }, "workspaces": [ @@ -88,8 +88,8 @@ "@rollup/plugin-replace": "^3.0.1", "@rollup/plugin-sucrase": "^4.0.3", "@rollup/plugin-typescript": "^8.3.1", - "@size-limit/preset-small-lib": "~9.0.0", - "@size-limit/webpack": "~9.0.0", + "@size-limit/file": "~11.0.1", + "@size-limit/webpack": "~11.0.1", "@strictsoftware/typedoc-plugin-monorepo": "^0.3.1", "@types/chai": "^4.1.3", "@types/jest": "^27.4.1", @@ -124,7 +124,7 @@ "rollup-plugin-license": "^2.6.1", "rollup-plugin-terser": "^7.0.2", "sinon": "^7.3.2", - "size-limit": "~9.0.0", + "size-limit": "~11.0.1", "ts-jest": "^27.1.4", "ts-node": "10.9.1", "typedoc": "^0.18.0", diff --git a/packages/browser-integration-tests/utils/replayHelpers.ts b/packages/browser-integration-tests/utils/replayHelpers.ts index b487342f5593..613bd5b447f1 100644 --- a/packages/browser-integration-tests/utils/replayHelpers.ts +++ b/packages/browser-integration-tests/utils/replayHelpers.ts @@ -105,40 +105,35 @@ export function waitForReplayRequest( * Wait until a callback returns true, collecting all replay responses along the way. * This can be useful when you don't know if stuff will be in one or multiple replay requests. */ -export function waitForReplayRequests( +export async function waitForReplayRequests( page: Page, callback: (event: ReplayEvent, res: Response) => boolean, timeout?: number, ): Promise { const responses: Response[] = []; - return new Promise(resolve => { - void page.waitForResponse( - res => { - const req = res.request(); + await page.waitForResponse( + res => { + const req = res.request(); - const event = getReplayEventFromRequest(req); + const event = getReplayEventFromRequest(req); - if (!event) { - return false; - } + if (!event) { + return false; + } - responses.push(res); + responses.push(res); - try { - if (callback(event, res)) { - resolve(responses); - return true; - } + try { + return callback(event, res); + } catch { + return false; + } + }, + timeout ? { timeout } : undefined, + ); - return false; - } catch { - return false; - } - }, - timeout ? { timeout } : undefined, - ); - }); + return responses; } export function isReplayEvent(event: Event): event is ReplayEvent { diff --git a/packages/browser/src/client.ts b/packages/browser/src/client.ts index 4e46314f6711..c8d4e4ce2f08 100644 --- a/packages/browser/src/client.ts +++ b/packages/browser/src/client.ts @@ -106,7 +106,10 @@ export class BrowserClient extends BaseClient { dsn: this.getDsn(), tunnel: this.getOptions().tunnel, }); - void this._sendEnvelope(envelope); + + // _sendEnvelope should not throw + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this._sendEnvelope(envelope); } /** @@ -137,6 +140,9 @@ export class BrowserClient extends BaseClient { DEBUG_BUILD && logger.log('Sending outcomes:', outcomes); const envelope = createClientReportEnvelope(outcomes, this._options.tunnel && dsnToString(this._dsn)); - void this._sendEnvelope(envelope); + + // _sendEnvelope should not throw + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this._sendEnvelope(envelope); } } 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 }; diff --git a/packages/browser/src/profiling/hubextensions.ts b/packages/browser/src/profiling/hubextensions.ts index 99e2772c8f25..f042ae016609 100644 --- a/packages/browser/src/profiling/hubextensions.ts +++ b/packages/browser/src/profiling/hubextensions.ts @@ -139,7 +139,8 @@ export function startProfileForTransaction(transaction: Transaction): Transactio ); } // If the timeout exceeds, we want to stop profiling, but not finish the transaction - void onProfileHandler(); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + onProfileHandler(); }, MAX_PROFILE_DURATION_MS); // We need to reference the original finish call to avoid creating an infinite loop diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index 3fbdd13250f8..fc0258167530 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -375,7 +375,10 @@ export abstract class BaseClient implements Client { */ public sendSession(session: Session | SessionAggregates): void { const env = createSessionEnvelope(session, this._dsn, this._options._metadata, this._options.tunnel); - void this._sendEnvelope(env); + + // _sendEnvelope should not throw + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this._sendEnvelope(env); } /** @@ -409,7 +412,10 @@ export abstract class BaseClient implements Client { this._options._metadata, this._options.tunnel, ); - void this._sendEnvelope(metricsEnvelope); + + // _sendEnvelope should not throw + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this._sendEnvelope(metricsEnvelope); } // Keep on() & emit() signatures in sync with types' client.ts interface diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 6468c312bbe1..114ac4a670fe 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -63,6 +63,7 @@ export { convertIntegrationFnToClass, } from './integration'; export { FunctionToString, InboundFilters, LinkedErrors } from './integrations'; +export { applyScopeDataToEvent } from './utils/applyScopeDataToEvent'; export { prepareEvent } from './utils/prepareEvent'; export { createCheckInEnvelope } from './checkin'; export { hasTracingEnabled } from './utils/hasTracingEnabled'; diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts index b7782fcfa65c..01a55081c04f 100644 --- a/packages/core/src/integration.ts +++ b/packages/core/src/integration.ts @@ -1,4 +1,14 @@ -import type { Client, Event, EventHint, Integration, IntegrationClass, IntegrationFn, Options } from '@sentry/types'; +import type { + Client, + Event, + EventHint, + EventProcessor, + Hub, + Integration, + IntegrationClass, + IntegrationFn, + Options, +} from '@sentry/types'; import { arrayify, logger } from '@sentry/utils'; import { DEBUG_BUILD } from './debug-build'; @@ -165,7 +175,11 @@ function findIndex(arr: T[], callback: (item: T) => boolean): number { export function convertIntegrationFnToClass( name: string, fn: Fn, -): IntegrationClass { +): IntegrationClass< + Integration & { + setupOnce: (addGlobalEventProcessor?: (callback: EventProcessor) => void, getCurrentHub?: () => Hub) => void; + } +> { return Object.assign( // eslint-disable-next-line @typescript-eslint/no-explicit-any function ConvertedIntegration(...rest: any[]) { @@ -176,5 +190,9 @@ export function convertIntegrationFnToClass( }; }, { id: name }, - ) as unknown as IntegrationClass; + ) as unknown as IntegrationClass< + Integration & { + setupOnce: (addGlobalEventProcessor?: (callback: EventProcessor) => void, getCurrentHub?: () => Hub) => void; + } + >; } diff --git a/packages/core/src/integrations/functiontostring.ts b/packages/core/src/integrations/functiontostring.ts index b1db3224bc64..124085c9c6c6 100644 --- a/packages/core/src/integrations/functiontostring.ts +++ b/packages/core/src/integrations/functiontostring.ts @@ -1,41 +1,33 @@ -import type { Integration, WrappedFunction } from '@sentry/types'; +import type { IntegrationFn, WrappedFunction } from '@sentry/types'; import { getOriginalFunction } from '@sentry/utils'; +import { convertIntegrationFnToClass } from '../integration'; let originalFunctionToString: () => void; -/** Patch toString calls to return proper name for wrapped functions */ -export class FunctionToString implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'FunctionToString'; - - /** - * @inheritDoc - */ - public name: string; +const INTEGRATION_NAME = 'FunctionToString'; - public constructor() { - this.name = FunctionToString.id; - } +const functionToStringIntegration: IntegrationFn = () => { + return { + name: INTEGRATION_NAME, + setupOnce() { + // eslint-disable-next-line @typescript-eslint/unbound-method + originalFunctionToString = Function.prototype.toString; - /** - * @inheritDoc - */ - public setupOnce(): void { - // eslint-disable-next-line @typescript-eslint/unbound-method - originalFunctionToString = Function.prototype.toString; + // intrinsics (like Function.prototype) might be immutable in some environments + // e.g. Node with --frozen-intrinsics, XS (an embedded JavaScript engine) or SES (a JavaScript proposal) + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + Function.prototype.toString = function (this: WrappedFunction, ...args: any[]): string { + const context = getOriginalFunction(this) || this; + return originalFunctionToString.apply(context, args); + }; + } catch { + // ignore errors here, just don't patch this + } + }, + }; +}; - // intrinsics (like Function.prototype) might be immutable in some environments - // e.g. Node with --frozen-intrinsics, XS (an embedded JavaScript engine) or SES (a JavaScript proposal) - try { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - Function.prototype.toString = function (this: WrappedFunction, ...args: any[]): string { - const context = getOriginalFunction(this) || this; - return originalFunctionToString.apply(context, args); - }; - } catch { - // ignore errors here, just don't patch this - } - } -} +/** Patch toString calls to return proper name for wrapped functions */ +// eslint-disable-next-line deprecation/deprecation +export const FunctionToString = convertIntegrationFnToClass(INTEGRATION_NAME, functionToStringIntegration); diff --git a/packages/core/src/integrations/linkederrors.ts b/packages/core/src/integrations/linkederrors.ts index 1b3ef1c39a19..aa9df808e2d8 100644 --- a/packages/core/src/integrations/linkederrors.ts +++ b/packages/core/src/integrations/linkederrors.ts @@ -1,59 +1,39 @@ -import type { Client, Event, EventHint, Integration } from '@sentry/types'; +import type { IntegrationFn } from '@sentry/types'; import { applyAggregateErrorsToEvent, exceptionFromError } from '@sentry/utils'; +import { convertIntegrationFnToClass } from '../integration'; + +interface LinkedErrorsOptions { + key?: string; + limit?: number; +} const DEFAULT_KEY = 'cause'; const DEFAULT_LIMIT = 5; -/** 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: string; - - /** - * @inheritDoc - */ - private readonly _limit: number; +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( + exceptionFromError, + options.stackParser, + options.maxValueLength, + key, + limit, + event, + hint, + ); + }, + }; +}; - /** - * @inheritDoc - */ - public constructor(options: { key?: string; limit?: number } = {}) { - this._key = options.key || DEFAULT_KEY; - this._limit = options.limit || DEFAULT_LIMIT; - this.name = LinkedErrors.id; - } - - /** @inheritdoc */ - public setupOnce(): void { - // noop - } - - /** - * @inheritDoc - */ - public preprocessEvent(event: Event, hint: EventHint | undefined, client: Client): void { - const options = client.getOptions(); - - applyAggregateErrorsToEvent( - exceptionFromError, - options.stackParser, - options.maxValueLength, - this._key, - this._limit, - event, - hint, - ); - } -} +/** Adds SDK info to an event. */ +// eslint-disable-next-line deprecation/deprecation +export const LinkedErrors = convertIntegrationFnToClass(INTEGRATION_NAME, linkedErrorsIntegration); diff --git a/packages/core/src/integrations/metadata.ts b/packages/core/src/integrations/metadata.ts index 1803640ea4dd..b94f252b5ce0 100644 --- a/packages/core/src/integrations/metadata.ts +++ b/packages/core/src/integrations/metadata.ts @@ -1,8 +1,42 @@ -import type { Client, Event, EventItem, EventProcessor, Hub, Integration } from '@sentry/types'; +import type { Event, EventItem, IntegrationFn } from '@sentry/types'; import { forEachEnvelopeItem } from '@sentry/utils'; +import { convertIntegrationFnToClass } from '../integration'; import { addMetadataToStackFrames, stripMetadataFromStackFrames } from '../metadata'; +const INTEGRATION_NAME = 'ModuleMetadata'; + +const moduleMetadataIntegration: IntegrationFn = () => { + return { + name: INTEGRATION_NAME, + setup(client) { + if (typeof client.on !== 'function') { + return; + } + + // We need to strip metadata from stack frames before sending them to Sentry since these are client side only. + client.on('beforeEnvelope', envelope => { + forEachEnvelopeItem(envelope, (item, type) => { + if (type === 'event') { + const event = Array.isArray(item) ? (item as EventItem)[1] : undefined; + + if (event) { + stripMetadataFromStackFrames(event); + item[1] = event; + } + } + }); + }); + }, + + processEvent(event, _hint, client) { + const stackParser = client.getOptions().stackParser; + addMetadataToStackFrames(stackParser, event); + return event; + }, + }; +}; + /** * Adds module metadata to stack frames. * @@ -12,53 +46,5 @@ import { addMetadataToStackFrames, stripMetadataFromStackFrames } from '../metad * under the `module_metadata` property. This can be used to help in tagging or routing of events from different teams * our sources */ -export class ModuleMetadata implements Integration { - /* - * @inheritDoc - */ - public static id: string = 'ModuleMetadata'; - - /** - * @inheritDoc - */ - public name: string; - - public constructor() { - this.name = ModuleMetadata.id; - } - - /** - * @inheritDoc - */ - public setupOnce(_addGlobalEventProcessor: (processor: EventProcessor) => void, _getCurrentHub: () => Hub): void { - // noop - } - - /** @inheritDoc */ - public setup(client: Client): void { - if (typeof client.on !== 'function') { - return; - } - - // We need to strip metadata from stack frames before sending them to Sentry since these are client side only. - client.on('beforeEnvelope', envelope => { - forEachEnvelopeItem(envelope, (item, type) => { - if (type === 'event') { - const event = Array.isArray(item) ? (item as EventItem)[1] : undefined; - - if (event) { - stripMetadataFromStackFrames(event); - item[1] = event; - } - } - }); - }); - } - - /** @inheritDoc */ - public processEvent(event: Event, _hint: unknown, client: Client): Event { - const stackParser = client.getOptions().stackParser; - addMetadataToStackFrames(stackParser, event); - return event; - } -} +// eslint-disable-next-line deprecation/deprecation +export const ModuleMetadata = convertIntegrationFnToClass(INTEGRATION_NAME, moduleMetadataIntegration); diff --git a/packages/core/src/integrations/requestdata.ts b/packages/core/src/integrations/requestdata.ts index 6aded915d854..fcf70ccced1a 100644 --- a/packages/core/src/integrations/requestdata.ts +++ b/packages/core/src/integrations/requestdata.ts @@ -1,6 +1,7 @@ -import type { Client, Event, EventProcessor, Hub, Integration, PolymorphicRequest, Transaction } from '@sentry/types'; +import type { Client, IntegrationFn, Transaction } from '@sentry/types'; import type { AddRequestDataToEventOptions, TransactionNamingScheme } from '@sentry/utils'; import { addRequestDataToEvent, extractPathForTransaction } from '@sentry/utils'; +import { convertIntegrationFnToClass } from '../integration'; export type RequestDataIntegrationOptions = { /** @@ -43,120 +44,93 @@ const DEFAULT_OPTIONS = { transactionNamingScheme: 'methodPath', }; -/** Add data about a request to an event. Primarily for use in Node-based SDKs, but included in `@sentry/integrations` - * so it can be used in cross-platform SDKs like `@sentry/nextjs`. */ -export class RequestData implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'RequestData'; +const INTEGRATION_NAME = 'RequestData'; - /** - * @inheritDoc - */ - public name: string; +const requestDataIntegration: IntegrationFn = (options: RequestDataIntegrationOptions = {}) => { + const _addRequestData = addRequestDataToEvent; + const _options: Required = { + ...DEFAULT_OPTIONS, + ...options, + include: { + // @ts-expect-error It's mad because `method` isn't a known `include` key. (It's only here and not set by default in + // `addRequestDataToEvent` for legacy reasons. TODO (v8): Change that.) + method: true, + ...DEFAULT_OPTIONS.include, + ...options.include, + user: + options.include && typeof options.include.user === 'boolean' + ? options.include.user + : { + ...DEFAULT_OPTIONS.include.user, + // Unclear why TS still thinks `options.include.user` could be a boolean at this point + ...((options.include || {}).user as Record), + }, + }, + }; - /** - * Function for adding request data to event. Defaults to `addRequestDataToEvent` from `@sentry/node` for now, but - * left as a property so this integration can be moved to `@sentry/core` as a base class in case we decide to use - * something similar in browser-based SDKs in the future. - */ - protected _addRequestData: (event: Event, req: PolymorphicRequest, options?: { [key: string]: unknown }) => Event; + return { + name: INTEGRATION_NAME, - private _options: Required; + processEvent(event, _hint, client) { + // Note: In the long run, most of the logic here should probably move into the request data utility functions. For + // the moment it lives here, though, until https://github.com/getsentry/sentry-javascript/issues/5718 is addressed. + // (TL;DR: Those functions touch many parts of the repo in many different ways, and need to be clened up. Once + // that's happened, it will be easier to add this logic in without worrying about unexpected side effects.) + const { transactionNamingScheme } = _options; - /** - * @inheritDoc - */ - public constructor(options: RequestDataIntegrationOptions = {}) { - this.name = RequestData.id; - this._addRequestData = addRequestDataToEvent; - this._options = { - ...DEFAULT_OPTIONS, - ...options, - include: { - // @ts-expect-error It's mad because `method` isn't a known `include` key. (It's only here and not set by default in - // `addRequestDataToEvent` for legacy reasons. TODO (v8): Change that.) - method: true, - ...DEFAULT_OPTIONS.include, - ...options.include, - user: - options.include && typeof options.include.user === 'boolean' - ? options.include.user - : { - ...DEFAULT_OPTIONS.include.user, - // Unclear why TS still thinks `options.include.user` could be a boolean at this point - ...((options.include || {}).user as Record), - }, - }, - }; - } + const { sdkProcessingMetadata = {} } = event; + const req = sdkProcessingMetadata.request; - /** - * @inheritDoc - */ - public setupOnce( - _addGlobalEventProcessor: (eventProcessor: EventProcessor) => void, - _getCurrentHub: () => Hub, - ): void { - // noop - } - - /** @inheritdoc */ - public processEvent(event: Event, _hint: unknown, client: Client): Event { - // Note: In the long run, most of the logic here should probably move into the request data utility functions. For - // the moment it lives here, though, until https://github.com/getsentry/sentry-javascript/issues/5718 is addressed. - // (TL;DR: Those functions touch many parts of the repo in many different ways, and need to be clened up. Once - // that's happened, it will be easier to add this logic in without worrying about unexpected side effects.) - const { transactionNamingScheme } = this._options; + if (!req) { + return event; + } - const { sdkProcessingMetadata = {} } = event; - const req = sdkProcessingMetadata.request; + // The Express request handler takes a similar `include` option to that which can be passed to this integration. + // If passed there, we store it in `sdkProcessingMetadata`. TODO(v8): Force express and GCP people to use this + // integration, so that all of this passing and conversion isn't necessary + const addRequestDataOptions = + sdkProcessingMetadata.requestDataOptionsFromExpressHandler || + sdkProcessingMetadata.requestDataOptionsFromGCPWrapper || + convertReqDataIntegrationOptsToAddReqDataOpts(_options); - if (!req) { - return event; - } + const processedEvent = _addRequestData(event, req, addRequestDataOptions); - // The Express request handler takes a similar `include` option to that which can be passed to this integration. - // If passed there, we store it in `sdkProcessingMetadata`. TODO(v8): Force express and GCP people to use this - // integration, so that all of this passing and conversion isn't necessary - const addRequestDataOptions = - sdkProcessingMetadata.requestDataOptionsFromExpressHandler || - sdkProcessingMetadata.requestDataOptionsFromGCPWrapper || - convertReqDataIntegrationOptsToAddReqDataOpts(this._options); + // Transaction events already have the right `transaction` value + if (event.type === 'transaction' || transactionNamingScheme === 'handler') { + return processedEvent; + } - const processedEvent = this._addRequestData(event, req, addRequestDataOptions); + // In all other cases, use the request's associated transaction (if any) to overwrite the event's `transaction` + // value with a high-quality one + const reqWithTransaction = req as { _sentryTransaction?: Transaction }; + const transaction = reqWithTransaction._sentryTransaction; + if (transaction) { + // TODO (v8): Remove the nextjs check and just base it on `transactionNamingScheme` for all SDKs. (We have to + // keep it the way it is for the moment, because changing the names of transactions in Sentry has the potential + // to break things like alert rules.) + const shouldIncludeMethodInTransactionName = + getSDKName(client) === 'sentry.javascript.nextjs' + ? transaction.name.startsWith('/api') + : transactionNamingScheme !== 'path'; + + const [transactionValue] = extractPathForTransaction(req, { + path: true, + method: shouldIncludeMethodInTransactionName, + customRoute: transaction.name, + }); + + processedEvent.transaction = transactionValue; + } - // Transaction events already have the right `transaction` value - if (event.type === 'transaction' || transactionNamingScheme === 'handler') { return processedEvent; - } - - // In all other cases, use the request's associated transaction (if any) to overwrite the event's `transaction` - // value with a high-quality one - const reqWithTransaction = req as { _sentryTransaction?: Transaction }; - const transaction = reqWithTransaction._sentryTransaction; - if (transaction) { - // TODO (v8): Remove the nextjs check and just base it on `transactionNamingScheme` for all SDKs. (We have to - // keep it the way it is for the moment, because changing the names of transactions in Sentry has the potential - // to break things like alert rules.) - const shouldIncludeMethodInTransactionName = - getSDKName(client) === 'sentry.javascript.nextjs' - ? transaction.name.startsWith('/api') - : transactionNamingScheme !== 'path'; - - const [transactionValue] = extractPathForTransaction(req, { - path: true, - method: shouldIncludeMethodInTransactionName, - customRoute: transaction.name, - }); - - processedEvent.transaction = transactionValue; - } + }, + }; +}; - return processedEvent; - } -} +/** Add data about a request to an event. Primarily for use in Node-based SDKs, but included in `@sentry/integrations` + * so it can be used in cross-platform SDKs like `@sentry/nextjs`. */ +// eslint-disable-next-line deprecation/deprecation +export const RequestData = convertIntegrationFnToClass(INTEGRATION_NAME, requestDataIntegration); /** Convert this integration's options to match what `addRequestDataToEvent` expects */ /** TODO: Can possibly be deleted once https://github.com/getsentry/sentry-javascript/issues/5718 is fixed */ diff --git a/packages/core/src/scope.ts b/packages/core/src/scope.ts index ae6fe70e3185..599b5c0f8d57 100644 --- a/packages/core/src/scope.ts +++ b/packages/core/src/scope.ts @@ -15,6 +15,7 @@ import type { RequestSession, Scope as ScopeInterface, ScopeContext, + ScopeData, Session, Severity, SeverityLevel, @@ -26,6 +27,7 @@ import { arrayify, dateTimestampInSeconds, isPlainObject, uuid4 } from '@sentry/ import { getGlobalEventProcessors, notifyEventProcessors } from './eventProcessors'; import { updateSession } from './session'; +import { applyScopeDataToEvent } from './utils/applyScopeDataToEvent'; /** * Default value for maximum number of breadcrumbs added to an event. @@ -466,78 +468,65 @@ export class Scope implements ScopeInterface { return this; } + /** @inheritDoc */ + public getScopeData(): ScopeData { + const { + _breadcrumbs, + _attachments, + _contexts, + _tags, + _extra, + _user, + _level, + _fingerprint, + _eventProcessors, + _propagationContext, + _sdkProcessingMetadata, + _transactionName, + _span, + } = this; + + return { + breadcrumbs: _breadcrumbs, + attachments: _attachments, + contexts: _contexts, + tags: _tags, + extra: _extra, + user: _user, + level: _level, + fingerprint: _fingerprint || [], + eventProcessors: _eventProcessors, + propagationContext: _propagationContext, + sdkProcessingMetadata: _sdkProcessingMetadata, + transactionName: _transactionName, + span: _span, + }; + } + /** * Applies data from the scope to the event and runs all event processors on it. * * @param event Event * @param hint Object containing additional information about the original exception, for use by the event processors. * @hidden + * @deprecated Use `applyScopeDataToEvent()` directly */ public applyToEvent( event: Event, hint: EventHint = {}, - additionalEventProcessors?: EventProcessor[], + additionalEventProcessors: EventProcessor[] = [], ): PromiseLike { - if (this._extra && Object.keys(this._extra).length) { - event.extra = { ...this._extra, ...event.extra }; - } - if (this._tags && Object.keys(this._tags).length) { - event.tags = { ...this._tags, ...event.tags }; - } - if (this._user && Object.keys(this._user).length) { - event.user = { ...this._user, ...event.user }; - } - if (this._contexts && Object.keys(this._contexts).length) { - event.contexts = { ...this._contexts, ...event.contexts }; - } - if (this._level) { - event.level = this._level; - } - if (this._transactionName) { - event.transaction = this._transactionName; - } - - // We want to set the trace context for normal events only if there isn't already - // a trace context on the event. There is a product feature in place where we link - // errors with transaction and it relies on that. - if (this._span) { - event.contexts = { trace: this._span.getTraceContext(), ...event.contexts }; - const transaction = this._span.transaction; - if (transaction) { - event.sdkProcessingMetadata = { - dynamicSamplingContext: transaction.getDynamicSamplingContext(), - ...event.sdkProcessingMetadata, - }; - const transactionName = transaction.name; - if (transactionName) { - event.tags = { transaction: transactionName, ...event.tags }; - } - } - } - - this._applyFingerprint(event); - - const scopeBreadcrumbs = this._getBreadcrumbs(); - const breadcrumbs = [...(event.breadcrumbs || []), ...scopeBreadcrumbs]; - event.breadcrumbs = breadcrumbs.length > 0 ? breadcrumbs : undefined; - - event.sdkProcessingMetadata = { - ...event.sdkProcessingMetadata, - ...this._sdkProcessingMetadata, - propagationContext: this._propagationContext, - }; + applyScopeDataToEvent(event, this.getScopeData()); // TODO (v8): Update this order to be: Global > Client > Scope - return notifyEventProcessors( - [ - ...(additionalEventProcessors || []), - // eslint-disable-next-line deprecation/deprecation - ...getGlobalEventProcessors(), - ...this._eventProcessors, - ], - event, - hint, - ); + const eventProcessors: EventProcessor[] = [ + ...additionalEventProcessors, + // eslint-disable-next-line deprecation/deprecation + ...getGlobalEventProcessors(), + ...this._eventProcessors, + ]; + + return notifyEventProcessors(eventProcessors, event, hint); } /** @@ -564,13 +553,6 @@ export class Scope implements ScopeInterface { return this._propagationContext; } - /** - * Get the breadcrumbs for this scope. - */ - protected _getBreadcrumbs(): Breadcrumb[] { - return this._breadcrumbs; - } - /** * This will be called on every set call. */ @@ -586,25 +568,6 @@ export class Scope implements ScopeInterface { this._notifyingListeners = false; } } - - /** - * Applies fingerprint from the scope to the event if there's one, - * uses message if there's one instead or get rid of empty fingerprint - */ - private _applyFingerprint(event: Event): void { - // Make sure it's an array first and we actually have something in place - event.fingerprint = event.fingerprint ? arrayify(event.fingerprint) : []; - - // If we have something on the scope, then merge it with event - if (this._fingerprint) { - event.fingerprint = event.fingerprint.concat(this._fingerprint); - } - - // If we have no data at all, remove empty array default - if (event.fingerprint && !event.fingerprint.length) { - delete event.fingerprint; - } - } } function generatePropagationContext(): PropagationContext { diff --git a/packages/core/src/server-runtime-client.ts b/packages/core/src/server-runtime-client.ts index 719e2b81f086..9cffaca15faf 100644 --- a/packages/core/src/server-runtime-client.ts +++ b/packages/core/src/server-runtime-client.ts @@ -193,7 +193,11 @@ export class ServerRuntimeClient< ); DEBUG_BUILD && logger.info('Sending checkin:', checkIn.monitorSlug, checkIn.status); - void this._sendEnvelope(envelope); + + // _sendEnvelope should not throw + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this._sendEnvelope(envelope); + return id; } diff --git a/packages/core/src/utils/applyScopeDataToEvent.ts b/packages/core/src/utils/applyScopeDataToEvent.ts new file mode 100644 index 000000000000..cc63e7c26cb6 --- /dev/null +++ b/packages/core/src/utils/applyScopeDataToEvent.ts @@ -0,0 +1,97 @@ +import type { Breadcrumb, Event, PropagationContext, ScopeData, Span } from '@sentry/types'; +import { arrayify } from '@sentry/utils'; + +/** + * Applies data from the scope to the event and runs all event processors on it. + */ +export function applyScopeDataToEvent(event: Event, data: ScopeData): void { + const { fingerprint, span, breadcrumbs, sdkProcessingMetadata, propagationContext } = data; + + // Apply general data + applyDataToEvent(event, data); + + // We want to set the trace context for normal events only if there isn't already + // a trace context on the event. There is a product feature in place where we link + // errors with transaction and it relies on that. + if (span) { + applySpanToEvent(event, span); + } + + applyFingerprintToEvent(event, fingerprint); + applyBreadcrumbsToEvent(event, breadcrumbs); + applySdkMetadataToEvent(event, sdkProcessingMetadata, propagationContext); +} + +function applyDataToEvent(event: Event, data: ScopeData): void { + const { extra, tags, user, contexts, level, transactionName } = data; + + if (extra && Object.keys(extra).length) { + event.extra = { ...extra, ...event.extra }; + } + if (tags && Object.keys(tags).length) { + event.tags = { ...tags, ...event.tags }; + } + if (user && Object.keys(user).length) { + event.user = { ...user, ...event.user }; + } + if (contexts && Object.keys(contexts).length) { + event.contexts = { ...contexts, ...event.contexts }; + } + if (level) { + event.level = level; + } + if (transactionName) { + event.transaction = transactionName; + } +} + +function applyBreadcrumbsToEvent(event: Event, breadcrumbs: Breadcrumb[]): void { + const mergedBreadcrumbs = [...(event.breadcrumbs || []), ...breadcrumbs]; + event.breadcrumbs = mergedBreadcrumbs.length ? mergedBreadcrumbs : undefined; +} + +function applySdkMetadataToEvent( + event: Event, + sdkProcessingMetadata: ScopeData['sdkProcessingMetadata'], + propagationContext: PropagationContext, +): void { + event.sdkProcessingMetadata = { + ...event.sdkProcessingMetadata, + ...sdkProcessingMetadata, + propagationContext: propagationContext, + }; +} + +function applySpanToEvent(event: Event, span: Span): void { + event.contexts = { trace: span.getTraceContext(), ...event.contexts }; + const transaction = span.transaction; + if (transaction) { + event.sdkProcessingMetadata = { + dynamicSamplingContext: transaction.getDynamicSamplingContext(), + ...event.sdkProcessingMetadata, + }; + const transactionName = transaction.name; + if (transactionName) { + event.tags = { transaction: transactionName, ...event.tags }; + } + } +} + +/** + * Applies fingerprint from the scope to the event if there's one, + * uses message if there's one instead or get rid of empty fingerprint + */ +function applyFingerprintToEvent(event: Event, fingerprint: ScopeData['fingerprint'] | undefined): void { + // Make sure it's an array first and we actually have something in place + event.fingerprint = event.fingerprint ? arrayify(event.fingerprint) : []; + + // If we have something on the scope, then merge it with event + if (fingerprint) { + event.fingerprint = event.fingerprint.concat(fingerprint); + } + + // If we have no data at all, remove empty array default + if (event.fingerprint && !event.fingerprint.length) { + delete event.fingerprint; + } +} diff --git a/packages/core/src/utils/prepareEvent.ts b/packages/core/src/utils/prepareEvent.ts index 46b26070653f..76307b4f45e6 100644 --- a/packages/core/src/utils/prepareEvent.ts +++ b/packages/core/src/utils/prepareEvent.ts @@ -9,19 +9,12 @@ import type { StackFrame, StackParser, } from '@sentry/types'; -import { - GLOBAL_OBJ, - addExceptionMechanism, - dateTimestampInSeconds, - normalize, - resolvedSyncPromise, - truncate, - uuid4, -} from '@sentry/utils'; +import { GLOBAL_OBJ, addExceptionMechanism, dateTimestampInSeconds, normalize, truncate, uuid4 } from '@sentry/utils'; import { DEFAULT_ENVIRONMENT } from '../constants'; import { getGlobalEventProcessors, notifyEventProcessors } from '../eventProcessors'; import { Scope } from '../scope'; +import { applyScopeDataToEvent } from './applyScopeDataToEvent'; /** * This type makes sure that we get either a CaptureContext, OR an EventHint. @@ -80,10 +73,13 @@ export function prepareEvent( addExceptionMechanism(prepared, hint.mechanism); } - // We prepare the result here with a resolved Event. - let result = resolvedSyncPromise(prepared); - const clientEventProcessors = client && client.getEventProcessors ? client.getEventProcessors() : []; + // TODO (v8): Update this order to be: Global > Client > Scope + const eventProcessors = [ + ...clientEventProcessors, + // eslint-disable-next-line deprecation/deprecation + ...getGlobalEventProcessors(), + ]; // This should be the last thing called, since we want that // {@link Hub.addEventProcessor} gets the finished prepared event. @@ -102,22 +98,15 @@ export function prepareEvent( } } - // In case we have a hub we reassign it. - result = finalScope.applyToEvent(prepared, hint, clientEventProcessors); - } else { - // Apply client & global event processors even if there is no scope - // TODO (v8): Update the order to be Global > Client - result = notifyEventProcessors( - [ - ...clientEventProcessors, - // eslint-disable-next-line deprecation/deprecation - ...getGlobalEventProcessors(), - ], - prepared, - hint, - ); + const scopeData = finalScope.getScopeData(); + applyScopeDataToEvent(prepared, scopeData); + + // Run scope event processors _after_ all other processors + eventProcessors.push(...scopeData.eventProcessors); } + const result = notifyEventProcessors(eventProcessors, prepared, hint); + return result.then(evt => { if (evt) { // We apply the debug_meta field only after all event processors have ran, so that if any event processors modified diff --git a/packages/deno/src/integrations/globalhandlers.ts b/packages/deno/src/integrations/globalhandlers.ts index 27745d6d6765..4160e3f4b3c6 100644 --- a/packages/deno/src/integrations/globalhandlers.ts +++ b/packages/deno/src/integrations/globalhandlers.ts @@ -79,10 +79,16 @@ function installGlobalErrorHandler(client: Client): void { data.preventDefault(); isExiting = true; - void flush().then(() => { - // rethrow to replicate Deno default behavior - throw error; - }); + flush().then( + () => { + // rethrow to replicate Deno default behavior + throw error; + }, + () => { + // rethrow to replicate Deno default behavior + throw error; + }, + ); }); } @@ -122,10 +128,16 @@ function installGlobalUnhandledRejectionHandler(client: Client): void { e.preventDefault(); isExiting = true; - void flush().then(() => { - // rethrow to replicate Deno default behavior - throw error; - }); + flush().then( + () => { + // rethrow to replicate Deno default behavior + throw error; + }, + () => { + // rethrow to replicate Deno default behavior + throw error; + }, + ); }); } diff --git a/packages/e2e-tests/prepare.ts b/packages/e2e-tests/prepare.ts index 7fee7677bf94..9d9c233f580d 100644 --- a/packages/e2e-tests/prepare.ts +++ b/packages/e2e-tests/prepare.ts @@ -21,4 +21,5 @@ async function run(): Promise { } } -void run(); +// eslint-disable-next-line @typescript-eslint/no-floating-promises +run(); diff --git a/packages/e2e-tests/run.ts b/packages/e2e-tests/run.ts index bdc269a867a8..a5002d7bcff9 100644 --- a/packages/e2e-tests/run.ts +++ b/packages/e2e-tests/run.ts @@ -80,4 +80,5 @@ async function run(): Promise { } } -void run(); +// eslint-disable-next-line @typescript-eslint/no-floating-promises +run(); diff --git a/packages/eslint-config-sdk/src/base.js b/packages/eslint-config-sdk/src/base.js index 982d5887c51d..a76af21e7a51 100644 --- a/packages/eslint-config-sdk/src/base.js +++ b/packages/eslint-config-sdk/src/base.js @@ -84,7 +84,7 @@ module.exports = { '@typescript-eslint/no-unused-expressions': ['error', { allowShortCircuit: true }], // Make sure Promises are handled appropriately - '@typescript-eslint/no-floating-promises': 'error', + '@typescript-eslint/no-floating-promises': ['error', { ignoreVoid: false }], // Disallow delete operator. We should make this operation opt in (by disabling this rule). '@typescript-eslint/no-dynamic-delete': 'error', @@ -158,7 +158,17 @@ module.exports = { env: { jest: true, }, - files: ['test.ts', '*.test.ts', '*.test.tsx', '*.test.js', '*.test.jsx', 'test/**/*.ts', 'test/**/*.js'], + files: [ + 'test.ts', + '*.test.ts', + '*.test.tsx', + '*.test.js', + '*.test.jsx', + 'test/**/*.ts', + 'test/**/*.js', + 'tests/**/*.ts', + 'tests/**/*.js', + ], rules: { 'max-lines': 'off', '@typescript-eslint/explicit-function-return-type': 'off', @@ -171,6 +181,7 @@ module.exports = { '@typescript-eslint/no-empty-function': 'off', '@sentry-internal/sdk/no-optional-chaining': 'off', '@sentry-internal/sdk/no-nullish-coalescing': 'off', + '@typescript-eslint/no-floating-promises': 'off', }, }, { diff --git a/packages/feedback/test/unit/util/prepareFeedbackEvent.test.ts b/packages/feedback/test/unit/util/prepareFeedbackEvent.test.ts index 0958b164e34d..46311f1f0374 100644 --- a/packages/feedback/test/unit/util/prepareFeedbackEvent.test.ts +++ b/packages/feedback/test/unit/util/prepareFeedbackEvent.test.ts @@ -16,7 +16,7 @@ describe('Unit | util | prepareFeedbackEvent', () => { hub.bindClient(client); client = hub.getClient()!; - scope = hub.getScope()!; + scope = hub.getScope(); }); afterEach(() => { diff --git a/packages/integrations/scripts/buildBundles.ts b/packages/integrations/scripts/buildBundles.ts index 98a2a3456300..c3c61ed66ef3 100644 --- a/packages/integrations/scripts/buildBundles.ts +++ b/packages/integrations/scripts/buildBundles.ts @@ -47,7 +47,8 @@ if (runParallel) { process.exit(1); }); } else { - void (async () => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + (async () => { for (const integration of getIntegrations()) { await buildBundle(integration, 'es5'); await buildBundle(integration, 'es6'); diff --git a/packages/integrations/src/captureconsole.ts b/packages/integrations/src/captureconsole.ts index 8d7d2540bc2c..a1792573c9b1 100644 --- a/packages/integrations/src/captureconsole.ts +++ b/packages/integrations/src/captureconsole.ts @@ -1,4 +1,5 @@ -import type { EventProcessor, Hub, Integration } from '@sentry/types'; +import { captureException, captureMessage, getClient, withScope } from '@sentry/core'; +import type { CaptureContext, Client, EventProcessor, Hub, Integration } from '@sentry/types'; import { CONSOLE_LEVELS, GLOBAL_OBJ, @@ -36,7 +37,12 @@ export class CaptureConsole implements Integration { /** * @inheritDoc */ - public setupOnce(_: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { + public setupOnce(_: (callback: EventProcessor) => void, _getCurrentHub: () => Hub): void { + // noop + } + + /** @inheritdoc */ + public setup(client: Client): void { if (!('console' in GLOBAL_OBJ)) { return; } @@ -44,25 +50,24 @@ export class CaptureConsole implements Integration { const levels = this._levels; addConsoleInstrumentationHandler(({ args, level }) => { - if (!levels.includes(level)) { + if (getClient() !== client || !levels.includes(level)) { return; } - const hub = getCurrentHub(); - - if (!hub.getIntegration(CaptureConsole)) { - return; - } - - consoleHandler(hub, args, level); + consoleHandler(args, level); }); } } -function consoleHandler(hub: Hub, args: unknown[], level: string): void { - hub.withScope(scope => { - scope.setLevel(severityLevelFromString(level)); - scope.setExtra('arguments', args); +function consoleHandler(args: unknown[], level: string): void { + const captureContext: CaptureContext = { + level: severityLevelFromString(level), + extra: { + arguments: args, + }, + }; + + withScope(scope => { scope.addEventProcessor(event => { event.logger = 'console'; @@ -74,18 +79,20 @@ function consoleHandler(hub: Hub, args: unknown[], level: string): void { return event; }); - let message = safeJoin(args, ' '); + if (level === 'assert' && args[0] === false) { + const message = `Assertion failed: ${safeJoin(args.slice(1), ' ') || 'console.assert'}`; + scope.setExtra('arguments', args.slice(1)); + captureMessage(message, captureContext); + return; + } + const error = args.find(arg => arg instanceof Error); - if (level === 'assert') { - if (args[0] === false) { - message = `Assertion failed: ${safeJoin(args.slice(1), ' ') || 'console.assert'}`; - scope.setExtra('arguments', args.slice(1)); - hub.captureMessage(message); - } - } else if (level === 'error' && error) { - hub.captureException(error); - } else { - hub.captureMessage(message); + if (level === 'error' && error) { + captureException(error, captureContext); + return; } + + const message = safeJoin(args, ' '); + captureMessage(message, captureContext); }); } diff --git a/packages/integrations/src/debug.ts b/packages/integrations/src/debug.ts index 1c2459636510..bb8ed8924254 100644 --- a/packages/integrations/src/debug.ts +++ b/packages/integrations/src/debug.ts @@ -1,4 +1,4 @@ -import type { Event, EventHint, EventProcessor, Hub, Integration } from '@sentry/types'; +import type { Client, Event, EventHint, EventProcessor, Hub, Integration } from '@sentry/types'; import { consoleSandbox } from '@sentry/utils'; interface DebugOptions { @@ -38,32 +38,40 @@ export class Debug implements Integration { /** * @inheritDoc */ - public setupOnce(_addGlobalEventProcessor: (eventProcessor: EventProcessor) => void, getCurrentHub: () => Hub): void { - const client = getCurrentHub().getClient(); + public setupOnce( + _addGlobalEventProcessor: (eventProcessor: EventProcessor) => void, + _getCurrentHub: () => Hub, + ): void { + // noop + } - if (client && client.on) { - client.on('beforeSendEvent', (event: Event, hint?: EventHint) => { - if (this._options.debugger) { - // eslint-disable-next-line no-debugger - debugger; - } + /** @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; + } - /* 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); - } + /* 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)); } - }); - /* eslint-enable no-console */ + } else { + console.log(event); + if (hint && Object.keys(hint).length) { + console.log(hint); + } + } }); - } + /* eslint-enable no-console */ + }); } } diff --git a/packages/integrations/src/httpclient.ts b/packages/integrations/src/httpclient.ts index c03cd63e6840..bcb4429b7aeb 100644 --- a/packages/integrations/src/httpclient.ts +++ b/packages/integrations/src/httpclient.ts @@ -1,5 +1,6 @@ -import { getClient, isSentryRequestUrl } from '@sentry/core'; +import { captureEvent, getClient, isSentryRequestUrl } from '@sentry/core'; import type { + Client, Event as SentryEvent, EventProcessor, Hub, @@ -20,6 +21,7 @@ import { DEBUG_BUILD } from './debug-build'; export type HttpStatusCodeRange = [number, number] | number; export type HttpRequestTarget = string | RegExp; + interface HttpClientOptions { /** * HTTP status codes that should be considered failed. @@ -55,11 +57,6 @@ export class HttpClient implements Integration { private readonly _options: HttpClientOptions; - /** - * Returns current hub. - */ - private _getCurrentHub?: () => Hub; - /** * @inheritDoc * @@ -79,10 +76,14 @@ export class HttpClient implements Integration { * * @param options */ - public setupOnce(_: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { - this._getCurrentHub = getCurrentHub; - this._wrapFetch(); - this._wrapXHR(); + public setupOnce(_: (callback: EventProcessor) => void, _getCurrentHub: () => Hub): void { + // noop + } + + /** @inheritdoc */ + public setup(client: Client): void { + this._wrapFetch(client); + this._wrapXHR(client); } /** @@ -93,13 +94,12 @@ export class HttpClient implements Integration { * @param requestInit The request init object */ private _fetchResponseHandler(requestInfo: RequestInfo, response: Response, requestInit?: RequestInit): void { - if (this._getCurrentHub && this._shouldCaptureResponse(response.status, response.url)) { + if (this._shouldCaptureResponse(response.status, response.url)) { const request = _getRequest(requestInfo, requestInit); - const hub = this._getCurrentHub(); let requestHeaders, responseHeaders, requestCookies, responseCookies; - if (hub.shouldSendDefaultPii()) { + if (_shouldSendDefaultPii()) { [{ headers: requestHeaders, cookies: requestCookies }, { headers: responseHeaders, cookies: responseCookies }] = [ { cookieHeader: 'Cookie', obj: request }, @@ -135,7 +135,7 @@ export class HttpClient implements Integration { responseCookies, }); - hub.captureEvent(event); + captureEvent(event); } } @@ -147,11 +147,10 @@ export class HttpClient implements Integration { * @param headers The HTTP headers */ private _xhrResponseHandler(xhr: XMLHttpRequest, method: string, headers: Record): void { - if (this._getCurrentHub && this._shouldCaptureResponse(xhr.status, xhr.responseURL)) { + if (this._shouldCaptureResponse(xhr.status, xhr.responseURL)) { let requestHeaders, responseCookies, responseHeaders; - const hub = this._getCurrentHub(); - if (hub.shouldSendDefaultPii()) { + if (_shouldSendDefaultPii()) { try { const cookieString = xhr.getResponseHeader('Set-Cookie') || xhr.getResponseHeader('set-cookie') || undefined; @@ -181,7 +180,7 @@ export class HttpClient implements Integration { responseCookies, }); - hub.captureEvent(event); + captureEvent(event); } } @@ -296,12 +295,16 @@ export class HttpClient implements Integration { /** * Wraps `fetch` function to capture request and response data */ - private _wrapFetch(): void { + private _wrapFetch(client: Client): void { if (!supportsNativeFetch()) { return; } addFetchInstrumentationHandler(handlerData => { + if (getClient() !== client) { + return; + } + const { response, args } = handlerData; const [requestInfo, requestInit] = args as [RequestInfo, RequestInit | undefined]; @@ -316,12 +319,16 @@ export class HttpClient implements Integration { /** * Wraps XMLHttpRequest to capture request and response data */ - private _wrapXHR(): void { + private _wrapXHR(client: Client): void { if (!('XMLHttpRequest' in GLOBAL_OBJ)) { return; } addXhrInstrumentationHandler(handlerData => { + if (getClient() !== client) { + return; + } + const xhr = handlerData.xhr as SentryWrappedXMLHttpRequest & XMLHttpRequest; const sentryXhrData = xhr[SENTRY_XHR_DATA_KEY]; @@ -418,3 +425,8 @@ function _getRequest(requestInfo: RequestInfo, requestInit?: RequestInit): Reque return new Request(requestInfo, requestInit); } + +function _shouldSendDefaultPii(): boolean { + const client = getClient(); + return client ? Boolean(client.getOptions().sendDefaultPii) : false; +} diff --git a/packages/integrations/src/reportingobserver.ts b/packages/integrations/src/reportingobserver.ts index 65d3996963b1..dbcae7f014e2 100644 --- a/packages/integrations/src/reportingobserver.ts +++ b/packages/integrations/src/reportingobserver.ts @@ -1,4 +1,5 @@ -import type { EventProcessor, Hub, Integration } from '@sentry/types'; +import { captureMessage, getClient, withScope } from '@sentry/core'; +import type { Client, EventProcessor, Hub, Integration } from '@sentry/types'; import { GLOBAL_OBJ, supportsReportingObserver } from '@sentry/utils'; const WINDOW = GLOBAL_OBJ as typeof GLOBAL_OBJ & Window; @@ -39,6 +40,8 @@ interface InterventionReportBody { columnNumber?: number; } +const SETUP_CLIENTS: Client[] = []; + /** Reporting API integration - https://w3c.github.io/reporting/ */ export class ReportingObserver implements Integration { /** @@ -51,11 +54,6 @@ export class ReportingObserver implements Integration { */ public readonly name: string; - /** - * Returns current hub. - */ - private _getCurrentHub?: () => Hub; - private readonly _types: ReportTypes[]; /** @@ -74,13 +72,11 @@ export class ReportingObserver implements Integration { /** * @inheritDoc */ - public setupOnce(_: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { + public setupOnce(_: (callback: EventProcessor) => void, _getCurrentHub: () => Hub): void { if (!supportsReportingObserver()) { return; } - this._getCurrentHub = getCurrentHub; - // 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, @@ -91,16 +87,25 @@ export class ReportingObserver implements Integration { observer.observe(); } + /** @inheritdoc */ + public setup(client: Client): void { + if (!supportsReportingObserver()) { + return; + } + + SETUP_CLIENTS.push(client); + } + /** * @inheritDoc */ public handler(reports: Report[]): void { - const hub = this._getCurrentHub && this._getCurrentHub(); - if (!hub || !hub.getIntegration(ReportingObserver)) { + if (!SETUP_CLIENTS.includes(getClient() as Client)) { return; } + for (const report of reports) { - hub.withScope(scope => { + withScope(scope => { scope.setExtra('url', report.url); const label = `ReportingObserver [${report.type}]`; @@ -129,7 +134,7 @@ export class ReportingObserver implements Integration { } } - hub.captureMessage(`${label}: ${details}`); + captureMessage(`${label}: ${details}`); }); } } diff --git a/packages/integrations/test/captureconsole.test.ts b/packages/integrations/test/captureconsole.test.ts index 49770f0adf09..23a410f0bb33 100644 --- a/packages/integrations/test/captureconsole.test.ts +++ b/packages/integrations/test/captureconsole.test.ts @@ -1,5 +1,7 @@ /* eslint-disable @typescript-eslint/unbound-method */ -import type { ConsoleLevel, Event, Hub, Integration } from '@sentry/types'; + +import * as SentryCore from '@sentry/core'; +import type { Client, ConsoleLevel, Event } from '@sentry/types'; import { CONSOLE_LEVELS, GLOBAL_OBJ, @@ -20,34 +22,32 @@ const mockConsole: { [key in ConsoleLevel]: jest.Mock } = { trace: jest.fn(), }; -function getMockHub(integration: Integration): Hub { +describe('CaptureConsole setup', () => { + // Ensure we've initialized the instrumentation so we can get the original one + addConsoleInstrumentationHandler(() => {}); + const _originalConsoleMethods = Object.assign({}, originalConsoleMethods); + + let mockClient: Client; + const mockScope = { - setLevel: jest.fn(), setExtra: jest.fn(), addEventProcessor: jest.fn(), }; - const mockHub = { - withScope: jest.fn(callback => { - callback(mockScope); - }), - captureMessage: jest.fn(), - captureException: jest.fn(), - getScope: jest.fn(() => mockScope), - }; + const captureMessage = jest.fn(); + const captureException = jest.fn(); + const withScope = jest.fn(callback => { + return callback(mockScope); + }); - return { - ...mockHub, - getIntegration: jest.fn(() => integration), - } as unknown as Hub; -} + beforeEach(() => { + mockClient = {} as Client; -describe('CaptureConsole setup', () => { - // Ensure we've initialized the instrumentation so we can get the original one - addConsoleInstrumentationHandler(() => {}); - const _originalConsoleMethods = Object.assign({}, originalConsoleMethods); + jest.spyOn(SentryCore, 'captureMessage').mockImplementation(captureMessage); + jest.spyOn(SentryCore, 'captureException').mockImplementation(captureException); + jest.spyOn(SentryCore, 'getClient').mockImplementation(() => mockClient); + jest.spyOn(SentryCore, 'withScope').mockImplementation(withScope); - beforeEach(() => { CONSOLE_LEVELS.forEach(key => { originalConsoleMethods[key] = mockConsole[key]; }); @@ -66,26 +66,18 @@ describe('CaptureConsole setup', () => { describe('monkeypatching', () => { it('should patch user-configured console levels', () => { const captureConsoleIntegration = new CaptureConsole({ levels: ['log', 'warn'] }); - const mockHub = getMockHub(captureConsoleIntegration); - captureConsoleIntegration.setupOnce( - () => undefined, - () => mockHub, - ); + captureConsoleIntegration.setup(mockClient); GLOBAL_OBJ.console.error('msg 1'); GLOBAL_OBJ.console.log('msg 2'); GLOBAL_OBJ.console.warn('msg 3'); - expect(mockHub.captureMessage).toHaveBeenCalledTimes(2); + expect(captureMessage).toHaveBeenCalledTimes(2); }); it('should fall back to default console levels if none are provided', () => { const captureConsoleIntegration = new CaptureConsole(); - const mockHub = getMockHub(captureConsoleIntegration); - captureConsoleIntegration.setupOnce( - () => undefined, - () => mockHub, - ); + captureConsoleIntegration.setup(mockClient); // Assert has a special handling (['debug', 'info', 'warn', 'error', 'log', 'trace'] as const).forEach(key => { @@ -94,22 +86,18 @@ describe('CaptureConsole setup', () => { GLOBAL_OBJ.console.assert(false); - expect(mockHub.captureMessage).toHaveBeenCalledTimes(7); + expect(captureMessage).toHaveBeenCalledTimes(7); }); it('should not wrap any functions with an empty levels option', () => { const captureConsoleIntegration = new CaptureConsole({ levels: [] }); - const mockHub = getMockHub(captureConsoleIntegration); - captureConsoleIntegration.setupOnce( - () => undefined, - () => mockHub, - ); + captureConsoleIntegration.setup(mockClient); CONSOLE_LEVELS.forEach(key => { GLOBAL_OBJ.console[key]('msg'); }); - expect(mockHub.captureMessage).toHaveBeenCalledTimes(0); + expect(captureMessage).toHaveBeenCalledTimes(0); }); }); @@ -119,76 +107,27 @@ describe('CaptureConsole setup', () => { delete GLOBAL_OBJ.console; const captureConsoleIntegration = new CaptureConsole(); - const mockHub = getMockHub(captureConsoleIntegration); expect(() => { - captureConsoleIntegration.setupOnce( - () => undefined, - () => mockHub, - ); + captureConsoleIntegration.setup(mockClient); }).not.toThrow(); // reinstate initial console GLOBAL_OBJ.console = consoleRef; }); - it('should set a level in the scope when console function is called', () => { - const captureConsoleIntegration = new CaptureConsole({ levels: ['error'] }); - const mockHub = getMockHub(captureConsoleIntegration); - captureConsoleIntegration.setupOnce( - () => undefined, - () => mockHub, - ); - - const mockScope = mockHub.getScope(); - - // call a wrapped function - GLOBAL_OBJ.console.error('some logging message'); - - expect(mockScope.setLevel).toHaveBeenCalledTimes(1); - expect(mockScope.setLevel).toHaveBeenCalledWith('error'); - }); - - it('should send arguments as extra data', () => { - const captureConsoleIntegration = new CaptureConsole({ levels: ['log'] }); - const mockHub = getMockHub(captureConsoleIntegration); - captureConsoleIntegration.setupOnce( - () => undefined, - () => mockHub, - ); - - const mockScope = mockHub.getScope(); - - GLOBAL_OBJ.console.log('some arg 1', 'some arg 2'); - - expect(mockScope.setExtra).toHaveBeenCalledTimes(1); - expect(mockScope.setExtra).toHaveBeenCalledWith('arguments', ['some arg 1', 'some arg 2']); - }); - it('should send empty arguments as extra data', () => { const captureConsoleIntegration = new CaptureConsole({ levels: ['log'] }); - const mockHub = getMockHub(captureConsoleIntegration); - captureConsoleIntegration.setupOnce( - () => undefined, - () => mockHub, - ); - - const mockScope = mockHub.getScope(); + captureConsoleIntegration.setup(mockClient); GLOBAL_OBJ.console.log(); - expect(mockScope.setExtra).toHaveBeenCalledTimes(1); - expect(mockScope.setExtra).toHaveBeenCalledWith('arguments', []); + expect(captureMessage).toHaveBeenCalledTimes(1); + expect(captureMessage).toHaveBeenCalledWith('', { extra: { arguments: [] }, level: 'log' }); }); it('should add an event processor that sets the `logger` field of events', () => { const captureConsoleIntegration = new CaptureConsole({ levels: ['log'] }); - const mockHub = getMockHub(captureConsoleIntegration); - captureConsoleIntegration.setupOnce( - () => undefined, - () => mockHub, - ); - - const mockScope = mockHub.getScope(); + captureConsoleIntegration.setup(mockClient); // call a wrapped function GLOBAL_OBJ.console.log('some message'); @@ -204,135 +143,119 @@ describe('CaptureConsole setup', () => { it('should capture message on a failed assertion', () => { const captureConsoleIntegration = new CaptureConsole({ levels: ['assert'] }); - const mockHub = getMockHub(captureConsoleIntegration); - captureConsoleIntegration.setupOnce( - () => undefined, - () => mockHub, - ); - - const mockScope = mockHub.getScope(); + captureConsoleIntegration.setup(mockClient); GLOBAL_OBJ.console.assert(1 + 1 === 3); expect(mockScope.setExtra).toHaveBeenLastCalledWith('arguments', []); - expect(mockHub.captureMessage).toHaveBeenCalledTimes(1); - expect(mockHub.captureMessage).toHaveBeenCalledWith('Assertion failed: console.assert'); + expect(captureMessage).toHaveBeenCalledTimes(1); + expect(captureMessage).toHaveBeenCalledWith('Assertion failed: console.assert', { + extra: { arguments: [false] }, + level: 'log', + }); }); it('should capture correct message on a failed assertion with message', () => { const captureConsoleIntegration = new CaptureConsole({ levels: ['assert'] }); - const mockHub = getMockHub(captureConsoleIntegration); - captureConsoleIntegration.setupOnce( - () => undefined, - () => mockHub, - ); - - const mockScope = mockHub.getScope(); + captureConsoleIntegration.setup(mockClient); GLOBAL_OBJ.console.assert(1 + 1 === 3, 'expression is false'); expect(mockScope.setExtra).toHaveBeenLastCalledWith('arguments', ['expression is false']); - expect(mockHub.captureMessage).toHaveBeenCalledTimes(1); - expect(mockHub.captureMessage).toHaveBeenCalledWith('Assertion failed: expression is false'); + expect(captureMessage).toHaveBeenCalledTimes(1); + expect(captureMessage).toHaveBeenCalledWith('Assertion failed: expression is false', { + extra: { arguments: [false, 'expression is false'] }, + level: 'log', + }); }); it('should not capture message on a successful assertion', () => { const captureConsoleIntegration = new CaptureConsole({ levels: ['assert'] }); - const mockHub = getMockHub(captureConsoleIntegration); - captureConsoleIntegration.setupOnce( - () => undefined, - () => mockHub, - ); + captureConsoleIntegration.setup(mockClient); GLOBAL_OBJ.console.assert(1 + 1 === 2); }); it('should capture exception when console logs an error object with level set to "error"', () => { const captureConsoleIntegration = new CaptureConsole({ levels: ['error'] }); - const mockHub = getMockHub(captureConsoleIntegration); - captureConsoleIntegration.setupOnce( - () => undefined, - () => mockHub, - ); + captureConsoleIntegration.setup(mockClient); const someError = new Error('some error'); GLOBAL_OBJ.console.error(someError); - expect(mockHub.captureException).toHaveBeenCalledTimes(1); - expect(mockHub.captureException).toHaveBeenCalledWith(someError); + expect(captureException).toHaveBeenCalledTimes(1); + expect(captureException).toHaveBeenCalledWith(someError, { + extra: { arguments: [someError] }, + level: 'error', + }); }); it('should capture exception on `console.error` when no levels are provided in constructor', () => { const captureConsoleIntegration = new CaptureConsole(); - const mockHub = getMockHub(captureConsoleIntegration); - captureConsoleIntegration.setupOnce( - () => undefined, - () => mockHub, - ); + captureConsoleIntegration.setup(mockClient); const someError = new Error('some error'); GLOBAL_OBJ.console.error(someError); - expect(mockHub.captureException).toHaveBeenCalledTimes(1); - expect(mockHub.captureException).toHaveBeenCalledWith(someError); + expect(captureException).toHaveBeenCalledTimes(1); + expect(captureException).toHaveBeenCalledWith(someError, { + extra: { arguments: [someError] }, + level: 'error', + }); }); it('should capture exception when console logs an error object in any of the args when level set to "error"', () => { const captureConsoleIntegration = new CaptureConsole({ levels: ['error'] }); - const mockHub = getMockHub(captureConsoleIntegration); - captureConsoleIntegration.setupOnce( - () => undefined, - () => mockHub, - ); + captureConsoleIntegration.setup(mockClient); const someError = new Error('some error'); GLOBAL_OBJ.console.error('Something went wrong', someError); - expect(mockHub.captureException).toHaveBeenCalledTimes(1); - expect(mockHub.captureException).toHaveBeenCalledWith(someError); + expect(captureException).toHaveBeenCalledTimes(1); + expect(captureException).toHaveBeenCalledWith(someError, { + extra: { arguments: ['Something went wrong', someError] }, + level: 'error', + }); }); it('should capture message on `console.log` when no levels are provided in constructor', () => { const captureConsoleIntegration = new CaptureConsole(); - const mockHub = getMockHub(captureConsoleIntegration); - captureConsoleIntegration.setupOnce( - () => undefined, - () => mockHub, - ); + captureConsoleIntegration.setup(mockClient); GLOBAL_OBJ.console.error('some message'); - expect(mockHub.captureMessage).toHaveBeenCalledTimes(1); - expect(mockHub.captureMessage).toHaveBeenCalledWith('some message'); + expect(captureMessage).toHaveBeenCalledTimes(1); + expect(captureMessage).toHaveBeenCalledWith('some message', { + extra: { arguments: ['some message'] }, + level: 'error', + }); }); it('should capture message when console logs a non-error object with level set to "error"', () => { const captureConsoleIntegration = new CaptureConsole({ levels: ['error'] }); - const mockHub = getMockHub(captureConsoleIntegration); - captureConsoleIntegration.setupOnce( - () => undefined, - () => mockHub, - ); + captureConsoleIntegration.setup(mockClient); GLOBAL_OBJ.console.error('some non-error message'); - expect(mockHub.captureMessage).toHaveBeenCalledTimes(1); - expect(mockHub.captureMessage).toHaveBeenCalledWith('some non-error message'); - expect(mockHub.captureException).not.toHaveBeenCalled(); + expect(captureMessage).toHaveBeenCalledTimes(1); + expect(captureMessage).toHaveBeenCalledWith('some non-error message', { + extra: { arguments: ['some non-error message'] }, + level: 'error', + }); + expect(captureException).not.toHaveBeenCalled(); }); it('should capture a message for non-error log levels', () => { const captureConsoleIntegration = new CaptureConsole({ levels: ['info'] }); - const mockHub = getMockHub(captureConsoleIntegration); - captureConsoleIntegration.setupOnce( - () => undefined, - () => mockHub, - ); + captureConsoleIntegration.setup(mockClient); GLOBAL_OBJ.console.info('some message'); - expect(mockHub.captureMessage).toHaveBeenCalledTimes(1); - expect(mockHub.captureMessage).toHaveBeenCalledWith('some message'); + expect(captureMessage).toHaveBeenCalledTimes(1); + expect(captureMessage).toHaveBeenCalledWith('some message', { + extra: { arguments: ['some message'] }, + level: 'info', + }); }); it('should call the original console function when console members are called', () => { @@ -342,11 +265,7 @@ describe('CaptureConsole setup', () => { GLOBAL_OBJ.console.log = mockConsoleLog; const captureConsoleIntegration = new CaptureConsole({ levels: ['log'] }); - const mockHub = getMockHub(captureConsoleIntegration); - captureConsoleIntegration.setupOnce( - () => undefined, - () => mockHub, - ); + captureConsoleIntegration.setup(mockClient); GLOBAL_OBJ.console.log('some message 1', 'some message 2'); @@ -359,11 +278,7 @@ describe('CaptureConsole setup', () => { it('should not wrap any levels that are not members of console', () => { const captureConsoleIntegration = new CaptureConsole({ levels: ['log', 'someNonExistingLevel', 'error'] }); - const mockHub = getMockHub(captureConsoleIntegration); - captureConsoleIntegration.setupOnce( - () => undefined, - () => mockHub, - ); + captureConsoleIntegration.setup(mockClient); // The provided level should not be created expect((GLOBAL_OBJ.console as any)['someNonExistingLevel']).toBeUndefined(); @@ -371,26 +286,19 @@ describe('CaptureConsole setup', () => { it('should wrap the console when the client does not have a registered captureconsole integration, but not capture any messages', () => { const captureConsoleIntegration = new CaptureConsole({ levels: ['log', 'error'] }); - const mockHub = getMockHub(null as any); // simulate not having the integration registered - captureConsoleIntegration.setupOnce( - () => undefined, - () => mockHub, - ); + // when `setup` is not called on the current client, it will not trigger + captureConsoleIntegration.setup({} as Client); // Should not capture messages GLOBAL_OBJ.console.log('some message'); - expect(mockHub.captureMessage).not.toHaveBeenCalledWith(); + expect(captureMessage).not.toHaveBeenCalledWith(); }); it("should not crash when the original console methods don't exist at time of invocation", () => { originalConsoleMethods.log = undefined; const captureConsoleIntegration = new CaptureConsole({ levels: ['log'] }); - const mockHub = getMockHub(captureConsoleIntegration); - captureConsoleIntegration.setupOnce( - () => undefined, - () => mockHub, - ); + captureConsoleIntegration.setup(mockClient); expect(() => { GLOBAL_OBJ.console.log('some message'); @@ -401,13 +309,7 @@ describe('CaptureConsole setup', () => { // const addExceptionMechanismSpy = jest.spyOn(utils, 'addExceptionMechanism'); const captureConsoleIntegration = new CaptureConsole({ levels: ['error'] }); - const mockHub = getMockHub(captureConsoleIntegration); - captureConsoleIntegration.setupOnce( - () => undefined, - () => mockHub, - ); - - const mockScope = mockHub.getScope(); + captureConsoleIntegration.setup(mockClient); const someError = new Error('some error'); GLOBAL_OBJ.console.error(someError); @@ -420,7 +322,7 @@ describe('CaptureConsole setup', () => { }; addedEventProcessor(someEvent); - expect(mockHub.captureException).toHaveBeenCalledTimes(1); + expect(captureException).toHaveBeenCalledTimes(1); expect(mockScope.addEventProcessor).toHaveBeenCalledTimes(1); expect(someEvent.exception?.values?.[0].mechanism).toEqual({ diff --git a/packages/integrations/test/debug.test.ts b/packages/integrations/test/debug.test.ts index 953fcdb0258e..eefd9c8b9240 100644 --- a/packages/integrations/test/debug.test.ts +++ b/packages/integrations/test/debug.test.ts @@ -2,7 +2,7 @@ import type { Client, Event, EventHint, Hub, Integration } from '@sentry/types'; import { Debug } from '../src/debug'; -function testEventLogged(integration: Integration, testEvent?: Event, testEventHint?: EventHint) { +function testEventLogged(integration: Debug, testEvent?: Event, testEventHint?: EventHint) { const callbacks: ((event: Event, hint?: EventHint) => void)[] = []; const client: Client = { @@ -12,15 +12,7 @@ function testEventLogged(integration: Integration, testEvent?: Event, testEventH }, } as Client; - function getCurrentHub() { - return { - getClient: jest.fn(() => { - return client; - }), - } as unknown as Hub; - } - - integration.setupOnce(() => {}, getCurrentHub); + integration.setup(client); expect(callbacks.length).toEqual(1); diff --git a/packages/integrations/test/reportingobserver.test.ts b/packages/integrations/test/reportingobserver.test.ts index 1a8759fda23e..6378a456c854 100644 --- a/packages/integrations/test/reportingobserver.test.ts +++ b/packages/integrations/test/reportingobserver.test.ts @@ -1,4 +1,5 @@ -import type { Hub, Integration } from '@sentry/types'; +import * as SentryCore from '@sentry/core'; +import type { Client, Hub } from '@sentry/types'; import { ReportingObserver } from '../src/reportingobserver'; @@ -6,18 +7,13 @@ const mockScope = { setExtra: jest.fn(), }; -const mockHub = { - withScope: jest.fn(callback => { - callback(mockScope); - }), - captureMessage: jest.fn(), -}; +const withScope = jest.fn(callback => { + return callback(mockScope); +}); + +const captureMessage = jest.fn(); -const getMockHubWithIntegration = (integration: Integration) => - ({ - ...mockHub, - getIntegration: jest.fn(() => integration), - }) as unknown as Hub; +const mockHub = {} as unknown as Hub; const mockReportingObserverConstructor = jest.fn(); const mockObserve = jest.fn(); @@ -31,8 +27,16 @@ class MockReportingObserver { } describe('ReportingObserver', () => { + let mockClient: Client; + beforeEach(() => { (global as any).ReportingObserver = MockReportingObserver; + + mockClient = {} as Client; + + jest.spyOn(SentryCore, 'captureMessage').mockImplementation(captureMessage); + jest.spyOn(SentryCore, 'getClient').mockImplementation(() => mockClient); + jest.spyOn(SentryCore, 'withScope').mockImplementation(withScope); }); afterEach(() => { @@ -50,7 +54,7 @@ describe('ReportingObserver', () => { expect(() => { reportingObserverIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(null as any), + () => mockHub, ); }).not.toThrow(); @@ -62,8 +66,9 @@ describe('ReportingObserver', () => { const reportingObserverIntegration = new ReportingObserver(); reportingObserverIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(reportingObserverIntegration), + () => mockHub, ); + reportingObserverIntegration.setup(mockClient); expect(mockReportingObserverConstructor).toHaveBeenCalledTimes(1); expect(mockReportingObserverConstructor).toHaveBeenCalledWith( @@ -76,8 +81,9 @@ describe('ReportingObserver', () => { const reportingObserverIntegration = new ReportingObserver({ types: ['crash'] }); reportingObserverIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(reportingObserverIntegration), + () => mockHub, ); + reportingObserverIntegration.setup(mockClient); expect(mockReportingObserverConstructor).toHaveBeenCalledTimes(1); expect(mockReportingObserverConstructor).toHaveBeenCalledWith( @@ -90,8 +96,9 @@ describe('ReportingObserver', () => { const reportingObserverIntegration = new ReportingObserver(); reportingObserverIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(reportingObserverIntegration), + () => mockHub, ); + reportingObserverIntegration.setup(mockClient); expect(mockReportingObserverConstructor).toHaveBeenCalledTimes(1); expect(mockReportingObserverConstructor).toHaveBeenCalledWith( @@ -104,8 +111,9 @@ describe('ReportingObserver', () => { const reportingObserverIntegration = new ReportingObserver(); reportingObserverIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(reportingObserverIntegration), + () => mockHub, ); + reportingObserverIntegration.setup(mockClient); expect(mockObserve).toHaveBeenCalledTimes(1); }); @@ -116,37 +124,40 @@ describe('ReportingObserver', () => { const reportingObserverIntegration = new ReportingObserver(); reportingObserverIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(null as any), + () => mockHub, ); + // without calling setup, the integration is not registered expect(() => { reportingObserverIntegration.handler([{ type: 'crash', url: 'some url' }]); }).not.toThrow(); - expect(mockHub.captureMessage).not.toHaveBeenCalled(); + expect(captureMessage).not.toHaveBeenCalled(); }); it('should capture messages', () => { const reportingObserverIntegration = new ReportingObserver(); reportingObserverIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(reportingObserverIntegration), + () => mockHub, ); + reportingObserverIntegration.setup(mockClient); reportingObserverIntegration.handler([ { type: 'crash', url: 'some url' }, { type: 'deprecation', url: 'some url' }, ]); - expect(mockHub.captureMessage).toHaveBeenCalledTimes(2); + expect(captureMessage).toHaveBeenCalledTimes(2); }); it('should set extra including the url of a report', () => { const reportingObserverIntegration = new ReportingObserver(); reportingObserverIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(reportingObserverIntegration), + () => mockHub, ); + reportingObserverIntegration.setup(mockClient); reportingObserverIntegration.handler([ { type: 'crash', url: 'some url 1' }, @@ -161,8 +172,9 @@ describe('ReportingObserver', () => { const reportingObserverIntegration = new ReportingObserver(); reportingObserverIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(reportingObserverIntegration), + () => mockHub, ); + reportingObserverIntegration.setup(mockClient); 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; @@ -177,8 +189,9 @@ describe('ReportingObserver', () => { const reportingObserverIntegration = new ReportingObserver(); reportingObserverIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(reportingObserverIntegration), + () => mockHub, ); + reportingObserverIntegration.setup(mockClient); reportingObserverIntegration.handler([{ type: 'crash', url: 'some url' }]); @@ -189,8 +202,9 @@ describe('ReportingObserver', () => { const reportingObserverIntegration = new ReportingObserver(); reportingObserverIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(reportingObserverIntegration), + () => mockHub, ); + reportingObserverIntegration.setup(mockClient); const report = { type: 'crash', @@ -199,17 +213,18 @@ describe('ReportingObserver', () => { } as const; reportingObserverIntegration.handler([report]); - expect(mockHub.captureMessage).toHaveBeenCalledWith(expect.stringContaining(report.type)); - expect(mockHub.captureMessage).toHaveBeenCalledWith(expect.stringContaining(report.body.crashId)); - expect(mockHub.captureMessage).toHaveBeenCalledWith(expect.stringContaining(report.body.reason)); + expect(captureMessage).toHaveBeenCalledWith(expect.stringContaining(report.type)); + expect(captureMessage).toHaveBeenCalledWith(expect.stringContaining(report.body.crashId)); + expect(captureMessage).toHaveBeenCalledWith(expect.stringContaining(report.body.reason)); }); it('should capture report message from body on deprecation report', () => { const reportingObserverIntegration = new ReportingObserver(); reportingObserverIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(reportingObserverIntegration), + () => mockHub, ); + reportingObserverIntegration.setup(mockClient); const report = { type: 'deprecation', @@ -218,16 +233,17 @@ describe('ReportingObserver', () => { } as const; reportingObserverIntegration.handler([report]); - expect(mockHub.captureMessage).toHaveBeenCalledWith(expect.stringContaining(report.type)); - expect(mockHub.captureMessage).toHaveBeenCalledWith(expect.stringContaining(report.body.message)); + expect(captureMessage).toHaveBeenCalledWith(expect.stringContaining(report.type)); + expect(captureMessage).toHaveBeenCalledWith(expect.stringContaining(report.body.message)); }); it('should capture report message from body on intervention report', () => { const reportingObserverIntegration = new ReportingObserver(); reportingObserverIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(reportingObserverIntegration), + () => mockHub, ); + reportingObserverIntegration.setup(mockClient); const report = { type: 'intervention', @@ -236,16 +252,17 @@ describe('ReportingObserver', () => { } as const; reportingObserverIntegration.handler([report]); - expect(mockHub.captureMessage).toHaveBeenCalledWith(expect.stringContaining(report.type)); - expect(mockHub.captureMessage).toHaveBeenCalledWith(expect.stringContaining(report.body.message)); + expect(captureMessage).toHaveBeenCalledWith(expect.stringContaining(report.type)); + expect(captureMessage).toHaveBeenCalledWith(expect.stringContaining(report.body.message)); }); it('should use fallback message when no body is available', () => { const reportingObserverIntegration = new ReportingObserver(); reportingObserverIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(reportingObserverIntegration), + () => mockHub, ); + reportingObserverIntegration.setup(mockClient); const report = { type: 'intervention', @@ -253,30 +270,32 @@ describe('ReportingObserver', () => { } as const; reportingObserverIntegration.handler([report]); - expect(mockHub.captureMessage).toHaveBeenCalledWith(expect.stringContaining(report.type)); - expect(mockHub.captureMessage).toHaveBeenCalledWith(expect.stringContaining('No details available')); + expect(captureMessage).toHaveBeenCalledWith(expect.stringContaining(report.type)); + expect(captureMessage).toHaveBeenCalledWith(expect.stringContaining('No details available')); }); it('should use fallback message when no body details are available for crash report', () => { const reportingObserverIntegration = new ReportingObserver(); reportingObserverIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(reportingObserverIntegration), + () => mockHub, ); + reportingObserverIntegration.setup(mockClient); const report = { type: 'crash', url: 'some url', body: { crashId: '', reason: '' } } as const; reportingObserverIntegration.handler([report]); - expect(mockHub.captureMessage).toHaveBeenCalledWith(expect.stringContaining(report.type)); - expect(mockHub.captureMessage).toHaveBeenCalledWith(expect.stringContaining('No details available')); + expect(captureMessage).toHaveBeenCalledWith(expect.stringContaining(report.type)); + expect(captureMessage).toHaveBeenCalledWith(expect.stringContaining('No details available')); }); it('should use fallback message when no body message is available for deprecation report', () => { const reportingObserverIntegration = new ReportingObserver(); reportingObserverIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(reportingObserverIntegration), + () => mockHub, ); + reportingObserverIntegration.setup(mockClient); const report = { type: 'deprecation', @@ -285,16 +304,17 @@ describe('ReportingObserver', () => { } as const; reportingObserverIntegration.handler([report]); - expect(mockHub.captureMessage).toHaveBeenCalledWith(expect.stringContaining(report.type)); - expect(mockHub.captureMessage).toHaveBeenCalledWith(expect.stringContaining('No details available')); + expect(captureMessage).toHaveBeenCalledWith(expect.stringContaining(report.type)); + expect(captureMessage).toHaveBeenCalledWith(expect.stringContaining('No details available')); }); it('should use fallback message when no body message is available for intervention report', () => { const reportingObserverIntegration = new ReportingObserver(); reportingObserverIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(reportingObserverIntegration), + () => mockHub, ); + reportingObserverIntegration.setup(mockClient); const report = { type: 'intervention', @@ -303,8 +323,8 @@ describe('ReportingObserver', () => { } as const; reportingObserverIntegration.handler([report]); - expect(mockHub.captureMessage).toHaveBeenCalledWith(expect.stringContaining(report.type)); - expect(mockHub.captureMessage).toHaveBeenCalledWith(expect.stringContaining('No details available')); + expect(captureMessage).toHaveBeenCalledWith(expect.stringContaining(report.type)); + expect(captureMessage).toHaveBeenCalledWith(expect.stringContaining('No details available')); }); }); }); diff --git a/packages/nextjs/src/common/utils/responseEnd.ts b/packages/nextjs/src/common/utils/responseEnd.ts index 4d0fc40082ec..64a3d2c59c52 100644 --- a/packages/nextjs/src/common/utils/responseEnd.ts +++ b/packages/nextjs/src/common/utils/responseEnd.ts @@ -26,7 +26,7 @@ import type { ResponseEndMethod, WrappedResponseEndMethod } from '../types'; export function autoEndTransactionOnResponseEnd(transaction: Transaction, res: ServerResponse): void { const wrapEndMethod = (origEnd: ResponseEndMethod): WrappedResponseEndMethod => { return function sentryWrappedEnd(this: ServerResponse, ...args: unknown[]) { - void finishTransaction(transaction, this); + finishTransaction(transaction, this); return origEnd.call(this, ...args); }; }; @@ -39,7 +39,7 @@ export function autoEndTransactionOnResponseEnd(transaction: Transaction, res: S } /** Finish the given response's transaction and set HTTP status data */ -export async function finishTransaction(transaction: Transaction | undefined, res: ServerResponse): Promise { +export function finishTransaction(transaction: Transaction | undefined, res: ServerResponse): void { if (transaction) { transaction.setHttpStatus(res.statusCode); transaction.finish(); diff --git a/packages/nextjs/src/common/withServerActionInstrumentation.ts b/packages/nextjs/src/common/withServerActionInstrumentation.ts index d87429ad528c..d850b3e362f9 100644 --- a/packages/nextjs/src/common/withServerActionInstrumentation.ts +++ b/packages/nextjs/src/common/withServerActionInstrumentation.ts @@ -129,7 +129,9 @@ async function withServerActionInstrumentationImplementation any> } }, () => { - void flushQueue(); + // flushQueue should not throw + // eslint-disable-next-line @typescript-eslint/no-floating-promises + flushQueue(); }, ); diff --git a/packages/node-experimental/src/sdk/scope.ts b/packages/node-experimental/src/sdk/scope.ts index f3195c7141b6..5bf220708a6a 100644 --- a/packages/node-experimental/src/sdk/scope.ts +++ b/packages/node-experimental/src/sdk/scope.ts @@ -1,15 +1,5 @@ -import { notifyEventProcessors } from '@sentry/core'; import { OpenTelemetryScope } from '@sentry/opentelemetry'; -import type { - Attachment, - Breadcrumb, - Client, - Event, - EventHint, - EventProcessor, - Severity, - SeverityLevel, -} from '@sentry/types'; +import type { Attachment, Breadcrumb, Client, Event, EventHint, Severity, SeverityLevel } from '@sentry/types'; import { uuid4 } from '@sentry/utils'; import { getGlobalCarrier } from './globals'; @@ -116,13 +106,7 @@ export class Scope extends OpenTelemetryScope implements ScopeInterface { /** @inheritdoc */ public getAttachments(): Attachment[] { - const data = getGlobalScope().getScopeData(); - const isolationScopeData = this._getIsolationScope().getScopeData(); - const scopeData = this.getScopeData(); - - // Merge data together, in order - mergeData(data, isolationScopeData); - mergeData(data, scopeData); + const data = this.getScopeData(); return data.attachments; } @@ -200,7 +184,7 @@ export class Scope extends OpenTelemetryScope implements ScopeInterface { } /** Get all relevant data for this scope. */ - public getScopeData(): ScopeData { + public getPerScopeData(): ScopeData { const { _breadcrumbs, _attachments, @@ -230,63 +214,17 @@ export class Scope extends OpenTelemetryScope implements ScopeInterface { }; } - /** - * Applies data from the scope to the event and runs all event processors on it. - * - * @param event Event - * @param hint Object containing additional information about the original exception, for use by the event processors. - * @hidden - */ - public applyToEvent( - event: Event, - hint: EventHint = {}, - additionalEventProcessors: EventProcessor[] = [], - ): PromiseLike { - const data = getGlobalScope().getScopeData(); - const isolationScopeData = this._getIsolationScope().getScopeData(); - const scopeData = this.getScopeData(); + /** @inheritdoc */ + public getScopeData(): ScopeData { + const data = getGlobalScope().getPerScopeData(); + const isolationScopeData = this._getIsolationScope().getPerScopeData(); + const scopeData = this.getPerScopeData(); // Merge data together, in order mergeData(data, isolationScopeData); mergeData(data, scopeData); - // Apply the data - const { extra, tags, user, contexts, level, sdkProcessingMetadata, breadcrumbs, fingerprint, eventProcessors } = - data; - - mergePropKeep(event, 'extra', extra); - mergePropKeep(event, 'tags', tags); - mergePropKeep(event, 'user', user); - mergePropKeep(event, 'contexts', contexts); - mergePropKeep(event, 'sdkProcessingMetadata', sdkProcessingMetadata); - event.sdkProcessingMetadata = { - ...event.sdkProcessingMetadata, - propagationContext: this._propagationContext, - }; - - mergeArray(event, 'breadcrumbs', breadcrumbs); - mergeArray(event, 'fingerprint', fingerprint); - - if (level) { - event.level = level; - } - - const allEventProcessors = [...additionalEventProcessors, ...eventProcessors]; - - // Apply additional things to the event - if (this._transactionName) { - event.transaction = this._transactionName; - } - - return notifyEventProcessors(allEventProcessors, event, hint); - } - - /** - * Get all breadcrumbs attached to this scope. - * @internal - */ - public getBreadcrumbs(): Breadcrumb[] { - return this._breadcrumbs; + return data; } /** Get the isolation scope for this scope. */ @@ -308,6 +246,7 @@ export function mergeData(data: ScopeData, mergeData: ScopeData): void { fingerprint, eventProcessors, attachments, + propagationContext, } = mergeData; mergePropOverwrite(data, 'extra', extra); @@ -335,6 +274,8 @@ export function mergeData(data: ScopeData, mergeData: ScopeData): void { if (attachments.length) { data.attachments = [...data.attachments, ...attachments]; } + + data.propagationContext = { ...data.propagationContext, ...propagationContext }; } /** diff --git a/packages/node-experimental/test/sdk/scope.test.ts b/packages/node-experimental/test/sdk/scope.test.ts index a0e179373626..9fb5c02bea69 100644 --- a/packages/node-experimental/test/sdk/scope.test.ts +++ b/packages/node-experimental/test/sdk/scope.test.ts @@ -1,3 +1,4 @@ +import { applyScopeDataToEvent } from '@sentry/core'; import type { Attachment, Breadcrumb, Client, EventProcessor } from '@sentry/types'; import { Scope, getIsolationScope } from '../../src'; import { getGlobalScope, mergeArray, mergeData, mergePropKeep, mergePropOverwrite } from '../../src/sdk/scope'; @@ -295,8 +296,7 @@ describe('Unit | Scope', () => { extra: { extra1: 'aa', extra2: 'bb', extra3: 'bb' }, contexts: { os: { name: 'os2' }, culture: { display_name: 'name1' } }, attachments: [attachment1, attachment2, attachment3], - // This is not merged, we always use the one from the scope here anyhow - propagationContext: { spanId: '1', traceId: '1' }, + propagationContext: { spanId: '2', traceId: '2' }, sdkProcessingMetadata: { aa: 'aa', bb: 'bb', cc: 'bb' }, fingerprint: ['aa', 'bb', 'cc'], }); @@ -309,7 +309,8 @@ describe('Unit | Scope', () => { const scope = new Scope(); - const event = await scope.applyToEvent({ message: 'foo' }); + const event = { message: 'foo' }; + applyScopeDataToEvent(event, scope.getScopeData()); expect(event).toEqual({ message: 'foo', @@ -357,11 +358,9 @@ describe('Unit | Scope', () => { isolationScope.addEventProcessor(eventProcessor3); globalScope.setSDKProcessingMetadata({ bb: 'bb' }); - const event = await scope.applyToEvent({ - message: 'foo', - breadcrumbs: [breadcrumb4], - fingerprint: ['dd'], - }); + const event = { message: 'foo', breadcrumbs: [breadcrumb4], fingerprint: ['dd'] }; + + applyScopeDataToEvent(event, scope.getScopeData()); expect(event).toEqual({ message: 'foo', diff --git a/packages/node-integration-tests/suites/tracing-new/apollo-graphql/scenario.ts b/packages/node-integration-tests/suites/tracing-new/apollo-graphql/scenario.ts index 7fdbfce0351c..0c53294d1f4f 100644 --- a/packages/node-integration-tests/suites/tracing-new/apollo-graphql/scenario.ts +++ b/packages/node-integration-tests/suites/tracing-new/apollo-graphql/scenario.ts @@ -31,7 +31,8 @@ const transaction = Sentry.startTransaction({ name: 'test_transaction', op: 'tra Sentry.getCurrentScope().setSpan(transaction); -void (async () => { +// eslint-disable-next-line @typescript-eslint/no-floating-promises +(async () => { // Ref: https://www.apollographql.com/docs/apollo-server/testing/testing/#testing-using-executeoperation await server.executeOperation({ query: '{hello}', diff --git a/packages/node-integration-tests/suites/tracing-new/auto-instrument/mongodb/scenario.ts b/packages/node-integration-tests/suites/tracing-new/auto-instrument/mongodb/scenario.ts index cae4627e7096..9d747e2eff4b 100644 --- a/packages/node-integration-tests/suites/tracing-new/auto-instrument/mongodb/scenario.ts +++ b/packages/node-integration-tests/suites/tracing-new/auto-instrument/mongodb/scenario.ts @@ -41,4 +41,5 @@ async function run(): Promise { } } -void run(); +// eslint-disable-next-line @typescript-eslint/no-floating-promises +run(); diff --git a/packages/node-integration-tests/suites/tracing-new/prisma-orm/scenario.ts b/packages/node-integration-tests/suites/tracing-new/prisma-orm/scenario.ts index c7a5ef761a82..ee73dc922747 100644 --- a/packages/node-integration-tests/suites/tracing-new/prisma-orm/scenario.ts +++ b/packages/node-integration-tests/suites/tracing-new/prisma-orm/scenario.ts @@ -42,4 +42,5 @@ async function run(): Promise { } } -void run(); +// eslint-disable-next-line @typescript-eslint/no-floating-promises +run(); diff --git a/packages/node-integration-tests/suites/tracing/apollo-graphql/scenario.ts b/packages/node-integration-tests/suites/tracing/apollo-graphql/scenario.ts index 4a4d5a989227..0e5e0bd9edd0 100644 --- a/packages/node-integration-tests/suites/tracing/apollo-graphql/scenario.ts +++ b/packages/node-integration-tests/suites/tracing/apollo-graphql/scenario.ts @@ -33,7 +33,8 @@ const transaction = Sentry.startTransaction({ name: 'test_transaction', op: 'tra Sentry.getCurrentScope().setSpan(transaction); -void (async () => { +// eslint-disable-next-line @typescript-eslint/no-floating-promises +(async () => { // Ref: https://www.apollographql.com/docs/apollo-server/testing/testing/#testing-using-executeoperation await server.executeOperation({ query: '{hello}', diff --git a/packages/node-integration-tests/suites/tracing/auto-instrument/mongodb/scenario.ts b/packages/node-integration-tests/suites/tracing/auto-instrument/mongodb/scenario.ts index 5bd16772d50f..7979ea57483b 100644 --- a/packages/node-integration-tests/suites/tracing/auto-instrument/mongodb/scenario.ts +++ b/packages/node-integration-tests/suites/tracing/auto-instrument/mongodb/scenario.ts @@ -42,4 +42,5 @@ async function run(): Promise { } } -void run(); +// eslint-disable-next-line @typescript-eslint/no-floating-promises +run(); diff --git a/packages/node-integration-tests/suites/tracing/prisma-orm/scenario.ts b/packages/node-integration-tests/suites/tracing/prisma-orm/scenario.ts index 0014717b5fc4..578c5802fea0 100644 --- a/packages/node-integration-tests/suites/tracing/prisma-orm/scenario.ts +++ b/packages/node-integration-tests/suites/tracing/prisma-orm/scenario.ts @@ -44,4 +44,5 @@ async function run(): Promise { } } -void run(); +// eslint-disable-next-line @typescript-eslint/no-floating-promises +run(); diff --git a/packages/node-integration-tests/utils/index.ts b/packages/node-integration-tests/utils/index.ts index ced5b2475aee..b2b81361c0ea 100644 --- a/packages/node-integration-tests/utils/index.ts +++ b/packages/node-integration-tests/utils/index.ts @@ -184,7 +184,8 @@ export class TestEnv { envelopeTypeArray, ); - void makeRequest(options.method, options.url || this.url, this._axiosConfig); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + makeRequest(options.method, options.url || this.url, this._axiosConfig); return resProm; } @@ -305,7 +306,8 @@ export class TestEnv { nock.cleanAll(); - void this._closeServer().then(() => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this._closeServer().then(() => { resolve(reqCount); }); }, diff --git a/packages/node-integration-tests/utils/run-tests.ts b/packages/node-integration-tests/utils/run-tests.ts index 3ff976e18443..90b78bf2521c 100644 --- a/packages/node-integration-tests/utils/run-tests.ts +++ b/packages/node-integration-tests/utils/run-tests.ts @@ -70,7 +70,8 @@ const workers = os.cpus().map(async (_, i) => { } }); -void Promise.all(workers).then(() => { +// eslint-disable-next-line @typescript-eslint/no-floating-promises +Promise.all(workers).then(() => { console.log('-------------------'); console.log(`Successfully ran ${numTests} tests.`); if (fails.length > 0) { diff --git a/packages/node/src/anr/index.ts b/packages/node/src/anr/index.ts index 32117f21372b..13ac5c52c6ef 100644 --- a/packages/node/src/anr/index.ts +++ b/packages/node/src/anr/index.ts @@ -183,10 +183,16 @@ function handleChildProcess(options: Options): void { captureEvent(createAnrEvent(options.anrThreshold, frames)); - void flush(3000).then(() => { - // We only capture one event to avoid spamming users with errors - process.exit(); - }); + flush(3000).then( + () => { + // We only capture one event to avoid spamming users with errors + process.exit(); + }, + () => { + // We only capture one event to avoid spamming users with errors + process.exit(); + }, + ); } addEventProcessor(event => { diff --git a/packages/node/src/integrations/console.ts b/packages/node/src/integrations/console.ts index 6c5142ae2a40..008d3fba591b 100644 --- a/packages/node/src/integrations/console.ts +++ b/packages/node/src/integrations/console.ts @@ -1,6 +1,6 @@ import * as util from 'util'; -import { addBreadcrumb, getCurrentHub } from '@sentry/core'; -import type { Integration } from '@sentry/types'; +import { addBreadcrumb, getClient } from '@sentry/core'; +import type { Client, Integration } from '@sentry/types'; import { addConsoleInstrumentationHandler, severityLevelFromString } from '@sentry/utils'; /** Console module integration */ @@ -19,10 +19,13 @@ export class Console implements Integration { * @inheritDoc */ public setupOnce(): void { - addConsoleInstrumentationHandler(({ args, level }) => { - const hub = getCurrentHub(); + // noop + } - if (!hub.getIntegration(Console)) { + /** @inheritdoc */ + public setup(client: Client): void { + addConsoleInstrumentationHandler(({ args, level }) => { + if (getClient() !== client) { return; } diff --git a/packages/node/src/integrations/hapi/index.ts b/packages/node/src/integrations/hapi/index.ts index 42e7d27bca9e..5e158af810ca 100644 --- a/packages/node/src/integrations/hapi/index.ts +++ b/packages/node/src/integrations/hapi/index.ts @@ -38,6 +38,7 @@ function sendErrorToSentry(errorData: object): void { export const hapiErrorPlugin = { name: 'SentryHapiErrorPlugin', version: SDK_VERSION, + // eslint-disable-next-line @typescript-eslint/no-explicit-any register: async function (serverArg: Record) { const server = serverArg as unknown as Server; @@ -61,6 +62,7 @@ export const hapiErrorPlugin = { export const hapiTracingPlugin = { name: 'SentryHapiTracingPlugin', version: SDK_VERSION, + // eslint-disable-next-line @typescript-eslint/no-explicit-any register: async function (serverArg: Record) { const server = serverArg as unknown as Server; @@ -122,6 +124,7 @@ export const hapiTracingPlugin = { export type HapiOptions = { /** Hapi server instance */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any server?: Record; }; diff --git a/packages/node/src/integrations/onuncaughtexception.ts b/packages/node/src/integrations/onuncaughtexception.ts index 9fcd2fe5fa26..d6e1b50a4eb9 100644 --- a/packages/node/src/integrations/onuncaughtexception.ts +++ b/packages/node/src/integrations/onuncaughtexception.ts @@ -1,5 +1,5 @@ -import type { Scope } from '@sentry/core'; -import { getClient, getCurrentHub } from '@sentry/core'; +import { captureException } from '@sentry/core'; +import { getClient } from '@sentry/core'; import type { Integration } from '@sentry/types'; import { logger } from '@sentry/utils'; @@ -50,11 +50,6 @@ export class OnUncaughtException implements Integration { */ public name: string = OnUncaughtException.id; - /** - * @inheritDoc - */ - public readonly handler: (error: Error) => void = this._makeErrorHandler(); - // CAREFUL: Please think twice before updating the way _options looks because the Next.js SDK depends on it in `index.server.ts` private readonly _options: OnUncaughtExceptionOptions; @@ -68,31 +63,46 @@ export class OnUncaughtException implements Integration { }; } + /** + * @deprecated This does nothing anymore. + */ + public readonly handler: (error: Error) => void = () => { + // noop + }; + /** * @inheritDoc */ public setupOnce(): void { - global.process.on('uncaughtException', this.handler); + // noop } - /** - * @hidden - */ - private _makeErrorHandler(): (error: Error) => void { - const timeout = 2000; - let caughtFirstError: boolean = false; - let caughtSecondError: boolean = false; - let calledFatalError: boolean = false; - let firstError: Error; - - return (error: Error): void => { + /** @inheritdoc */ + public setup(client: NodeClient): void { + global.process.on('uncaughtException', makeErrorHandler(client, this._options)); + } +} + +type ErrorHandler = { _errorHandler: boolean } & ((error: Error) => void); + +/** Exported only for tests */ +export function makeErrorHandler(client: NodeClient, options: OnUncaughtExceptionOptions): ErrorHandler { + const timeout = 2000; + let caughtFirstError: boolean = false; + let caughtSecondError: boolean = false; + let calledFatalError: boolean = false; + let firstError: Error; + + const clientOptions = client.getOptions(); + + return Object.assign( + (error: Error): void => { let onFatalError: OnFatalErrorHandler = logAndExitProcess; - const client = getClient(); - if (this._options.onFatalError) { - onFatalError = this._options.onFatalError; - } else if (client && client.getOptions().onFatalError) { - onFatalError = client.getOptions().onFatalError as OnFatalErrorHandler; + if (options.onFatalError) { + onFatalError = options.onFatalError; + } else if (clientOptions.onFatalError) { + onFatalError = clientOptions.onFatalError as OnFatalErrorHandler; } // Attaching a listener to `uncaughtException` will prevent the node process from exiting. We generally do not @@ -107,7 +117,7 @@ export class OnUncaughtException implements Integration { // There are 3 listeners we ignore: listener.name === 'domainUncaughtExceptionClear' || // as soon as we're using domains this listener is attached by node itself (listener.tag && listener.tag === 'sentry_tracingErrorCallback') || // the handler we register for tracing - listener === this.handler // the handler we register in this integration + (listener as ErrorHandler)._errorHandler // the handler we register in this integration ) { return acc; } else { @@ -116,34 +126,31 @@ export class OnUncaughtException implements Integration { }, 0); const processWouldExit = userProvidedListenersCount === 0; - const shouldApplyFatalHandlingLogic = this._options.exitEvenIfOtherHandlersAreRegistered || processWouldExit; + const shouldApplyFatalHandlingLogic = options.exitEvenIfOtherHandlersAreRegistered || processWouldExit; if (!caughtFirstError) { - const hub = getCurrentHub(); - // this is the first uncaught error and the ultimate reason for shutting down // we want to do absolutely everything possible to ensure it gets captured // also we want to make sure we don't go recursion crazy if more errors happen after this one firstError = error; caughtFirstError = true; - if (hub.getIntegration(OnUncaughtException)) { - hub.withScope((scope: Scope) => { - scope.setLevel('fatal'); - hub.captureException(error, { - originalException: error, - data: { mechanism: { handled: false, type: 'onuncaughtexception' } }, - }); - if (!calledFatalError && shouldApplyFatalHandlingLogic) { - calledFatalError = true; - onFatalError(error); - } + if (getClient() === client) { + captureException(error, { + originalException: error, + captureContext: { + level: 'fatal', + }, + mechanism: { + handled: false, + type: 'onuncaughtexception', + }, }); - } else { - if (!calledFatalError && shouldApplyFatalHandlingLogic) { - calledFatalError = true; - onFatalError(error); - } + } + + if (!calledFatalError && shouldApplyFatalHandlingLogic) { + calledFatalError = true; + onFatalError(error); } } else { if (shouldApplyFatalHandlingLogic) { @@ -182,6 +189,7 @@ export class OnUncaughtException implements Integration { } } } - }; - } + }, + { _errorHandler: true }, + ); } diff --git a/packages/node/src/integrations/onunhandledrejection.ts b/packages/node/src/integrations/onunhandledrejection.ts index 1504b6dee6e0..cc5209233761 100644 --- a/packages/node/src/integrations/onunhandledrejection.ts +++ b/packages/node/src/integrations/onunhandledrejection.ts @@ -1,12 +1,19 @@ -import type { Scope } from '@sentry/core'; -import { getCurrentHub } from '@sentry/core'; -import type { Integration } from '@sentry/types'; +import { captureException, getClient } from '@sentry/core'; +import type { Client, Integration } from '@sentry/types'; import { consoleSandbox } from '@sentry/utils'; import { logAndExitProcess } from './utils/errorhandling'; type UnhandledRejectionMode = 'none' | 'warn' | 'strict'; +interface OnUnhandledRejectionOptions { + /** + * Option deciding what to do after capturing unhandledRejection, + * that mimicks behavior of node's --unhandled-rejection flag. + */ + mode: UnhandledRejectionMode; +} + /** Global Promise Rejection handler */ export class OnUnhandledRejection implements Integration { /** @@ -22,67 +29,80 @@ export class OnUnhandledRejection implements Integration { /** * @inheritDoc */ - public constructor( - private readonly _options: { - /** - * Option deciding what to do after capturing unhandledRejection, - * that mimicks behavior of node's --unhandled-rejection flag. - */ - mode: UnhandledRejectionMode; - } = { mode: 'warn' }, - ) {} + public constructor(private readonly _options: OnUnhandledRejectionOptions = { mode: 'warn' }) {} /** * @inheritDoc */ public setupOnce(): void { - global.process.on('unhandledRejection', this.sendUnhandledPromise.bind(this)); + // noop } - /** - * Send an exception with reason - * @param reason string - * @param promise promise - */ - public sendUnhandledPromise(reason: unknown, promise: unknown): void { - const hub = getCurrentHub(); - if (hub.getIntegration(OnUnhandledRejection)) { - hub.withScope((scope: Scope) => { - scope.setExtra('unhandledPromiseRejection', true); - hub.captureException(reason, { - originalException: promise, - data: { mechanism: { handled: false, type: 'onunhandledrejection' } }, - }); - }); - } - this._handleRejection(reason); + /** @inheritdoc */ + public setup(client: Client): void { + global.process.on('unhandledRejection', makeUnhandledPromiseHandler(client, this._options)); } +} - /** - * Handler for `mode` option - */ +/** + * Send an exception with reason + * @param reason string + * @param promise promise + * + * Exported only for tests. + */ +export function makeUnhandledPromiseHandler( + client: Client, + options: OnUnhandledRejectionOptions, +): (reason: unknown, promise: unknown) => void { + return function sendUnhandledPromise(reason: unknown, promise: unknown): void { + if (getClient() !== client) { + return; + } + + captureException(reason, { + originalException: promise, + captureContext: { + extra: { unhandledPromiseRejection: true }, + }, + mechanism: { + handled: false, + type: 'onunhandledrejection', + }, + }); + + handleRejection(reason, options); + }; +} + +/** + * Handler for `mode` option + + */ +function handleRejection( // eslint-disable-next-line @typescript-eslint/no-explicit-any - private _handleRejection(reason: any): void { - // https://github.com/nodejs/node/blob/7cf6f9e964aa00772965391c23acda6d71972a9a/lib/internal/process/promises.js#L234-L240 - const rejectionWarning = - 'This error originated either by ' + - 'throwing inside of an async function without a catch block, ' + - 'or by rejecting a promise which was not handled with .catch().' + - ' The promise rejected with the reason:'; + reason: any, + options: OnUnhandledRejectionOptions, +): void { + // https://github.com/nodejs/node/blob/7cf6f9e964aa00772965391c23acda6d71972a9a/lib/internal/process/promises.js#L234-L240 + const rejectionWarning = + 'This error originated either by ' + + 'throwing inside of an async function without a catch block, ' + + 'or by rejecting a promise which was not handled with .catch().' + + ' The promise rejected with the reason:'; - /* eslint-disable no-console */ - if (this._options.mode === 'warn') { - consoleSandbox(() => { - console.warn(rejectionWarning); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - console.error(reason && reason.stack ? reason.stack : reason); - }); - } else if (this._options.mode === 'strict') { - consoleSandbox(() => { - console.warn(rejectionWarning); - }); - logAndExitProcess(reason); - } - /* eslint-enable no-console */ + /* eslint-disable no-console */ + if (options.mode === 'warn') { + consoleSandbox(() => { + console.warn(rejectionWarning); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + console.error(reason && reason.stack ? reason.stack : reason); + }); + } else if (options.mode === 'strict') { + consoleSandbox(() => { + console.warn(rejectionWarning); + }); + logAndExitProcess(reason); } + /* eslint-enable no-console */ } diff --git a/packages/node/test/integrations/requestdata.test.ts b/packages/node/test/integrations/requestdata.test.ts index 7b5dc41434db..61f71873fff4 100644 --- a/packages/node/test/integrations/requestdata.test.ts +++ b/packages/node/test/integrations/requestdata.test.ts @@ -1,5 +1,7 @@ import * as http from 'http'; import type { RequestDataIntegrationOptions } from '@sentry/core'; +import { applyScopeDataToEvent } from '@sentry/core'; +import { getCurrentScope } from '@sentry/core'; import { RequestData, getCurrentHub } from '@sentry/core'; import type { Event, EventProcessor, PolymorphicRequest } from '@sentry/types'; import * as sentryUtils from '@sentry/utils'; @@ -67,7 +69,7 @@ describe('`RequestData` integration', () => { sentryRequestMiddleware(req, res, next); - await getCurrentHub().getScope()!.applyToEvent(event, {}); + applyScopeDataToEvent(event, getCurrentScope().getScopeData()); void requestDataEventProcessor(event, {}); const passedOptions = addRequestDataToEventSpy.mock.calls[0][2]; @@ -80,7 +82,7 @@ describe('`RequestData` integration', () => { type GCPHandler = (req: PolymorphicRequest, res: http.ServerResponse) => void; const mockGCPWrapper = (origHandler: GCPHandler, options: Record): GCPHandler => { const wrappedHandler: GCPHandler = (req, res) => { - getCurrentHub().getScope().setSDKProcessingMetadata({ + getCurrentScope().setSDKProcessingMetadata({ request: req, requestDataOptionsFromGCPWrapper: options, }); @@ -96,7 +98,7 @@ describe('`RequestData` integration', () => { wrappedGCPFunction(req, res); - await getCurrentHub().getScope()!.applyToEvent(event, {}); + applyScopeDataToEvent(event, getCurrentScope().getScopeData()); void requestDataEventProcessor(event, {}); const passedOptions = addRequestDataToEventSpy.mock.calls[0][2]; diff --git a/packages/node/test/onuncaughtexception.test.ts b/packages/node/test/onuncaughtexception.test.ts index 54b49fdb8587..7d2544e63f91 100644 --- a/packages/node/test/onuncaughtexception.test.ts +++ b/packages/node/test/onuncaughtexception.test.ts @@ -1,34 +1,50 @@ -import { Hub } from '@sentry/core'; +import * as SentryCore from '@sentry/core'; +import type { NodeClient } from '../src/client'; -import { OnUncaughtException } from '../src/integrations/onuncaughtexception'; +import { OnUncaughtException, makeErrorHandler } from '../src/integrations/onuncaughtexception'; + +const client = { + getOptions: () => ({}), + close: () => Promise.resolve(true), +} as unknown as NodeClient; jest.mock('@sentry/core', () => { // we just want to short-circuit it, so dont worry about types const original = jest.requireActual('@sentry/core'); - original.Hub.prototype.getIntegration = () => true; return { ...original, - getCurrentHub: () => new Hub(), + getClient: () => client, }; }); describe('uncaught exceptions', () => { test('install global listener', () => { const integration = new OnUncaughtException(); - integration.setupOnce(); + integration.setup(client); expect(process.listeners('uncaughtException')).toHaveLength(1); }); - test('sendUncaughtException', () => { - const integration = new OnUncaughtException({ onFatalError: jest.fn() }); - integration.setupOnce(); - - const captureException = jest.spyOn(Hub.prototype, 'captureException'); + test('makeErrorHandler', () => { + const captureExceptionMock = jest.spyOn(SentryCore, 'captureException'); + const handler = makeErrorHandler(client, { + exitEvenIfOtherHandlersAreRegistered: true, + onFatalError: () => {}, + }); - integration.handler({ message: 'message', name: 'name' }); + handler({ message: 'message', name: 'name' }); - expect(captureException.mock.calls[0][1]?.data).toEqual({ - mechanism: { handled: false, type: 'onuncaughtexception' }, + expect(captureExceptionMock.mock.calls[0][1]).toEqual({ + originalException: { + message: 'message', + name: 'name', + }, + captureContext: { + level: 'fatal', + }, + mechanism: { + handled: false, + type: 'onuncaughtexception', + }, }); }); }); diff --git a/packages/node/test/onunhandledrejection.test.ts b/packages/node/test/onunhandledrejection.test.ts index 3c5e60883eee..0667cd9570b2 100644 --- a/packages/node/test/onunhandledrejection.test.ts +++ b/packages/node/test/onunhandledrejection.test.ts @@ -1,32 +1,31 @@ import { Hub } from '@sentry/core'; +import type { NodeClient } from '../src/client'; -import { OnUnhandledRejection } from '../src/integrations/onunhandledrejection'; +import { OnUnhandledRejection, makeUnhandledPromiseHandler } from '../src/integrations/onunhandledrejection'; // don't log the test errors we're going to throw, so at a quick glance it doesn't look like the test itself has failed global.console.warn = () => null; global.console.error = () => null; +const client = { getOptions: () => ({}) } as unknown as NodeClient; + jest.mock('@sentry/core', () => { // we just want to short-circuit it, so dont worry about types const original = jest.requireActual('@sentry/core'); - original.Hub.prototype.getIntegration = () => true; return { ...original, - getCurrentHub: () => new Hub(), + getClient: () => client, }; }); describe('unhandled promises', () => { test('install global listener', () => { const integration = new OnUnhandledRejection(); - integration.setupOnce(); + integration.setup(client); expect(process.listeners('unhandledRejection')).toHaveLength(1); }); - test('sendUnhandledPromise', () => { - const integration = new OnUnhandledRejection(); - integration.setupOnce(); - + test('makeUnhandledPromiseHandler', () => { const promise = { domain: { sentryContext: { @@ -39,10 +38,37 @@ describe('unhandled promises', () => { const captureException = jest.spyOn(Hub.prototype, 'captureException'); - integration.sendUnhandledPromise('bla', promise); + const handler = makeUnhandledPromiseHandler(client, { + mode: 'warn', + }); + + handler('bla', promise); - expect(captureException.mock.calls[0][1]?.data).toEqual({ - mechanism: { handled: false, type: 'onunhandledrejection' }, + expect(captureException.mock.calls[0][1]).toEqual({ + originalException: { + domain: { + sentryContext: { + extra: { + extra: '1', + }, + tags: { + tag: '2', + }, + user: { + id: 1, + }, + }, + }, + }, + captureContext: { + extra: { + unhandledPromiseRejection: true, + }, + }, + mechanism: { + handled: false, + type: 'onunhandledrejection', + }, }); expect(captureException.mock.calls[0][0]).toBe('bla'); }); diff --git a/packages/opentelemetry/src/custom/scope.ts b/packages/opentelemetry/src/custom/scope.ts index e08f8484d87d..c6bdfb164900 100644 --- a/packages/opentelemetry/src/custom/scope.ts +++ b/packages/opentelemetry/src/custom/scope.ts @@ -1,7 +1,7 @@ import type { Span } from '@opentelemetry/api'; import type { TimedEvent } from '@opentelemetry/sdk-trace-base'; import { Scope } from '@sentry/core'; -import type { Breadcrumb, SeverityLevel, Span as SentrySpan } from '@sentry/types'; +import type { Breadcrumb, ScopeData, SeverityLevel, Span as SentrySpan } from '@sentry/types'; import { dateTimestampInSeconds, dropUndefinedKeys, logger, normalize } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; @@ -90,6 +90,15 @@ export class OpenTelemetryScope extends Scope { return this._addBreadcrumb(breadcrumb, maxBreadcrumbs); } + /** @inheritDoc */ + public getScopeData(): ScopeData { + const data = super.getScopeData(); + + data.breadcrumbs = this._getBreadcrumbs(); + + return data; + } + /** Add a breadcrumb to this scope. */ protected _addBreadcrumb(breadcrumb: Breadcrumb, maxBreadcrumbs?: number): this { return super.addBreadcrumb(breadcrumb, maxBreadcrumbs); diff --git a/packages/overhead-metrics/src/perf/network.ts b/packages/overhead-metrics/src/perf/network.ts index c6755f091ebf..03b76d2fcc4d 100644 --- a/packages/overhead-metrics/src/perf/network.ts +++ b/packages/overhead-metrics/src/perf/network.ts @@ -71,11 +71,13 @@ export class NetworkUsageCollector { this._events.push(event); // Note: playwright would error out on file:/// requests. They are used to access local test app resources. if (url.startsWith('file:///')) { - void route.continue(); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + route.continue(); } else { const response = await route.fetch(); const body = await response.body(); - void route.fulfill({ response, body }); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + route.fulfill({ response, body }); event.responseTimeNs = process.hrtime.bigint(); event.responseSize = body.length; } diff --git a/packages/remix/src/utils/instrumentServer.ts b/packages/remix/src/utils/instrumentServer.ts index ea87b961493c..36f41968edb5 100644 --- a/packages/remix/src/utils/instrumentServer.ts +++ b/packages/remix/src/utils/instrumentServer.ts @@ -91,7 +91,9 @@ export function wrapRemixHandleError(err: unknown, { request }: DataFunctionArgs return; } - void captureRemixServerException(err, 'remix.server.handleError', request); + captureRemixServerException(err, 'remix.server.handleError', request).then(null, e => { + DEBUG_BUILD && logger.warn('Failed to capture Remix Server exception.', e); + }); } /** diff --git a/packages/replay/src/constants.ts b/packages/replay/src/constants.ts index 13ccea43df22..ec3325a4253a 100644 --- a/packages/replay/src/constants.ts +++ b/packages/replay/src/constants.ts @@ -53,3 +53,33 @@ export const MAX_REPLAY_DURATION = 3_600_000; // 60 minutes in ms; /** Default attributes to be ignored when `maskAllText` is enabled */ export const DEFAULT_IGNORED_ATTRIBUTES = ['title', 'placeholder']; + +export const CANVAS_QUALITY = { + low: { + sampling: { + canvas: 1, + }, + dataURLOptions: { + type: 'image/webp', + quality: 0.25, + }, + }, + medium: { + sampling: { + canvas: 2, + }, + dataURLOptions: { + type: 'image/webp', + quality: 0.4, + }, + }, + high: { + sampling: { + canvas: 4, + }, + dataURLOptions: { + type: 'image/webp', + quality: 0.5, + }, + }, +}; diff --git a/packages/replay/src/coreHandlers/handleAfterSendEvent.ts b/packages/replay/src/coreHandlers/handleAfterSendEvent.ts index 5cc2adf77a59..28e112e736f0 100644 --- a/packages/replay/src/coreHandlers/handleAfterSendEvent.ts +++ b/packages/replay/src/coreHandlers/handleAfterSendEvent.ts @@ -74,7 +74,9 @@ function handleErrorEvent(replay: ReplayContainer, event: ErrorEvent): void { setTimeout(() => { // Capture current event buffer as new replay - void replay.sendBufferedReplayOrFlush(); + // This should never reject + // eslint-disable-next-line @typescript-eslint/no-floating-promises + replay.sendBufferedReplayOrFlush(); }); } diff --git a/packages/replay/src/coreHandlers/handleBeforeSendEvent.ts b/packages/replay/src/coreHandlers/handleBeforeSendEvent.ts index d7276897497e..9427bc60e45b 100644 --- a/packages/replay/src/coreHandlers/handleBeforeSendEvent.ts +++ b/packages/replay/src/coreHandlers/handleBeforeSendEvent.ts @@ -31,8 +31,9 @@ function handleHydrationError(replay: ReplayContainer, event: ErrorEvent): void // Example https://reactjs.org/docs/error-decoder.html?invariant=423 exceptionValue.match(/reactjs\.org\/docs\/error-decoder\.html\?invariant=(418|419|422|423|425)/) || // Development builds of react-dom - // Example Text: content did not match. Server: "A" Client: "B" - exceptionValue.match(/(hydration|content does not match|did not match)/i) + // Error 1: Hydration failed because the initial UI does not match what was rendered on the server. + // Error 2: Text content does not match server-rendered HTML. Warning: Text content did not match. + exceptionValue.match(/(does not match server-rendered HTML|Hydration failed because)/i) ) { const breadcrumb = createBreadcrumb({ category: 'replay.hydrate-error', diff --git a/packages/replay/src/coreHandlers/handleGlobalEvent.ts b/packages/replay/src/coreHandlers/handleGlobalEvent.ts index 3dbe1498f987..39983a85d72e 100644 --- a/packages/replay/src/coreHandlers/handleGlobalEvent.ts +++ b/packages/replay/src/coreHandlers/handleGlobalEvent.ts @@ -44,7 +44,9 @@ export function handleGlobalEventListener( } if (isFeedbackEvent(event)) { - void replay.flush(); + // This should never reject + // eslint-disable-next-line @typescript-eslint/no-floating-promises + replay.flush(); event.contexts.feedback.replay_id = replay.getSessionId(); // Add a replay breadcrumb for this piece of feedback addFeedbackBreadcrumb(replay, event); diff --git a/packages/replay/src/coreHandlers/handleNetworkBreadcrumbs.ts b/packages/replay/src/coreHandlers/handleNetworkBreadcrumbs.ts index 1ee12c7f5fe0..4a201ae9b65e 100644 --- a/packages/replay/src/coreHandlers/handleNetworkBreadcrumbs.ts +++ b/packages/replay/src/coreHandlers/handleNetworkBreadcrumbs.ts @@ -79,7 +79,9 @@ export function beforeAddNetworkBreadcrumb( // So any async mutations to it will not be reflected in the final breadcrumb enrichXhrBreadcrumb(breadcrumb, hint, options); - void captureXhrBreadcrumbToReplay(breadcrumb, hint, options); + // This call should not reject + // eslint-disable-next-line @typescript-eslint/no-floating-promises + captureXhrBreadcrumbToReplay(breadcrumb, hint, options); } if (_isFetchBreadcrumb(breadcrumb) && _isFetchHint(hint)) { @@ -88,7 +90,9 @@ export function beforeAddNetworkBreadcrumb( // So any async mutations to it will not be reflected in the final breadcrumb enrichFetchBreadcrumb(breadcrumb, hint, options); - void captureFetchBreadcrumbToReplay(breadcrumb, hint, options); + // This call should not reject + // eslint-disable-next-line @typescript-eslint/no-floating-promises + captureFetchBreadcrumbToReplay(breadcrumb, hint, options); } } catch (e) { DEBUG_BUILD && logger.warn('Error when enriching network breadcrumb'); diff --git a/packages/replay/src/coreHandlers/util/addBreadcrumbEvent.ts b/packages/replay/src/coreHandlers/util/addBreadcrumbEvent.ts index 947fb12f1ae4..a324e5b24b25 100644 --- a/packages/replay/src/coreHandlers/util/addBreadcrumbEvent.ts +++ b/packages/replay/src/coreHandlers/util/addBreadcrumbEvent.ts @@ -19,7 +19,9 @@ export function addBreadcrumbEvent(replay: ReplayContainer, breadcrumb: Breadcru } replay.addUpdate(() => { - void replay.throttledAddEvent({ + // This should never reject + // eslint-disable-next-line @typescript-eslint/no-floating-promises + replay.throttledAddEvent({ type: EventType.Custom, // TODO: We were converting from ms to seconds for breadcrumbs, spans, // but maybe we should just keep them as milliseconds diff --git a/packages/replay/src/coreHandlers/util/addFeedbackBreadcrumb.ts b/packages/replay/src/coreHandlers/util/addFeedbackBreadcrumb.ts index 5f2808d9b57d..0e08b459d3ca 100644 --- a/packages/replay/src/coreHandlers/util/addFeedbackBreadcrumb.ts +++ b/packages/replay/src/coreHandlers/util/addFeedbackBreadcrumb.ts @@ -15,7 +15,9 @@ export function addFeedbackBreadcrumb(replay: ReplayContainer, event: FeedbackEv return true; } - void replay.throttledAddEvent({ + // This should never reject + // eslint-disable-next-line @typescript-eslint/no-floating-promises + replay.throttledAddEvent({ type: EventType.Custom, timestamp: event.timestamp * 1000, data: { diff --git a/packages/replay/src/eventBuffer/EventBufferCompressionWorker.ts b/packages/replay/src/eventBuffer/EventBufferCompressionWorker.ts index 6e707638386f..21206ea652ac 100644 --- a/packages/replay/src/eventBuffer/EventBufferCompressionWorker.ts +++ b/packages/replay/src/eventBuffer/EventBufferCompressionWorker.ts @@ -1,6 +1,8 @@ import type { ReplayRecordingData } from '@sentry/types'; +import { logger } from '@sentry/utils'; import { REPLAY_MAX_EVENT_BUFFER_SIZE } from '../constants'; +import { DEBUG_BUILD } from '../debug-build'; import type { AddEventResult, EventBuffer, EventBufferType, RecordingEvent } from '../types'; import { timestampToMs } from '../util/timestamp'; import { WorkerHandler } from './WorkerHandler'; @@ -85,7 +87,9 @@ export class EventBufferCompressionWorker implements EventBuffer { this.hasCheckout = false; // We do not wait on this, as we assume the order of messages is consistent for the worker - void this._worker.postMessage('clear'); + this._worker.postMessage('clear').then(null, e => { + DEBUG_BUILD && logger.warn('[Replay] Sending "clear" message to worker failed', e); + }); } /** @inheritdoc */ diff --git a/packages/replay/src/replay.ts b/packages/replay/src/replay.ts index d0dc3097bcf6..75ec17f3627e 100644 --- a/packages/replay/src/replay.ts +++ b/packages/replay/src/replay.ts @@ -6,6 +6,7 @@ import { logger } from '@sentry/utils'; import { BUFFER_CHECKOUT_TIME, + CANVAS_QUALITY, SESSION_IDLE_EXPIRE_DURATION, SESSION_IDLE_PAUSE_DURATION, SLOW_CLICK_SCROLL_TIMEOUT, @@ -340,15 +341,12 @@ export class ReplayContainer implements ReplayContainerInterface { ...(this.recordingMode === 'buffer' && { checkoutEveryNms: BUFFER_CHECKOUT_TIME }), emit: getHandleRecordingEmit(this), onMutation: this._onMutationHandler, - ...(canvas && { - recordCanvas: true, - sampling: { canvas: canvas.fps || 4 }, - dataURLOptions: { - type: canvas.type || 'image/webp', - quality: canvas.quality || 0.6, - }, - getCanvasManager: canvas.manager, - }), + ...(canvas && + canvas.manager && { + recordCanvas: true, + getCanvasManager: canvas.manager, + ...(CANVAS_QUALITY[canvas.quality || 'medium'] || CANVAS_QUALITY.medium), + }), }); } catch (err) { this._handleException(err); @@ -788,7 +786,9 @@ export class ReplayContainer implements ReplayContainerInterface { maxReplayDuration: this._options.maxReplayDuration, }) ) { - void this._refreshSession(currentSession); + // This should never reject + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this._refreshSession(currentSession); return false; } @@ -927,6 +927,8 @@ export class ReplayContainer implements ReplayContainerInterface { // Send replay when the page/tab becomes hidden. There is no reason to send // replay if it becomes visible, since no actions we care about were done // while it was hidden + // This should never reject + // eslint-disable-next-line @typescript-eslint/no-floating-promises void this.conditionalFlush(); } @@ -975,7 +977,9 @@ export class ReplayContainer implements ReplayContainerInterface { */ private _createCustomBreadcrumb(breadcrumb: ReplayBreadcrumbFrame): void { this.addUpdate(() => { - void this.throttledAddEvent({ + // This should never reject + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.throttledAddEvent({ type: EventType.Custom, timestamp: breadcrumb.timestamp || 0, data: { @@ -1116,7 +1120,9 @@ export class ReplayContainer implements ReplayContainerInterface { // This means we retried 3 times and all of them failed, // or we ran into a problem we don't want to retry, like rate limiting. // In this case, we want to completely stop the replay - otherwise, we may get inconsistent segments - void this.stop({ reason: 'sendReplay' }); + // This should never reject + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.stop({ reason: 'sendReplay' }); const client = getClient(); @@ -1240,7 +1246,9 @@ export class ReplayContainer implements ReplayContainerInterface { // Stop replay if over the mutation limit if (overMutationLimit) { - void this.stop({ reason: 'mutationLimit', forceFlush: this.recordingMode === 'session' }); + // This should never reject + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.stop({ reason: 'mutationLimit', forceFlush: this.recordingMode === 'session' }); return false; } diff --git a/packages/replay/src/types/replay.ts b/packages/replay/src/types/replay.ts index fb1f91c0e1a9..1b2b19d4d4cd 100644 --- a/packages/replay/src/types/replay.ts +++ b/packages/replay/src/types/replay.ts @@ -233,9 +233,7 @@ export interface ReplayPluginOptions extends ReplayNetworkOptions { captureExceptions: boolean; traceInternals: boolean; canvas: { - fps?: number; - quality?: number; - type?: string; + quality?: 'low' | 'medium' | 'high'; manager: (options: GetCanvasManagerOptions) => CanvasManagerInterface; }; }>; diff --git a/packages/replay/src/util/addEvent.ts b/packages/replay/src/util/addEvent.ts index 7ea5d165eac5..893c6b7d01a4 100644 --- a/packages/replay/src/util/addEvent.ts +++ b/packages/replay/src/util/addEvent.ts @@ -25,7 +25,9 @@ export function addEventSync(replay: ReplayContainer, event: RecordingEvent, isC return false; } - void _addEvent(replay, event, isCheckout); + // This should never reject + // eslint-disable-next-line @typescript-eslint/no-floating-promises + _addEvent(replay, event, isCheckout); return true; } diff --git a/packages/replay/src/util/addGlobalListeners.ts b/packages/replay/src/util/addGlobalListeners.ts index 1824e1fa606c..85f253363c7a 100644 --- a/packages/replay/src/util/addGlobalListeners.ts +++ b/packages/replay/src/util/addGlobalListeners.ts @@ -65,7 +65,9 @@ export function addGlobalListeners(replay: ReplayContainer): void { client.on('beforeSendFeedback', (feedbackEvent, options) => { const replayId = replay.getSessionId(); if (options && options.includeReplay && replay.isEnabled() && replayId) { - void replay.flush(); + // This should never reject + // eslint-disable-next-line @typescript-eslint/no-floating-promises + replay.flush(); if (feedbackEvent.contexts && feedbackEvent.contexts.feedback) { feedbackEvent.contexts.feedback.replay_id = replayId; } diff --git a/packages/replay/src/util/handleRecordingEmit.ts b/packages/replay/src/util/handleRecordingEmit.ts index 3ab420a717b1..1d0fd10d0aa3 100644 --- a/packages/replay/src/util/handleRecordingEmit.ts +++ b/packages/replay/src/util/handleRecordingEmit.ts @@ -100,6 +100,9 @@ export function getHandleRecordingEmit(replay: ReplayContainer): RecordingEmitCa // a previous session ID. In this case, we want to buffer events // for a set amount of time before flushing. This can help avoid // capturing replays of users that immediately close the window. + + // This should never reject + // eslint-disable-next-line @typescript-eslint/no-floating-promises void replay.flush(); } diff --git a/packages/replay/test/integration/coreHandlers/handleBeforeSendEvent.test.ts b/packages/replay/test/integration/coreHandlers/handleBeforeSendEvent.test.ts index 92cdc5a88698..a1462b974711 100644 --- a/packages/replay/test/integration/coreHandlers/handleBeforeSendEvent.test.ts +++ b/packages/replay/test/integration/coreHandlers/handleBeforeSendEvent.test.ts @@ -27,7 +27,8 @@ describe('Integration | coreHandlers | handleBeforeSendEvent', () => { const addBreadcrumbSpy = jest.spyOn(replay, 'throttledAddEvent'); const error = Error(); - error.exception.values[0].value = 'Text content did not match. Server: "A" Client: "B"'; + error.exception.values[0].value = + 'Text content does not match server-rendered HTML. Warning: Text content did not match.'; handler(error); expect(addBreadcrumbSpy).toHaveBeenCalledTimes(1); diff --git a/packages/replay/test/integration/rrweb.test.ts b/packages/replay/test/integration/rrweb.test.ts index 82dd18f2d6ec..3543a6771ce4 100644 --- a/packages/replay/test/integration/rrweb.test.ts +++ b/packages/replay/test/integration/rrweb.test.ts @@ -1,3 +1,4 @@ +import type { CanvasManagerInterface } from '../../src/types'; import { resetSdkMock } from '../mocks/resetSdkMock'; import { useFakeTimers } from '../utils/use-fake-timers'; @@ -40,4 +41,33 @@ describe('Integration | rrweb', () => { } `); }); + + it('calls rrweb.record with default canvas options', async () => { + const { mockRecord } = await resetSdkMock({ + replayOptions: { + _experiments: { + canvas: { + // @ts-expect-error This should return + // CanvasManagerInterface, but we don't care about it + // for this test + manager: () => null, + }, + }, + }, + }); + + expect(mockRecord).toHaveBeenLastCalledWith( + expect.objectContaining({ + recordCanvas: true, + getCanvasManager: expect.any(Function), + dataURLOptions: { + quality: 0.4, + type: 'image/webp', + }, + sampling: { + canvas: 2, + }, + }), + ); + }); }); diff --git a/packages/replay/test/unit/util/prepareReplayEvent.test.ts b/packages/replay/test/unit/util/prepareReplayEvent.test.ts index 33019507046a..867bb801907a 100644 --- a/packages/replay/test/unit/util/prepareReplayEvent.test.ts +++ b/packages/replay/test/unit/util/prepareReplayEvent.test.ts @@ -17,7 +17,7 @@ describe('Unit | util | prepareReplayEvent', () => { hub.bindClient(client); client = hub.getClient()!; - scope = hub.getScope()!; + scope = hub.getScope(); jest.spyOn(client, 'getSdkMetadata').mockImplementation(() => { return { diff --git a/packages/serverless/scripts/buildLambdaLayer.ts b/packages/serverless/scripts/buildLambdaLayer.ts index 99140ccb0513..540a1cab7451 100644 --- a/packages/serverless/scripts/buildLambdaLayer.ts +++ b/packages/serverless/scripts/buildLambdaLayer.ts @@ -54,7 +54,8 @@ async function buildLambdaLayer(): Promise { run(`zip -r -y ${zipFilename} .`, { cwd: 'build/aws/dist-serverless' }); } -void buildLambdaLayer(); +// eslint-disable-next-line @typescript-eslint/no-floating-promises +buildLambdaLayer(); /** * Make a directory synchronously, overwriting the old directory if necessary. diff --git a/packages/serverless/src/gcpfunction/cloud_events.ts b/packages/serverless/src/gcpfunction/cloud_events.ts index 63303470d9e9..0e89216a2fd7 100644 --- a/packages/serverless/src/gcpfunction/cloud_events.ts +++ b/packages/serverless/src/gcpfunction/cloud_events.ts @@ -54,7 +54,8 @@ function _wrapCloudEventFunction( } transaction?.finish(); - void flush(options.flushTimeout) + // eslint-disable-next-line @typescript-eslint/no-floating-promises + flush(options.flushTimeout) .then(null, e => { DEBUG_BUILD && logger.error(e); }) diff --git a/packages/serverless/src/gcpfunction/events.ts b/packages/serverless/src/gcpfunction/events.ts index 29d151593990..b69335b272d7 100644 --- a/packages/serverless/src/gcpfunction/events.ts +++ b/packages/serverless/src/gcpfunction/events.ts @@ -56,7 +56,8 @@ function _wrapEventFunction } transaction?.finish(); - void flush(options.flushTimeout) + // eslint-disable-next-line @typescript-eslint/no-floating-promises + flush(options.flushTimeout) .then(null, e => { DEBUG_BUILD && logger.error(e); }) diff --git a/packages/serverless/src/gcpfunction/http.ts b/packages/serverless/src/gcpfunction/http.ts index 95c84cafeb80..726f56c91a52 100644 --- a/packages/serverless/src/gcpfunction/http.ts +++ b/packages/serverless/src/gcpfunction/http.ts @@ -110,7 +110,8 @@ function _wrapHttpFunction(fn: HttpFunction, wrapOptions: Partial { DEBUG_BUILD && logger.error(e); }) diff --git a/packages/serverless/test/__mocks__/@sentry/node.ts b/packages/serverless/test/__mocks__/@sentry/node.ts index b9eba4b132a9..f9322057f1d5 100644 --- a/packages/serverless/test/__mocks__/@sentry/node.ts +++ b/packages/serverless/test/__mocks__/@sentry/node.ts @@ -41,6 +41,7 @@ export const captureException = jest.fn(); export const captureMessage = jest.fn(); export const withScope = jest.fn(cb => cb(fakeScope)); export const flush = jest.fn(() => Promise.resolve()); +export const getClient = jest.fn(() => ({})); export const resetMocks = (): void => { fakeTransaction.setHttpStatus.mockClear(); @@ -68,4 +69,5 @@ export const resetMocks = (): void => { captureMessage.mockClear(); withScope.mockClear(); flush.mockClear(); + getClient.mockClear(); }; diff --git a/packages/serverless/test/gcpfunction.test.ts b/packages/serverless/test/gcpfunction.test.ts index 90ff6e082c29..f60c26c00986 100644 --- a/packages/serverless/test/gcpfunction.test.ts +++ b/packages/serverless/test/gcpfunction.test.ts @@ -1,6 +1,6 @@ import * as domain from 'domain'; import * as SentryNode from '@sentry/node'; -import type { Event } from '@sentry/types'; +import type { Event, Integration } from '@sentry/types'; import * as Sentry from '../src'; import { wrapCloudEventFunction, wrapEventFunction, wrapHttpFunction } from '../src/gcpfunction'; @@ -234,8 +234,6 @@ describe('GCPFunction', () => { // integration is included in the defaults and the necessary data is stored in `sdkProcessingMetadata`. The // integration's tests cover testing that it uses that data correctly. test('wrapHttpFunction request data prereqs', async () => { - expect.assertions(2); - Sentry.GCPFunction.init({}); const handler: HttpFunction = (_req, res) => { @@ -245,11 +243,10 @@ describe('GCPFunction', () => { await handleHttp(wrappedHandler); - expect(SentryNode.init).toHaveBeenCalledWith( - expect.objectContaining({ - defaultIntegrations: expect.arrayContaining([expect.any(SentryNode.Integrations.RequestData)]), - }), - ); + const initOptions = (SentryNode.init as unknown as jest.SpyInstance).mock.calls[0]; + const defaultIntegrations = initOptions[0].defaultIntegrations.map((i: Integration) => i.name); + + expect(defaultIntegrations).toContain('RequestData'); // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeScope.setSDKProcessingMetadata).toHaveBeenCalledWith({ diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json index 6c37a65e67bf..f3ac37df21b8 100644 --- a/packages/sveltekit/package.json +++ b/packages/sveltekit/package.json @@ -20,7 +20,8 @@ "import": "./build/esm/index.client.js", "require": "./build/cjs/index.client.js" }, - "node": "./build/cjs/index.server.js" + "node": "./build/cjs/index.server.js", + "types": "./build/types/index.types.d.ts" } }, "publishConfig": { diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index a2d75a193414..34e4cd40a741 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -72,7 +72,7 @@ export type { ReplayEvent, ReplayRecordingData, ReplayRecordingMode } from './re export type { FeedbackEvent } from './feedback'; export type { QueryParams, Request, SanitizedRequestData } from './request'; export type { Runtime } from './runtime'; -export type { CaptureContext, Scope, ScopeContext } from './scope'; +export type { CaptureContext, Scope, ScopeContext, ScopeData } from './scope'; export type { SdkInfo } from './sdkinfo'; export type { SdkMetadata } from './sdkmetadata'; export type { diff --git a/packages/types/src/scope.ts b/packages/types/src/scope.ts index 4a315aa7adda..50ef4da5987f 100644 --- a/packages/types/src/scope.ts +++ b/packages/types/src/scope.ts @@ -27,6 +27,22 @@ export interface ScopeContext { propagationContext: PropagationContext; } +export interface ScopeData { + eventProcessors: EventProcessor[]; + breadcrumbs: Breadcrumb[]; + user: User; + tags: { [key: string]: Primitive }; + extra: Extras; + contexts: Contexts; + attachments: Attachment[]; + propagationContext: PropagationContext; + sdkProcessingMetadata: { [key: string]: unknown }; + fingerprint: string[]; + level?: SeverityLevel; + transactionName?: string; + span?: Span; +} + /** * Holds additional event information. {@link Scope.applyToEvent} will be called by the client before an event is sent. */ @@ -34,6 +50,9 @@ export interface Scope { /** Add new event processor that will be called after {@link applyToEvent}. */ addEventProcessor(callback: EventProcessor): this; + /** Get the data of this scope, which is applied to an event during processing. */ + getScopeData(): ScopeData; + /** * Updates user context information for future events. * diff --git a/packages/vercel-edge/src/transports/index.ts b/packages/vercel-edge/src/transports/index.ts index d73e7fd4341b..b2b899ecdb42 100644 --- a/packages/vercel-edge/src/transports/index.ts +++ b/packages/vercel-edge/src/transports/index.ts @@ -60,7 +60,9 @@ export class IsolatedPromiseBuffer { } }, timeout); - void Promise.all( + // This cannot reject + // eslint-disable-next-line @typescript-eslint/no-floating-promises + Promise.all( oldTaskProducers.map(taskProducer => taskProducer().then(null, () => { // catch all failed requests diff --git a/yarn.lock b/yarn.lock index 81114ac992a0..64244109473e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5479,6 +5479,11 @@ resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd" integrity sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow== +"@sindresorhus/merge-streams@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/merge-streams/-/merge-streams-1.0.0.tgz#9cd84cc15bc865a5ca35fcaae198eb899f7b5c90" + integrity sha512-rUV5WyJrJLoloD4NDN1V1+LDMDWOa4OTsT4yYJwQNpTU6FWxkxHpL7eu4w+DmiH8x/EAM1otkPE1+LaspIbplw== + "@sinonjs/commons@^1", "@sinonjs/commons@^1.3.0", "@sinonjs/commons@^1.4.0", "@sinonjs/commons@^1.7.0": version "1.8.2" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.2.tgz#858f5c4b48d80778fde4b9d541f27edc0d56488b" @@ -5545,37 +5550,18 @@ resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5" integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ== -"@size-limit/esbuild@9.0.0": - version "9.0.0" - resolved "https://registry.yarnpkg.com/@size-limit/esbuild/-/esbuild-9.0.0.tgz#08e0138d01e8a693e0d7ed274ac36b4065afe814" - integrity sha512-y3NY0inaFeLqV6SRXNVILhawQdQcODxF30qft6OalsrqtQtBjt++6ZeahYhUbrVexUEWRh6c7yPCe8RvHn8hlA== - dependencies: - esbuild "^0.19.2" - nanoid "^3.3.6" - -"@size-limit/file@9.0.0": - version "9.0.0" - resolved "https://registry.yarnpkg.com/@size-limit/file/-/file-9.0.0.tgz#eed5415f5bcc8407979e47ffa49ffaf12d2d2378" - integrity sha512-oM2UaH2FRq4q22k+R+P6xCpzET10T94LFdSjb9svVu/vOD7NaB9LGcG6se8TW1BExXiyXO4GEhLsBt3uMKM3qA== - dependencies: - semver "7.5.4" - -"@size-limit/preset-small-lib@~9.0.0": - version "9.0.0" - resolved "https://registry.yarnpkg.com/@size-limit/preset-small-lib/-/preset-small-lib-9.0.0.tgz#cbac7f3460fb4fac935d0f39a5757864f627f4e2" - integrity sha512-nkbZxn12pTpABYVyX5nsjQuLFpn8wDmd2XKoq/MiqKOc3ocz5BBwXTruqTL5ZKDW1OxEAWZMQlxf2kg3kY3X1Q== - dependencies: - "@size-limit/esbuild" "9.0.0" - "@size-limit/file" "9.0.0" - size-limit "9.0.0" +"@size-limit/file@~11.0.1": + version "11.0.1" + resolved "https://registry.yarnpkg.com/@size-limit/file/-/file-11.0.1.tgz#882ca942341f58fea048aa2f403e73a8a34cd882" + integrity sha512-ioSYJ1WY66kc9+3dgTHi5mT/gcaNNCJ22xU87cjzfKiNxmol+lGsNKbplmrJf+QezvPH9kRIFOWxBjGY+DOt3g== -"@size-limit/webpack@~9.0.0": - version "9.0.0" - resolved "https://registry.yarnpkg.com/@size-limit/webpack/-/webpack-9.0.0.tgz#4514851d3607490e228bf22bc95286643f64a490" - integrity sha512-0YwdvmBj9rS4bXE/PY9vSdc5lCiQXmT0794EsG7yvlDMWyrWa/dsgcRok/w0MoZstfuLaS6lv03VI5UJRFU/lg== +"@size-limit/webpack@~11.0.1": + version "11.0.1" + resolved "https://registry.yarnpkg.com/@size-limit/webpack/-/webpack-11.0.1.tgz#dd7bd770314e6c017724c34659481d62fbbd39e2" + integrity sha512-aQLzpXpp0Xx/AqijgpXnxJGQ3bypLo0acLt8ar3OH83w4cKE1choYROxBsqGFpPSMlwXjCIAwLxLmU98pClPzA== dependencies: - nanoid "^3.3.6" - webpack "^5.88.2" + nanoid "^5.0.4" + webpack "^5.89.0" "@socket.io/base64-arraybuffer@~1.0.2": version "1.0.2" @@ -14255,7 +14241,12 @@ es-module-lexer@^0.9.0: resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.9.3.tgz#6f13db00cc38417137daf74366f535c8eb438f19" integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ== -es-module-lexer@^1.2.1, es-module-lexer@^1.3.0: +es-module-lexer@^1.2.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.4.1.tgz#41ea21b43908fe6a287ffcbe4300f790555331f5" + integrity sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w== + +es-module-lexer@^1.3.0: version "1.3.1" resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.3.1.tgz#c1b0dd5ada807a3b3155315911f364dc4e909db1" integrity sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q== @@ -15307,6 +15298,17 @@ fast-glob@^3.3.1: merge2 "^1.3.0" micromatch "^4.0.4" +fast-glob@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" + integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + fast-json-stable-stringify@2.1.0, fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" @@ -16608,6 +16610,18 @@ globby@^13.1.2: merge2 "^1.4.1" slash "^4.0.0" +globby@^14.0.0: + version "14.0.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-14.0.0.tgz#ea9c062a3614e33f516804e778590fcf055256b9" + integrity sha512-/1WM/LNHRAOH9lZta77uGbq0dAEQM+XjNesWwhlERDVenqothRbnzTrL3/LrIoEPPjeUHC3vrS6TwoyxeHs7MQ== + dependencies: + "@sindresorhus/merge-streams" "^1.0.0" + fast-glob "^3.3.2" + ignore "^5.2.4" + path-type "^5.0.0" + slash "^5.1.0" + unicorn-magic "^0.1.0" + globby@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" @@ -20157,10 +20171,10 @@ lilconfig@^2.0.3: resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.6.tgz#32a384558bd58af3d4c6e077dd1ad1d397bc69d4" integrity sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg== -lilconfig@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" - integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ== +lilconfig@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.0.0.tgz#f8067feb033b5b74dab4602a5f5029420be749bc" + integrity sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g== line-column@^1.0.2: version "1.0.2" @@ -22314,6 +22328,11 @@ nanoid@^3.3.7: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== +nanoid@^5.0.4: + version "5.0.4" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-5.0.4.tgz#d2b608d8169d7da669279127615535705aa52edf" + integrity sha512-vAjmBf13gsmhXSgBrtIclinISzFFy22WwCYoyilZlsrRXNIHSwgFQ1bEdjRwMT3aoadeIF6HMuDRlOxzfXV8ig== + nanomatch@^1.2.9: version "1.2.13" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" @@ -24278,6 +24297,11 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== +path-type@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-5.0.0.tgz#14b01ed7aea7ddf9c7c3f46181d4d04f9c785bb8" + integrity sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg== + pathe@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.0.tgz#e2e13f6c62b31a3289af4ba19886c230f295ec03" @@ -27845,7 +27869,7 @@ semver@7.5.3: dependencies: lru-cache "^6.0.0" -semver@7.5.4, semver@7.x, semver@^7.0.0, semver@^7.1.1, semver@^7.1.3, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4: +semver@7.x, semver@^7.0.0, semver@^7.1.1, semver@^7.1.3, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4: version "7.5.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== @@ -28179,15 +28203,15 @@ sisteransi@^1.0.5: resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== -size-limit@9.0.0, size-limit@~9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/size-limit/-/size-limit-9.0.0.tgz#203c47303462a8351976eb26175acea5f4e80447" - integrity sha512-DrA7o2DeRN3s+vwCA9nn7Ck9Y4pn9t0GNUwQRpKqBtBmNkl6LA2s/NlNCdtKHrEkRTeYA1ZQ65mnYveo9rUqgA== +size-limit@~11.0.1: + version "11.0.1" + resolved "https://registry.yarnpkg.com/size-limit/-/size-limit-11.0.1.tgz#e34ab3302b83833843d578e70a2bf3c6da29f123" + integrity sha512-6L80ocVspWPrhIRg8kPl41VypqTGH8/lu9e6TJiSJpkNLtOR2h/EEqdAO/wNJOv/sUVtjX+lVEWrzBpItGP+gQ== dependencies: bytes-iec "^3.1.1" chokidar "^3.5.3" - globby "^11.1.0" - lilconfig "^2.1.0" + globby "^14.0.0" + lilconfig "^3.0.0" nanospinner "^1.1.0" picocolors "^1.0.0" @@ -28211,6 +28235,11 @@ slash@^4.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7" integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew== +slash@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-5.1.0.tgz#be3adddcdf09ac38eebe8dcdc7b1a57a75b095ce" + integrity sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg== + slice-ansi@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" @@ -29793,9 +29822,9 @@ terser@^5.0.0, terser@^5.10.0, terser@^5.7.2: source-map-support "~0.5.20" terser@^5.16.8: - version "5.22.0" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.22.0.tgz#4f18103f84c5c9437aafb7a14918273310a8a49d" - integrity sha512-hHZVLgRA2z4NWcN6aS5rQDc+7Dcy58HOf2zbYwmFcQ+ua3h6eEFf5lIDKTzbWwlazPyOZsFQO8V80/IjVNExEw== + version "5.26.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.26.0.tgz#ee9f05d929f4189a9c28a0feb889d96d50126fe1" + integrity sha512-dytTGoE2oHgbNV9nTzgBEPaqAWvcJNl66VZ0BkJqlvp71IjO8CxdBx/ykCNb47cLnCmCvRZ6ZR0tLkqvZCdVBQ== dependencies: "@jridgewell/source-map" "^0.3.3" acorn "^8.8.2" @@ -30589,6 +30618,11 @@ unicode-property-aliases-ecmascript@^2.0.0: resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.0.0.tgz#0a36cb9a585c4f6abd51ad1deddb285c165297c8" integrity sha512-5Zfuy9q/DFr4tfO7ZPeVXb1aPoeQSdeFMLpYuFebehDAhbuevLs5yxSZmIFN1tP5F9Wl4IpJrYojg85/zgyZHQ== +unicorn-magic@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/unicorn-magic/-/unicorn-magic-0.1.0.tgz#1bb9a51c823aaf9d73a8bfcd3d1a23dde94b0ce4" + integrity sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ== + unified@^10.0.0, unified@^10.1.2: version "10.1.2" resolved "https://registry.yarnpkg.com/unified/-/unified-10.1.2.tgz#b1d64e55dafe1f0b98bb6c719881103ecf6c86df" @@ -31816,7 +31850,7 @@ webpack@^5.52.0, webpack@~5.74.0: watchpack "^2.4.0" webpack-sources "^3.2.3" -webpack@^5.88.2: +webpack@^5.89.0: version "5.89.0" resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.89.0.tgz#56b8bf9a34356e93a6625770006490bf3a7f32dc" integrity sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw==