diff --git a/packages/angular/src/tracing.ts b/packages/angular/src/tracing.ts index 991f580a52eb..8e0564cff2d9 100644 --- a/packages/angular/src/tracing.ts +++ b/packages/angular/src/tracing.ts @@ -1,6 +1,7 @@ import { AfterViewInit, Directive, Injectable, Input, NgModule, OnDestroy, OnInit } from '@angular/core'; import { Event, NavigationEnd, NavigationStart, Router } from '@angular/router'; import { getCurrentHub } from '@sentry/browser'; +import { getScopeTransaction, getHubScope } from '@sentry/hub'; import { Span, Transaction, TransactionContext } from '@sentry/types'; import { getGlobalObject, logger, stripUrlQueryAndFragment, timestampWithMs } from '@sentry/utils'; import { Observable, Subscription } from 'rxjs'; @@ -44,9 +45,9 @@ export function getActiveTransaction(): Transaction | undefined { const currentHub = getCurrentHub(); if (currentHub) { - const scope = currentHub.getScope(); + const scope = getHubScope(currentHub); if (scope) { - return scope.getTransaction(); + return getScopeTransaction(scope); } } diff --git a/packages/browser/package.json b/packages/browser/package.json index 2eb895b8421e..b5d3809489f7 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -16,6 +16,7 @@ "access": "public" }, "dependencies": { + "@sentry/hub": "6.17.0-beta.0", "@sentry/core": "6.17.0-beta.0", "@sentry/types": "6.17.0-beta.0", "@sentry/utils": "6.17.0-beta.0", diff --git a/packages/browser/src/integrations/breadcrumbs.ts b/packages/browser/src/integrations/breadcrumbs.ts index f946430d9a2b..3931ca6536cc 100644 --- a/packages/browser/src/integrations/breadcrumbs.ts +++ b/packages/browser/src/integrations/breadcrumbs.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable max-lines */ import { getCurrentHub } from '@sentry/core'; +import { addHubBreadcrumb } from '@sentry/hub'; import { Event, Integration } from '@sentry/types'; import { addInstrumentationHandler, @@ -62,7 +63,8 @@ export class Breadcrumbs implements Integration { if (!this._options.sentry) { return; } - getCurrentHub().addBreadcrumb( + addHubBreadcrumb( + getCurrentHub(), { category: `sentry.${event.type === 'transaction' ? 'transaction' : 'event'}`, event_id: event.event_id, @@ -130,7 +132,8 @@ function _domBreadcrumb(dom: BreadcrumbsOptions['dom']): (handlerData: { [key: s return; } - getCurrentHub().addBreadcrumb( + addHubBreadcrumb( + getCurrentHub(), { category: `ui.${handlerData.name}`, message: target, @@ -171,7 +174,7 @@ function _consoleBreadcrumb(handlerData: { [key: string]: any }): void { } } - getCurrentHub().addBreadcrumb(breadcrumb, { + addHubBreadcrumb(getCurrentHub(), breadcrumb, { input: handlerData.args, level: handlerData.level, }); @@ -190,7 +193,8 @@ function _xhrBreadcrumb(handlerData: { [key: string]: any }): void { const { method, url, status_code, body } = handlerData.xhr.__sentry_xhr__ || {}; - getCurrentHub().addBreadcrumb( + addHubBreadcrumb( + getCurrentHub(), { category: 'xhr', data: { @@ -226,7 +230,8 @@ function _fetchBreadcrumb(handlerData: { [key: string]: any }): void { } if (handlerData.error) { - getCurrentHub().addBreadcrumb( + addHubBreadcrumb( + getCurrentHub(), { category: 'fetch', data: handlerData.fetchData, @@ -239,7 +244,8 @@ function _fetchBreadcrumb(handlerData: { [key: string]: any }): void { }, ); } else { - getCurrentHub().addBreadcrumb( + addHubBreadcrumb( + getCurrentHub(), { category: 'fetch', data: { @@ -282,7 +288,7 @@ function _historyBreadcrumb(handlerData: { [key: string]: any }): void { from = parsedFrom.relative; } - getCurrentHub().addBreadcrumb({ + addHubBreadcrumb(getCurrentHub(), { category: 'navigation', data: { from, diff --git a/packages/browser/src/integrations/dedupe.ts b/packages/browser/src/integrations/dedupe.ts index 641823bbde3c..51e14c1e4465 100644 --- a/packages/browser/src/integrations/dedupe.ts +++ b/packages/browser/src/integrations/dedupe.ts @@ -1,5 +1,6 @@ -import { Event, EventProcessor, Exception, Hub, Integration, StackFrame } from '@sentry/types'; +import { Event, EventProcessor, Exception, Integration, StackFrame } from '@sentry/types'; import { logger } from '@sentry/utils'; +import { Hub, getHubIntegration } from '@sentry/hub'; /** Deduplication filter */ export class Dedupe implements Integration { @@ -16,14 +17,14 @@ export class Dedupe implements Integration { /** * @inheritDoc */ - private _previousEvent?: Event; + public _previousEvent?: Event; /** * @inheritDoc */ public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { addGlobalEventProcessor((currentEvent: Event) => { - const self = getCurrentHub().getIntegration(Dedupe); + const self = getHubIntegration(getCurrentHub(), Dedupe); if (self) { // Juuust in case something goes wrong try { diff --git a/packages/browser/src/integrations/globalhandlers.ts b/packages/browser/src/integrations/globalhandlers.ts index e71962d567d8..d18c82ff4603 100644 --- a/packages/browser/src/integrations/globalhandlers.ts +++ b/packages/browser/src/integrations/globalhandlers.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { getCurrentHub } from '@sentry/core'; -import { Event, EventHint, Hub, Integration, Primitive } from '@sentry/types'; +import { captureHubEvent, getHubClient, getHubIntegration, Hub } from '@sentry/hub'; +import { Event, EventHint, Integration, Primitive } from '@sentry/types'; import { addExceptionMechanism, addInstrumentationHandler, @@ -80,7 +81,7 @@ function _installGlobalOnErrorHandler(): void { // eslint-disable-next-line @typescript-eslint/no-explicit-any (data: { msg: any; url: any; line: any; column: any; error: any }) => { const [hub, attachStacktrace] = getHubAndAttachStacktrace(); - if (!hub.getIntegration(GlobalHandlers)) { + if (!getHubIntegration(hub, GlobalHandlers)) { return; } const { msg, url, line, column, error } = data; @@ -113,7 +114,7 @@ function _installGlobalOnUnhandledRejectionHandler(): void { // eslint-disable-next-line @typescript-eslint/no-explicit-any (e: any) => { const [hub, attachStacktrace] = getHubAndAttachStacktrace(); - if (!hub.getIntegration(GlobalHandlers)) { + if (!getHubIntegration(hub, GlobalHandlers)) { return; } let error = e; @@ -250,14 +251,14 @@ function addMechanismAndCapture(hub: Hub, error: EventHint['originalException'], handled: false, type, }); - hub.captureEvent(event, { + captureHubEvent(hub, event, { originalException: error, }); } function getHubAndAttachStacktrace(): [Hub, boolean | undefined] { const hub = getCurrentHub(); - const client = hub.getClient(); + const client = getHubClient(hub); const attachStacktrace = client && client.getOptions().attachStacktrace; return [hub, attachStacktrace]; } diff --git a/packages/browser/src/integrations/linkederrors.ts b/packages/browser/src/integrations/linkederrors.ts index 8870e689f8ef..d28d5df3b7bd 100644 --- a/packages/browser/src/integrations/linkederrors.ts +++ b/packages/browser/src/integrations/linkederrors.ts @@ -1,4 +1,5 @@ import { addGlobalEventProcessor, getCurrentHub } from '@sentry/core'; +import { getHubIntegration } from '@sentry/hub'; import { Event, EventHint, Exception, ExtendedError, Integration } from '@sentry/types'; import { isInstanceOf } from '@sentry/utils'; @@ -28,12 +29,12 @@ export class LinkedErrors implements Integration { /** * @inheritDoc */ - private readonly _key: LinkedErrorsOptions['key']; + public readonly _key: LinkedErrorsOptions['key']; /** * @inheritDoc */ - private readonly _limit: LinkedErrorsOptions['limit']; + public readonly _limit: LinkedErrorsOptions['limit']; /** * @inheritDoc @@ -48,7 +49,7 @@ export class LinkedErrors implements Integration { */ public setupOnce(): void { addGlobalEventProcessor((event: Event, hint?: EventHint) => { - const self = getCurrentHub().getIntegration(LinkedErrors); + const self = getHubIntegration(getCurrentHub(), LinkedErrors); return self ? _handler(self._key, self._limit, event, hint) : event; }); } diff --git a/packages/browser/src/integrations/useragent.ts b/packages/browser/src/integrations/useragent.ts index 1160320f9d93..ebc7f2e35c19 100644 --- a/packages/browser/src/integrations/useragent.ts +++ b/packages/browser/src/integrations/useragent.ts @@ -1,4 +1,5 @@ import { addGlobalEventProcessor, getCurrentHub } from '@sentry/core'; +import { getHubIntegration } from '@sentry/hub'; import { Event, Integration } from '@sentry/types'; import { getGlobalObject } from '@sentry/utils'; @@ -21,7 +22,7 @@ export class UserAgent implements Integration { */ public setupOnce(): void { addGlobalEventProcessor((event: Event) => { - if (getCurrentHub().getIntegration(UserAgent)) { + if (getHubIntegration(getCurrentHub(), UserAgent)) { // if none of the information we want exists, don't bother if (!global.navigator && !global.location && !global.document) { return event; diff --git a/packages/browser/src/sdk.ts b/packages/browser/src/sdk.ts index ed3f0c8ba888..c9ec6a8e78ea 100644 --- a/packages/browser/src/sdk.ts +++ b/packages/browser/src/sdk.ts @@ -1,5 +1,13 @@ import { getCurrentHub, initAndBind, Integrations as CoreIntegrations } from '@sentry/core'; -import { Hub } from '@sentry/types'; +import { + getScopeUser, + captureHubSession, + getHubClient, + getHubLastEventId, + getHubScope, + Hub, + startHubSession, +} from '@sentry/hub'; import { addInstrumentationHandler, getGlobalObject, isDebugBuild, logger, resolvedSyncPromise } from '@sentry/utils'; import { BrowserOptions } from './backend'; @@ -107,18 +115,18 @@ export function init(options: BrowserOptions = {}): void { */ export function showReportDialog(options: ReportDialogOptions = {}): void { const hub = getCurrentHub(); - const scope = hub.getScope(); + const scope = getHubScope(hub); if (scope) { options.user = { - ...scope.getUser(), + ...getScopeUser(scope), ...options.user, }; } if (!options.eventId) { - options.eventId = hub.lastEventId(); + options.eventId = getHubLastEventId(hub); } - const client = hub.getClient(); + const client = getHubClient(hub); if (client) { client.showReportDialog(options); } @@ -130,7 +138,7 @@ export function showReportDialog(options: ReportDialogOptions = {}): void { * @returns The last event id of a captured event. */ export function lastEventId(): string | undefined { - return getCurrentHub().lastEventId(); + return getHubLastEventId(getCurrentHub()); } /** @@ -158,7 +166,7 @@ export function onLoad(callback: () => void): void { * doesn't (or if there's no client defined). */ export function flush(timeout?: number): PromiseLike { - const client = getCurrentHub().getClient(); + const client = getHubClient(getCurrentHub()); if (client) { return client.flush(timeout); } @@ -177,7 +185,7 @@ export function flush(timeout?: number): PromiseLike { * doesn't (or if there's no client defined). */ export function close(timeout?: number): PromiseLike { - const client = getCurrentHub().getClient(); + const client = getHubClient(getCurrentHub()); if (client) { return client.close(timeout); } @@ -200,8 +208,8 @@ export function wrap(fn: (...args: any) => any): any { } function startSessionOnHub(hub: Hub): void { - hub.startSession({ ignoreDuration: true }); - hub.captureSession(); + startHubSession(hub, { ignoreDuration: true }); + captureHubSession(hub); } /** @@ -226,9 +234,10 @@ function startSessionTracking(): void { // https://github.com/getsentry/sentry-javascript/issues/3207 and // https://github.com/getsentry/sentry-javascript/issues/3234 and // https://github.com/getsentry/sentry-javascript/issues/3278. - if (!hub.captureSession) { - return; - } + // TODO: Follow up on this + // if (!hub.captureSession) { + // return; + // } // The session duration for browser sessions does not track a meaningful // concept that can be used as a metric. diff --git a/packages/browser/test/unit/index.test.ts b/packages/browser/test/unit/index.test.ts index f5f4554c552a..868d411a51b2 100644 --- a/packages/browser/test/unit/index.test.ts +++ b/packages/browser/test/unit/index.test.ts @@ -17,6 +17,7 @@ import { wrap, } from '../../src'; import { SimpleTransport } from './mocks/simpletransport'; +import { getClient } from '@sentry/hub'; const dsn = 'https://53039209a22b4ec1bcc296a3c9fdecd6@sentry.io/4291'; @@ -285,7 +286,7 @@ describe('SentryBrowser initialization', () => { }, }); - const sdkData = (getCurrentHub().getClient() as any)._backend._transport._api.metadata?.sdk; + const sdkData = (getClient(getCurrentHub()) as any)._backend._transport._api.metadata?.sdk; expect(sdkData.name).toBe('sentry.javascript.angular'); expect(sdkData.packages[0].name).toBe('npm:@sentry/angular'); diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index 755cbd741416..656a1b36fe6d 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -1,5 +1,13 @@ /* eslint-disable max-lines */ -import { Scope, Session } from '@sentry/hub'; +import { + applyScopeToEvent, + cloneScope, + getScopeSession, + Scope, + Session, + updateScope, + updateSession, +} from '@sentry/hub'; import { Client, DsnComponents, @@ -186,7 +194,7 @@ export abstract class BaseClient implement } else { this._sendSession(session); // After sending, we set init false to indicate it's not the first occurrence - session.update({ init: false }); + updateSession(session, { init: false }); } } @@ -278,7 +286,7 @@ export abstract class BaseClient implement const shouldUpdateAndSend = (sessionNonTerminal && session.errors === 0) || (sessionNonTerminal && crashed); if (shouldUpdateAndSend) { - session.update({ + updateSession(session, { ...(crashed && { status: 'crashed' }), errors: session.errors || Number(errored || crashed), }); @@ -360,7 +368,7 @@ export abstract class BaseClient implement // This allows us to prevent unnecessary copying of data if `captureContext` is not provided. let finalScope = scope; if (hint && hint.captureContext) { - finalScope = Scope.clone(finalScope).update(hint.captureContext); + finalScope = updateScope(cloneScope(finalScope), hint.captureContext); } // We prepare the result here with a resolved Event. @@ -370,7 +378,7 @@ export abstract class BaseClient implement // {@link Hub.addEventProcessor} gets the finished prepared event. if (finalScope) { // In case we have a hub we reassign it. - result = finalScope.applyToEvent(prepared, hint); + result = applyScopeToEvent(finalScope, prepared, hint); } return result.then(evt => { @@ -576,7 +584,7 @@ export abstract class BaseClient implement throw new SentryError('`beforeSend` returned `null`, will not send event.'); } - const session = scope && scope.getSession && scope.getSession(); + const session = getScopeSession(scope); if (!isTransaction && session) { this._updateSessionFromEvent(session, processedEvent); } diff --git a/packages/core/src/integrations/inboundfilters.ts b/packages/core/src/integrations/inboundfilters.ts index f779c3be6266..c28e23ab34da 100644 --- a/packages/core/src/integrations/inboundfilters.ts +++ b/packages/core/src/integrations/inboundfilters.ts @@ -1,4 +1,4 @@ -import { addGlobalEventProcessor, getCurrentHub } from '@sentry/hub'; +import { addGlobalEventProcessor, getHubClient, getCurrentHub, getHubIntegration } from '@sentry/hub'; import { Event, Integration, StackFrame } from '@sentry/types'; import { getEventDescription, isDebugBuild, isMatchingPattern, logger } from '@sentry/utils'; @@ -42,19 +42,24 @@ export class InboundFilters implements Integration { if (!hub) { return event; } - const self = hub.getIntegration(InboundFilters); + // TODO: this is really confusing, why would ask back the integration? + // setupOnce() belongs already to `self` (this) so it is confusing to ask for it. No? + const self = getHubIntegration(hub, InboundFilters); if (self) { - const client = hub.getClient(); + const client = getHubClient(hub); const clientOptions = client ? client.getOptions() : {}; // This checks prevents most of the occurrences of the bug linked below: // https://github.com/getsentry/sentry-javascript/issues/2622 // The bug is caused by multiple SDK instances, where one is minified and one is using non-mangled code. // Unfortunatelly we cannot fix it reliably (thus reserved property in rollup's terser config), // as we cannot force people using multiple instances in their apps to sync SDK versions. + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access const options = typeof self._mergeOptions === 'function' ? self._mergeOptions(clientOptions) : {}; + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access if (typeof self._shouldDropEvent !== 'function') { return event; } + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access return self._shouldDropEvent(event, options) ? null : event; } return event; @@ -62,8 +67,8 @@ export class InboundFilters implements Integration { } /** JSDoc */ - private _shouldDropEvent(event: Event, options: Partial): boolean { - if (this._isSentryError(event, options)) { + public _shouldDropEvent(event: Event, options: Partial): boolean { + if (isSentryError(event, options)) { if (isDebugBuild()) { logger.warn(`Event dropped due to being internal Sentry Error.\nEvent: ${getEventDescription(event)}`); } @@ -82,7 +87,7 @@ export class InboundFilters implements Integration { logger.warn( `Event dropped due to being matched by \`denyUrls\` option.\nEvent: ${getEventDescription( event, - )}.\nUrl: ${this._getEventFilterUrl(event)}`, + )}.\nUrl: ${getEventFilterUrl(event)}`, ); } return true; @@ -92,7 +97,7 @@ export class InboundFilters implements Integration { logger.warn( `Event dropped due to not being matched by \`allowUrls\` option.\nEvent: ${getEventDescription( event, - )}.\nUrl: ${this._getEventFilterUrl(event)}`, + )}.\nUrl: ${getEventFilterUrl(event)}`, ); } return true; @@ -101,56 +106,7 @@ export class InboundFilters implements Integration { } /** JSDoc */ - private _isSentryError(event: Event, options: Partial): boolean { - if (!options.ignoreInternal) { - return false; - } - - try { - // @ts-ignore can't be a sentry error if undefined - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - return event.exception.values[0].type === 'SentryError'; - } catch (e) { - // ignore - } - - return false; - } - - /** JSDoc */ - private _isIgnoredError(event: Event, options: Partial): boolean { - if (!options.ignoreErrors || !options.ignoreErrors.length) { - return false; - } - - return this._getPossibleEventMessages(event).some(message => - // Not sure why TypeScript complains here... - (options.ignoreErrors as Array).some(pattern => isMatchingPattern(message, pattern)), - ); - } - - /** JSDoc */ - private _isDeniedUrl(event: Event, options: Partial): boolean { - // TODO: Use Glob instead? - if (!options.denyUrls || !options.denyUrls.length) { - return false; - } - const url = this._getEventFilterUrl(event); - return !url ? false : options.denyUrls.some(pattern => isMatchingPattern(url, pattern)); - } - - /** JSDoc */ - private _isAllowedUrl(event: Event, options: Partial): boolean { - // TODO: Use Glob instead? - if (!options.allowUrls || !options.allowUrls.length) { - return true; - } - const url = this._getEventFilterUrl(event); - return !url ? true : options.allowUrls.some(pattern => isMatchingPattern(url, pattern)); - } - - /** JSDoc */ - private _mergeOptions(clientOptions: Partial = {}): Partial { + public _mergeOptions(clientOptions: Partial = {}): Partial { return { allowUrls: [ // eslint-disable-next-line deprecation/deprecation @@ -178,56 +134,105 @@ export class InboundFilters implements Integration { } /** JSDoc */ - private _getPossibleEventMessages(event: Event): string[] { - if (event.message) { - return [event.message]; - } - if (event.exception) { - try { - const { type = '', value = '' } = (event.exception.values && event.exception.values[0]) || {}; - return [`${value}`, `${type}: ${value}`]; - } catch (oO) { - if (isDebugBuild()) { - logger.error(`Cannot extract message for event ${getEventDescription(event)}`); - } - return []; - } + private _isIgnoredError(event: Event, options: Partial): boolean { + if (!options.ignoreErrors || !options.ignoreErrors.length) { + return false; } - return []; + + return getPossibleEventMessages(event).some(message => + // Not sure why TypeScript complains here... + (options.ignoreErrors as Array).some(pattern => isMatchingPattern(message, pattern)), + ); } /** JSDoc */ - private _getLastValidUrl(frames: StackFrame[] = []): string | null { - for (let i = frames.length - 1; i >= 0; i--) { - const frame = frames[i]; + private _isDeniedUrl(event: Event, options: Partial): boolean { + // TODO: Use Glob instead? + if (!options.denyUrls || !options.denyUrls.length) { + return false; + } + const url = getEventFilterUrl(event); + return !url ? false : options.denyUrls.some(pattern => isMatchingPattern(url, pattern)); + } - if (frame && frame.filename !== '' && frame.filename !== '[native code]') { - return frame.filename || null; - } + /** JSDoc */ + private _isAllowedUrl(event: Event, options: Partial): boolean { + // TODO: Use Glob instead? + if (!options.allowUrls || !options.allowUrls.length) { + return true; } + const url = getEventFilterUrl(event); + return !url ? true : options.allowUrls.some(pattern => isMatchingPattern(url, pattern)); + } +} +/** JSDoc */ +function getEventFilterUrl(event: Event): string | null { + try { + if (event.stacktrace) { + return getLastValidUrl(event.stacktrace.frames); + } + let frames; + try { + // @ts-ignore we only care about frames if the whole thing here is defined + frames = event.exception.values[0].stacktrace.frames; + } catch (e) { + // ignore + } + return frames ? getLastValidUrl(frames) : null; + } catch (oO) { + if (isDebugBuild()) { + logger.error(`Cannot extract url for event ${getEventDescription(event)}`); + } return null; } +} - /** JSDoc */ - private _getEventFilterUrl(event: Event): string | null { +/** JSDoc */ +function getPossibleEventMessages(event: Event): string[] { + if (event.message) { + return [event.message]; + } + if (event.exception) { try { - if (event.stacktrace) { - return this._getLastValidUrl(event.stacktrace.frames); - } - let frames; - try { - // @ts-ignore we only care about frames if the whole thing here is defined - frames = event.exception.values[0].stacktrace.frames; - } catch (e) { - // ignore - } - return frames ? this._getLastValidUrl(frames) : null; + const { type = '', value = '' } = (event.exception.values && event.exception.values[0]) || {}; + return [`${value}`, `${type}: ${value}`]; } catch (oO) { if (isDebugBuild()) { - logger.error(`Cannot extract url for event ${getEventDescription(event)}`); + logger.error(`Cannot extract message for event ${getEventDescription(event)}`); } - return null; + return []; } } + return []; +} + +/** JSDoc */ +function getLastValidUrl(frames: StackFrame[] = []): string | null { + for (let i = frames.length - 1; i >= 0; i--) { + const frame = frames[i]; + + if (frame && frame.filename !== '' && frame.filename !== '[native code]') { + return frame.filename || null; + } + } + + return null; +} + +/** JSDoc */ +function isSentryError(event: Event, options: Partial): boolean { + if (!options.ignoreInternal) { + return false; + } + + try { + // @ts-ignore can't be a sentry error if undefined + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + return event.exception.values[0].type === 'SentryError'; + } catch (e) { + // ignore + } + + return false; } diff --git a/packages/core/src/sdk.ts b/packages/core/src/sdk.ts index e6af9a5e2336..fe39b27ba143 100644 --- a/packages/core/src/sdk.ts +++ b/packages/core/src/sdk.ts @@ -1,4 +1,4 @@ -import { getCurrentHub } from '@sentry/hub'; +import { bindHubClient, getCurrentHub, getHubScope, updateScope } from '@sentry/hub'; import { Client, Options } from '@sentry/types'; import { logger } from '@sentry/utils'; @@ -17,10 +17,10 @@ export function initAndBind(clientClass: Cl logger.enable(); } const hub = getCurrentHub(); - const scope = hub.getScope(); + const scope = getHubScope(hub); if (scope) { - scope.update(options.initialScope); + updateScope(scope, options.initialScope); } const client = new clientClass(options); - hub.bindClient(client); + bindHubClient(hub, client); } diff --git a/packages/core/test/mocks/integration.ts b/packages/core/test/mocks/integration.ts index 465fac64576a..1b00c309e514 100644 --- a/packages/core/test/mocks/integration.ts +++ b/packages/core/test/mocks/integration.ts @@ -1,4 +1,4 @@ -import { getCurrentHub } from '@sentry/hub'; +import { getCurrentHub, getIntegration } from '@sentry/hub'; import { configureScope } from '@sentry/minimal'; import { Event, Integration } from '@sentry/types'; @@ -10,7 +10,7 @@ export class TestIntegration implements Integration { public setupOnce(): void { configureScope(scope => { scope.addEventProcessor((event: Event) => { - if (!getCurrentHub().getIntegration(TestIntegration)) { + if (!getIntegration(getCurrentHub(), TestIntegration)) { return event; } diff --git a/packages/ember/addon/instance-initializers/sentry-performance.ts b/packages/ember/addon/instance-initializers/sentry-performance.ts index c77e9a80738e..ef03f56bfe37 100644 --- a/packages/ember/addon/instance-initializers/sentry-performance.ts +++ b/packages/ember/addon/instance-initializers/sentry-performance.ts @@ -8,6 +8,7 @@ import { getActiveTransaction } from '..'; import { browserPerformanceTimeOrigin, getGlobalObject, timestampWithMs } from '@sentry/utils'; import { macroCondition, isTesting, getOwnConfig } from '@embroider/macros'; import { EmberSentryConfig, GlobalConfig, OwnConfig } from '../types'; +import { getIntegration } from '@sentry/hub'; function getSentryConfig() { const _global = getGlobalObject(); @@ -266,6 +267,7 @@ function _instrumentComponents(config: EmberSentryConfig) { const beforeComponentDefinitionEntries = {} as RenderEntries; const subscribe = Ember.subscribe; + function _subscribeToRenderEvents() { subscribe('render.component', { before(_name: string, _timestamp: number, payload: Payload) { @@ -288,6 +290,7 @@ function _instrumentComponents(config: EmberSentryConfig) { }); } } + _subscribeToRenderEvents(); } @@ -366,9 +369,12 @@ export async function instrumentForPerformance(appInstance: ApplicationInstance) }), ]; - if (isTesting() && Sentry.getCurrentHub()?.getIntegration(tracing.Integrations.BrowserTracing)) { - // Initializers are called more than once in tests, causing the integrations to not be setup correctly. - return; + if (isTesting()) { + const hub = Sentry.getCurrentHub(); + if (getIntegration(hub, tracing.Integrations.BrowserTracing)) { + // Initializers are called more than once in tests, causing the integrations to not be setup correctly. + return; + } } Sentry.init(sentryConfig); // Call init again to rebind client with new integration list in addition to the defaults diff --git a/packages/hub/src/hub.ts b/packages/hub/src/hub.ts index c7fd4cac85e4..4e426bf59570 100644 --- a/packages/hub/src/hub.ts +++ b/packages/hub/src/hub.ts @@ -1,4 +1,3 @@ -/* eslint-disable max-lines */ import { Breadcrumb, BreadcrumbHint, @@ -8,7 +7,6 @@ import { EventHint, Extra, Extras, - Hub as HubInterface, Integration, IntegrationClass, Primitive, @@ -22,8 +20,21 @@ import { } from '@sentry/types'; import { consoleSandbox, dateTimestampInSeconds, getGlobalObject, isNodeEnv, logger, uuid4 } from '@sentry/utils'; -import { Scope } from './scope'; -import { Session } from './session'; +import { + addScopeBreadcrumb, + cloneScope, + getScopeSession, + getScopeUser, + Scope, + setScopeContext, + setScopeExtra, + setScopeExtras, + setScopeSession, + setScopeTag, + setScopeTags, + setScopeUser, +} from './scope'; +import { closeSession, Session, updateSession } from './session'; /** * API compatibility version of this hub. @@ -83,14 +94,18 @@ export interface DomainAsCarrier extends Carrier { } /** - * @inheritDoc + * Internal class used to make sure we always have the latest internal functions + * working in case we have a version conflict. */ -export class Hub implements HubInterface { +export class Hub { /** Is a {@link Layer}[] containing the client and scope */ - private readonly _stack: Layer[] = [{}]; + public readonly stack: Layer[] = [{}]; /** Contains the last event id of a captured event. */ - private _lastEventId?: string; + public lastEventId?: string; + + /** Higher number means higher priority. */ + public readonly version: number; /** * Creates a new instance of the hub, will push one {@link Layer} into the @@ -100,410 +115,473 @@ export class Hub implements HubInterface { * @param scope bound to the hub. * @param version number, higher number means higher priority. */ - public constructor(client?: Client, scope: Scope = new Scope(), private readonly _version: number = API_VERSION) { - this.getStackTop().scope = scope; + public constructor(client?: Client, scope: Scope = new Scope(), version: number = API_VERSION) { + this.version = version; + getHubStackTop(this).scope = scope; if (client) { - this.bindClient(client); + bindHubClient(this, client); } } +} - /** - * @inheritDoc - */ - public isOlderThan(version: number): boolean { - return this._version < version; - } - - /** - * @inheritDoc - */ - public bindClient(client?: Client): void { - const top = this.getStackTop(); - top.client = client; - if (client && client.setupIntegrations) { - client.setupIntegrations(); - } - } +/** + * Returns the topmost scope layer in the order domain > local > process. + * + * @hidden + * */ +export function getHubStackTop(hub: Hub): Layer { + return hub.stack[hub.stack.length - 1]; +} - /** - * @inheritDoc - */ - public pushScope(): Scope { - // We want to clone the content of prev scope - const scope = Scope.clone(this.getScope()); - this.getStack().push({ - client: this.getClient(), - scope, - }); - return scope; - } +/** Returns the scope stack for domains or the process. */ +export function getHubStack(hub: Hub): Layer[] { + return hub.stack; +} - /** - * @inheritDoc - */ - public popScope(): boolean { - if (this.getStack().length <= 1) return false; - return !!this.getStack().pop(); +/** + * This binds the given client to the current scope. + * @param hub The Hub instance. + * @param client An SDK client (client) instance. + */ +export function bindHubClient(hub: Hub, client?: Client): void { + const top = getHubStackTop(hub); + top.client = client; + if (client && client.setupIntegrations) { + client.setupIntegrations(); } +} - /** - * @inheritDoc - */ - public withScope(callback: (scope: Scope) => void): void { - const scope = this.pushScope(); - try { - callback(scope); - } finally { - this.popScope(); - } - } +/** + * Removes a previously pushed scope from the stack. + * + * This restores the state before the scope was pushed. All breadcrumbs and + * context information added since the last call to {@link pushHubScope} are + * discarded. + */ +export function popHubScope(hub: Hub): boolean { + if (getHubStack(hub).length <= 1) return false; + return !!getHubStack(hub).pop(); +} - /** - * @inheritDoc - */ - public getClient(): C | undefined { - return this.getStackTop().client as C; - } +/** + * Create a new scope to store context information. + * + * The scope will be layered on top of the current one. It is isolated, i.e. all + * breadcrumbs and context information added to this scope will be removed once + * the scope ends. Be sure to always remove this scope with {@link popHubScope} + * when the operation finishes or throws. + * + * @returns Scope, the new cloned scope + */ +export function pushHubScope(hub: Hub): Scope { + // We want to clone the content of prev scope + const scope = cloneScope(getHubScope(hub)); + getHubStack(hub).push({ + client: getHubClient(hub), + scope, + }); + return scope; +} - /** Returns the scope of the top stack. */ - public getScope(): Scope | undefined { - return this.getStackTop().scope; - } +/** + * Checks if this hub's version is older than the given version. + * + * @param hub The hub to check the version on. + * @param version A version number to compare to. + * @return True if the given version is newer; otherwise false. + * + * @hidden + */ +export function isOlderThan(hub: Hub, version: number): boolean { + return hub.version < version; +} - /** Returns the scope stack for domains or the process. */ - public getStack(): Layer[] { - return this._stack; +/** + * Creates a new scope with and executes the given operation within. + * The scope is automatically removed once the operation + * finishes or throws. + * + * This is essentially a convenience function for: + * + * pushScope(); + * callback(); + * popScope(); + * + * @param hub The Hub instance. + * @param callback that will be enclosed into push/popScope. + */ +export function withHubScope(hub: Hub, callback: (scope: Scope) => void): void { + const scope = pushHubScope(hub); + try { + callback(scope); + } finally { + popHubScope(hub); } +} - /** Returns the topmost scope layer in the order domain > local > process. */ - public getStackTop(): Layer { - return this._stack[this._stack.length - 1]; - } +/** Returns the client of the top stack. */ +export function getHubClient(hub: Hub): C | undefined { + return getHubStackTop(hub).client as C; +} - /** - * @inheritDoc - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types - public captureException(exception: any, hint?: EventHint): string { - const eventId = (this._lastEventId = uuid4()); - let finalHint = hint; - - // If there's no explicit hint provided, mimic the same thing that would happen - // in the minimal itself to create a consistent behavior. - // We don't do this in the client, as it's the lowest level API, and doing this, - // would prevent user from having full control over direct calls. - if (!hint) { - let syntheticException: Error; - try { - throw new Error('Sentry syntheticException'); - } catch (exception) { - syntheticException = exception as Error; - } - finalHint = { - originalException: exception, - syntheticException, - }; - } +/** + * Updates user context information for future events. + * + * @param hub The Hub instance. + * @param user User context object to be set in the current context. Pass `null` to unset the user. + */ +export function setHubUser(hub: Hub, user: User | null): void { + const scope = getHubScope(hub); + if (scope) setScopeUser(scope, user); +} - this._invokeClient('captureException', exception, { - ...finalHint, - event_id: eventId, - }); - return eventId; - } +/** Returns the scope of the top stack. */ +export function getHubScope(hub: Hub): Scope | undefined { + return getHubStackTop(hub).scope; +} - /** - * @inheritDoc - */ - public captureMessage(message: string, level?: SeverityLevel, hint?: EventHint): string { - const eventId = (this._lastEventId = uuid4()); - let finalHint = hint; - - // If there's no explicit hint provided, mimic the same thing that would happen - // in the minimal itself to create a consistent behavior. - // We don't do this in the client, as it's the lowest level API, and doing this, - // would prevent user from having full control over direct calls. - if (!hint) { - let syntheticException: Error; - try { - throw new Error(message); - } catch (exception) { - syntheticException = exception as Error; - } - finalHint = { - originalException: message, - syntheticException, - }; - } +/** + * This is the getter for lastEventId. + * + * @returns The last event id of a captured event. + */ +export function getHubLastEventId(hub: Hub): string | undefined { + return hub.lastEventId; +} - this._invokeClient('captureMessage', message, level, { - ...finalHint, - event_id: eventId, - }); - return eventId; +/** + * Sends the current session on the scope to Sentry + * @param hub The Hub instance + * @param shouldEndSession If set the session will be marked as exited and removed from the scope + */ +export function captureHubSession(hub: Hub, shouldEndSession: boolean = false): void { + // both send the update and pull the session from the scope + if (shouldEndSession) { + return endHubSession(hub); } - /** - * @inheritDoc - */ - public captureEvent(event: Event, hint?: EventHint): string { - const eventId = uuid4(); - if (event.type !== 'transaction') { - this._lastEventId = eventId; - } + // only send the update + sendSessionUpdate(hub); +} - this._invokeClient('captureEvent', event, { - ...hint, - event_id: eventId, - }); - return eventId; - } +/** + * Starts a new `Session`, sets on the current scope and returns it. + * + * To finish a `session`, it has to be passed directly to `client.captureSession`, which is done automatically + * when using `endHubSession(hub)` for the session currently stored on the scope. + * + * When there's already an existing session on the scope, it'll be automatically ended. + * + * @param hub The Hub instance. + * @param context Optional properties of the new `Session`. + * + * @returns The session which was just started + * + */ +export function startHubSession(hub: Hub, context?: SessionContext): Session { + const { scope, client } = getHubStackTop(hub); + const { release, environment } = (client && client.getOptions()) || {}; + + // Will fetch userAgent if called from browser sdk + const global = getGlobalObject<{ navigator?: { userAgent?: string } }>(); + const { userAgent } = global.navigator || {}; + + const session = new Session({ + release, + environment, + ...(scope && { user: getScopeUser(scope) }), + ...(userAgent && { userAgent }), + ...context, + }); + + if (scope) { + // End existing session if there's one + const currentSession = getScopeSession(scope); + if (currentSession && currentSession.status === 'ok') { + updateSession(currentSession, { status: 'exited' }); + } + endHubSession(hub); - /** - * @inheritDoc - */ - public lastEventId(): string | undefined { - return this._lastEventId; + // Afterwards we set the new session on the scope + setScopeSession(scope, session); } - /** - * @inheritDoc - */ - public addBreadcrumb(breadcrumb: Breadcrumb, hint?: BreadcrumbHint): void { - const { scope, client } = this.getStackTop(); - - if (!scope || !client) return; - - // eslint-disable-next-line @typescript-eslint/unbound-method - const { beforeBreadcrumb = null, maxBreadcrumbs = DEFAULT_BREADCRUMBS } = - (client.getOptions && client.getOptions()) || {}; - - if (maxBreadcrumbs <= 0) return; - - const timestamp = dateTimestampInSeconds(); - const mergedBreadcrumb = { timestamp, ...breadcrumb }; - const finalBreadcrumb = beforeBreadcrumb - ? (consoleSandbox(() => beforeBreadcrumb(mergedBreadcrumb, hint)) as Breadcrumb | null) - : mergedBreadcrumb; - - if (finalBreadcrumb === null) return; + return session; +} - scope.addBreadcrumb(finalBreadcrumb, maxBreadcrumbs); +/** + * Captures an exception event and sends it to Sentry. + * + * @param hub The Hub instance. + * @param exception An exception-like object. + * @param hint May contain additional information about the original exception. + * @returns The generated eventId. + */ +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export function captureHubException(hub: Hub, exception: any, hint?: EventHint): string { + const eventId = (hub.lastEventId = uuid4()); + let finalHint = hint; + + // If there's no explicit hint provided, mimic the same thing that would happen + // in the minimal itself to create a consistent behavior. + // We don't do this in the client, as it's the lowest level API, and doing this, + // would prevent user from having full control over direct calls. + if (!hint) { + let syntheticException: Error; + try { + throw new Error('Sentry syntheticException'); + } catch (exception) { + syntheticException = exception as Error; + } + finalHint = { + originalException: exception, + syntheticException, + }; } - /** - * @inheritDoc - */ - public setUser(user: User | null): void { - const scope = this.getScope(); - if (scope) scope.setUser(user); - } + _invokeHubClient(hub, 'captureException', exception, { + ...finalHint, + event_id: eventId, + }); + return eventId; +} - /** - * @inheritDoc - */ - public setTags(tags: { [key: string]: Primitive }): void { - const scope = this.getScope(); - if (scope) scope.setTags(tags); +/** + * Captures a message event and sends it to Sentry. + * + * @param hub The Hub instance. + * @param message The message to send to Sentry. + * @param level Define the level of the message. + * @param hint May contain additional information about the original exception. + * @returns The generated eventId. + */ +export function captureHubMessage(hub: Hub, message: string, level?: SeverityLevel, hint?: EventHint): string { + const eventId = (hub.lastEventId = uuid4()); + let finalHint = hint; + + // If there's no explicit hint provided, mimic the same thing that would happen + // in the minimal itself to create a consistent behavior. + // We don't do this in the client, as it's the lowest level API, and doing this, + // would prevent user from having full control over direct calls. + if (!hint) { + let syntheticException: Error; + try { + throw new Error(message); + } catch (exception) { + syntheticException = exception as Error; + } + finalHint = { + originalException: message, + syntheticException, + }; } - /** - * @inheritDoc - */ - public setExtras(extras: Extras): void { - const scope = this.getScope(); - if (scope) scope.setExtras(extras); - } + _invokeHubClient(hub, 'captureMessage', message, level, { + ...finalHint, + event_id: eventId, + }); + return eventId; +} - /** - * @inheritDoc - */ - public setTag(key: string, value: Primitive): void { - const scope = this.getScope(); - if (scope) scope.setTag(key, value); +/** + * Captures a manually created event and sends it to Sentry. + * + * @param hub The Hub instance. + * @param event The event to send to Sentry. + * @param hint May contain additional information about the original exception. + */ +export function captureHubEvent(hub: Hub, event: Event, hint?: EventHint): string { + const eventId = uuid4(); + if (event.type !== 'transaction') { + hub.lastEventId = eventId; } - /** - * @inheritDoc - */ - public setExtra(key: string, extra: Extra): void { - const scope = this.getScope(); - if (scope) scope.setExtra(key, extra); - } + _invokeHubClient(hub, 'captureEvent', event, { + ...hint, + event_id: eventId, + }); + return eventId; +} - /** - * @inheritDoc - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - public setContext(name: string, context: { [key: string]: any } | null): void { - const scope = this.getScope(); - if (scope) scope.setContext(name, context); - } +/** + * Records a new breadcrumb which will be attached to future events. + * + * Breadcrumbs will be added to subsequent events to provide more context on + * user's actions prior to an error or crash. + * + * @param hub The Hub instance. + * @param breadcrumb The breadcrumb to record. + * @param hint May contain additional information about the original breadcrumb. + */ +export function addHubBreadcrumb(hub: Hub, breadcrumb: Breadcrumb, hint?: BreadcrumbHint): void { + const { scope, client } = getHubStackTop(hub); - /** - * @inheritDoc - */ - public configureScope(callback: (scope: Scope) => void): void { - const { scope, client } = this.getStackTop(); - if (scope && client) { - callback(scope); - } - } + if (!scope || !client) return; - /** - * @inheritDoc - */ - public run(callback: (hub: Hub) => void): void { - const oldHub = makeMain(this); - try { - callback(this); - } finally { - makeMain(oldHub); - } - } + // eslint-disable-next-line @typescript-eslint/unbound-method + const { beforeBreadcrumb = null, maxBreadcrumbs = DEFAULT_BREADCRUMBS } = + (client.getOptions && client.getOptions()) || {}; - /** - * @inheritDoc - */ - public getIntegration(integration: IntegrationClass): T | null { - const client = this.getClient(); - if (!client) return null; - try { - return client.getIntegration(integration); - } catch (_oO) { - logger.warn(`Cannot retrieve integration ${integration.id} from the current Hub`); - return null; - } - } + if (maxBreadcrumbs <= 0) return; - /** - * @inheritDoc - */ - public startSpan(context: SpanContext): Span { - return this._callExtensionMethod('startSpan', context); - } + const timestamp = dateTimestampInSeconds(); + const mergedBreadcrumb = { timestamp, ...breadcrumb }; + const finalBreadcrumb = beforeBreadcrumb + ? (consoleSandbox(() => beforeBreadcrumb(mergedBreadcrumb, hint)) as Breadcrumb | null) + : mergedBreadcrumb; - /** - * @inheritDoc - */ - public startTransaction(context: TransactionContext, customSamplingContext?: CustomSamplingContext): Transaction { - return this._callExtensionMethod('startTransaction', context, customSamplingContext); - } + if (finalBreadcrumb === null) return; - /** - * @inheritDoc - */ - public traceHeaders(): { [key: string]: string } { - return this._callExtensionMethod<{ [key: string]: string }>('traceHeaders'); - } + addScopeBreadcrumb(scope, finalBreadcrumb, maxBreadcrumbs); +} - /** - * @inheritDoc - */ - public captureSession(endSession: boolean = false): void { - // both send the update and pull the session from the scope - if (endSession) { - return this.endSession(); - } +/** + * Set an object that will be merged sent as tags data with the event. + * + * @param hub The Hub instance. + * @param tags Tags context object to merge into current context. + */ +export function setHubTags(hub: Hub, tags: { [key: string]: Primitive }): void { + const scope = getHubScope(hub); + if (scope) setScopeTags(scope, tags); +} - // only send the update - this._sendSessionUpdate(); - } +/** + * Set an object that will be merged sent as extra data with the event. + * @param hub The Hub instance. + * @param extras Extras object to merge into current context. + */ +export function setHubExtras(hub: Hub, extras: Extras): void { + const scope = getHubScope(hub); + if (scope) setScopeExtras(scope, extras); +} - /** - * @inheritDoc - */ - public endSession(): void { - const layer = this.getStackTop(); - const scope = layer && layer.scope; - const session = scope && scope.getSession(); - if (session) { - session.close(); - } - this._sendSessionUpdate(); +/** + * Set key:value that will be sent as tags data with the event. + * + * Can also be used to unset a tag, by passing `undefined`. + * + * @param hub The Hub instance. + * @param key String key of tag + * @param value Value of tag + */ +export function setHubTag(hub: Hub, key: string, value: Primitive): void { + const scope = getHubScope(hub); + if (scope) setScopeTag(scope, key, value); +} - // the session is over; take it off of the scope - if (scope) { - scope.setSession(); - } - } +/** + * Set key:value that will be sent as extra data with the event. + * @param hub The Hub instance. + * @param key String of extra + * @param extra Any kind of data. This data will be normalized. + */ +export function setHubExtra(hub: Hub, key: string, extra: Extra): void { + const scope = getHubScope(hub); + if (scope) setScopeExtra(scope, key, extra); +} - /** - * @inheritDoc - */ - public startSession(context?: SessionContext): Session { - const { scope, client } = this.getStackTop(); - const { release, environment } = (client && client.getOptions()) || {}; - - // Will fetch userAgent if called from browser sdk - const global = getGlobalObject<{ navigator?: { userAgent?: string } }>(); - const { userAgent } = global.navigator || {}; - - const session = new Session({ - release, - environment, - ...(scope && { user: scope.getUser() }), - ...(userAgent && { userAgent }), - ...context, - }); - - if (scope) { - // End existing session if there's one - const currentSession = scope.getSession && scope.getSession(); - if (currentSession && currentSession.status === 'ok') { - currentSession.update({ status: 'exited' }); - } - this.endSession(); - - // Afterwards we set the new session on the scope - scope.setSession(session); - } +/** + * Sets context data with the given name. + * @param hub The Hub instance. + * @param name of the context + * @param context Any kind of data. This data will be normalized. + */ +export function setHubContext(hub: Hub, name: string, context: { [key: string]: any } | null): void { + const scope = getHubScope(hub); + if (scope) setScopeContext(scope, name, context); +} - return session; +/** + * Callback to set context information onto the scope. + * + * @param hub The Hub instance. + * @param callback Callback function that receives Scope. + */ +export function configureHubScope(hub: Hub, callback: (scope: Scope) => void): void { + const { scope, client } = getHubStackTop(hub); + if (scope && client) { + callback(scope); } +} - /** - * Sends the current Session on the scope - */ - private _sendSessionUpdate(): void { - const { scope, client } = this.getStackTop(); - if (!scope) return; - - const session = scope.getSession && scope.getSession(); - if (session) { - if (client && client.captureSession) { - client.captureSession(session); - } - } +/** + * For the duration of the callback, this hub will be set as the global current Hub. + * This function is useful if you want to run your own client and hook into an already initialized one + * e.g.: Reporting issues to your own sentry when running in your component while still using the users configuration. + */ +export function run(hub: Hub, callback: (hub: Hub) => void): void { + const oldHub = makeMain(hub); + try { + callback(hub); + } finally { + makeMain(oldHub); } +} - /** - * Internal helper function to call a method on the top client if it exists. - * - * @param method The method to call on the client. - * @param args Arguments to pass to the client function. - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private _invokeClient(method: M, ...args: any[]): void { - const { scope, client } = this.getStackTop(); - if (client && client[method]) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any - (client as any)[method](...args, scope); - } +/** Returns the integration if installed on the current client. */ +export function getHubIntegration(hub: Hub, integration: IntegrationClass): T | null { + const client = getHubClient(hub); + if (!client) return null; + try { + return client.getIntegration(integration); + } catch (_oO) { + logger.warn(`Cannot retrieve integration ${integration.id} from the current Hub`); + return null; } +} - /** - * Calls global extension method and binding current instance to the function call - */ - // @ts-ignore Function lacks ending return statement and return type does not include 'undefined'. ts(2366) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private _callExtensionMethod(method: string, ...args: any[]): T { - const carrier = getMainCarrier(); - const sentry = carrier.__SENTRY__; - if (sentry && sentry.extensions && typeof sentry.extensions[method] === 'function') { - return sentry.extensions[method].apply(this, args); - } - logger.warn(`Extension method ${method} couldn't be found, doing nothing.`); +/** + * @deprecated No longer does anything. Use use {@link Transaction.startChild} instead. + */ +export function startHubSpan(hub: Hub, context: SpanContext): Span { + return callExtensionMethod(hub, 'startSpan', context); +} + +/** + * Starts a new `Transaction` and returns it. This is the entry point to manual tracing instrumentation. + * + * A tree structure can be built by adding child spans to the transaction, and child spans to other spans. To start a + * new child span within the transaction or any span, call the respective `.startChild()` method. + * + * Every child span must be finished before the transaction is finished, otherwise the unfinished spans are discarded. + * + * The transaction must be finished with a call to its `.finish()` method, at which point the transaction with all its + * finished child spans will be sent to Sentry. + * + * @param hub The Hub i + * @param context Properties of the new `Transaction`. + * @param customSamplingContext Information given to the transaction sampling function (along with context-dependent + * default values). See {@link Options.tracesSampler}. + * + * @returns The transaction which was just started + */ +export function startHubTransaction( + hub: Hub, + context: TransactionContext, + customSamplingContext?: CustomSamplingContext, +): Transaction { + return callExtensionMethod(hub, 'startTransaction', context, customSamplingContext); +} + +/** Returns all trace headers that are currently on the top scope. */ +export function traceHeaders(hub: Hub): { [key: string]: string } { + return callExtensionMethod<{ [key: string]: string }>(hub, 'traceHeaders'); +} + +/** + * Internal helper function to call a method on the top client if it exists. + * + * @param hub + * @param method The method to call on the client. + * @param args Arguments to pass to the client function. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function _invokeHubClient(hub: Hub, method: M, ...args: any[]): void { + const { scope, client } = getHubStackTop(hub); + if (client && client[method]) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any + (client as any)[method](...args, scope); } } @@ -547,7 +625,7 @@ export function getCurrentHub(): Hub { const registry = getMainCarrier(); // If there's no hub, or its an old API, assign a new one - if (!hasHubOnCarrier(registry) || getHubFromCarrier(registry).isOlderThan(API_VERSION)) { + if (!hasHubOnCarrier(registry) || isOlderThan(getHubFromCarrier(registry), API_VERSION)) { setHubOnCarrier(registry, new Hub()); } @@ -573,6 +651,32 @@ export function getActiveDomain(): DomainAsCarrier | undefined { return sentry && sentry.extensions && sentry.extensions.domain && sentry.extensions.domain.active; } +/** + * This will create a new {@link Hub} and add to the passed object on + * __SENTRY__.hub. + * @param carrier object + * @hidden + */ +export function getHubFromCarrier(carrier: Carrier): Hub { + if (carrier && carrier.__SENTRY__ && carrier.__SENTRY__.hub) return carrier.__SENTRY__.hub; + carrier.__SENTRY__ = carrier.__SENTRY__ || {}; + carrier.__SENTRY__.hub = new Hub(); + return carrier.__SENTRY__.hub; +} + +/** + * This will set passed {@link Hub} on the passed object's __SENTRY__.hub attribute + * @param carrier object + * @param hub Hub + * @returns A boolean indicating success or failure + */ +export function setHubOnCarrier(carrier: Carrier, hub: Hub): boolean { + if (!carrier) return false; + carrier.__SENTRY__ = carrier.__SENTRY__ || {}; + carrier.__SENTRY__.hub = hub; + return true; +} + /** * Try to read the hub from an active domain, and fallback to the registry if one doesn't exist * @returns discovered hub @@ -588,9 +692,9 @@ function getHubFromActiveDomain(registry: Carrier): Hub { } // If there's no hub on current domain, or it's an old API, assign a new one - if (!hasHubOnCarrier(activeDomain) || getHubFromCarrier(activeDomain).isOlderThan(API_VERSION)) { - const registryHubTopStack = getHubFromCarrier(registry).getStackTop(); - setHubOnCarrier(activeDomain, new Hub(registryHubTopStack.client, Scope.clone(registryHubTopStack.scope))); + if (!hasHubOnCarrier(activeDomain) || isOlderThan(getHubFromCarrier(activeDomain), API_VERSION)) { + const registryHubTopStack = getHubStackTop(getHubFromCarrier(registry)); + setHubOnCarrier(activeDomain, new Hub(registryHubTopStack.client, cloneScope(registryHubTopStack.scope))); } // Return hub that lives on a domain @@ -610,27 +714,49 @@ function hasHubOnCarrier(carrier: Carrier): boolean { } /** - * This will create a new {@link Hub} and add to the passed object on - * __SENTRY__.hub. - * @param carrier object - * @hidden + * Sends the current Session on the scope */ -export function getHubFromCarrier(carrier: Carrier): Hub { - if (carrier && carrier.__SENTRY__ && carrier.__SENTRY__.hub) return carrier.__SENTRY__.hub; - carrier.__SENTRY__ = carrier.__SENTRY__ || {}; - carrier.__SENTRY__.hub = new Hub(); - return carrier.__SENTRY__.hub; +function sendSessionUpdate(hub: Hub): void { + const { scope, client } = getHubStackTop(hub); + if (!scope) return; + + const session = getScopeSession(scope); + if (session) { + if (client && client.captureSession) { + client.captureSession(session); + } + } } /** - * This will set passed {@link Hub} on the passed object's __SENTRY__.hub attribute - * @param carrier object - * @param hub Hub - * @returns A boolean indicating success or failure + * Ends the session that lives on the current scope and sends it to Sentry */ -export function setHubOnCarrier(carrier: Carrier, hub: Hub): boolean { - if (!carrier) return false; - carrier.__SENTRY__ = carrier.__SENTRY__ || {}; - carrier.__SENTRY__.hub = hub; - return true; +export function endHubSession(hub: Hub): void { + const layer = getHubStackTop(hub); + const scope = layer && layer.scope; + const session = getScopeSession(scope); + if (session) { + closeSession(session); + } + + sendSessionUpdate(hub); + + // the session is over; take it off of the scope + if (scope) { + setScopeSession(scope); + } +} + +/** + * Calls global extension method and binding current instance to the function call + */ +// @ts-ignore Function lacks ending return statement and return type does not include 'undefined'. ts(2366) +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function callExtensionMethod(hub: Hub, method: string, ...args: any[]): T { + const carrier = getMainCarrier(); + const sentry = carrier.__SENTRY__; + if (sentry && sentry.extensions && typeof sentry.extensions[method] === 'function') { + return sentry.extensions[method].apply(hub, args); + } + logger.warn(`Extension method ${method} couldn't be found, doing nothing.`); } diff --git a/packages/hub/src/index.ts b/packages/hub/src/index.ts index 9c0a77625dc2..095c275f53a9 100644 --- a/packages/hub/src/index.ts +++ b/packages/hub/src/index.ts @@ -1,17 +1,69 @@ -export { addGlobalEventProcessor, Scope } from './scope'; -export { Session } from './session'; -export { SessionFlusher } from './sessionflusher'; +export { + getScopeRequestSession, + setScopeExtra, + setScopeUser, + setScopeTags, + setScopeExtras, + updateScope, + applyScopeToEvent, + setScopeLevel, + addGlobalEventProcessor, + cloneScope, + getScopeSession, + setScopeSpan, + setScopeRequestSession, + addScopeEventProcessor, + setScopeSession, + getScopeSpan, + getScopeUser, + setScopeContext, + getScopeTransaction, + addScopeBreadcrumb, + Scope, +} from './scope'; +export { Session, updateSession } from './session'; +export { + SessionFlusher, + closeSessionFlusher, + incrementSessionStatusCount, + SessionFlusherTransporter, +} from './sessionflusher'; export { // eslint-disable-next-line deprecation/deprecation getActiveDomain, getCurrentHub, - getHubFromCarrier, - getMainCarrier, + bindHubClient, + popHubScope, + endHubSession, + pushHubScope, + withHubScope, + getHubClient, + getHubScope, + setHubUser, + getHubLastEventId, + captureHubSession, + startHubSession, + addHubBreadcrumb, + captureHubEvent, + captureHubException, + getHubIntegration, + captureHubMessage, + configureHubScope, + startHubTransaction, Hub, + setHubTags, + setHubTag, + setHubExtras, + setHubExtra, makeMain, - setHubOnCarrier, Carrier, // eslint-disable-next-line deprecation/deprecation DomainAsCarrier, Layer, + getHubFromCarrier, + setHubContext, + setHubOnCarrier, + getMainCarrier, + // TODO: This is being used from outside ... weird + _invokeHubClient, } from './hub'; diff --git a/packages/hub/src/scope.ts b/packages/hub/src/scope.ts index 940d0443c9f7..4c097e48fee1 100644 --- a/packages/hub/src/scope.ts +++ b/packages/hub/src/scope.ts @@ -1,4 +1,3 @@ -/* eslint-disable max-lines */ import { Breadcrumb, CaptureContext, @@ -11,16 +10,18 @@ import { Extras, Primitive, RequestSession, - Scope as ScopeInterface, ScopeContext, SeverityLevel, Span, Transaction, User, } from '@sentry/types'; +import { CaptureContextCallback } from '@sentry/types/src/scope'; import { dateTimestampInSeconds, getGlobalObject, isPlainObject, isThenable, SyncPromise } from '@sentry/utils'; -import { Session } from './session'; +import { Session, updateSession } from './session'; + +type ScopeListener = (scope: Scope) => void; /** * Absolute maximum number of breadcrumbs added to an event. @@ -29,489 +30,539 @@ import { Session } from './session'; const MAX_BREADCRUMBS = 100; /** - * Holds additional event information. {@link Scope.applyToEvent} will be + * Holds additional event information. {@link applyScopeToEvent} will be * called by the client before an event will be sent. */ -export class Scope implements ScopeInterface { +export class Scope { /** Flag if notifying is happening. */ - protected _notifyingListeners: boolean = false; + public notifyingListeners: boolean = false; /** Callback for client to receive scope changes. */ - protected _scopeListeners: Array<(scope: Scope) => void> = []; + public scopeListeners: Array<(scope: Scope) => void> = []; - /** Callback list that will be called after {@link applyToEvent}. */ - protected _eventProcessors: EventProcessor[] = []; + /** Callback list that will be called after {@link applyScopeToEvent}. */ + public eventProcessors: EventProcessor[] = []; /** Array of breadcrumbs. */ - protected _breadcrumbs: Breadcrumb[] = []; + public breadcrumbs: Breadcrumb[] = []; /** User */ - protected _user: User = {}; + public user: User = {}; /** Tags */ - protected _tags: { [key: string]: Primitive } = {}; + public tags: Record = {}; /** Extra */ - protected _extra: Extras = {}; + public extra: Extras = {}; /** Contexts */ - protected _contexts: Contexts = {}; + public contexts: Contexts = {}; /** Fingerprint */ - protected _fingerprint?: string[]; + public fingerprint?: string[]; /** Severity */ - protected _level?: SeverityLevel; + public level?: SeverityLevel; /** Transaction Name */ - protected _transactionName?: string; + public transactionName?: string; /** Span */ - protected _span?: Span; + public span?: Span; /** Session */ - protected _session?: Session; + public session?: Session; /** Request Mode Session Status */ - protected _requestSession?: RequestSession; - - /** - * Inherit values from the parent scope. - * @param scope to clone. - */ - public static clone(scope?: Scope): Scope { - const newScope = new Scope(); - if (scope) { - newScope._breadcrumbs = [...scope._breadcrumbs]; - newScope._tags = { ...scope._tags }; - newScope._extra = { ...scope._extra }; - newScope._contexts = { ...scope._contexts }; - newScope._user = scope._user; - newScope._level = scope._level; - newScope._span = scope._span; - newScope._session = scope._session; - newScope._transactionName = scope._transactionName; - newScope._fingerprint = scope._fingerprint; - newScope._eventProcessors = [...scope._eventProcessors]; - newScope._requestSession = scope._requestSession; - } - return newScope; - } + public requestSession?: RequestSession; +} - /** - * Add internal on change listener. Used for sub SDKs that need to store the scope. - * @hidden - */ - public addScopeListener(callback: (scope: Scope) => void): void { - this._scopeListeners.push(callback); +/** + * Inherit values from the parent scope. + * @param scope to clone. + */ +export function cloneScope(scope?: Scope): Scope { + const newScope = new Scope(); + if (scope) { + newScope.breadcrumbs = [...scope.breadcrumbs]; + newScope.tags = { ...scope.tags }; + newScope.extra = { ...scope.extra }; + newScope.contexts = { ...scope.contexts }; + newScope.user = scope.user; + newScope.level = scope.level; + newScope.span = scope.span; + newScope.session = scope.session; + newScope.transactionName = scope.transactionName; + newScope.fingerprint = scope.fingerprint; + newScope.eventProcessors = [...scope.eventProcessors]; + newScope.requestSession = scope.requestSession; } + return newScope; +} - /** - * @inheritDoc - */ - public addEventProcessor(callback: EventProcessor): this { - this._eventProcessors.push(callback); - return this; - } +/** + * Returns the `Session` if there is one + */ +export function getScopeSession(scope?: Scope): Session | undefined { + return scope && scope.session; +} - /** - * @inheritDoc - */ - public setUser(user: User | null): this { - this._user = user || {}; - if (this._session) { - this._session.update({ user }); - } - this._notifyScopeListeners(); - return this; - } +/** + * Add internal on change listener. Used for sub SDKs that need to store the scope. + * @hidden + */ +export function addScopeListener(scope: Scope, callback: ScopeListener): Scope { + scope.scopeListeners.push(callback); + return scope; +} - /** - * @inheritDoc - */ - public getUser(): User | undefined { - return this._user; - } +/** Add new event processor that will be called after {@link applyScopeToEvent}. */ +export function addScopeEventProcessor(scope: Scope, callback: EventProcessor): Scope { + scope.eventProcessors.push(callback); + return scope; +} - /** - * @inheritDoc - */ - public getRequestSession(): RequestSession | undefined { - return this._requestSession; - } +/** + * Set key:value that will be sent as tags data with the event. + * + * Can also be used to unset a tag by passing `undefined`. + * + * @param scope + * @param key String key of tag + * @param value Value of tag + */ +export function setScopeTag(scope: Scope, key: string, value: Primitive): Scope { + scope.tags = { ...scope.tags, [key]: value }; + return notifyListeners(scope); +} - /** - * @inheritDoc - */ - public setRequestSession(requestSession?: RequestSession): this { - this._requestSession = requestSession; - return this; - } +/** + * Set an object that will be merged sent as extra data with the event. + * @param scope + * @param extras Extras object to merge into current context. + */ +export function setScopeExtras(scope: Scope, extras: Extras): Scope { + scope.extra = { + ...scope.extra, + ...extras, + }; + return notifyListeners(scope); +} - /** - * @inheritDoc - */ - public setTags(tags: { [key: string]: Primitive }): this { - this._tags = { - ...this._tags, - ...tags, - }; - this._notifyScopeListeners(); - return this; - } +/** + * Set key:value that will be sent as extra data with the event. + * @param scope + * @param key String of extra + * @param extra Any kind of data. This data will be normalized. + */ +export function setScopeExtra(scope: Scope, key: string, extra: Extra): Scope { + scope.extra = { ...scope.extra, [key]: extra }; + return notifyListeners(scope); +} - /** - * @inheritDoc - */ - public setTag(key: string, value: Primitive): this { - this._tags = { ...this._tags, [key]: value }; - this._notifyScopeListeners(); - return this; - } +/** + * Sets the fingerprint on the scope to send with the events. + * @param scope + * @param fingerprint string[] to group events in Sentry. + */ +export function setScopeFingerprint(scope: Scope, fingerprint: string[]): Scope { + scope.fingerprint = fingerprint; + return notifyListeners(scope); +} - /** - * @inheritDoc - */ - public setExtras(extras: Extras): this { - this._extra = { - ...this._extra, - ...extras, - }; - this._notifyScopeListeners(); - return this; - } +/** + * Sets the level on the scope for future events. + * @param scope + * @param level string {@link Severity} + */ +export function setScopeLevel(scope: Scope, level: SeverityLevel): Scope { + scope.level = level; + return notifyListeners(scope); +} - /** - * @inheritDoc - */ - public setExtra(key: string, extra: Extra): this { - this._extra = { ...this._extra, [key]: extra }; - this._notifyScopeListeners(); - return this; - } +/** + * Sets the transaction name on the scope for future events. + */ +export function setScopeTransactionName(scope: Scope, name?: string): Scope { + scope.transactionName = name; + return notifyListeners(scope); +} - /** - * @inheritDoc - */ - public setFingerprint(fingerprint: string[]): this { - this._fingerprint = fingerprint; - this._notifyScopeListeners(); - return this; - } +/** + * Sets the transaction name on the scope for future events. + */ +export function setScopeTransaction(scope: Scope, name?: string): Scope { + return setScopeTransactionName(scope, name); +} - /** - * @inheritDoc - */ - public setLevel(level: SeverityLevel): this { - this._level = level; - this._notifyScopeListeners(); - return this; +/** + * Sets context data with the given name. + * @param scope + * @param key + * @param context an object containing context data. This data will be normalized. Pass `null` to unset the context. + */ +export function setScopeContext(scope: Scope, key: string, context: Context | null): Scope { + if (context === null) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete scope.contexts[key]; + } else { + scope.contexts = { ...scope.contexts, [key]: context }; } - /** - * @inheritDoc - */ - public setTransactionName(name?: string): this { - this._transactionName = name; - this._notifyScopeListeners(); - return this; - } + return notifyListeners(scope); +} - /** - * Can be removed in major version. - * @deprecated in favor of {@link this.setTransactionName} - */ - public setTransaction(name?: string): this { - return this.setTransactionName(name); - } +/** + * Sets the Span on the scope. + * @param scope + * @param span Span + */ +export function setScopeSpan(scope: Scope, span?: Span): Scope { + scope.span = span; + return notifyListeners(scope); +} - /** - * @inheritDoc - */ - public setContext(key: string, context: Context | null): this { - if (context === null) { - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete this._contexts[key]; - } else { - this._contexts = { ...this._contexts, [key]: context }; - } +/** + * @inheritDoc + */ +export function getScopeSpan(scope: Scope): Span | undefined { + return scope.span; +} + +/** + * Returns the `Transaction` attached to the scope (if there is one) + */ +export function getScopeTransaction(scope: Scope): Transaction | undefined { + // often, this span will be a transaction, but it's not guaranteed to be + const span = getScopeSpan(scope) as undefined | (Span & { spanRecorder: { spans: Span[] } }); - this._notifyScopeListeners(); - return this; + // try it the new way first + if (span && span.transaction) { + return span.transaction; } - /** - * @inheritDoc - */ - public setSpan(span?: Span): this { - this._span = span; - this._notifyScopeListeners(); - return this; + // fallback to the old way (known bug: this only finds transactions with sampled = true) + if (span && span.spanRecorder && span.spanRecorder.spans[0]) { + return span.spanRecorder.spans[0] as Transaction; } - /** - * @inheritDoc - */ - public getSpan(): Span | undefined { - return this._span; + // neither way found a transaction + return undefined; +} + +/** + * Updates user context information for future events. + * + * @param scope + * @param user User context object to be set in the current context. Pass `null` to unset the user. + */ +export function setScopeUser(scope: Scope, user: User | null): Scope { + scope.user = user || {}; + if (scope.session) { + updateSession(scope.session, { user }); } + return notifyListeners(scope); +} - /** - * @inheritDoc - */ - public getTransaction(): Transaction | undefined { - // often, this span will be a transaction, but it's not guaranteed to be - const span = this.getSpan() as undefined | (Span & { spanRecorder: { spans: Span[] } }); +/** + * Returns the `User` if there is one + */ +export function getScopeUser(scope: Scope): User | undefined { + return scope.user; +} - // try it the new way first - if (span && span.transaction) { - return span.transaction; - } +/** + * Returns the `RequestSession` if there is one + */ +export function getScopeRequestSession(scope: Scope): RequestSession | undefined { + return scope.requestSession; +} - // fallback to the old way (known bug: this only finds transactions with sampled = true) - if (span && span.spanRecorder && span.spanRecorder.spans[0]) { - return span.spanRecorder.spans[0] as Transaction; - } +/** + * Set an object that will be merged sent as tags data with the event. + * @param scope + * @param tags Tags context object to merge into current context. + */ +export function setScopeTags(scope: Scope, tags: { [key: string]: Primitive }): Scope { + scope.tags = { + ...scope.tags, + ...tags, + }; + return notifyListeners(scope); +} - // neither way found a transaction - return undefined; +/** + * Sets the `RequestSession` on the scope + */ +export function setScopeRequestSession(scope: Scope, requestSession?: RequestSession): Scope { + scope.requestSession = requestSession; + return scope; +} + +/** + * Sets the `Session` on the scope + */ +export function setScopeSession(scope: Scope, session?: Session): Scope { + if (!session) { + delete scope.session; + } else { + scope.session = session; } + return notifyListeners(scope); +} - /** - * @inheritDoc - */ - public setSession(session?: Session): this { - if (!session) { - delete this._session; - } else { - this._session = session; - } - this._notifyScopeListeners(); - return this; +/** + * Updates the scope with provided data. Can work in three variations: + * - plain object containing updatable attributes + * - Scope instance that'll extract the attributes from + * - callback function that'll receive the current scope as an argument and allow for modifications + * @param scope + * @param captureContext scope modifier to be used + */ +export function updateScope(scope: Scope, captureContext?: CaptureContext): Scope { + if (!captureContext) { + return scope; } - /** - * @inheritDoc - */ - public getSession(): Session | undefined { - return this._session; + if (isCaptureContextCallback(captureContext)) { + const updatedScope = captureContext(scope); + // TODO: It seems to be defensive programming to check to check, since the + // the type says you need to return a Scope back. + return updatedScope instanceof Scope ? updatedScope : scope; } - /** - * @inheritDoc - */ - public update(captureContext?: CaptureContext): this { - if (!captureContext) { - return this; - } + if (captureContext instanceof Scope) { + return mergeScopes(scope, captureContext); + } else if (isScopeContext(captureContext)) { + return mergeScopeContext(scope, captureContext); + } - if (typeof captureContext === 'function') { - const updatedScope = (captureContext as (scope: T) => T)(this); - return updatedScope instanceof Scope ? updatedScope : this; - } + return scope; +} - if (captureContext instanceof Scope) { - this._tags = { ...this._tags, ...captureContext._tags }; - this._extra = { ...this._extra, ...captureContext._extra }; - this._contexts = { ...this._contexts, ...captureContext._contexts }; - if (captureContext._user && Object.keys(captureContext._user).length) { - this._user = captureContext._user; - } - if (captureContext._level) { - this._level = captureContext._level; - } - if (captureContext._fingerprint) { - this._fingerprint = captureContext._fingerprint; - } - if (captureContext._requestSession) { - this._requestSession = captureContext._requestSession; - } - } else if (isPlainObject(captureContext)) { - // eslint-disable-next-line no-param-reassign - captureContext = captureContext as ScopeContext; - this._tags = { ...this._tags, ...captureContext.tags }; - this._extra = { ...this._extra, ...captureContext.extra }; - this._contexts = { ...this._contexts, ...captureContext.contexts }; - if (captureContext.user) { - this._user = captureContext.user; - } - if (captureContext.level) { - this._level = captureContext.level; - } - if (captureContext.fingerprint) { - this._fingerprint = captureContext.fingerprint; - } - if (captureContext.requestSession) { - this._requestSession = captureContext.requestSession; - } - } +/** + * Clears the current scope and resets its properties. + * */ +export function clearScope(scope: Scope): Scope { + scope.breadcrumbs = []; + scope.tags = {}; + scope.extra = {}; + scope.user = {}; + scope.contexts = {}; + scope.level = undefined; + scope.transactionName = undefined; + scope.fingerprint = undefined; + scope.requestSession = undefined; + scope.span = undefined; + scope.session = undefined; + notifyListeners(scope); + return scope; +} - return this; - } +/** + * Sets the breadcrumbs in the scope + * @param scope + * @param breadcrumb + * @param maxBreadcrumbs number of max breadcrumbs to merged into event. + */ +export function addScopeBreadcrumb(scope: Scope, breadcrumb: Breadcrumb, maxBreadcrumbs?: number): Scope { + // TODO: Defensive programming checking for `number` + const maxCrumbs = typeof maxBreadcrumbs === 'number' ? Math.min(maxBreadcrumbs, MAX_BREADCRUMBS) : MAX_BREADCRUMBS; - /** - * @inheritDoc - */ - public clear(): this { - this._breadcrumbs = []; - this._tags = {}; - this._extra = {}; - this._user = {}; - this._contexts = {}; - this._level = undefined; - this._transactionName = undefined; - this._fingerprint = undefined; - this._requestSession = undefined; - this._span = undefined; - this._session = undefined; - this._notifyScopeListeners(); - return this; + // No data has been changed, so don't notify scope listeners + if (maxCrumbs <= 0) { + return scope; } - /** - * @inheritDoc - */ - public addBreadcrumb(breadcrumb: Breadcrumb, maxBreadcrumbs?: number): this { - const maxCrumbs = typeof maxBreadcrumbs === 'number' ? Math.min(maxBreadcrumbs, MAX_BREADCRUMBS) : MAX_BREADCRUMBS; + const mergedBreadcrumb = { + timestamp: dateTimestampInSeconds(), + ...breadcrumb, + }; + scope.breadcrumbs = [...scope.breadcrumbs, mergedBreadcrumb].slice(-maxCrumbs); - // No data has been changed, so don't notify scope listeners - if (maxCrumbs <= 0) { - return this; - } + return notifyListeners(scope); +} - const mergedBreadcrumb = { - timestamp: dateTimestampInSeconds(), - ...breadcrumb, - }; - this._breadcrumbs = [...this._breadcrumbs, mergedBreadcrumb].slice(-maxCrumbs); - this._notifyScopeListeners(); +/** + * Clears all currently set Breadcrumbs. + */ +export function clearScopeBreadcrumbs(scope: Scope): Scope { + scope.breadcrumbs = []; + return notifyListeners(scope); +} - return this; +/** + * Applies the current context and fingerprint to the event. + * Note that breadcrumbs will be added by the client. + * Also if the event has already breadcrumbs on it, we do not merge them. + * @param scope The Scope to apply the event to. + * @param event Event + * @param hint May contain additional information about the original exception. + * @hidden + */ +export function applyScopeToEvent(scope: Scope, event: Event, hint?: EventHint): PromiseLike { + if (scope.extra && Object.keys(scope.extra).length) { + event.extra = { ...scope.extra, ...event.extra }; } - - /** - * @inheritDoc - */ - public clearBreadcrumbs(): this { - this._breadcrumbs = []; - this._notifyScopeListeners(); - return this; + if (scope.tags && Object.keys(scope.tags).length) { + event.tags = { ...scope.tags, ...event.tags }; } - - /** - * Applies the current context and fingerprint to the event. - * Note that breadcrumbs will be added by the client. - * Also if the event has already breadcrumbs on it, we do not merge them. - * @param event Event - * @param hint May contain additional information about the original exception. - * @hidden - */ - public applyToEvent(event: Event, hint?: EventHint): 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 transactionName = this._span.transaction && this._span.transaction.name; - if (transactionName) { - event.tags = { transaction: transactionName, ...event.tags }; - } + if (scope.user && Object.keys(scope.user).length) { + event.user = { ...scope.user, ...event.user }; + } + if (scope.contexts && Object.keys(scope.contexts).length) { + event.contexts = { ...scope.contexts, ...event.contexts }; + } + if (scope.level) { + event.level = scope.level; + } + if (scope.transactionName) { + event.transaction = scope.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 (scope.span) { + event.contexts = { trace: scope.span.getTraceContext(), ...event.contexts }; + const transactionName = scope.span.transaction && scope.span.transaction.name; + if (transactionName) { + event.tags = { transaction: transactionName, ...event.tags }; } + } - this._applyFingerprint(event); + applyFingerprint(scope, event); - event.breadcrumbs = [...(event.breadcrumbs || []), ...this._breadcrumbs]; - event.breadcrumbs = event.breadcrumbs.length > 0 ? event.breadcrumbs : undefined; + event.breadcrumbs = [...(event.breadcrumbs || []), ...scope.breadcrumbs]; + event.breadcrumbs = event.breadcrumbs.length > 0 ? event.breadcrumbs : undefined; - return this._notifyEventProcessors([...getGlobalEventProcessors(), ...this._eventProcessors], event, hint); + return notifyEventProcessors(scope, [...getGlobalEventProcessors(), ...scope.eventProcessors], event, hint); +} + +/** + * Add a EventProcessor to be kept globally. + * @param callback EventProcessor to add + */ +export function addGlobalEventProcessor(callback: EventProcessor): void { + getGlobalEventProcessors().push(callback); +} + +function mergeScopeContext(scope: Scope, captureContext: Partial): Scope { + scope.tags = { ...scope.tags, ...captureContext.tags }; + scope.extra = { ...scope.extra, ...captureContext.extra }; + scope.contexts = { ...scope.contexts, ...captureContext.contexts }; + if (captureContext.user) { + scope.user = captureContext.user; + } + if (captureContext.level) { + scope.level = captureContext.level; + } + if (captureContext.fingerprint) { + scope.fingerprint = captureContext.fingerprint; + } + if (captureContext.requestSession) { + scope.requestSession = captureContext.requestSession; } - /** - * This will be called after {@link applyToEvent} is finished. - */ - protected _notifyEventProcessors( - processors: EventProcessor[], - event: Event | null, - hint?: EventHint, - index: number = 0, - ): PromiseLike { - return new SyncPromise((resolve, reject) => { - const processor = processors[index]; - if (event === null || typeof processor !== 'function') { - resolve(event); - } else { - const result = processor({ ...event }, hint) as Event | null; - if (isThenable(result)) { - void (result as PromiseLike) - .then(final => this._notifyEventProcessors(processors, final, hint, index + 1).then(resolve)) - .then(null, reject); - } else { - void this._notifyEventProcessors(processors, result, hint, index + 1) - .then(resolve) - .then(null, reject); - } - } - }); + return scope; +} + +function mergeScopes(scope: Scope, newScope: Scope): Scope { + scope.tags = { ...scope.tags, ...newScope.tags }; + scope.extra = { ...scope.extra, ...newScope.extra }; + scope.contexts = { ...scope.contexts, ...newScope.contexts }; + if (newScope.user && Object.keys(newScope.user).length) { + scope.user = newScope.user; + } + if (newScope.level) { + scope.level = newScope.level; + } + if (newScope.fingerprint) { + scope.fingerprint = newScope.fingerprint; + } + if (newScope.requestSession) { + scope.requestSession = newScope.requestSession; } - /** - * This will be called on every set call. - */ - protected _notifyScopeListeners(): void { - // We need this check for this._notifyingListeners to be able to work on scope during updates - // If this check is not here we'll produce endless recursion when something is done with the scope - // during the callback. - if (!this._notifyingListeners) { - this._notifyingListeners = true; - this._scopeListeners.forEach(callback => { - callback(this); - }); - this._notifyingListeners = false; + return scope; +} + +function isCaptureContextCallback(val: unknown): val is CaptureContextCallback { + return typeof val === 'function'; +} + +function isScopeContext(val: unknown): val is Partial { + return isPlainObject(val); +} + +/** + * This will be called after {@link applyScopeToEvent} is finished. + */ +function notifyEventProcessors( + scope: Scope, + processors: EventProcessor[], + event: Event | null, + hint?: EventHint, + index: number = 0, +): PromiseLike { + return new SyncPromise((resolve, reject) => { + const processor = processors[index]; + if (event === null || typeof processor !== 'function') { + resolve(event); + } else { + const result = processor({ ...event }, hint) as Event | null; + if (isThenable(result)) { + void (result as PromiseLike) + .then(final => notifyEventProcessors(scope, processors, final, hint, index + 1).then(resolve)) + .then(null, reject); + } else { + void notifyEventProcessors(scope, processors, result, hint, index + 1) + .then(resolve) + .then(null, reject); + } } + }); +} + +/** + * This will be called on every set call. + */ +function notifyListeners(scope: Scope): Scope { + // We need this check for this._notifyingListeners to be able to work on scope during updates + // If this check is not here we'll produce endless recursion when something is done with the scope + // during the callback. + if (!scope.notifyingListeners) { + scope.notifyingListeners = true; + scope.scopeListeners.forEach(callback => callback(scope)); + scope.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 - ? Array.isArray(event.fingerprint) - ? event.fingerprint - : [event.fingerprint] - : []; - - // If we have something on the scope, then merge it with event - if (this._fingerprint) { - event.fingerprint = event.fingerprint.concat(this._fingerprint); - } + return scope; +} - // If we have no data at all, remove empty array default - if (event.fingerprint && !event.fingerprint.length) { - delete event.fingerprint; - } +/** + * 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 applyFingerprint(scope: Scope, event: Event): void { + // Make sure it's an array first and we actually have something in place + event.fingerprint = event.fingerprint + ? Array.isArray(event.fingerprint) + ? event.fingerprint + : [event.fingerprint] + : []; + + // If we have something on the scope, then merge it with event + if (scope.fingerprint) { + event.fingerprint = event.fingerprint.concat(scope.fingerprint); + } + + // If we have no data at all, remove empty array default + if (event.fingerprint && !event.fingerprint.length) { + delete event.fingerprint; } } +// TODO: I would move this out of there and move it to some globals package like `getGlobalObject` is /** * Returns the global event processors. */ @@ -520,14 +571,6 @@ function getGlobalEventProcessors(): EventProcessor[] { const global = getGlobalObject(); global.__SENTRY__ = global.__SENTRY__ || {}; global.__SENTRY__.globalEventProcessors = global.__SENTRY__.globalEventProcessors || []; - return global.__SENTRY__.globalEventProcessors; + return global.__SENTRY__.globalEventProcessors ?? []; /* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access */ } - -/** - * Add a EventProcessor to be kept globally. - * @param callback EventProcessor to add - */ -export function addGlobalEventProcessor(callback: EventProcessor): void { - getGlobalEventProcessors().push(callback); -} diff --git a/packages/hub/src/session.ts b/packages/hub/src/session.ts index 3206ef9306dc..23718c5b75e7 100644 --- a/packages/hub/src/session.ts +++ b/packages/hub/src/session.ts @@ -1,10 +1,10 @@ -import { Session as SessionInterface, SessionContext, SessionStatus } from '@sentry/types'; +import { SessionContext, SessionStatus } from '@sentry/types'; import { dropUndefinedKeys, timestampInSeconds, uuid4 } from '@sentry/utils'; /** - * @inheritdoc + * Session Context */ -export class Session implements SessionInterface { +export class Session { public userAgent?: string; public errors: number = 0; public release?: string; @@ -25,112 +25,114 @@ export class Session implements SessionInterface { this.timestamp = startingTime; this.started = startingTime; if (context) { - this.update(context); + updateSession(this, context); } } +} - /** JSDoc */ - // eslint-disable-next-line complexity - public update(context: SessionContext = {}): void { - if (context.user) { - if (!this.ipAddress && context.user.ip_address) { - this.ipAddress = context.user.ip_address; - } - - if (!this.did && !context.did) { - this.did = context.user.id || context.user.email || context.user.username; - } +/** JSDoc */ +// eslint-disable-next-line complexity +export function updateSession(session: Session, context: SessionContext = {}): void { + if (context.user) { + if (!session.ipAddress && context.user.ip_address) { + session.ipAddress = context.user.ip_address; } - this.timestamp = context.timestamp || timestampInSeconds(); - if (context.ignoreDuration) { - this.ignoreDuration = context.ignoreDuration; - } - if (context.sid) { - // Good enough uuid validation. — Kamil - this.sid = context.sid.length === 32 ? context.sid : uuid4(); - } - if (context.init !== undefined) { - this.init = context.init; - } - if (!this.did && context.did) { - this.did = `${context.did}`; - } - if (typeof context.started === 'number') { - this.started = context.started; - } - if (this.ignoreDuration) { - this.duration = undefined; - } else if (typeof context.duration === 'number') { - this.duration = context.duration; - } else { - const duration = this.timestamp - this.started; - this.duration = duration >= 0 ? duration : 0; - } - if (context.release) { - this.release = context.release; - } - if (context.environment) { - this.environment = context.environment; - } - if (!this.ipAddress && context.ipAddress) { - this.ipAddress = context.ipAddress; - } - if (!this.userAgent && context.userAgent) { - this.userAgent = context.userAgent; - } - if (typeof context.errors === 'number') { - this.errors = context.errors; - } - if (context.status) { - this.status = context.status; + if (!session.did && !context.did) { + session.did = context.user.id || context.user.email || context.user.username; } } - /** JSDoc */ - public close(status?: Exclude): void { - if (status) { - this.update({ status }); - } else if (this.status === 'ok') { - this.update({ status: 'exited' }); - } else { - this.update(); - } + session.timestamp = context.timestamp || timestampInSeconds(); + if (context.ignoreDuration) { + session.ignoreDuration = context.ignoreDuration; } + if (context.sid) { + // Good enough uuid validation. — Kamil + session.sid = context.sid.length === 32 ? context.sid : uuid4(); + } + if (context.init !== undefined) { + session.init = context.init; + } + if (!session.did && context.did) { + session.did = `${context.did}`; + } + if (typeof context.started === 'number') { + session.started = context.started; + } + if (session.ignoreDuration) { + session.duration = undefined; + } else if (typeof context.duration === 'number') { + session.duration = context.duration; + } else { + const duration = session.timestamp - session.started; + session.duration = duration >= 0 ? duration : 0; + } + if (context.release) { + session.release = context.release; + } + if (context.environment) { + session.environment = context.environment; + } + if (!session.ipAddress && context.ipAddress) { + session.ipAddress = context.ipAddress; + } + if (!session.userAgent && context.userAgent) { + session.userAgent = context.userAgent; + } + if (typeof context.errors === 'number') { + session.errors = context.errors; + } + if (context.status) { + session.status = context.status; + } +} - /** JSDoc */ - public toJSON(): { - init: boolean; - sid: string; - did?: string; - timestamp: string; - started: string; - duration?: number; - status: SessionStatus; - errors: number; - attrs?: { - release?: string; - environment?: string; - user_agent?: string; - ip_address?: string; - }; - } { - return dropUndefinedKeys({ - sid: `${this.sid}`, - init: this.init, - // Make sure that sec is converted to ms for date constructor - started: new Date(this.started * 1000).toISOString(), - timestamp: new Date(this.timestamp * 1000).toISOString(), - status: this.status, - errors: this.errors, - did: typeof this.did === 'number' || typeof this.did === 'string' ? `${this.did}` : undefined, - duration: this.duration, - attrs: { - release: this.release, - environment: this.environment, - ip_address: this.ipAddress, - user_agent: this.userAgent, - }, - }); +/** JSDoc */ +export function closeSession(session: Session, status?: Exclude): void { + if (status) { + updateSession(session, { status }); + } else if (session.status === 'ok') { + updateSession(session, { status: 'exited' }); + } else { + updateSession(session); } } + +/** JSDoc */ +export function toJSON( + session: Session, +): { + init: boolean; + sid: string; + did?: string; + timestamp: string; + started: string; + duration?: number; + status: SessionStatus; + errors: number; + attrs?: { + release?: string; + environment?: string; + user_agent?: string; + ip_address?: string; + }; +} { + return dropUndefinedKeys({ + sid: `${session.sid}`, + init: session.init, + // Make sure that sec is converted to ms for date constructor + started: new Date(session.started * 1000).toISOString(), + timestamp: new Date(session.timestamp * 1000).toISOString(), + status: session.status, + errors: session.errors, + did: typeof session.did === 'number' || typeof session.did === 'string' ? `${session.did}` : undefined, + duration: session.duration, + attrs: { + release: session.release, + environment: session.environment, + ip_address: session.ipAddress, + user_agent: session.userAgent, + }, + }); +} diff --git a/packages/hub/src/sessionflusher.ts b/packages/hub/src/sessionflusher.ts index 82aa0af8c4c2..0c39edb63d24 100644 --- a/packages/hub/src/sessionflusher.ts +++ b/packages/hub/src/sessionflusher.ts @@ -1,127 +1,134 @@ -import { - AggregationCounts, - RequestSessionStatus, - SessionAggregates, - SessionFlusherLike, - Transport, -} from '@sentry/types'; +import { AggregationCounts, Event, EventStatus, RequestSessionStatus, SessionAggregates } from '@sentry/types'; +import { EventType } from '@sentry/types/src/event'; import { dropUndefinedKeys, logger } from '@sentry/utils'; +import { Session } from './session'; +import { getCurrentHub, getHubScope } from './hub'; +import { getScopeRequestSession, setScopeRequestSession } from './scope'; -import { getCurrentHub } from './hub'; +export interface Response { + status: EventStatus; + event?: Event | Session; + type?: EventType; + reason?: string; +} -type ReleaseHealthAttributes = { - environment?: string; - release: string; -}; +export type SessionFlusherTransporter = (session: Session | SessionAggregates) => PromiseLike; /** - * @inheritdoc + * ... */ -export class SessionFlusher implements SessionFlusherLike { - public readonly flushTimeout: number = 60; - private _pendingAggregates: Record = {}; - private _sessionAttrs: ReleaseHealthAttributes; - private _intervalId: ReturnType; - private _isEnabled: boolean = true; - private _transport: Transport; +export class SessionFlusher { + /** + * Flush the session every ~60 seconds. + */ + public readonly flushTimeout: number = 60 * 1000; + public pendingAggregates: Record = {}; + public intervalId: ReturnType; + public isEnabled: boolean = true; + public transport: SessionFlusherTransporter; + public environment?: string; + public release: string; - public constructor(transport: Transport, attrs: ReleaseHealthAttributes) { - this._transport = transport; - // Call to setInterval, so that flush is called every 60 seconds - this._intervalId = setInterval(() => this.flush(), this.flushTimeout * 1000); - this._sessionAttrs = attrs; + public constructor(opts: { environment?: string; release: string; transporter: SessionFlusherTransporter }) { + this.transport = opts.transporter; + this.environment = opts.environment; + this.release = opts.release; + this.intervalId = setInterval(() => flush(this), this.flushTimeout); } +} - /** Sends session aggregates to Transport */ - public sendSessionAggregates(sessionAggregates: SessionAggregates): void { - if (!this._transport.sendSession) { - logger.warn("Dropping session because custom transport doesn't implement sendSession"); - return; - } - void this._transport.sendSession(sessionAggregates).then(null, reason => { - logger.error(`Error while sending session: ${reason}`); - }); +/** + * Empties Aggregate Buckets and Sends them to Transport Buffer. + * Checks if `pendingAggregates` has entries, and if it does flushes them by calling `sendSessions` + * */ +function flush(sessionFlusher: SessionFlusher): void { + const sessionAggregates = getSessionAggregates(sessionFlusher); + if (sessionAggregates.aggregates.length === 0) { + return; } + sessionFlusher.pendingAggregates = {}; + void sessionFlusher.transport(sessionAggregates).then(null, reason => { + logger.error(`Error while sending session: ${reason}`); + }); +} - /** Checks if `pendingAggregates` has entries, and if it does flushes them by calling `sendSessions` */ - public flush(): void { - const sessionAggregates = this.getSessionAggregates(); - if (sessionAggregates.aggregates.length === 0) { - return; - } - this._pendingAggregates = {}; - this.sendSessionAggregates(sessionAggregates); - } +/** Massages the entries in `pendingAggregates` and returns aggregated sessions */ +export function getSessionAggregates(sessionFlusher: SessionFlusher): SessionAggregates { + const aggregates: AggregationCounts[] = Object.keys(sessionFlusher.pendingAggregates).map((key: string) => { + return sessionFlusher.pendingAggregates[parseInt(key)]; + }); - /** Massages the entries in `pendingAggregates` and returns aggregated sessions */ - public getSessionAggregates(): SessionAggregates { - const aggregates: AggregationCounts[] = Object.keys(this._pendingAggregates).map((key: string) => { - return this._pendingAggregates[parseInt(key)]; - }); + const sessionAggregates: SessionAggregates = { + attrs: { + environment: sessionFlusher.environment, + release: sessionFlusher.release, + }, + aggregates, + }; + return dropUndefinedKeys(sessionAggregates); +} - const sessionAggregates: SessionAggregates = { - attrs: this._sessionAttrs, - aggregates, - }; - return dropUndefinedKeys(sessionAggregates); - } +/** Clears setInterval and calls flush */ +export function closeSessionFlusher(sessionFlusher: SessionFlusher): void { + clearInterval(sessionFlusher.intervalId); + sessionFlusher.isEnabled = false; + flush(sessionFlusher); +} - /** JSDoc */ - public close(): void { - clearInterval(this._intervalId); - this._isEnabled = false; - this.flush(); +/** + * Increments the Session Status bucket in SessionAggregates Object corresponding to the status of the session captured. + * + * Wrapper function for _incrementSessionStatusCount that checks if the instance of SessionFlusher is enabled then + * fetches the session status of the request from `Scope.getRequestSession().status` on the scope and passes them to + * `_incrementSessionStatusCount` along with the start date + */ +export function incrementSessionStatusCount(sessionFlusher: SessionFlusher): void { + if (!sessionFlusher.isEnabled) { + return; } + const scope = getHubScope(getCurrentHub()); + const requestSession = scope && getScopeRequestSession(scope); - /** - * Wrapper function for _incrementSessionStatusCount that checks if the instance of SessionFlusher is enabled then - * fetches the session status of the request from `Scope.getRequestSession().status` on the scope and passes them to - * `_incrementSessionStatusCount` along with the start date - */ - public incrementSessionStatusCount(): void { - if (!this._isEnabled) { - return; - } - const scope = getCurrentHub().getScope(); - const requestSession = scope && scope.getRequestSession(); - - if (requestSession && requestSession.status) { - this._incrementSessionStatusCount(requestSession.status, new Date()); - // This is not entirely necessarily but is added as a safe guard to indicate the bounds of a request and so in - // case captureRequestSession is called more than once to prevent double count - if (scope) { - scope.setRequestSession(undefined); - } - /* eslint-enable @typescript-eslint/no-unsafe-member-access */ + if (requestSession && requestSession.status) { + _incrementSessionStatusCount(sessionFlusher, requestSession.status, new Date()); + // This is not entirely necessarily but is added as a safe guard to indicate the bounds of a request and so in + // case captureRequestSession is called more than once to prevent double count + if (scope) { + setScopeRequestSession(scope, undefined); } + /* eslint-enable @typescript-eslint/no-unsafe-member-access */ } +} - /** - * Increments status bucket in pendingAggregates buffer (internal state) corresponding to status of - * the session received - */ - private _incrementSessionStatusCount(status: RequestSessionStatus, date: Date): number { - // Truncate minutes and seconds on Session Started attribute to have one minute bucket keys - const sessionStartedTrunc = new Date(date).setSeconds(0, 0); - this._pendingAggregates[sessionStartedTrunc] = this._pendingAggregates[sessionStartedTrunc] || {}; +/** + * Increments status bucket in pendingAggregates buffer (internal state) corresponding to status of + * the session received + */ +export function _incrementSessionStatusCount( + sessionFlusher: SessionFlusher, + status: RequestSessionStatus, + date: Date, +): number { + // Truncate minutes and seconds on Session Started attribute to have one minute bucket keys + const sessionStartedTrunc = new Date(date).setSeconds(0, 0); + sessionFlusher.pendingAggregates[sessionStartedTrunc] = sessionFlusher.pendingAggregates[sessionStartedTrunc] || {}; - // corresponds to aggregated sessions in one specific minute bucket - // for example, {"started":"2021-03-16T08:00:00.000Z","exited":4, "errored": 1} - const aggregationCounts: AggregationCounts = this._pendingAggregates[sessionStartedTrunc]; - if (!aggregationCounts.started) { - aggregationCounts.started = new Date(sessionStartedTrunc).toISOString(); - } + // corresponds to aggregated sessions in one specific minute bucket + // for example, {"started":"2021-03-16T08:00:00.000Z","exited":4, "errored": 1} + const aggregationCounts: AggregationCounts = sessionFlusher.pendingAggregates[sessionStartedTrunc]; + if (!aggregationCounts.started) { + aggregationCounts.started = new Date(sessionStartedTrunc).toISOString(); + } - switch (status) { - case 'errored': - aggregationCounts.errored = (aggregationCounts.errored || 0) + 1; - return aggregationCounts.errored; - case 'ok': - aggregationCounts.exited = (aggregationCounts.exited || 0) + 1; - return aggregationCounts.exited; - default: - aggregationCounts.crashed = (aggregationCounts.crashed || 0) + 1; - return aggregationCounts.crashed; - } + switch (status) { + case 'errored': + aggregationCounts.errored = (aggregationCounts.errored || 0) + 1; + return aggregationCounts.errored; + case 'ok': + aggregationCounts.exited = (aggregationCounts.exited || 0) + 1; + return aggregationCounts.exited; + default: + aggregationCounts.crashed = (aggregationCounts.crashed || 0) + 1; + return aggregationCounts.crashed; } } diff --git a/packages/hub/test/hub.test.ts b/packages/hub/test/hub.test.ts index 4e865fa10412..22ab2ff12a1f 100644 --- a/packages/hub/test/hub.test.ts +++ b/packages/hub/test/hub.test.ts @@ -1,9 +1,44 @@ import { Event } from '@sentry/types'; -import { getCurrentHub, Hub, Scope } from '../src'; +import { + _invokeHubClient, + addHubBreadcrumb, + bindHubClient, + captureHubEvent, + captureHubException, + captureHubMessage, + configureHubScope, + getHubClient, + getCurrentHub, + getHubScope, + getHubStack, + getHubStackTop, + Hub, + isOlderThan, + getHubLastEventId, + popHubScope, + pushHubScope, + run, + withHubScope, +} from '../src/hub'; +import { addScopeBreadcrumb, addScopeEventProcessor, applyScopeToEvent, Scope, setScopeExtra } from '../src/scope'; const clientFn: any = jest.fn(); +function makeClient() { + return { + getOptions: jest.fn(), + captureEvent: jest.fn(), + captureException: jest.fn(), + close: jest.fn(), + flush: jest.fn(), + getDsn: jest.fn(), + getIntegration: jest.fn(), + setupIntegrations: jest.fn(), + captureMessage: jest.fn(), + }; +} + describe('Hub', () => { afterEach(() => { jest.restoreAllMocks(); @@ -11,79 +46,78 @@ describe('Hub', () => { }); test('call bindClient with provided client when constructing new instance', () => { - const testClient: any = { setupIntegrations: jest.fn() }; - const spy = jest.spyOn(Hub.prototype, 'bindClient'); - new Hub(testClient); - expect(spy).toHaveBeenCalledWith(testClient); + const testClient = makeClient(); + const hub = new Hub(testClient); + expect(getHubStackTop(hub).client).toBe(testClient); }); test('push process into stack', () => { const hub = new Hub(); - expect(hub.getStack()).toHaveLength(1); + expect(getHubStack(hub)).toHaveLength(1); }); test('pass in filled layer', () => { const hub = new Hub(clientFn); - expect(hub.getStack()).toHaveLength(1); + expect(getHubStack(hub)).toHaveLength(1); }); test("don't invoke client sync with wrong func", () => { const hub = new Hub(clientFn); // @ts-ignore we want to able to call private method - hub._invokeClient('funca', true); + _invokeHubClient(hub, 'funca', true); expect(clientFn).not.toHaveBeenCalled(); }); test('isOlderThan', () => { const hub = new Hub(); - expect(hub.isOlderThan(0)).toBeFalsy(); + expect(isOlderThan(hub, 0)).toBeFalsy(); }); describe('pushScope', () => { test('simple', () => { const localScope = new Scope(); - localScope.setExtra('a', 'b'); + setScopeExtra(localScope, 'a', 'b'); const hub = new Hub(undefined, localScope); - hub.pushScope(); - expect(hub.getStack()).toHaveLength(2); - expect(hub.getStack()[1].scope).not.toBe(localScope); - expect(((hub.getStack()[1].scope as Scope) as any)._extra).toEqual({ a: 'b' }); + pushHubScope(hub); + expect(getHubStack(hub)).toHaveLength(2); + expect(getHubStack(hub)[1].scope).not.toBe(localScope); + expect((getHubStack(hub)[1].scope as Scope).extra).toEqual({ a: 'b' }); }); test('inherit client', () => { const testClient: any = { bla: 'a' }; const hub = new Hub(testClient); - hub.pushScope(); - expect(hub.getStack()).toHaveLength(2); - expect(hub.getStack()[1].client).toBe(testClient); + pushHubScope(hub); + expect(getHubStack(hub)).toHaveLength(2); + expect(getHubStack(hub)[1].client).toBe(testClient); }); describe('bindClient', () => { test('should override current client', () => { - const testClient: any = { setupIntegrations: jest.fn() }; - const nextClient: any = { setupIntegrations: jest.fn() }; + const testClient = makeClient(); + const nextClient = makeClient(); const hub = new Hub(testClient); - hub.bindClient(nextClient); - expect(hub.getStack()).toHaveLength(1); - expect(hub.getStack()[0].client).toBe(nextClient); + bindHubClient(hub, nextClient); + expect(getHubStack(hub)).toHaveLength(1); + expect(getHubStack(hub)[0].client).toBe(nextClient); }); test('should bind client to the top-most layer', () => { const testClient: any = { bla: 'a' }; const nextClient: any = { foo: 'bar' }; const hub = new Hub(testClient); - hub.pushScope(); - hub.bindClient(nextClient); - expect(hub.getStack()).toHaveLength(2); - expect(hub.getStack()[0].client).toBe(testClient); - expect(hub.getStack()[1].client).toBe(nextClient); + pushHubScope(hub); + bindHubClient(hub, nextClient); + expect(getHubStack(hub)).toHaveLength(2); + expect(getHubStack(hub)[0].client).toBe(testClient); + expect(getHubStack(hub)[1].client).toBe(nextClient); }); test('should call setupIntegration method of passed client', () => { - const testClient: any = { setupIntegrations: jest.fn() }; - const nextClient: any = { setupIntegrations: jest.fn() }; + const testClient = makeClient(); + const nextClient = makeClient(); const hub = new Hub(testClient); - hub.bindClient(nextClient); + bindHubClient(hub, nextClient); expect(testClient.setupIntegrations).toHaveBeenCalled(); expect(nextClient.setupIntegrations).toHaveBeenCalled(); }); @@ -95,18 +129,18 @@ describe('Hub', () => { extra: { b: 3 }, }; const localScope = new Scope(); - localScope.setExtra('a', 'b'); + setScopeExtra(localScope, 'a', 'b'); const hub = new Hub({ a: 'b' } as any, localScope); - localScope.addEventProcessor(async (processedEvent: Event) => { + addScopeEventProcessor(localScope, async (processedEvent: Event) => { processedEvent.dist = '1'; return processedEvent; }); - hub.pushScope(); - const pushedScope = hub.getStackTop().scope; + pushHubScope(hub); + const pushedScope = getHubStackTop(hub).scope; - return pushedScope!.applyToEvent(event).then(final => { + return applyScopeToEvent(pushedScope!, event).then(final => { expect(final!.dist).toEqual('1'); }); }); @@ -114,10 +148,10 @@ describe('Hub', () => { test('popScope', () => { const hub = new Hub(); - hub.pushScope(); - expect(hub.getStack()).toHaveLength(2); - hub.popScope(); - expect(hub.getStack()).toHaveLength(1); + pushHubScope(hub); + expect(getHubStack(hub)).toHaveLength(2); + popHubScope(hub); + expect(getHubStack(hub)).toHaveLength(1); }); describe('withScope', () => { @@ -128,26 +162,26 @@ describe('Hub', () => { }); test('simple', () => { - hub.withScope(() => { - expect(hub.getStack()).toHaveLength(2); + withHubScope(hub, () => { + expect(getHubStack(hub)).toHaveLength(2); }); - expect(hub.getStack()).toHaveLength(1); + expect(getHubStack(hub)).toHaveLength(1); }); test('bindClient', () => { const testClient: any = { bla: 'a' }; - hub.withScope(() => { - hub.bindClient(testClient); - expect(hub.getStack()).toHaveLength(2); - expect(hub.getStack()[1].client).toBe(testClient); + withHubScope(hub, () => { + bindHubClient(hub, testClient); + expect(getHubStack(hub)).toHaveLength(2); + expect(getHubStack(hub)[1].client).toBe(testClient); }); - expect(hub.getStack()).toHaveLength(1); + expect(getHubStack(hub)).toHaveLength(1); }); test('should bubble up exceptions', () => { const error = new Error('test'); expect(() => { - hub.withScope(() => { + withHubScope(hub, () => { throw error; }); }).toThrow(error); @@ -157,150 +191,136 @@ describe('Hub', () => { test('getCurrentClient', () => { const testClient: any = { bla: 'a' }; const hub = new Hub(testClient); - expect(hub.getClient()).toBe(testClient); + expect(getHubClient(hub)).toBe(testClient); }); test('getStack', () => { const client: any = { a: 'b' }; const hub = new Hub(client); - expect(hub.getStack()[0].client).toBe(client); + expect(getHubStack(hub)[0].client).toBe(client); }); test('getStackTop', () => { const testClient: any = { bla: 'a' }; const hub = new Hub(); - hub.pushScope(); - hub.pushScope(); - hub.bindClient(testClient); - expect(hub.getStackTop().client).toEqual({ bla: 'a' }); + pushHubScope(hub); + pushHubScope(hub); + bindHubClient(hub, testClient); + expect(getHubStackTop(hub).client).toEqual({ bla: 'a' }); }); describe('configureScope', () => { test('should have an access to provide scope', () => { const localScope = new Scope(); - localScope.setExtra('a', 'b'); + setScopeExtra(localScope, 'a', 'b'); const hub = new Hub({} as any, localScope); const cb = jest.fn(); - hub.configureScope(cb); + configureHubScope(hub, cb); expect(cb).toHaveBeenCalledWith(localScope); }); test('should not invoke without client and scope', () => { const hub = new Hub(); const cb = jest.fn(); - hub.configureScope(cb); + configureHubScope(hub, cb); expect(cb).not.toHaveBeenCalled(); }); }); describe('captureException', () => { test('simple', () => { - const hub = new Hub(); - const spy = jest.spyOn(hub as any, '_invokeClient'); - hub.captureException('a'); - expect(spy).toHaveBeenCalled(); - expect(spy.mock.calls[0][0]).toBe('captureException'); - expect(spy.mock.calls[0][1]).toBe('a'); + const testClient = makeClient(); + const hub = new Hub(testClient); + captureHubException(hub, 'a'); + expect(testClient.captureException).toHaveBeenCalled(); + expect(testClient.captureException.mock.calls[0][0]).toBe('a'); }); test('should set event_id in hint', () => { - const hub = new Hub(); - const spy = jest.spyOn(hub as any, '_invokeClient'); - hub.captureException('a'); - // @ts-ignore Says mock object is type unknown - expect(spy.mock.calls[0][2].event_id).toBeTruthy(); + const testClient = makeClient(); + const hub = new Hub(testClient); + captureHubException(hub, 'a'); + expect(testClient.captureException.mock.calls[0][1].event_id).toBeTruthy(); }); test('should generate hint if not provided in the call', () => { - const hub = new Hub(); - const spy = jest.spyOn(hub as any, '_invokeClient'); + const testClient = makeClient(); + const hub = new Hub(testClient); const ex = new Error('foo'); - hub.captureException(ex); - // @ts-ignore Says mock object is type unknown - expect(spy.mock.calls[0][2].originalException).toBe(ex); - // @ts-ignore Says mock object is type unknown - expect(spy.mock.calls[0][2].syntheticException).toBeInstanceOf(Error); - // @ts-ignore Says mock object is type unknown - expect(spy.mock.calls[0][2].syntheticException.message).toBe('Sentry syntheticException'); + captureHubException(hub, ex); + expect(testClient.captureException.mock.calls[0][1].originalException).toBe(ex); + expect(testClient.captureException.mock.calls[0][1].syntheticException).toBeInstanceOf(Error); + expect(testClient.captureException.mock.calls[0][1].syntheticException.message).toBe('Sentry syntheticException'); }); }); describe('captureMessage', () => { test('simple', () => { - const hub = new Hub(); - const spy = jest.spyOn(hub as any, '_invokeClient'); - hub.captureMessage('a'); - expect(spy).toHaveBeenCalled(); - expect(spy.mock.calls[0][0]).toBe('captureMessage'); - expect(spy.mock.calls[0][1]).toBe('a'); + const testClient = makeClient(); + const hub = new Hub(testClient); + captureHubMessage(hub, 'a'); + expect(testClient.captureMessage).toHaveBeenCalled(); + expect(testClient.captureMessage.mock.calls[0][0]).toBe('a'); }); test('should set event_id in hint', () => { - const hub = new Hub(); - const spy = jest.spyOn(hub as any, '_invokeClient'); - hub.captureMessage('a'); - // @ts-ignore Says mock object is type unknown - expect(spy.mock.calls[0][3].event_id).toBeTruthy(); + const testClient = makeClient(); + const hub = new Hub(testClient); + captureHubMessage(hub, 'a'); + expect(testClient.captureMessage.mock.calls[0][2].event_id).toBeTruthy(); }); test('should generate hint if not provided in the call', () => { - const hub = new Hub(); - const spy = jest.spyOn(hub as any, '_invokeClient'); - hub.captureMessage('foo'); - // @ts-ignore Says mock object is type unknown - expect(spy.mock.calls[0][3].originalException).toBe('foo'); - // @ts-ignore Says mock object is type unknown - expect(spy.mock.calls[0][3].syntheticException).toBeInstanceOf(Error); - // @ts-ignore Says mock object is type unknown - expect(spy.mock.calls[0][3].syntheticException.message).toBe('foo'); + const testClient = makeClient(); + const hub = new Hub(testClient); + captureHubMessage(hub, 'foo'); + expect(testClient.captureMessage.mock.calls[0][2].originalException).toBe('foo'); + expect(testClient.captureMessage.mock.calls[0][2].syntheticException).toBeInstanceOf(Error); + expect(testClient.captureMessage.mock.calls[0][2].syntheticException.message).toBe('foo'); }); }); describe('captureEvent', () => { test('simple', () => { + const testClient = makeClient(); const event: Event = { extra: { b: 3 }, }; - const hub = new Hub(); - const spy = jest.spyOn(hub as any, '_invokeClient'); - hub.captureEvent(event); - expect(spy).toHaveBeenCalled(); - expect(spy.mock.calls[0][0]).toBe('captureEvent'); - expect(spy.mock.calls[0][1]).toBe(event); + const hub = new Hub(testClient); + captureHubEvent(hub, event); + expect(testClient.captureEvent).toHaveBeenCalled(); + expect(testClient.captureEvent.mock.calls[0][0]).toBe(event); }); test('should set event_id in hint', () => { + const testClient = makeClient(); const event: Event = { extra: { b: 3 }, }; - const hub = new Hub(); - const spy = jest.spyOn(hub as any, '_invokeClient'); - hub.captureEvent(event); - // @ts-ignore Says mock object is type unknown - expect(spy.mock.calls[0][2].event_id).toBeTruthy(); + const hub = new Hub(testClient); + captureHubEvent(hub, event); + expect(testClient.captureEvent.mock.calls[0][1].event_id).toBeTruthy(); }); test('sets lastEventId', () => { + const testClient = makeClient(); const event: Event = { extra: { b: 3 }, }; - const hub = new Hub(); - const spy = jest.spyOn(hub as any, '_invokeClient'); - hub.captureEvent(event); - // @ts-ignore Says mock object is type unknown - expect(spy.mock.calls[0][2].event_id).toEqual(hub.lastEventId()); + const hub = new Hub(testClient); + captureHubEvent(hub, event); + expect(testClient.captureEvent.mock.calls[0][1].event_id).toEqual(getHubLastEventId(hub)); }); test('transactions do not set lastEventId', () => { + const testClient = makeClient(); const event: Event = { extra: { b: 3 }, type: 'transaction', }; - const hub = new Hub(); - const spy = jest.spyOn(hub as any, '_invokeClient'); - hub.captureEvent(event); - // @ts-ignore Says mock object is type unknown - expect(spy.mock.calls[0][2].event_id).not.toEqual(hub.lastEventId()); + const hub = new Hub(testClient); + captureHubEvent(hub, event); + expect(testClient.captureEvent.mock.calls[0][1].event_id).not.toEqual(getHubLastEventId(hub)); }); }); @@ -309,8 +329,8 @@ describe('Hub', () => { extra: { b: 3 }, }; const hub = new Hub(); - const eventId = hub.captureEvent(event); - expect(eventId).toBe(hub.lastEventId()); + const eventId = captureHubEvent(hub, event); + expect(eventId).toBe(getHubLastEventId(hub)); }); describe('run', () => { @@ -318,11 +338,11 @@ describe('Hub', () => { const currentHub = getCurrentHub(); const myScope = new Scope(); const myClient: any = { a: 'b' }; - myScope.setExtra('a', 'b'); + setScopeExtra(myScope, 'a', 'b'); const myHub = new Hub(myClient, myScope); - myHub.run(hub => { - expect(hub.getScope()).toBe(myScope); - expect(hub.getClient()).toBe(myClient); + run(myHub, hub => { + expect(getHubScope(hub)).toBe(myScope); + expect(getHubClient(hub)).toBe(myClient); expect(hub).toBe(getCurrentHub()); }); expect(currentHub).toBe(getCurrentHub()); @@ -332,7 +352,7 @@ describe('Hub', () => { const hub = new Hub(); const error = new Error('test'); expect(() => { - hub.run(() => { + run(hub, () => { throw error; }); }).toThrow(error); @@ -343,12 +363,11 @@ describe('Hub', () => { test('withScope', () => { expect.assertions(6); const hub = new Hub(clientFn); - hub.addBreadcrumb({ message: 'My Breadcrumb' }); - hub.withScope(scope => { - scope.addBreadcrumb({ message: 'scope breadcrumb' }); + addHubBreadcrumb(hub, { message: 'My Breadcrumb' }); + withHubScope(hub, scope => { + addScopeBreadcrumb(scope, { message: 'scope breadcrumb' }); const event: Event = {}; - void scope - .applyToEvent(event) + void applyScopeToEvent(scope, event) .then((appliedEvent: Event | null) => { expect(appliedEvent).toBeTruthy(); expect(appliedEvent!.breadcrumbs).toHaveLength(2); diff --git a/packages/hub/test/scope.test.ts b/packages/hub/test/scope.test.ts index 9b1760dcd11c..f3c3728e2018 100644 --- a/packages/hub/test/scope.test.ts +++ b/packages/hub/test/scope.test.ts @@ -1,189 +1,238 @@ import { Event, EventHint } from '@sentry/types'; import { getGlobalObject } from '@sentry/utils'; -import { addGlobalEventProcessor, Scope } from '../src'; +import { + addGlobalEventProcessor, + addScopeBreadcrumb, + addScopeEventProcessor, + addScopeListener, + applyScopeToEvent, + clearScope, + clearScopeBreadcrumbs, + cloneScope, + getScopeRequestSession, + getScopeSession, + Scope, + setScopeContext, + setScopeExtra, + setScopeExtras, + setScopeFingerprint, + setScopeLevel, + setScopeRequestSession, + setScopeSession, + setScopeSpan, + setScopeTag, + setScopeTags, + setScopeTransactionName, + setScopeUser, + updateScope, +} from '../src/scope'; +import { Session } from '../src/session'; describe('Scope', () => { afterEach(() => { jest.resetAllMocks(); - jest.useRealTimers(); getGlobalObject().__SENTRY__.globalEventProcessors = undefined; }); + describe('setScopeSession', () => { + test('given an session then set the session to the scope', () => { + // GIVEN + const session = new Session(); + const scope = new Scope(); + // WHEN + setScopeSession(scope, session); + // THEN + expect(getScopeSession(scope)).toEqual(session); + }); + test('given an undefined or null session then removes the existing session', () => { + // GIVEN + const session = new Session(); + const scope = new Scope(); + setScopeSession(scope, session); + // WHEN + setScopeSession(scope, undefined); + // THEN + expect(getScopeSession(scope)).toBeUndefined(); + }); + }); + describe('attributes modification', () => { test('setFingerprint', () => { const scope = new Scope(); - scope.setFingerprint(['abcd']); - expect((scope as any)._fingerprint).toEqual(['abcd']); + setScopeFingerprint(scope, ['abcd']); + expect(scope.fingerprint).toEqual(['abcd']); }); test('setExtra', () => { const scope = new Scope(); - scope.setExtra('a', 1); - expect((scope as any)._extra).toEqual({ a: 1 }); + setScopeExtra(scope, 'a', 1); + expect(scope.extra).toEqual({ a: 1 }); }); test('setExtras', () => { const scope = new Scope(); - scope.setExtras({ a: 1 }); - expect((scope as any)._extra).toEqual({ a: 1 }); + setScopeExtras(scope, { a: 1 }); + expect(scope.extra).toEqual({ a: 1 }); }); test('setExtras with undefined overrides the value', () => { const scope = new Scope(); - scope.setExtra('a', 1); - scope.setExtras({ a: undefined }); - expect((scope as any)._extra).toEqual({ a: undefined }); + setScopeExtra(scope, 'a', 1); + setScopeExtras(scope, { a: undefined }); + expect(scope.extra).toEqual({ a: undefined }); }); test('setTag', () => { const scope = new Scope(); - scope.setTag('a', 'b'); - expect((scope as any)._tags).toEqual({ a: 'b' }); + setScopeTag(scope, 'a', 'b'); + expect(scope.tags).toEqual({ a: 'b' }); }); test('setTags', () => { const scope = new Scope(); - scope.setTags({ a: 'b' }); - expect((scope as any)._tags).toEqual({ a: 'b' }); + setScopeTags(scope, { a: 'b' }); + expect(scope.tags).toEqual({ a: 'b' }); }); test('setUser', () => { const scope = new Scope(); - scope.setUser({ id: '1' }); - expect((scope as any)._user).toEqual({ id: '1' }); + setScopeUser(scope, { id: '1' }); + expect(scope.user).toEqual({ id: '1' }); }); test('setUser with null unsets the user', () => { const scope = new Scope(); - scope.setUser({ id: '1' }); - scope.setUser(null); - expect((scope as any)._user).toEqual({}); + setScopeUser(scope, { id: '1' }); + setScopeUser(scope, null); + expect(scope.user).toEqual({}); }); test('addBreadcrumb', () => { const scope = new Scope(); - scope.addBreadcrumb({ message: 'test' }); - expect((scope as any)._breadcrumbs[0]).toHaveProperty('message', 'test'); + addScopeBreadcrumb(scope, { message: 'test' }); + expect(scope.breadcrumbs[0]).toHaveProperty('message', 'test'); }); test('addBreadcrumb can be limited to hold up to N breadcrumbs', () => { const scope = new Scope(); for (let i = 0; i < 10; i++) { - scope.addBreadcrumb({ message: 'test' }, 5); + addScopeBreadcrumb(scope, { message: 'test' }, 5); } - expect((scope as any)._breadcrumbs).toHaveLength(5); + expect(scope.breadcrumbs).toHaveLength(5); }); test('addBreadcrumb cannot go over MAX_BREADCRUMBS value', () => { const scope = new Scope(); for (let i = 0; i < 111; i++) { - scope.addBreadcrumb({ message: 'test' }, 111); + addScopeBreadcrumb(scope, { message: 'test' }, 111); } - expect((scope as any)._breadcrumbs).toHaveLength(100); + expect(scope.breadcrumbs).toHaveLength(100); }); test('setLevel', () => { const scope = new Scope(); - scope.setLevel('critical'); - expect((scope as any)._level).toEqual('critical'); + setScopeLevel(scope, 'critical'); + expect(scope.level).toEqual('critical'); }); test('setTransactionName', () => { const scope = new Scope(); - scope.setTransactionName('/abc'); - expect((scope as any)._transactionName).toEqual('/abc'); + setScopeTransactionName(scope, '/abc'); + expect(scope.transactionName).toEqual('/abc'); }); test('setTransactionName with no value unsets it', () => { const scope = new Scope(); - scope.setTransactionName('/abc'); - scope.setTransactionName(); - expect((scope as any)._transactionName).toBeUndefined(); + setScopeTransactionName(scope, '/abc'); + setScopeTransactionName(scope); + expect(scope.transactionName).toBeUndefined(); }); test('setContext', () => { const scope = new Scope(); - scope.setContext('os', { id: '1' }); - expect((scope as any)._contexts.os).toEqual({ id: '1' }); + setScopeContext(scope, 'os', { id: '1' }); + expect(scope.contexts.os).toEqual({ id: '1' }); }); test('setContext with null unsets it', () => { const scope = new Scope(); - scope.setContext('os', { id: '1' }); - scope.setContext('os', null); - expect((scope as any)._user).toEqual({}); + setScopeContext(scope, 'os', { id: '1' }); + setScopeContext(scope, 'os', null); + expect(scope.user).toEqual({}); }); test('setSpan', () => { const scope = new Scope(); const span = { fake: 'span' } as any; - scope.setSpan(span); - expect((scope as any)._span).toEqual(span); + setScopeSpan(scope, span); + expect(scope.span).toEqual(span); }); test('setSpan with no value unsets it', () => { const scope = new Scope(); - scope.setSpan({ fake: 'span' } as any); - scope.setSpan(); - expect((scope as any)._span).toEqual(undefined); + const span = { fake: 'span' } as any; + setScopeSpan(scope, span); + setScopeSpan(scope); + expect(scope.span).toEqual(undefined); }); test('chaining', () => { const scope = new Scope(); - scope.setLevel('critical').setUser({ id: '1' }); - expect((scope as any)._level).toEqual('critical'); - expect((scope as any)._user).toEqual({ id: '1' }); + setScopeLevel(scope, 'critical'); + setScopeUser(scope, { id: '1' }); + expect(scope.level).toEqual('critical'); + expect(scope.user).toEqual({ id: '1' }); }); }); describe('clone', () => { test('basic inheritance', () => { const parentScope = new Scope(); - parentScope.setExtra('a', 1); - const scope = Scope.clone(parentScope); - expect((parentScope as any)._extra).toEqual((scope as any)._extra); + setScopeExtra(parentScope, 'a', 1); + const scope = cloneScope(parentScope); + expect(parentScope.extra).toEqual(scope.extra); }); test('_requestSession clone', () => { const parentScope = new Scope(); - parentScope.setRequestSession({ status: 'errored' }); - const scope = Scope.clone(parentScope); - expect(parentScope.getRequestSession()).toEqual(scope.getRequestSession()); + setScopeRequestSession(parentScope, { status: 'errored' }); + const scope = cloneScope(parentScope); + expect(getScopeRequestSession(parentScope)).toEqual(getScopeRequestSession(scope)); }); test('parent changed inheritance', () => { const parentScope = new Scope(); - const scope = Scope.clone(parentScope); - parentScope.setExtra('a', 2); - expect((scope as any)._extra).toEqual({}); - expect((parentScope as any)._extra).toEqual({ a: 2 }); + const scope = cloneScope(parentScope); + setScopeExtra(parentScope, 'a', 2); + expect(scope.extra).toEqual({}); + expect(parentScope.extra).toEqual({ a: 2 }); }); test('child override inheritance', () => { const parentScope = new Scope(); - parentScope.setExtra('a', 1); + setScopeExtra(parentScope, 'a', 1); - const scope = Scope.clone(parentScope); - scope.setExtra('a', 2); - expect((parentScope as any)._extra).toEqual({ a: 1 }); - expect((scope as any)._extra).toEqual({ a: 2 }); + const scope = cloneScope(parentScope); + setScopeExtra(scope, 'a', 2); + expect(parentScope.extra).toEqual({ a: 1 }); + expect(scope.extra).toEqual({ a: 2 }); }); test('child override should set the value of parent _requestSession', () => { // Test that ensures if the status value of `status` of `_requestSession` is changed in a child scope // that it should also change in parent scope because we are copying the reference to the object const parentScope = new Scope(); - parentScope.setRequestSession({ status: 'errored' }); + setScopeRequestSession(parentScope, { status: 'errored' }); - const scope = Scope.clone(parentScope); - const requestSession = scope.getRequestSession(); + const scope = cloneScope(parentScope); + const requestSession = getScopeRequestSession(scope); if (requestSession) { requestSession.status = 'ok'; } - expect(parentScope.getRequestSession()).toEqual({ status: 'ok' }); - expect(scope.getRequestSession()).toEqual({ status: 'ok' }); + expect(getScopeRequestSession(parentScope)).toEqual({ status: 'ok' }); + expect(getScopeRequestSession(scope)).toEqual({ status: 'ok' }); }); }); @@ -191,16 +240,17 @@ describe('Scope', () => { test('basic usage', () => { expect.assertions(8); const scope = new Scope(); - scope.setExtra('a', 2); - scope.setTag('a', 'b'); - scope.setUser({ id: '1' }); - scope.setFingerprint(['abcd']); - scope.setLevel('warning'); - scope.setTransactionName('/abc'); - scope.addBreadcrumb({ message: 'test' }); - scope.setContext('os', { id: '1' }); + + setScopeExtra(scope, 'a', 2); + setScopeTag(scope, 'a', 'b'); + setScopeUser(scope, { id: '1' }); + setScopeFingerprint(scope, ['abcd']); + setScopeLevel(scope, 'warning'); + setScopeTransactionName(scope, '/abc'); + addScopeBreadcrumb(scope, { message: 'test' }); + setScopeContext(scope, 'os', { id: '1' }); const event: Event = {}; - return scope.applyToEvent(event).then(processedEvent => { + return applyScopeToEvent(scope, event).then(processedEvent => { expect(processedEvent!.extra).toEqual({ a: 2 }); expect(processedEvent!.tags).toEqual({ a: 'b' }); expect(processedEvent!.user).toEqual({ id: '1' }); @@ -215,12 +265,12 @@ describe('Scope', () => { test('merge with existing event data', () => { expect.assertions(8); const scope = new Scope(); - scope.setExtra('a', 2); - scope.setTag('a', 'b'); - scope.setUser({ id: '1' }); - scope.setFingerprint(['abcd']); - scope.addBreadcrumb({ message: 'test' }); - scope.setContext('server', { id: '2' }); + setScopeExtra(scope, 'a', 2); + setScopeTag(scope, 'a', 'b'); + setScopeUser(scope, { id: '1' }); + setScopeFingerprint(scope, ['abcd']); + addScopeBreadcrumb(scope, { message: 'test' }); + setScopeContext(scope, 'server', { id: '2' }); const event: Event = { breadcrumbs: [{ message: 'test1' }], contexts: { os: { id: '1' } }, @@ -229,7 +279,7 @@ describe('Scope', () => { tags: { b: 'c' }, user: { id: '3' }, }; - return scope.applyToEvent(event).then(processedEvent => { + return applyScopeToEvent(scope, event).then(processedEvent => { expect(processedEvent!.extra).toEqual({ a: 2, b: 3 }); expect(processedEvent!.tags).toEqual({ a: 'b', b: 'c' }); expect(processedEvent!.user).toEqual({ id: '3' }); @@ -250,25 +300,25 @@ describe('Scope', () => { // @ts-ignore we want to be able to assign string value event.fingerprint = 'foo'; - await scope.applyToEvent(event).then(processedEvent => { + await applyScopeToEvent(scope, event).then(processedEvent => { expect(processedEvent!.fingerprint).toEqual(['foo']); }); // @ts-ignore we want to be able to assign string value event.fingerprint = 'bar'; - await scope.applyToEvent(event).then(processedEvent => { + await applyScopeToEvent(scope, event).then(processedEvent => { expect(processedEvent!.fingerprint).toEqual(['bar']); }); }); test('should merge fingerprint from event and scope', async () => { const scope = new Scope(); - scope.setFingerprint(['foo']); + setScopeFingerprint(scope, ['foo']); const event: Event = { fingerprint: ['bar'], }; - await scope.applyToEvent(event).then(processedEvent => { + await applyScopeToEvent(scope, event).then(processedEvent => { expect(processedEvent!.fingerprint).toEqual(['bar', 'foo']); }); }); @@ -276,7 +326,7 @@ describe('Scope', () => { test('should remove default empty fingerprint array if theres no data available', async () => { const scope = new Scope(); const event: Event = {}; - await scope.applyToEvent(event).then(processedEvent => { + await applyScopeToEvent(scope, event).then(processedEvent => { expect(processedEvent!.fingerprint).toEqual(undefined); }); }); @@ -284,10 +334,10 @@ describe('Scope', () => { test('scope level should have priority over event level', () => { expect.assertions(1); const scope = new Scope(); - scope.setLevel('warning'); + setScopeLevel(scope, 'warning'); const event: Event = {}; event.level = 'critical'; - return scope.applyToEvent(event).then(processedEvent => { + return applyScopeToEvent(scope, event).then(processedEvent => { expect(processedEvent!.level).toEqual('warning'); }); }); @@ -295,10 +345,10 @@ describe('Scope', () => { test('scope transaction should have priority over event transaction', () => { expect.assertions(1); const scope = new Scope(); - scope.setTransactionName('/abc'); + setScopeTransactionName(scope, '/abc'); const event: Event = {}; event.transaction = '/cdf'; - return scope.applyToEvent(event).then(processedEvent => { + return applyScopeToEvent(scope, event).then(processedEvent => { expect(processedEvent!.transaction).toEqual('/abc'); }); }); @@ -311,10 +361,10 @@ describe('Scope', () => { fake: 'span', getTraceContext: () => ({ a: 'b' }), } as any; - scope.setSpan(span); + setScopeSpan(scope, span); const event: Event = {}; - return scope.applyToEvent(event).then(processedEvent => { - expect((processedEvent!.contexts!.trace as any).a).toEqual('b'); + return applyScopeToEvent(scope, event).then(processedEvent => { + expect(processedEvent!.contexts!.trace.a).toEqual('b'); }); }); @@ -325,14 +375,14 @@ describe('Scope', () => { fake: 'span', getTraceContext: () => ({ a: 'b' }), } as any; - scope.setSpan(span); + setScopeSpan(scope, span); const event: Event = { contexts: { trace: { a: 'c' }, }, }; - return scope.applyToEvent(event).then(processedEvent => { - expect((processedEvent!.contexts!.trace as any).a).toEqual('c'); + return applyScopeToEvent(scope, event).then(processedEvent => { + expect(processedEvent!.contexts!.trace.a).toEqual('c'); }); }); @@ -345,9 +395,9 @@ describe('Scope', () => { name: 'fake transaction', } as any; transaction.transaction = transaction; // because this is a transaction, its transaction pointer points to itself - scope.setSpan(transaction); + setScopeSpan(scope, transaction); const event: Event = {}; - return scope.applyToEvent(event).then(processedEvent => { + return applyScopeToEvent(scope, event).then(processedEvent => { expect(processedEvent!.tags!.transaction).toEqual('fake transaction'); }); }); @@ -361,33 +411,33 @@ describe('Scope', () => { getTraceContext: () => ({ a: 'b' }), transaction, } as any; - scope.setSpan(span); + setScopeSpan(scope, span); const event: Event = {}; - return scope.applyToEvent(event).then(processedEvent => { + return applyScopeToEvent(scope, event).then(processedEvent => { expect(processedEvent!.tags!.transaction).toEqual('fake transaction'); }); }); test('clear', () => { const scope = new Scope(); - scope.setExtra('a', 2); - scope.setTag('a', 'b'); - scope.setUser({ id: '1' }); - scope.setFingerprint(['abcd']); - scope.addBreadcrumb({ message: 'test' }); - scope.setRequestSession({ status: 'ok' }); - expect((scope as any)._extra).toEqual({ a: 2 }); - scope.clear(); - expect((scope as any)._extra).toEqual({}); - expect((scope as any)._requestSession).toEqual(undefined); + setScopeExtra(scope, 'a', 2); + setScopeTag(scope, 'a', 'b'); + setScopeUser(scope, { id: '1' }); + setScopeFingerprint(scope, ['abcd']); + addScopeBreadcrumb(scope, { message: 'test' }); + setScopeRequestSession(scope, { status: 'ok' }); + expect(scope.extra).toEqual({ a: 2 }); + clearScope(scope); + expect(scope.extra).toEqual({}); + expect(scope.requestSession).toEqual(undefined); }); test('clearBreadcrumbs', () => { const scope = new Scope(); - scope.addBreadcrumb({ message: 'test' }); - expect((scope as any)._breadcrumbs).toHaveLength(1); - scope.clearBreadcrumbs(); - expect((scope as any)._breadcrumbs).toHaveLength(0); + addScopeBreadcrumb(scope, { message: 'test' }); + expect(scope.breadcrumbs).toHaveLength(1); + clearScopeBreadcrumbs(scope); + expect(scope.breadcrumbs).toHaveLength(0); }); describe('update', () => { @@ -395,24 +445,24 @@ describe('Scope', () => { beforeEach(() => { scope = new Scope(); - scope.setTags({ foo: '1', bar: '2' }); - scope.setExtras({ foo: '1', bar: '2' }); - scope.setContext('foo', { id: '1' }); - scope.setContext('bar', { id: '2' }); - scope.setUser({ id: '1337' }); - scope.setLevel('info'); - scope.setFingerprint(['foo']); - scope.setRequestSession({ status: 'ok' }); + setScopeTags(scope, { foo: '1', bar: '2' }); + setScopeExtras(scope, { foo: '1', bar: '2' }); + setScopeContext(scope, 'foo', { id: '1' }); + setScopeContext(scope, 'bar', { id: '2' }); + setScopeUser(scope, { id: '1337' }); + setScopeLevel(scope, 'info'); + setScopeFingerprint(scope, ['foo']); + setScopeRequestSession(scope, { status: 'ok' }); }); test('given no data, returns the original scope', () => { - const updatedScope = scope.update(); + const updatedScope = updateScope(scope); expect(updatedScope).toEqual(scope); }); test('given neither function, Scope or plain object, returns original scope', () => { // @ts-ignore we want to be able to update scope with string - const updatedScope = scope.update('wat'); + const updatedScope = updateScope(scope, 'wat'); expect(updatedScope).toEqual(scope); }); @@ -421,79 +471,79 @@ describe('Scope', () => { .fn() .mockImplementationOnce(v => v) .mockImplementationOnce(v => { - v.setTag('foo', 'bar'); + setScopeTag(v, 'foo', 'bar'); return v; }); - let updatedScope = scope.update(cb); + let updatedScope = updateScope(scope, cb); expect(cb).toHaveBeenNthCalledWith(1, scope); expect(updatedScope).toEqual(scope); - updatedScope = scope.update(cb); + updatedScope = updateScope(scope, cb); expect(cb).toHaveBeenNthCalledWith(2, scope); expect(updatedScope).toEqual(scope); }); test('given callback function, when it doesnt return instanceof Scope, ignore it and return original scope', () => { const cb = jest.fn().mockImplementationOnce(_v => 'wat'); - const updatedScope = scope.update(cb); + const updatedScope = updateScope(scope, cb); expect(cb).toHaveBeenCalledWith(scope); expect(updatedScope).toEqual(scope); }); test('given another instance of Scope, it should merge two together, with the passed scope having priority', () => { const localScope = new Scope(); - localScope.setTags({ bar: '3', baz: '4' }); - localScope.setExtras({ bar: '3', baz: '4' }); - localScope.setContext('bar', { id: '3' }); - localScope.setContext('baz', { id: '4' }); - localScope.setUser({ id: '42' }); - localScope.setLevel('warning'); - localScope.setFingerprint(['bar']); - (localScope as any)._requestSession = { status: 'ok' }; - - const updatedScope = scope.update(localScope) as any; - - expect(updatedScope._tags).toEqual({ + setScopeTags(localScope, { bar: '3', baz: '4' }); + setScopeExtras(localScope, { bar: '3', baz: '4' }); + setScopeContext(localScope, 'bar', { id: '3' }); + setScopeContext(localScope, 'baz', { id: '4' }); + setScopeUser(localScope, { id: '42' }); + setScopeLevel(localScope, 'warning'); + setScopeFingerprint(localScope, ['bar']); + localScope.requestSession = { status: 'ok' }; + + const updatedScope = updateScope(scope, localScope); + + expect(updatedScope.tags).toEqual({ bar: '3', baz: '4', foo: '1', }); - expect(updatedScope._extra).toEqual({ + expect(updatedScope.extra).toEqual({ bar: '3', baz: '4', foo: '1', }); - expect(updatedScope._contexts).toEqual({ + expect(updatedScope.contexts).toEqual({ bar: { id: '3' }, baz: { id: '4' }, foo: { id: '1' }, }); - expect(updatedScope._user).toEqual({ id: '42' }); - expect(updatedScope._level).toEqual('warning'); - expect(updatedScope._fingerprint).toEqual(['bar']); - expect(updatedScope._requestSession.status).toEqual('ok'); + expect(updatedScope.user).toEqual({ id: '42' }); + expect(updatedScope.level).toEqual('warning'); + expect(updatedScope.fingerprint).toEqual(['bar']); + expect(updatedScope.requestSession?.status).toEqual('ok'); }); test('given an empty instance of Scope, it should preserve all the original scope data', () => { - const updatedScope = scope.update(new Scope()) as any; + const updatedScope = updateScope(scope, new Scope()); - expect(updatedScope._tags).toEqual({ + expect(updatedScope.tags).toEqual({ bar: '2', foo: '1', }); - expect(updatedScope._extra).toEqual({ + expect(updatedScope.extra).toEqual({ bar: '2', foo: '1', }); - expect(updatedScope._contexts).toEqual({ + expect(updatedScope.contexts).toEqual({ bar: { id: '2' }, foo: { id: '1' }, }); - expect(updatedScope._user).toEqual({ id: '1337' }); - expect(updatedScope._level).toEqual('info'); - expect(updatedScope._fingerprint).toEqual(['foo']); - expect(updatedScope._requestSession.status).toEqual('ok'); + expect(updatedScope.user).toEqual({ id: '1337' }); + expect(updatedScope.level).toEqual('info'); + expect(updatedScope.fingerprint).toEqual(['foo']); + expect(updatedScope.requestSession?.status).toEqual('ok'); }); test('given a plain object, it should merge two together, with the passed object having priority', () => { @@ -506,27 +556,27 @@ describe('Scope', () => { user: { id: '42' }, requestSession: { status: 'errored' }, }; - const updatedScope = scope.update(localAttributes) as any; + const updatedScope = updateScope(scope, localAttributes); - expect(updatedScope._tags).toEqual({ + expect(updatedScope.tags).toEqual({ bar: '3', baz: '4', foo: '1', }); - expect(updatedScope._extra).toEqual({ + expect(updatedScope.extra).toEqual({ bar: '3', baz: '4', foo: '1', }); - expect(updatedScope._contexts).toEqual({ + expect(updatedScope.contexts).toEqual({ bar: { id: '3' }, baz: { id: '4' }, foo: { id: '1' }, }); - expect(updatedScope._user).toEqual({ id: '42' }); - expect(updatedScope._level).toEqual('warning'); - expect(updatedScope._fingerprint).toEqual(['bar']); - expect(updatedScope._requestSession).toEqual({ status: 'errored' }); + expect(updatedScope.user).toEqual({ id: '42' }); + expect(updatedScope.level).toEqual('warning'); + expect(updatedScope.fingerprint).toEqual(['bar']); + expect(updatedScope.requestSession).toEqual({ status: 'errored' }); }); }); @@ -537,21 +587,21 @@ describe('Scope', () => { extra: { b: 3 }, }; const localScope = new Scope(); - localScope.setExtra('a', 'b'); - localScope.addEventProcessor((processedEvent: Event) => { + setScopeExtra(localScope, 'a', 'b'); + addScopeEventProcessor(localScope, (processedEvent: Event) => { expect(processedEvent.extra).toEqual({ a: 'b', b: 3 }); return processedEvent; }); - localScope.addEventProcessor((processedEvent: Event) => { + addScopeEventProcessor(localScope, (processedEvent: Event) => { processedEvent.dist = '1'; return processedEvent; }); - localScope.addEventProcessor((processedEvent: Event) => { + addScopeEventProcessor(localScope, (processedEvent: Event) => { expect(processedEvent.dist).toEqual('1'); return processedEvent; }); - return localScope.applyToEvent(event).then(final => { + return applyScopeToEvent(localScope, event).then(final => { expect(final!.dist).toEqual('1'); }); }); @@ -562,24 +612,24 @@ describe('Scope', () => { extra: { b: 3 }, }; const localScope = new Scope(); - localScope.setExtra('a', 'b'); + setScopeExtra(localScope, 'a', 'b'); addGlobalEventProcessor((processedEvent: Event) => { processedEvent.dist = '1'; return processedEvent; }); - localScope.addEventProcessor((processedEvent: Event) => { + addScopeEventProcessor(localScope, (processedEvent: Event) => { expect(processedEvent.extra).toEqual({ a: 'b', b: 3 }); return processedEvent; }); - localScope.addEventProcessor((processedEvent: Event) => { + addScopeEventProcessor(localScope, (processedEvent: Event) => { expect(processedEvent.dist).toEqual('1'); return processedEvent; }); - return localScope.applyToEvent(event).then(final => { + return applyScopeToEvent(localScope, event).then(final => { expect(final!.dist).toEqual('1'); }); }); @@ -591,14 +641,15 @@ describe('Scope', () => { extra: { b: 3 }, }; const localScope = new Scope(); - localScope.setExtra('a', 'b'); + setScopeExtra(localScope, 'a', 'b'); const callCounter = jest.fn(); - localScope.addEventProcessor((processedEvent: Event) => { + addScopeEventProcessor(localScope, (processedEvent: Event) => { callCounter(1); expect(processedEvent.extra).toEqual({ a: 'b', b: 3 }); return processedEvent; }); - localScope.addEventProcessor( + addScopeEventProcessor( + localScope, async (processedEvent: Event) => new Promise(resolve => { callCounter(2); @@ -610,12 +661,12 @@ describe('Scope', () => { jest.runAllTimers(); }), ); - localScope.addEventProcessor((processedEvent: Event) => { + addScopeEventProcessor(localScope, (processedEvent: Event) => { callCounter(4); return processedEvent; }); - return localScope.applyToEvent(event).then(processedEvent => { + return applyScopeToEvent(localScope, event).then(processedEvent => { expect(callCounter.mock.calls[0][0]).toBe(1); expect(callCounter.mock.calls[1][0]).toBe(2); expect(callCounter.mock.calls[2][0]).toBe(3); @@ -631,14 +682,15 @@ describe('Scope', () => { extra: { b: 3 }, }; const localScope = new Scope(); - localScope.setExtra('a', 'b'); + setScopeExtra(localScope, 'a', 'b'); const callCounter = jest.fn(); - localScope.addEventProcessor((processedEvent: Event) => { + addScopeEventProcessor(localScope, (processedEvent: Event) => { callCounter(1); expect(processedEvent.extra).toEqual({ a: 'b', b: 3 }); return processedEvent; }); - localScope.addEventProcessor( + addScopeEventProcessor( + localScope, async (_processedEvent: Event) => new Promise((_, reject) => { setTimeout(() => { @@ -647,12 +699,12 @@ describe('Scope', () => { jest.runAllTimers(); }), ); - localScope.addEventProcessor((processedEvent: Event) => { + addScopeEventProcessor(localScope, (processedEvent: Event) => { callCounter(4); return processedEvent; }); - return localScope.applyToEvent(event).then(null, reason => { + return applyScopeToEvent(localScope, event).then(null, reason => { expect(reason).toEqual('bla'); }); }); @@ -663,9 +715,9 @@ describe('Scope', () => { extra: { b: 3 }, }; const localScope = new Scope(); - localScope.setExtra('a', 'b'); - localScope.addEventProcessor(async (_: Event) => null); - return localScope.applyToEvent(event).then(processedEvent => { + setScopeExtra(localScope, 'a', 'b'); + addScopeEventProcessor(localScope, async (_: Event) => null); + return applyScopeToEvent(localScope, event).then(processedEvent => { expect(processedEvent).toBeNull(); }); }); @@ -676,13 +728,13 @@ describe('Scope', () => { extra: { b: 3 }, }; const localScope = new Scope(); - localScope.setExtra('a', 'b'); - localScope.addEventProcessor(async (internalEvent: Event, hint?: EventHint) => { + setScopeExtra(localScope, 'a', 'b'); + addScopeEventProcessor(localScope, async (internalEvent: Event, hint?: EventHint) => { expect(hint).toBeTruthy(); expect(hint!.syntheticException).toBeTruthy(); return internalEvent; }); - return localScope.applyToEvent(event, { syntheticException: new Error('what') }).then(processedEvent => { + return applyScopeToEvent(localScope, event, { syntheticException: new Error('what') }).then(processedEvent => { expect(processedEvent).toEqual(event); }); }); @@ -691,11 +743,11 @@ describe('Scope', () => { jest.useFakeTimers(); const scope = new Scope(); const listener = jest.fn(); - scope.addScopeListener(listener); - scope.setExtra('a', 2); + addScopeListener(scope, listener); + setScopeExtra(scope, 'a', 2); jest.runAllTimers(); expect(listener).toHaveBeenCalled(); - expect(listener.mock.calls[0][0]._extra).toEqual({ a: 2 }); + expect(listener.mock.calls[0][0].extra).toEqual({ a: 2 }); }); }); }); diff --git a/packages/hub/test/session.test.ts b/packages/hub/test/session.test.ts index f25e5ad4189b..9ead830b211a 100644 --- a/packages/hub/test/session.test.ts +++ b/packages/hub/test/session.test.ts @@ -1,11 +1,11 @@ import { SessionContext } from '@sentry/types'; import { timestampInSeconds } from '@sentry/utils'; -import { Session } from '../src/session'; +import { closeSession, Session, toJSON, updateSession } from '../src/session'; describe('Session', () => { it('initializes with the proper defaults', () => { - const session = new Session().toJSON(); + const session = toJSON(new Session()); // Grab current year to check if we are converting from sec -> ms correctly const currentYear = new Date(timestampInSeconds() * 1000).toISOString().slice(0, 4); @@ -83,10 +83,10 @@ describe('Session', () => { const DEFAULT_OUT = { duration: expect.any(Number), timestamp: expect.any(String) }; const session = new Session(); - const initSessionProps = session.toJSON(); + const initSessionProps = toJSON(session); - session.update(test[1]); - expect(session.toJSON()).toEqual({ ...initSessionProps, ...DEFAULT_OUT, ...test[2] }); + updateSession(session, test[1]); + expect(toJSON(session)).toEqual({ ...initSessionProps, ...DEFAULT_OUT, ...test[2] }); }); }); @@ -94,7 +94,7 @@ describe('Session', () => { it('exits a normal session', () => { const session = new Session(); expect(session.status).toEqual('ok'); - session.close(); + closeSession(session); expect(session.status).toEqual('exited'); }); @@ -102,16 +102,16 @@ describe('Session', () => { const session = new Session(); expect(session.status).toEqual('ok'); - session.close('abnormal'); + closeSession(session, 'abnormal'); expect(session.status).toEqual('abnormal'); }); it('only changes status ok to exited', () => { const session = new Session(); - session.update({ status: 'crashed' }); + updateSession(session, { status: 'crashed' }); expect(session.status).toEqual('crashed'); - session.close(); + closeSession(session); expect(session.status).toEqual('crashed'); }); }); diff --git a/packages/hub/test/sessionflusher.test.ts b/packages/hub/test/sessionflusher.test.ts index 3c7dc9782615..c8953e8d51f5 100644 --- a/packages/hub/test/sessionflusher.test.ts +++ b/packages/hub/test/sessionflusher.test.ts @@ -1,89 +1,98 @@ -import { SessionFlusher } from '../src/sessionflusher'; +import { EventStatus } from '@sentry/types'; -describe('Session Flusher', () => { - let sendSession: jest.Mock; - let transport: { - sendEvent: jest.Mock; - sendSession: jest.Mock; - close: jest.Mock; - }; - - beforeEach(() => { - jest.useFakeTimers(); - sendSession = jest.fn(() => Promise.resolve({ status: 'success' })); - transport = { - sendEvent: jest.fn(), - sendSession, - close: jest.fn(), - }; - }); +import { + _incrementSessionStatusCount, + closeSessionFlusher, + getSessionAggregates, + SessionFlusher, +} from '../src/sessionflusher'; - afterEach(() => { - jest.restoreAllMocks(); - }); +function makeTransporter() { + return jest.fn(() => Promise.resolve({ status: 'success' as EventStatus })); +} - test('test incrementSessionStatusCount updates the internal SessionFlusher state', () => { - const flusher = new SessionFlusher(transport, { release: '1.0.0', environment: 'dev' }); +describe('Session Flusher', () => { + beforeEach(() => jest.useFakeTimers()); + afterEach(() => jest.clearAllTimers()); + test('test incrementSessionStatusCount updates the internal SessionFlusher state', () => { + const transporter = makeTransporter(); + const flusher = new SessionFlusher({ + transporter: transporter, + release: '1.0.0', + environment: 'dev', + }); const date = new Date('2021-04-08T12:18:23.043Z'); - let count = (flusher as any)._incrementSessionStatusCount('ok', date); + + let count = _incrementSessionStatusCount(flusher, 'ok', date); expect(count).toEqual(1); - count = (flusher as any)._incrementSessionStatusCount('ok', date); + count = _incrementSessionStatusCount(flusher, 'ok', date); expect(count).toEqual(2); - count = (flusher as any)._incrementSessionStatusCount('errored', date); + count = _incrementSessionStatusCount(flusher, 'errored', date); expect(count).toEqual(1); date.setMinutes(date.getMinutes() + 1); - count = (flusher as any)._incrementSessionStatusCount('ok', date); + count = _incrementSessionStatusCount(flusher, 'ok', date); expect(count).toEqual(1); - count = (flusher as any)._incrementSessionStatusCount('errored', date); + count = _incrementSessionStatusCount(flusher, 'errored', date); expect(count).toEqual(1); - expect(flusher.getSessionAggregates().aggregates).toEqual([ + expect(getSessionAggregates(flusher).aggregates).toEqual([ { errored: 1, exited: 2, started: '2021-04-08T12:18:00.000Z' }, { errored: 1, exited: 1, started: '2021-04-08T12:19:00.000Z' }, ]); - expect(flusher.getSessionAggregates().attrs).toEqual({ release: '1.0.0', environment: 'dev' }); + expect(getSessionAggregates(flusher).attrs).toEqual({ release: '1.0.0', environment: 'dev' }); }); test('test undefined attributes are excluded, on incrementSessionStatusCount call', () => { - const flusher = new SessionFlusher(transport, { release: '1.0.0' }); - + const transporter = makeTransporter(); + const flusher = new SessionFlusher({ transporter, release: '1.0.0' }); const date = new Date('2021-04-08T12:18:23.043Z'); - (flusher as any)._incrementSessionStatusCount('ok', date); - (flusher as any)._incrementSessionStatusCount('errored', date); - expect(flusher.getSessionAggregates()).toEqual({ + _incrementSessionStatusCount(flusher, 'ok', date); + _incrementSessionStatusCount(flusher, 'errored', date); + + expect(getSessionAggregates(flusher)).toEqual({ aggregates: [{ errored: 1, exited: 1, started: '2021-04-08T12:18:00.000Z' }], attrs: { release: '1.0.0' }, }); }); - test('flush is called every 60 seconds after initialisation of an instance of SessionFlusher', () => { - const flusher = new SessionFlusher(transport, { release: '1.0.0', environment: 'dev' }); - const flusherFlushFunc = jest.spyOn(flusher, 'flush'); + test('flush is called every ~60 seconds after initialisation of an instance of SessionFlusher', () => { + const transporter = makeTransporter(); + const flusher = new SessionFlusher({ transporter, release: '1.0.0', environment: 'dev' }); + jest.advanceTimersByTime(59000); - expect(flusherFlushFunc).toHaveBeenCalledTimes(0); + expect(transporter).toHaveBeenCalledTimes(0); + + _incrementSessionStatusCount(flusher, 'ok', new Date()); jest.advanceTimersByTime(2000); - expect(flusherFlushFunc).toHaveBeenCalledTimes(1); + + _incrementSessionStatusCount(flusher, 'ok', new Date()); + expect(transporter).toHaveBeenCalledTimes(1); + + _incrementSessionStatusCount(flusher, 'ok', new Date()); jest.advanceTimersByTime(58000); - expect(flusherFlushFunc).toHaveBeenCalledTimes(1); + expect(transporter).toHaveBeenCalledTimes(1); + + _incrementSessionStatusCount(flusher, 'ok', new Date()); jest.advanceTimersByTime(2000); - expect(flusherFlushFunc).toHaveBeenCalledTimes(2); + expect(transporter).toHaveBeenCalledTimes(2); }); - test('sendSessions is called on flush if sessions were captured', () => { - const flusher = new SessionFlusher(transport, { release: '1.0.0', environment: 'dev' }); - const flusherFlushFunc = jest.spyOn(flusher, 'flush'); + test('transporter is called on flush if sessions were captured', () => { + const transporter = makeTransporter(); + const flusher = new SessionFlusher({ transporter, release: '1.0.0', environment: 'dev' }); const date = new Date('2021-04-08T12:18:23.043Z'); - (flusher as any)._incrementSessionStatusCount('ok', date); - (flusher as any)._incrementSessionStatusCount('ok', date); - expect(sendSession).toHaveBeenCalledTimes(0); + _incrementSessionStatusCount(flusher, 'ok', date); + _incrementSessionStatusCount(flusher, 'ok', date); + + expect(transporter).toHaveBeenCalledTimes(0); jest.advanceTimersByTime(61000); - expect(flusherFlushFunc).toHaveBeenCalledTimes(1); - expect(sendSession).toHaveBeenCalledWith( + expect(transporter).toHaveBeenCalledTimes(1); + expect(transporter).toHaveBeenCalledWith( expect.objectContaining({ attrs: { release: '1.0.0', environment: 'dev' }, aggregates: [{ started: '2021-04-08T12:18:00.000Z', exited: 2 }], @@ -91,32 +100,40 @@ describe('Session Flusher', () => { ); }); - test('sendSessions is not called on flush if no sessions were captured', () => { - const flusher = new SessionFlusher(transport, { release: '1.0.0', environment: 'dev' }); - const flusherFlushFunc = jest.spyOn(flusher, 'flush'); + test('transporter is not called on flush if no sessions were captured', () => { + const transporter = makeTransporter(); - expect(sendSession).toHaveBeenCalledTimes(0); + new SessionFlusher({ transporter, release: '1.0.0', environment: 'dev' }); + + expect(transporter).toHaveBeenCalledTimes(0); jest.advanceTimersByTime(61000); - expect(flusherFlushFunc).toHaveBeenCalledTimes(1); - expect(sendSession).toHaveBeenCalledTimes(0); + expect(transporter).toHaveBeenCalledTimes(0); }); test('calling close on SessionFlusher should disable SessionFlusher', () => { - const flusher = new SessionFlusher(transport, { release: '1.0.x' }); - flusher.close(); - expect((flusher as any)._isEnabled).toEqual(false); + const transporter = makeTransporter(); + const flusher = new SessionFlusher({ transporter, release: '1.0.x' }); + + closeSessionFlusher(flusher); + + expect(flusher.isEnabled).toEqual(false); }); test('calling close on SessionFlusher will force call flush', () => { - const flusher = new SessionFlusher(transport, { release: '1.0.x' }); - const flusherFlushFunc = jest.spyOn(flusher, 'flush'); + const transporter = makeTransporter(); + const flusher = new SessionFlusher({ transporter, release: '1.0.x' }); const date = new Date('2021-04-08T12:18:23.043Z'); - (flusher as any)._incrementSessionStatusCount('ok', date); - (flusher as any)._incrementSessionStatusCount('ok', date); - flusher.close(); - expect(flusherFlushFunc).toHaveBeenCalledTimes(1); - expect(sendSession).toHaveBeenCalledWith( + // TODO: code-smell using internal function + // why can we call the public API instead of the internal one? + _incrementSessionStatusCount(flusher, 'ok', date); + _incrementSessionStatusCount(flusher, 'ok', date); + + closeSessionFlusher(flusher); + + expect(flusher.isEnabled).toEqual(false); + expect(flusher.pendingAggregates).toEqual({}); + expect(transporter).toHaveBeenCalledWith( expect.objectContaining({ attrs: { release: '1.0.x' }, aggregates: [{ started: '2021-04-08T12:18:00.000Z', exited: 2 }], diff --git a/packages/integrations/package.json b/packages/integrations/package.json index 98f75c54b0ca..75dc5677979e 100644 --- a/packages/integrations/package.json +++ b/packages/integrations/package.json @@ -16,6 +16,7 @@ "module": "esm/index.js", "types": "dist/index.d.ts", "dependencies": { + "@sentry/hub": "6.17.0-beta.0", "@sentry/types": "6.17.0-beta.0", "@sentry/utils": "6.17.0-beta.0", "localforage": "^1.8.1", diff --git a/packages/integrations/src/angular.ts b/packages/integrations/src/angular.ts index f1e5eaccaf8c..66664e6c83de 100644 --- a/packages/integrations/src/angular.ts +++ b/packages/integrations/src/angular.ts @@ -1,4 +1,12 @@ -import { Event, EventProcessor, Hub, Integration } from '@sentry/types'; +import { + addScopeEventProcessor, + captureHubException, + getHubIntegration, + Hub, + setScopeExtra, + withHubScope, +} from '@sentry/hub'; +import { Event, EventProcessor, Integration } from '@sentry/types'; import { getGlobalObject, logger } from '@sentry/utils'; // See https://github.com/angular/angular.js/blob/v1.4.7/src/minErr.js @@ -64,7 +72,7 @@ export class Angular implements Integration { /** * @inheritDoc */ - public setupOnce(_: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { + public setupOnce(_: (callback: EventProcessor) => void, getCurrentHub: () => Hub | any): void { if (!this._module) { return; } @@ -90,13 +98,13 @@ export class Angular implements Integration { return (exception: Error, cause?: string): void => { const hub = this._getCurrentHub && this._getCurrentHub(); - if (hub && hub.getIntegration(Angular)) { - hub.withScope(scope => { + if (hub && getHubIntegration(hub, Angular)) { + withHubScope(hub, scope => { if (cause) { - scope.setExtra('cause', cause); + setScopeExtra(scope, 'cause', cause); } - scope.addEventProcessor((event: Event) => { + addScopeEventProcessor(scope, (event: Event) => { const ex = event.exception && event.exception.values && event.exception.values[0]; if (ex) { @@ -118,7 +126,7 @@ export class Angular implements Integration { return event; }); - hub.captureException(exception); + captureHubException(hub, exception); }); } $delegate(exception, cause); diff --git a/packages/integrations/src/captureconsole.ts b/packages/integrations/src/captureconsole.ts index bd3a7c055083..3a95c8c0d979 100644 --- a/packages/integrations/src/captureconsole.ts +++ b/packages/integrations/src/captureconsole.ts @@ -1,4 +1,14 @@ -import { EventProcessor, Hub, Integration } from '@sentry/types'; +import { + addScopeEventProcessor, + captureHubException, + captureHubMessage, + getHubIntegration, + Hub, + setScopeExtra, + setScopeLevel, + withHubScope, +} from '@sentry/hub'; +import { EventProcessor, Integration } from '@sentry/types'; import { fill, getGlobalObject, safeJoin, severityFromString } from '@sentry/utils'; const global = getGlobalObject(); @@ -46,11 +56,11 @@ export class CaptureConsole implements Integration { fill(global.console, level, (originalConsoleLevel: () => any) => (...args: any[]): void => { const hub = getCurrentHub(); - if (hub.getIntegration(CaptureConsole)) { - hub.withScope(scope => { - scope.setLevel(severityFromString(level)); - scope.setExtra('arguments', args); - scope.addEventProcessor(event => { + if (getHubIntegration(hub, CaptureConsole)) { + withHubScope(hub, scope => { + setScopeLevel(scope, severityFromString(level)); + setScopeExtra(scope, 'arguments', args); + addScopeEventProcessor(scope, event => { event.logger = 'console'; return event; }); @@ -59,13 +69,13 @@ export class CaptureConsole implements Integration { 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); + setScopeExtra(scope, 'arguments', args.slice(1)); + captureHubMessage(hub, message); } } else if (level === 'error' && args[0] instanceof Error) { - hub.captureException(args[0]); + captureHubException(hub, args[0]); } else { - hub.captureMessage(message); + captureHubMessage(hub, message); } }); } diff --git a/packages/integrations/src/debug.ts b/packages/integrations/src/debug.ts index b54e96d2ee4a..f27a6b06bc66 100644 --- a/packages/integrations/src/debug.ts +++ b/packages/integrations/src/debug.ts @@ -1,4 +1,5 @@ -import { Event, EventHint, EventProcessor, Hub, Integration } from '@sentry/types'; +import { getHubIntegration, Hub } from '@sentry/hub'; +import { Event, EventHint, EventProcessor, Integration } from '@sentry/types'; import { consoleSandbox } from '@sentry/utils'; /** JSDoc */ @@ -38,7 +39,7 @@ export class Debug implements Integration { */ public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { addGlobalEventProcessor((event: Event, hint?: EventHint) => { - const self = getCurrentHub().getIntegration(Debug); + const self = getHubIntegration(getCurrentHub(), Debug); if (self) { if (self._options.debugger) { // eslint-disable-next-line no-debugger diff --git a/packages/integrations/src/dedupe.ts b/packages/integrations/src/dedupe.ts index f36e9770b908..2a143d483de0 100644 --- a/packages/integrations/src/dedupe.ts +++ b/packages/integrations/src/dedupe.ts @@ -1,4 +1,5 @@ -import { Event, EventProcessor, Exception, Hub, Integration, StackFrame } from '@sentry/types'; +import { getHubIntegration, Hub } from '@sentry/hub'; +import { Event, EventProcessor, Exception, Integration, StackFrame } from '@sentry/types'; import { logger } from '@sentry/utils'; /** Deduplication filter */ @@ -23,7 +24,7 @@ export class Dedupe implements Integration { */ public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { addGlobalEventProcessor((currentEvent: Event) => { - const self = getCurrentHub().getIntegration(Dedupe); + const self = getHubIntegration(getCurrentHub(), Dedupe); if (self) { // Juuust in case something goes wrong try { diff --git a/packages/integrations/src/ember.ts b/packages/integrations/src/ember.ts index 811e8b4852f8..34daaf493601 100644 --- a/packages/integrations/src/ember.ts +++ b/packages/integrations/src/ember.ts @@ -1,4 +1,12 @@ -import { EventProcessor, Hub, Integration } from '@sentry/types'; +import { + captureHubException, + captureHubMessage, + getHubIntegration, + Hub, + setScopeExtra, + withHubScope, +} from '@sentry/hub'; +import { EventProcessor, Integration } from '@sentry/types'; import { getGlobalObject, isInstanceOf, logger } from '@sentry/utils'; /** JSDoc */ @@ -41,8 +49,8 @@ export class Ember implements Integration { const oldOnError = this._Ember.onerror; this._Ember.onerror = (error: Error): void => { - if (getCurrentHub().getIntegration(Ember)) { - getCurrentHub().captureException(error, { originalException: error }); + if (getHubIntegration(getCurrentHub(), Ember)) { + captureHubException(getCurrentHub(), error, { originalException: error }); } if (typeof oldOnError === 'function') { @@ -54,18 +62,19 @@ export class Ember implements Integration { // eslint-disable-next-line @typescript-eslint/no-explicit-any this._Ember.RSVP.on('error', (reason: unknown): void => { - if (getCurrentHub().getIntegration(Ember)) { - getCurrentHub().withScope(scope => { + if (getHubIntegration(getCurrentHub(), Ember)) { + withHubScope(getCurrentHub(), scope => { if (isInstanceOf(reason, Error)) { - scope.setExtra('context', 'Unhandled Promise error detected'); - getCurrentHub().captureException(reason, { originalException: reason as Error }); + setScopeExtra(scope, 'context', 'Unhandled Promise error detected'); + captureHubException(getCurrentHub(), reason, { originalException: reason as Error }); } else { - scope.setExtra('reason', reason); - getCurrentHub().captureMessage('Unhandled Promise error detected'); + setScopeExtra(scope, 'reason', reason); + captureHubMessage(getCurrentHub(), 'Unhandled Promise error detected'); } }); } }); } + /* eslint-enable @typescript-eslint/no-unsafe-member-access */ } diff --git a/packages/integrations/src/extraerrordata.ts b/packages/integrations/src/extraerrordata.ts index de01df6605c0..fec5f6e81e7b 100644 --- a/packages/integrations/src/extraerrordata.ts +++ b/packages/integrations/src/extraerrordata.ts @@ -1,4 +1,5 @@ -import { Event, EventHint, EventProcessor, ExtendedError, Hub, Integration } from '@sentry/types'; +import { getHubIntegration, Hub } from '@sentry/hub'; +import { Event, EventHint, EventProcessor, ExtendedError, Integration } from '@sentry/types'; import { isError, isPlainObject, logger, normalize } from '@sentry/utils'; /** JSDoc */ @@ -28,7 +29,7 @@ export class ExtraErrorData implements Integration { */ public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { addGlobalEventProcessor((event: Event, hint?: EventHint) => { - const self = getCurrentHub().getIntegration(ExtraErrorData); + const self = getHubIntegration(getCurrentHub(), ExtraErrorData); if (!self) { return event; } diff --git a/packages/integrations/src/offline.ts b/packages/integrations/src/offline.ts index 5cf1be2cd069..9a6493f2a75d 100644 --- a/packages/integrations/src/offline.ts +++ b/packages/integrations/src/offline.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ -import { Event, EventProcessor, Hub, Integration } from '@sentry/types'; +import { captureHubEvent, getHubIntegration, Hub } from '@sentry/hub'; +import { Event, EventProcessor, Integration } from '@sentry/types'; import { getGlobalObject, logger, normalize, uuid4 } from '@sentry/utils'; import localForage from 'localforage'; @@ -76,7 +77,7 @@ export class Offline implements Integration { } addGlobalEventProcessor((event: Event) => { - if (this.hub && this.hub.getIntegration(Offline)) { + if (this.hub && getHubIntegration(this.hub, Offline)) { // cache if we are positively offline if ('navigator' in this.global && 'onLine' in this.global.navigator && !this.global.navigator.onLine) { void this._cacheEvent(event) @@ -157,7 +158,7 @@ export class Offline implements Integration { private async _sendEvents(): Promise { return this.offlineEventStore.iterate((event: Event, cacheKey: string, _index: number): void => { if (this.hub) { - this.hub.captureEvent(event); + captureHubEvent(this.hub, event); void this._purgeEvent(cacheKey).catch((_error): void => { logger.warn('could not purge event from cache'); diff --git a/packages/integrations/src/reportingobserver.ts b/packages/integrations/src/reportingobserver.ts index 4ae8f0f2d5c1..5908ea9b18b4 100644 --- a/packages/integrations/src/reportingobserver.ts +++ b/packages/integrations/src/reportingobserver.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { EventProcessor, Hub, Integration } from '@sentry/types'; +import { captureHubMessage, getHubIntegration, Hub, setScopeExtra, withHubScope } from '@sentry/hub'; +import { EventProcessor, Integration } from '@sentry/types'; import { getGlobalObject, supportsReportingObserver } from '@sentry/utils'; /** JSDoc */ @@ -104,12 +105,12 @@ export class ReportingObserver implements Integration { */ public handler(reports: Report[]): void { const hub = this._getCurrentHub && this._getCurrentHub(); - if (!hub || !hub.getIntegration(ReportingObserver)) { + if (!hub || !getHubIntegration(hub, ReportingObserver)) { return; } for (const report of reports) { - hub.withScope(scope => { - scope.setExtra('url', report.url); + withHubScope(hub, scope => { + setScopeExtra(scope, 'url', report.url); const label = `ReportingObserver [${report.type}]`; let details = 'No details available'; @@ -125,7 +126,7 @@ export class ReportingObserver implements Integration { plainBody[prop] = report.body[prop]; } - scope.setExtra('body', plainBody); + setScopeExtra(scope, 'body', plainBody); if (report.type === ReportTypes.Crash) { const body = report.body as CrashReportBody; @@ -137,7 +138,7 @@ export class ReportingObserver implements Integration { } } - hub.captureMessage(`${label}: ${details}`); + captureHubMessage(hub, `${label}: ${details}`); }); } } diff --git a/packages/integrations/src/rewriteframes.ts b/packages/integrations/src/rewriteframes.ts index 9eb95e54f6d3..ef8cd0773143 100644 --- a/packages/integrations/src/rewriteframes.ts +++ b/packages/integrations/src/rewriteframes.ts @@ -1,4 +1,5 @@ -import { Event, EventProcessor, Hub, Integration, StackFrame, Stacktrace } from '@sentry/types'; +import { getHubIntegration, Hub } from '@sentry/hub'; +import { Event, EventProcessor, Integration, StackFrame, Stacktrace } from '@sentry/types'; import { basename, relative } from '@sentry/utils'; type StackFrameIteratee = (frame: StackFrame) => StackFrame; @@ -45,7 +46,7 @@ export class RewriteFrames implements Integration { */ public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { addGlobalEventProcessor(event => { - const self = getCurrentHub().getIntegration(RewriteFrames); + const self = getHubIntegration(getCurrentHub(), RewriteFrames); if (self) { return self.process(event); } diff --git a/packages/integrations/src/sessiontiming.ts b/packages/integrations/src/sessiontiming.ts index 0e45588e0f31..2dd8051825e0 100644 --- a/packages/integrations/src/sessiontiming.ts +++ b/packages/integrations/src/sessiontiming.ts @@ -1,4 +1,5 @@ -import { Event, EventProcessor, Hub, Integration } from '@sentry/types'; +import { getHubIntegration, Hub } from '@sentry/hub'; +import { Event, EventProcessor, Integration } from '@sentry/types'; /** This function adds duration since Sentry was initialized till the time event was sent */ export class SessionTiming implements Integration { @@ -20,7 +21,7 @@ export class SessionTiming implements Integration { */ public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { addGlobalEventProcessor(event => { - const self = getCurrentHub().getIntegration(SessionTiming); + const self = getHubIntegration(getCurrentHub(), SessionTiming); if (self) { return self.process(event); } diff --git a/packages/integrations/src/transaction.ts b/packages/integrations/src/transaction.ts index e6d0ac24f770..b4dac0847432 100644 --- a/packages/integrations/src/transaction.ts +++ b/packages/integrations/src/transaction.ts @@ -1,4 +1,5 @@ -import { Event, EventProcessor, Hub, Integration, StackFrame } from '@sentry/types'; +import { getHubIntegration, Hub } from '@sentry/hub'; +import { Event, EventProcessor, Integration, StackFrame } from '@sentry/types'; /** Add node transaction to the event */ export class Transaction implements Integration { @@ -17,7 +18,7 @@ export class Transaction implements Integration { */ public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { addGlobalEventProcessor(event => { - const self = getCurrentHub().getIntegration(Transaction); + const self = getHubIntegration(getCurrentHub(), Transaction); if (self) { return self.process(event); } diff --git a/packages/integrations/src/vue.ts b/packages/integrations/src/vue.ts index 0f8d9c20b683..b2568bb3fae0 100644 --- a/packages/integrations/src/vue.ts +++ b/packages/integrations/src/vue.ts @@ -1,6 +1,15 @@ /* eslint-disable max-lines */ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { EventProcessor, Hub, Integration, IntegrationClass, Scope, Span, Transaction } from '@sentry/types'; +import { + captureHubException, + getHubIntegration, + getHubScope, + getScopeTransaction, + Hub, + setScopeContext, + withHubScope, +} from '@sentry/hub'; +import { EventProcessor, Integration, IntegrationClass, Scope, Span, Transaction } from '@sentry/types'; import { basename, getGlobalObject, logger, timestampWithMs } from '@sentry/utils'; /** @@ -29,12 +38,14 @@ interface VueInstance { util?: { warn(...input: any): void; }; + mixin(hooks: { [key: string]: () => void }): void; } /** Representation of Vue component internals */ interface ViewModel { [key: string]: any; + // eslint-disable-next-line @typescript-eslint/ban-types $root: object; $options: { @@ -45,6 +56,7 @@ interface ViewModel { __file?: string; $_sentryPerfHook?: boolean; }; + $once(hook: string, cb: () => void): void; } @@ -95,6 +107,7 @@ interface TracingOptions { /** Optional metadata attached to Sentry Event */ interface Metadata { [key: string]: any; + componentName?: string; propsData?: { [key: string]: any }; lifecycleHook?: string; @@ -263,7 +276,7 @@ export class Vue implements Integration { // We also need to ask for the `.constructor`, as `pushActivity` and `popActivity` are static, not instance methods. /* eslint-disable @typescript-eslint/no-unsafe-member-access */ // eslint-disable-next-line deprecation/deprecation - const tracingIntegration = getCurrentHub().getIntegration(TRACING_GETTER); + const tracingIntegration = getHubIntegration(getCurrentHub(), TRACING_GETTER); if (tracingIntegration) { this._tracingActivity = (tracingIntegration as any).constructor.pushActivity('Vue Application Render'); const transaction = (tracingIntegration as any).constructor.getTransaction(); @@ -357,7 +370,7 @@ export class Vue implements Integration { // We do this whole dance with `TRACING_GETTER` to prevent `@sentry/apm` from becoming a peerDependency. // We also need to ask for the `.constructor`, as `pushActivity` and `popActivity` are static, not instance methods. // eslint-disable-next-line deprecation/deprecation - const tracingIntegration = getCurrentHub().getIntegration(TRACING_GETTER); + const tracingIntegration = getHubIntegration(getCurrentHub(), TRACING_GETTER); if (tracingIntegration) { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access (tracingIntegration as any).constructor.popActivity(this._tracingActivity); @@ -378,7 +391,10 @@ export class Vue implements Integration { this._options.Vue.mixin({ beforeCreate(this: ViewModel): void { // eslint-disable-next-line deprecation/deprecation - if (getCurrentHub().getIntegration(TRACING_GETTER) || getCurrentHub().getIntegration(BROWSER_TRACING_GETTER)) { + if ( + getHubIntegration(getCurrentHub(), TRACING_GETTER) || + getHubIntegration(getCurrentHub(), BROWSER_TRACING_GETTER) + ) { // `this` points to currently rendered component applyTracingHooks(this, getCurrentHub); } else { @@ -412,12 +428,12 @@ export class Vue implements Integration { metadata.lifecycleHook = info; } - if (getCurrentHub().getIntegration(Vue)) { + if (getHubIntegration(getCurrentHub(), Vue)) { // Capture exception in the next event loop, to make sure that all breadcrumbs are recorded in time. setTimeout(() => { - getCurrentHub().withScope(scope => { - scope.setContext('vue', metadata); - getCurrentHub().captureException(error); + withHubScope(getCurrentHub(), scope => { + setScopeContext(scope, 'vue', metadata); + captureHubException(getCurrentHub(), error); }); }); } @@ -437,16 +453,18 @@ export class Vue implements Integration { } } -interface HubType extends Hub { - getScope?(): Scope | undefined; -} +// interface HubType extends IHub { +// getScope?(): Scope | undefined; +// } /** Grabs active transaction off scope */ -export function getActiveTransaction(hub: HubType): T | undefined { - if (hub && hub.getScope) { - const scope = hub.getScope() as Scope; +export function getActiveTransaction(hub: Hub): T | undefined { + // TODO: I am confused about why the HubType and not IHub is used here. + // And why we need to check for `getScope`. So this may be wrong. + if (hub) { + const scope = getHubScope(hub) as Scope; if (scope) { - return scope.getTransaction() as T | undefined; + return getScopeTransaction(scope) as T | undefined; } } diff --git a/packages/minimal/src/index.ts b/packages/minimal/src/index.ts index 15fcf7e3eaab..10ee78103e44 100644 --- a/packages/minimal/src/index.ts +++ b/packages/minimal/src/index.ts @@ -1,4 +1,21 @@ -import { getCurrentHub, Hub, Scope } from '@sentry/hub'; +import { + _invokeHubClient, + addHubBreadcrumb, + captureHubEvent, + captureHubException, + captureHubMessage, + configureHubScope, + setHubUser, + withHubScope, + getCurrentHub, + setHubContext, + setHubExtra, + setHubExtras, + setHubTag, + setHubTags, + startHubTransaction, + Scope, +} from '@sentry/hub'; import { Breadcrumb, CaptureContext, @@ -13,19 +30,21 @@ import { User, } from '@sentry/types'; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type AnyFunction = (...args: any[]) => any; + /** * This calls a function on the current hub. * @param method function to call on hub. * @param args to pass to function. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -function callOnHub(method: string, ...args: any[]): T { +function callOnHub(method: TMethod, ...args: any[]): ReturnType { const hub = getCurrentHub(); - if (hub && hub[method as keyof Hub]) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return (hub[method as keyof Hub] as any)(...args); + if (hub) { + return method(hub, ...args); } - throw new Error(`No hub defined or ${method} was not found on the hub, please open a bug report.`); + throw new Error(`No hub defined please open a bug report.`); } /** @@ -42,7 +61,7 @@ export function captureException(exception: any, captureContext?: CaptureContext } catch (exception) { syntheticException = exception as Error; } - return callOnHub('captureException', exception, { + return callOnHub(captureHubException, exception, { captureContext, originalException: exception, syntheticException, @@ -69,7 +88,7 @@ export function captureMessage(message: string, captureContext?: CaptureContext const level = typeof captureContext === 'string' ? captureContext : undefined; const context = typeof captureContext !== 'string' ? { captureContext } : undefined; - return callOnHub('captureMessage', message, level, { + return callOnHub(captureHubMessage, message, level, { originalException: message, syntheticException, ...context, @@ -83,7 +102,7 @@ export function captureMessage(message: string, captureContext?: CaptureContext * @returns The generated eventId. */ export function captureEvent(event: Event): string { - return callOnHub('captureEvent', event); + return callOnHub(captureHubEvent, event); } /** @@ -91,7 +110,7 @@ export function captureEvent(event: Event): string { * @param callback Callback function that receives Scope. */ export function configureScope(callback: (scope: Scope) => void): void { - callOnHub('configureScope', callback); + callOnHub(configureHubScope, callback); } /** @@ -103,7 +122,7 @@ export function configureScope(callback: (scope: Scope) => void): void { * @param breadcrumb The breadcrumb to record. */ export function addBreadcrumb(breadcrumb: Breadcrumb): void { - callOnHub('addBreadcrumb', breadcrumb); + callOnHub(addHubBreadcrumb, breadcrumb); } /** @@ -113,7 +132,7 @@ export function addBreadcrumb(breadcrumb: Breadcrumb): void { */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export function setContext(name: string, context: { [key: string]: any } | null): void { - callOnHub('setContext', name, context); + callOnHub(setHubContext, name, context); } /** @@ -121,7 +140,7 @@ export function setContext(name: string, context: { [key: string]: any } | null) * @param extras Extras object to merge into current context. */ export function setExtras(extras: Extras): void { - callOnHub('setExtras', extras); + callOnHub(setHubExtras, extras); } /** @@ -129,7 +148,7 @@ export function setExtras(extras: Extras): void { * @param tags Tags context object to merge into current context. */ export function setTags(tags: { [key: string]: Primitive }): void { - callOnHub('setTags', tags); + callOnHub(setHubTags, tags); } /** @@ -138,7 +157,7 @@ export function setTags(tags: { [key: string]: Primitive }): void { * @param extra Any kind of data. This data will be normalized. */ export function setExtra(key: string, extra: Extra): void { - callOnHub('setExtra', key, extra); + callOnHub(setHubExtra, key, extra); } /** @@ -150,7 +169,7 @@ export function setExtra(key: string, extra: Extra): void { * @param value Value of tag */ export function setTag(key: string, value: Primitive): void { - callOnHub('setTag', key, value); + callOnHub(setHubTag, key, value); } /** @@ -159,7 +178,7 @@ export function setTag(key: string, value: Primitive): void { * @param user User context object to be set in the current context. Pass `null` to unset the user. */ export function setUser(user: User | null): void { - callOnHub('setUser', user); + callOnHub(setHubUser, user); } /** @@ -176,7 +195,7 @@ export function setUser(user: User | null): void { * @param callback that will be enclosed into push/popScope. */ export function withScope(callback: (scope: Scope) => void): void { - callOnHub('withScope', callback); + callOnHub(withHubScope, callback); } /** @@ -191,7 +210,7 @@ export function withScope(callback: (scope: Scope) => void): void { */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export function _callOnClient(method: string, ...args: any[]): void { - callOnHub('_invokeClient', method, ...args); + callOnHub(_invokeHubClient, method, ...args); } /** @@ -215,5 +234,5 @@ export function startTransaction( context: TransactionContext, customSamplingContext?: CustomSamplingContext, ): Transaction { - return callOnHub('startTransaction', { ...context }, customSamplingContext); + return callOnHub(startHubTransaction, { ...context }, customSamplingContext); } diff --git a/packages/nextjs/src/index.client.ts b/packages/nextjs/src/index.client.ts index 61a1b27da7f3..8ec88021cc9f 100644 --- a/packages/nextjs/src/index.client.ts +++ b/packages/nextjs/src/index.client.ts @@ -5,6 +5,8 @@ import { nextRouterInstrumentation } from './performance/client'; import { buildMetadata } from './utils/metadata'; import { NextjsOptions } from './utils/nextjsOptions'; import { addIntegration, UserIntegrations } from './utils/userIntegrations'; +import { addScopeEventProcessor } from '@sentry/hub'; +import { setScopeTag } from '@sentry/hub/dist/scope'; export * from '@sentry/react'; export { nextRouterInstrumentation } from './performance/client'; @@ -27,8 +29,10 @@ export function init(options: NextjsOptions): void { integrations, }); configureScope(scope => { - scope.setTag('runtime', 'browser'); - scope.addEventProcessor(event => (event.type === 'transaction' && event.transaction === '/404' ? null : event)); + setScopeTag(scope, 'runtime', 'browser'); + addScopeEventProcessor(scope, event => + event.type === 'transaction' && event.transaction === '/404' ? null : event, + ); }); } diff --git a/packages/nextjs/src/index.server.ts b/packages/nextjs/src/index.server.ts index ac3d4c4bb6ce..15425cdfa893 100644 --- a/packages/nextjs/src/index.server.ts +++ b/packages/nextjs/src/index.server.ts @@ -1,4 +1,13 @@ -import { Carrier, getHubFromCarrier, getMainCarrier } from '@sentry/hub'; +import { + addScopeEventProcessor, + bindHubClient, + Carrier, + getHubClient, + getHubFromCarrier, + getHubScope, + getMainCarrier, + updateScope, +} from '@sentry/hub'; import { RewriteFrames } from '@sentry/integrations'; import { configureScope, getCurrentHub, init as nodeInit, Integrations } from '@sentry/node'; import { Event } from '@sentry/types'; @@ -9,6 +18,7 @@ import * as path from 'path'; import { buildMetadata } from './utils/metadata'; import { NextjsOptions } from './utils/nextjsOptions'; import { addIntegration } from './utils/userIntegrations'; +import { setScopeTag } from '@sentry/hub/dist/scope'; export * from '@sentry/node'; @@ -62,12 +72,12 @@ export function init(options: NextjsOptions): void { nodeInit(options); configureScope(scope => { - scope.setTag('runtime', 'node'); + setScopeTag(scope, 'runtime', 'node'); if (isVercel) { - scope.setTag('vercel', true); + setScopeTag(scope, 'vercel', true); } - scope.addEventProcessor(filterTransactions); + addScopeEventProcessor(scope, filterTransactions); }); if (activeDomain) { @@ -75,10 +85,18 @@ export function init(options: NextjsOptions): void { const domainHub = getHubFromCarrier(activeDomain); // apply the changes made by `nodeInit` to the domain's hub also - domainHub.bindClient(globalHub.getClient()); - domainHub.getScope()?.update(globalHub.getScope()); + bindHubClient(domainHub, getHubClient(globalHub)); + const domainScope = getHubScope(domainHub); + const globalScope = getHubScope(globalHub); + if (domainScope) { + updateScope(domainScope, globalScope); + } + // `scope.update()` doesn’t copy over event processors, so we have to add it manually - domainHub.getScope()?.addEventProcessor(filterTransactions); + const scope = getHubScope(domainHub); + if (scope) { + addScopeEventProcessor(scope, filterTransactions); + } // restore the domain hub as the current one domain.active = activeDomain; @@ -89,7 +107,7 @@ export function init(options: NextjsOptions): void { function sdkAlreadyInitialized(): boolean { const hub = getCurrentHub(); - return !!hub.getClient(); + return !!getHubClient(hub); } function addServerIntegrations(options: NextjsOptions): void { diff --git a/packages/nextjs/src/utils/instrumentServer.ts b/packages/nextjs/src/utils/instrumentServer.ts index c05fa9cac226..d7bbae194aaf 100644 --- a/packages/nextjs/src/utils/instrumentServer.ts +++ b/packages/nextjs/src/utils/instrumentServer.ts @@ -14,6 +14,7 @@ import * as http from 'http'; import { default as createNextServer } from 'next'; import * as querystring from 'querystring'; import * as url from 'url'; +import { addScopeEventProcessor, getHubScope, setScopeSpan } from '@sentry/hub'; const { parseRequest } = Handlers; @@ -159,7 +160,7 @@ function makeWrappedErrorLogger(origErrorLogger: ErrorLogger): WrappedErrorLogge // gets its own scope. (`configureScope` has the advantage of not creating a clone of the current scope before // modifying it, which in this case is unnecessary.) configureScope(scope => { - scope.addEventProcessor(event => { + addScopeEventProcessor(scope, event => { addExceptionMechanism(event, { type: 'instrument', handled: true, @@ -217,10 +218,10 @@ function makeWrappedReqHandler(origReqHandler: ReqHandler): WrappedReqHandler { // local.on('error', Sentry.captureException); local.run(() => { - const currentScope = getCurrentHub().getScope(); + const currentScope = getHubScope(getCurrentHub()); if (currentScope) { - currentScope.addEventProcessor(event => parseRequest(event, req)); + addScopeEventProcessor(currentScope, event => parseRequest(event, req)); // We only want to record page and API requests if (hasTracingEnabled() && shouldTraceRequest(req.url, publicDirFiles)) { @@ -249,7 +250,7 @@ function makeWrappedReqHandler(origReqHandler: ReqHandler): WrappedReqHandler { { request: req }, ); - currentScope.setSpan(transaction); + setScopeSpan(currentScope, transaction); res.once('finish', () => { const transaction = getActiveTransaction(); diff --git a/packages/nextjs/src/utils/withSentry.ts b/packages/nextjs/src/utils/withSentry.ts index 6c0f1d26a764..86bed8048bec 100644 --- a/packages/nextjs/src/utils/withSentry.ts +++ b/packages/nextjs/src/utils/withSentry.ts @@ -1,3 +1,4 @@ +import { addScopeEventProcessor, getHubScope, setScopeSpan } from '@sentry/hub'; import { captureException, flush, getCurrentHub, Handlers, startTransaction } from '@sentry/node'; import { extractTraceparentData, hasTracingEnabled } from '@sentry/tracing'; import { Transaction } from '@sentry/types'; @@ -33,10 +34,10 @@ export const withSentry = (origHandler: NextApiHandler): WrappedNextApiHandler = // return a value. In our case, all any of the codepaths return is a promise of `void`, but nextjs still counts on // getting that before it will finish the response. const boundHandler = local.bind(async () => { - const currentScope = getCurrentHub().getScope(); + const currentScope = getHubScope(getCurrentHub()); if (currentScope) { - currentScope.addEventProcessor(event => parseRequest(event, req)); + addScopeEventProcessor(currentScope, event => parseRequest(event, req)); if (hasTracingEnabled()) { // If there is a trace header set, extract the data from it (parentSpanId, traceId, and sampling decision) @@ -68,7 +69,7 @@ export const withSentry = (origHandler: NextApiHandler): WrappedNextApiHandler = // extra context passed to the `tracesSampler` { request: req }, ); - currentScope.setSpan(transaction); + setScopeSpan(currentScope, transaction); // save a link to the transaction on the response, so that even if there's an error (landing us outside of // the domain), we can still finish it (albeit possibly missing some scope data) @@ -136,7 +137,7 @@ export const withSentry = (origHandler: NextApiHandler): WrappedNextApiHandler = const objectifiedErr = objectify(e); if (currentScope) { - currentScope.addEventProcessor(event => { + addScopeEventProcessor(currentScope, event => { addExceptionMechanism(event, { type: 'instrument', handled: true, diff --git a/packages/node/src/client.ts b/packages/node/src/client.ts index c5f19ed611d0..b9a561de3d92 100644 --- a/packages/node/src/client.ts +++ b/packages/node/src/client.ts @@ -1,5 +1,11 @@ import { BaseClient, Scope, SDK_VERSION } from '@sentry/core'; -import { SessionFlusher } from '@sentry/hub'; +import { + SessionFlusherTransporter, + closeSessionFlusher, + getScopeRequestSession, + incrementSessionStatusCount, + SessionFlusher, +} from '@sentry/hub'; import { Event, EventHint } from '@sentry/types'; import { logger } from '@sentry/utils'; @@ -44,7 +50,7 @@ export class NodeClient extends BaseClient { // when the `requestHandler` middleware is used, and hence the expectation is to have SessionAggregates payload // sent to the Server only when the `requestHandler` middleware is used if (this._options.autoSessionTracking && this._sessionFlusher && scope) { - const requestSession = scope.getRequestSession(); + const requestSession = getScopeRequestSession(scope); // Necessary checks to ensure this is code block is executed only within a request // Should override the status only if `requestSession.status` is `Ok`, which is its initial stage @@ -70,7 +76,7 @@ export class NodeClient extends BaseClient { // If the event is of type Exception, then a request session should be captured if (isException) { - const requestSession = scope.getRequestSession(); + const requestSession = getScopeRequestSession(scope); // Ensure that this is happening within the bounds of a request, and make sure not to override // Session Status if Errored / Crashed @@ -88,7 +94,9 @@ export class NodeClient extends BaseClient { * @inheritdoc */ public close(timeout?: number): PromiseLike { - this._sessionFlusher?.close(); + if (this._sessionFlusher) { + closeSessionFlusher(this._sessionFlusher); + } return super.close(timeout); } @@ -98,9 +106,14 @@ export class NodeClient extends BaseClient { if (!release) { logger.warn('Cannot initialise an instance of SessionFlusher if no release is provided!'); } else { - this._sessionFlusher = new SessionFlusher(this.getTransport(), { + this._sessionFlusher = new SessionFlusher({ release, environment, + // How to make this a required option? + // The existing transporter could omit implementing this method. + // Would it makesense to have a default implementation that does nothing? + // Or should the session flusher be optional? + transporter: this.getTransport().sendSession || (Function.prototype as SessionFlusherTransporter), }); } } @@ -124,7 +137,7 @@ export class NodeClient extends BaseClient { if (!this._sessionFlusher) { logger.warn('Discarded request mode session because autoSessionTracking option was disabled'); } else { - this._sessionFlusher.incrementSessionStatusCount(); + incrementSessionStatusCount(this._sessionFlusher); } } } diff --git a/packages/node/src/eventbuilder.ts b/packages/node/src/eventbuilder.ts index 782bd7494e20..0ad2cc181e61 100644 --- a/packages/node/src/eventbuilder.ts +++ b/packages/node/src/eventbuilder.ts @@ -1,4 +1,4 @@ -import { getCurrentHub } from '@sentry/hub'; +import { getCurrentHub, configureHubScope, setScopeExtra } from '@sentry/hub'; import { Event, EventHint, Mechanism, Options, SeverityLevel } from '@sentry/types'; import { addExceptionMechanism, @@ -32,8 +32,8 @@ export function eventFromException(options: Options, exception: unknown, hint?: // which is much better than creating new group when any key/value change const message = `Non-Error exception captured with keys: ${extractExceptionKeysForMessage(exception)}`; - getCurrentHub().configureScope(scope => { - scope.setExtra('__serialized__', normalizeToSize(exception as Record)); + configureHubScope(getCurrentHub(), scope => { + setScopeExtra(scope, '__serialized__', normalizeToSize(exception as Record)); }); ex = (hint && hint.syntheticException) || new Error(message); diff --git a/packages/node/src/handlers.ts b/packages/node/src/handlers.ts index 332d8593010a..87cdad504b00 100644 --- a/packages/node/src/handlers.ts +++ b/packages/node/src/handlers.ts @@ -1,6 +1,18 @@ /* eslint-disable max-lines */ /* eslint-disable @typescript-eslint/no-explicit-any */ import { captureException, getCurrentHub, startTransaction, withScope } from '@sentry/core'; +import { + getHubClient, + getHubScope, + getScopeRequestSession, + getScopeSession, + getScopeSpan, + setScopeSpan, + setScopeRequestSession, + addScopeEventProcessor, + configureHubScope, + setScopeSession, +} from '@sentry/hub'; import { extractTraceparentData, Span } from '@sentry/tracing'; import { Event, ExtractedNodeRequestData, Transaction } from '@sentry/types'; import { isPlainObject, isString, logger, normalize, stripUrlQueryAndFragment } from '@sentry/utils'; @@ -70,8 +82,8 @@ export function tracingHandler(): ( ); // We put the transaction on the scope so users can attach children to it - getCurrentHub().configureScope(scope => { - scope.setSpan(transaction); + configureHubScope(getCurrentHub(), scope => { + setScopeSpan(scope, transaction); }); // We also set __sentry_transaction on the response so people can grab the transaction there to add @@ -379,16 +391,16 @@ export function requestHandler( options?: RequestHandlerOptions, ): (req: http.IncomingMessage, res: http.ServerResponse, next: (error?: any) => void) => void { const currentHub = getCurrentHub(); - const client = currentHub.getClient(); + const client = getHubClient(currentHub); // Initialise an instance of SessionFlusher on the client when `autoSessionTracking` is enabled and the // `requestHandler` middleware is used indicating that we are running in SessionAggregates mode if (client && isAutoSessionTrackingEnabled(client)) { client.initSessionFlusher(); // If Scope contains a Single mode Session, it is removed in favor of using Session Aggregates mode - const scope = currentHub.getScope(); - if (scope && scope.getSession()) { - scope.setSession(); + const scope = getHubScope(currentHub); + if (scope && getScopeSession(scope)) { + setScopeSession(scope); } } return function sentryRequestMiddleware( @@ -417,20 +429,20 @@ export function requestHandler( local.run(() => { const currentHub = getCurrentHub(); - currentHub.configureScope(scope => { - scope.addEventProcessor((event: Event) => parseRequest(event, req, options)); - const client = currentHub.getClient(); + configureHubScope(currentHub, scope => { + addScopeEventProcessor(scope, (event: Event) => parseRequest(event, req, options)); + const client = getHubClient(currentHub); if (isAutoSessionTrackingEnabled(client)) { - const scope = currentHub.getScope(); + const scope = getHubScope(currentHub); if (scope) { // Set `status` of `RequestSession` to Ok, at the beginning of the request - scope.setRequestSession({ status: 'ok' }); + setScopeRequestSession(scope, { status: 'ok' }); } } }); res.once('finish', () => { - const client = currentHub.getClient(); + const client = getHubClient(currentHub); if (isAutoSessionTrackingEnabled(client)) { setImmediate(() => { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access @@ -500,11 +512,11 @@ export function errorHandler(options?: { // For some reason we need to set the transaction on the scope again // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access const transaction = (res as any).__sentry_transaction as Span; - if (transaction && _scope.getSpan() === undefined) { - _scope.setSpan(transaction); + if (transaction && getScopeSpan(_scope) === undefined) { + setScopeSpan(_scope, transaction); } - const client = getCurrentHub().getClient(); + const client = getHubClient(getCurrentHub()); if (client && isAutoSessionTrackingEnabled(client)) { // Check if the `SessionFlusher` is instantiated on the client to go into this branch that marks the // `requestSession.status` as `Crashed`, and this check is necessary because the `SessionFlusher` is only @@ -513,7 +525,7 @@ export function errorHandler(options?: { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access const isSessionAggregatesMode = (client as any)._sessionFlusher !== undefined; if (isSessionAggregatesMode) { - const requestSession = _scope.getRequestSession(); + const requestSession = getScopeRequestSession(_scope); // If an error bubbles to the `errorHandler`, then this is an unhandled error, and should be reported as a // Crashed session. The `_requestSession.status` is checked to ensure that this error is happening within // the bounds of a request, and if so the status is updated diff --git a/packages/node/src/integrations/console.ts b/packages/node/src/integrations/console.ts index 0f19f15ffeb7..9d1859904383 100644 --- a/packages/node/src/integrations/console.ts +++ b/packages/node/src/integrations/console.ts @@ -1,4 +1,5 @@ import { getCurrentHub } from '@sentry/core'; +import { addHubBreadcrumb, getHubIntegration } from '@sentry/hub'; import { Integration } from '@sentry/types'; import { fill, severityFromString } from '@sentry/utils'; import * as util from 'util'; @@ -34,8 +35,9 @@ function createConsoleWrapper(level: string): (originalConsoleMethod: () => void /* eslint-disable prefer-rest-params */ return function(this: typeof console): void { - if (getCurrentHub().getIntegration(Console)) { - getCurrentHub().addBreadcrumb( + if (getHubIntegration(getCurrentHub(), Console)) { + addHubBreadcrumb( + getCurrentHub(), { category: 'console', level: sentryLevel, diff --git a/packages/node/src/integrations/http.ts b/packages/node/src/integrations/http.ts index 6307f6008fc1..0e0ef4823247 100644 --- a/packages/node/src/integrations/http.ts +++ b/packages/node/src/integrations/http.ts @@ -12,6 +12,7 @@ import { RequestMethod, RequestMethodArgs, } from './utils/http'; +import { getScopeSpan, addHubBreadcrumb, getHubScope, getHubIntegration } from '@sentry/hub'; const NODE_VERSION = parseSemver(process.versions.node); @@ -108,9 +109,9 @@ function _createWrappedRequestMethodFactory( let span: Span | undefined; let parentSpan: Span | undefined; - const scope = getCurrentHub().getScope(); + const scope = getHubScope(getCurrentHub()); if (scope && tracingEnabled) { - parentSpan = scope.getSpan(); + parentSpan = getScopeSpan(scope); if (parentSpan) { span = parentSpan.startChild({ description: `${requestOptions.method || 'GET'} ${requestUrl}`, @@ -163,11 +164,12 @@ function _createWrappedRequestMethodFactory( * Captures Breadcrumb based on provided request/response pair */ function addRequestBreadcrumb(event: string, url: string, req: http.ClientRequest, res?: http.IncomingMessage): void { - if (!getCurrentHub().getIntegration(Http)) { + if (!getHubIntegration(getCurrentHub(), Http)) { return; } - getCurrentHub().addBreadcrumb( + addHubBreadcrumb( + getCurrentHub(), { category: 'http', data: { diff --git a/packages/node/src/integrations/linkederrors.ts b/packages/node/src/integrations/linkederrors.ts index 0b70aad73360..aabb9cc0803c 100644 --- a/packages/node/src/integrations/linkederrors.ts +++ b/packages/node/src/integrations/linkederrors.ts @@ -1,4 +1,5 @@ import { addGlobalEventProcessor, getCurrentHub } from '@sentry/core'; +import { getHubIntegration } from '@sentry/hub'; import { Event, EventHint, Exception, ExtendedError, Integration } from '@sentry/types'; import { isInstanceOf, resolvedSyncPromise, SyncPromise } from '@sentry/utils'; @@ -42,7 +43,7 @@ export class LinkedErrors implements Integration { */ public setupOnce(): void { addGlobalEventProcessor((event: Event, hint?: EventHint) => { - const self = getCurrentHub().getIntegration(LinkedErrors); + const self = getHubIntegration(getCurrentHub(), LinkedErrors); if (self) { const handler = self._handler && self._handler.bind(self); return typeof handler === 'function' ? handler(event, hint) : event; diff --git a/packages/node/src/integrations/modules.ts b/packages/node/src/integrations/modules.ts index df8bcc16d261..2e4d5f269eca 100644 --- a/packages/node/src/integrations/modules.ts +++ b/packages/node/src/integrations/modules.ts @@ -1,4 +1,5 @@ -import { EventProcessor, Hub, Integration } from '@sentry/types'; +import { getHubIntegration, Hub } from '@sentry/hub'; +import { EventProcessor, Integration } from '@sentry/types'; import { existsSync, readFileSync } from 'fs'; import { dirname, join } from 'path'; @@ -82,7 +83,7 @@ export class Modules implements Integration { */ public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { addGlobalEventProcessor(event => { - if (!getCurrentHub().getIntegration(Modules)) { + if (!getHubIntegration(getCurrentHub(), Modules)) { return event; } return { diff --git a/packages/node/src/integrations/onuncaughtexception.ts b/packages/node/src/integrations/onuncaughtexception.ts index b9f1824a2f77..ae4f91cf0bcb 100644 --- a/packages/node/src/integrations/onuncaughtexception.ts +++ b/packages/node/src/integrations/onuncaughtexception.ts @@ -1,4 +1,5 @@ import { getCurrentHub, Scope } from '@sentry/core'; +import { captureHubException, getHubClient, getHubIntegration, setScopeLevel, withHubScope } from '@sentry/hub'; import { Integration } from '@sentry/types'; import { logger } from '@sentry/utils'; @@ -37,6 +38,7 @@ export class OnUncaughtException implements Integration { onFatalError?(firstError: Error, secondError?: Error): void; } = {}, ) {} + /** * @inheritDoc */ @@ -56,7 +58,7 @@ export class OnUncaughtException implements Integration { return (error: Error): void => { let onFatalError: OnFatalErrorHandler = logAndExitProcess; - const client = getCurrentHub().getClient(); + const client = getHubClient(getCurrentHub()); if (this._options.onFatalError) { // eslint-disable-next-line @typescript-eslint/unbound-method @@ -75,10 +77,10 @@ export class OnUncaughtException implements Integration { firstError = error; caughtFirstError = true; - if (hub.getIntegration(OnUncaughtException)) { - hub.withScope((scope: Scope) => { - scope.setLevel('fatal'); - hub.captureException(error, { + if (getHubIntegration(hub, OnUncaughtException)) { + withHubScope(hub, (scope: Scope) => { + setScopeLevel(scope, 'fatal'); + captureHubException(hub, error, { originalException: error, data: { mechanism: { handled: false, type: 'onuncaughtexception' } }, }); diff --git a/packages/node/src/integrations/onunhandledrejection.ts b/packages/node/src/integrations/onunhandledrejection.ts index 19f733b1f908..e352f6174aeb 100644 --- a/packages/node/src/integrations/onunhandledrejection.ts +++ b/packages/node/src/integrations/onunhandledrejection.ts @@ -1,4 +1,13 @@ import { getCurrentHub, Scope } from '@sentry/core'; +import { + captureHubException, + getHubIntegration, + setScopeExtra, + setScopeExtras, + setScopeTags, + setScopeUser, + withHubScope, +} from '@sentry/hub'; import { Integration } from '@sentry/types'; import { consoleSandbox } from '@sentry/utils'; @@ -47,7 +56,7 @@ export class OnUnhandledRejection implements Integration { public sendUnhandledPromise(reason: any, promise: any): void { const hub = getCurrentHub(); - if (!hub.getIntegration(OnUnhandledRejection)) { + if (!getHubIntegration(hub, OnUnhandledRejection)) { this._handleRejection(reason); return; } @@ -55,21 +64,21 @@ export class OnUnhandledRejection implements Integration { /* eslint-disable @typescript-eslint/no-unsafe-member-access */ const context = (promise.domain && promise.domain.sentryContext) || {}; - hub.withScope((scope: Scope) => { - scope.setExtra('unhandledPromiseRejection', true); + withHubScope(hub, (scope: Scope) => { + setScopeExtra(scope, 'unhandledPromiseRejection', true); // Preserve backwards compatibility with raven-node for now if (context.user) { - scope.setUser(context.user); + setScopeUser(scope, context.user); } if (context.tags) { - scope.setTags(context.tags); + setScopeTags(scope, context.tags); } if (context.extra) { - scope.setExtras(context.extra); + setScopeExtras(scope, context.extra); } - hub.captureException(reason, { + captureHubException(hub, reason, { originalException: promise, data: { mechanism: { handled: false, type: 'onunhandledrejection' } }, }); diff --git a/packages/node/src/integrations/utils/errorhandling.ts b/packages/node/src/integrations/utils/errorhandling.ts index 4ef901adb061..e0c5ae7b7651 100644 --- a/packages/node/src/integrations/utils/errorhandling.ts +++ b/packages/node/src/integrations/utils/errorhandling.ts @@ -1,4 +1,5 @@ import { getCurrentHub } from '@sentry/core'; +import { getHubClient } from '@sentry/hub'; import { forget, logger } from '@sentry/utils'; import { NodeClient } from '../../client'; @@ -12,7 +13,7 @@ export function logAndExitProcess(error: Error): void { // eslint-disable-next-line no-console console.error(error && error.stack ? error.stack : error); - const client = getCurrentHub().getClient(); + const client = getHubClient(getCurrentHub()); if (client === undefined) { logger.warn('No NodeClient was defined, we are exiting the process now.'); diff --git a/packages/node/src/integrations/utils/http.ts b/packages/node/src/integrations/utils/http.ts index bb0e9a76cf00..c4b71e6f3340 100644 --- a/packages/node/src/integrations/utils/http.ts +++ b/packages/node/src/integrations/utils/http.ts @@ -1,4 +1,5 @@ import { getCurrentHub } from '@sentry/core'; +import { getHubClient } from '@sentry/hub'; import { parseSemver } from '@sentry/utils'; import * as http from 'http'; import * as https from 'https'; @@ -11,9 +12,7 @@ const NODE_VERSION = parseSemver(process.versions.node); * @param url url to verify */ export function isSentryRequest(url: string): boolean { - const dsn = getCurrentHub() - .getClient() - ?.getDsn(); + const dsn = getHubClient(getCurrentHub())?.getDsn(); return dsn ? url.includes(dsn.host) : false; } diff --git a/packages/node/src/sdk.ts b/packages/node/src/sdk.ts index 52d137277fd8..f63ef3d3c30d 100644 --- a/packages/node/src/sdk.ts +++ b/packages/node/src/sdk.ts @@ -1,5 +1,14 @@ import { getCurrentHub, initAndBind, Integrations as CoreIntegrations } from '@sentry/core'; -import { getMainCarrier, setHubOnCarrier } from '@sentry/hub'; +import { + getHubClient, + getHubLastEventId, + getMainCarrier, + getScopeSession, + setHubOnCarrier, + endHubSession, + getHubScope, + startHubSession, +} from '@sentry/hub'; import { SessionStatus } from '@sentry/types'; import { getGlobalObject, logger } from '@sentry/utils'; import * as domain from 'domain'; @@ -136,7 +145,7 @@ export function init(options: NodeOptions = {}): void { * @returns The last event id of a captured event. */ export function lastEventId(): string | undefined { - return getCurrentHub().lastEventId(); + return getHubLastEventId(getCurrentHub()); } /** @@ -148,7 +157,7 @@ export function lastEventId(): string | undefined { * doesn't (or if there's no client defined). */ export async function flush(timeout?: number): Promise { - const client = getCurrentHub().getClient(); + const client = getHubClient(getCurrentHub()); if (client) { return client.flush(timeout); } @@ -165,7 +174,7 @@ export async function flush(timeout?: number): Promise { * doesn't (or if there's no client defined). */ export async function close(timeout?: number): Promise { - const client = getCurrentHub().getClient(); + const client = getHubClient(getCurrentHub()); if (client) { return client.close(timeout); } @@ -225,18 +234,18 @@ export function getSentryRelease(fallback?: string): string | undefined { */ function startSessionTracking(): void { const hub = getCurrentHub(); - hub.startSession(); + startHubSession(hub); // Emitted in the case of healthy sessions, error of `mechanism.handled: true` and unhandledrejections because // The 'beforeExit' event is not emitted for conditions causing explicit termination, // such as calling process.exit() or uncaught exceptions. // Ref: https://nodejs.org/api/process.html#process_event_beforeexit process.on('beforeExit', () => { - const session = hub.getScope()?.getSession(); + const session = getScopeSession(getHubScope(hub)); const terminalStates: SessionStatus[] = ['exited', 'crashed']; // Only call endSession, if the Session exists on Scope and SessionStatus is not a // Terminal Status i.e. Exited or Crashed because // "When a session is moved away from ok it must not be updated anymore." // Ref: https://develop.sentry.dev/sdk/sessions/ - if (session && !terminalStates.includes(session.status)) hub.endSession(); + if (session && !terminalStates.includes(session.status)) endHubSession(hub); }); } diff --git a/packages/node/test/integrations/http.test.ts b/packages/node/test/integrations/http.test.ts index 36c17ec7cd51..8067f7839974 100644 --- a/packages/node/test/integrations/http.test.ts +++ b/packages/node/test/integrations/http.test.ts @@ -11,6 +11,7 @@ import * as nock from 'nock'; import { Breadcrumb } from '../../src'; import { NodeClient } from '../../src/client'; import { Http as HttpIntegration } from '../../src/integrations/http'; +import { startHubTransaction, bindHubClient } from '@sentry/hub'; const NODE_VERSION = parseSemver(process.versions.node); @@ -30,7 +31,7 @@ describe('tracing', () => { jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub); jest.spyOn(hubModule, 'getCurrentHub').mockReturnValue(hub); - const transaction = hub.startTransaction({ name: 'dogpark' }); + const transaction = startHubTransaction(hub, { name: 'dogpark' }); hub.getScope()?.setSpan(transaction); return transaction; @@ -107,7 +108,8 @@ describe('default protocols', () => { const p = new Promise(r => { resolve = r; }); - hub.bindClient( + bindHubClient( + hub, new NodeClient({ dsn: 'https://dogsarebadatkeepingsecrets@squirrelchasers.ingest.sentry.io/12312012', integrations: [new HttpIntegration({ breadcrumbs: true })], diff --git a/packages/react/src/profiler.tsx b/packages/react/src/profiler.tsx index acf764347c69..5fe0523d4a7a 100644 --- a/packages/react/src/profiler.tsx +++ b/packages/react/src/profiler.tsx @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-explicit-any */ import { getCurrentHub, Hub } from '@sentry/browser'; +import { getHubIntegration, getHubScope, getScopeTransaction } from '@sentry/hub'; import { Integration, IntegrationClass, Span, Transaction } from '@sentry/types'; import { timestampWithMs } from '@sentry/utils'; import hoistNonReactStatics from 'hoist-non-react-statics'; @@ -21,7 +22,7 @@ const getTracingIntegration = (): Integration | null => { return globalTracingIntegration; } - globalTracingIntegration = getCurrentHub().getIntegration(TRACING_GETTER); + globalTracingIntegration = getHubIntegration(getCurrentHub(), TRACING_GETTER); return globalTracingIntegration; }; @@ -89,22 +90,20 @@ export type ProfilerProps = { * spans based on component lifecycles. */ class Profiler extends React.Component { + // eslint-disable-next-line @typescript-eslint/member-ordering + public static defaultProps: Partial = { + disabled: false, + includeRender: true, + includeUpdates: true, + }; /** * The span of the mount activity * Made protected for the React Native SDK to access */ protected _mountSpan: Span | undefined = undefined; - // The activity representing how long it takes to mount a component. private _mountActivity: number | null = null; - // eslint-disable-next-line @typescript-eslint/member-ordering - public static defaultProps: Partial = { - disabled: false, - includeRender: true, - includeUpdates: true, - }; - public constructor(props: ProfilerProps) { super(props); const { name, disabled = false } = this.props; @@ -274,9 +273,9 @@ export { withProfiler, Profiler, useProfiler }; /** Grabs active transaction off scope */ export function getActiveTransaction(hub: Hub = getCurrentHub()): T | undefined { if (hub) { - const scope = hub.getScope(); + const scope = getHubScope(hub); if (scope) { - return scope.getTransaction() as T | undefined; + return getScopeTransaction(scope) as T | undefined; } } diff --git a/packages/react/src/redux.ts b/packages/react/src/redux.ts index ad4b4fe7b5e7..472039ac2ba8 100644 --- a/packages/react/src/redux.ts +++ b/packages/react/src/redux.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { configureScope } from '@sentry/minimal'; import { Scope } from '@sentry/types'; +import { addScopeBreadcrumb, setScopeContext } from '@sentry/hub'; interface Action { type: T; @@ -98,7 +99,7 @@ function createReduxEnhancer(enhancerOptions?: Partial): /* Action breadcrumbs */ const transformedAction = options.actionTransformer(action); if (typeof transformedAction !== 'undefined' && transformedAction !== null) { - scope.addBreadcrumb({ + addScopeBreadcrumb(scope, { category: ACTION_BREADCRUMB_CATEGORY, data: transformedAction, type: ACTION_BREADCRUMB_TYPE, @@ -108,9 +109,9 @@ function createReduxEnhancer(enhancerOptions?: Partial): /* Set latest state to scope */ const transformedState = options.stateTransformer(newState); if (typeof transformedState !== 'undefined' && transformedState !== null) { - scope.setContext(STATE_CONTEXT_KEY, transformedState); + setScopeContext(scope, STATE_CONTEXT_KEY, transformedState); } else { - scope.setContext(STATE_CONTEXT_KEY, null); + setScopeContext(scope, STATE_CONTEXT_KEY, null); } /* Allow user to configure scope with latest state */ diff --git a/packages/serverless/src/awslambda.ts b/packages/serverless/src/awslambda.ts index 6001613a91de..ebd773acd0bd 100644 --- a/packages/serverless/src/awslambda.ts +++ b/packages/serverless/src/awslambda.ts @@ -9,6 +9,7 @@ import { startTransaction, withScope, } from '@sentry/node'; +import { popScope, pushScope } from '@sentry/hub'; import { extractTraceparentData } from '@sentry/tracing'; import { Integration } from '@sentry/types'; import { isString, logger } from '@sentry/utils'; @@ -292,7 +293,7 @@ export function wrapHandler( }); const hub = getCurrentHub(); - const scope = hub.pushScope(); + const scope = pushScope(hub); let rv: TResult | undefined; try { enhanceScopeWithEnvironmentData(scope, context, START_TIME); @@ -315,7 +316,7 @@ export function wrapHandler( } finally { clearTimeout(timeoutWarningTimer); transaction.finish(); - hub.popScope(); + popScope(hub); await flush(options.flushTimeout); } return rv; diff --git a/packages/tracing/src/hubextensions.ts b/packages/tracing/src/hubextensions.ts index 7bc654094ef7..b8a47382fa2e 100644 --- a/packages/tracing/src/hubextensions.ts +++ b/packages/tracing/src/hubextensions.ts @@ -1,4 +1,4 @@ -import { getMainCarrier, Hub } from '@sentry/hub'; +import { getHubClient, getMainCarrier, getHubScope, Hub, getScopeSpan } from '@sentry/hub'; import { CustomSamplingContext, Integration, @@ -16,9 +16,9 @@ import { hasTracingEnabled } from './utils'; /** Returns all trace headers that are currently on the top scope. */ function traceHeaders(this: Hub): { [key: string]: string } { - const scope = this.getScope(); + const scope = getHubScope(this); if (scope) { - const span = scope.getSpan(); + const span = getScopeSpan(scope); if (span) { return { 'sentry-trace': span.toTraceparent(), @@ -165,7 +165,7 @@ function _startTransaction( transactionContext: TransactionContext, customSamplingContext?: CustomSamplingContext, ): Transaction { - const client = this.getClient(); + const client = getHubClient(this); const options = (client && client.getOptions()) || {}; let transaction = new Transaction(transactionContext, this); @@ -190,7 +190,7 @@ export function startIdleTransaction( onScope?: boolean, customSamplingContext?: CustomSamplingContext, ): IdleTransaction { - const client = hub.getClient(); + const client = getHubClient(hub); const options = (client && client.getOptions()) || {}; let transaction = new IdleTransaction(transactionContext, hub, idleTimeout, onScope); diff --git a/packages/tracing/src/idletransaction.ts b/packages/tracing/src/idletransaction.ts index 07839d6c6616..c522281ae7bc 100644 --- a/packages/tracing/src/idletransaction.ts +++ b/packages/tracing/src/idletransaction.ts @@ -1,4 +1,4 @@ -import { Hub } from '@sentry/hub'; +import { getHubScope, Hub, configureHubScope, setScopeSpan, getScopeTransaction } from '@sentry/hub'; import { TransactionContext } from '@sentry/types'; import { logger, timestampWithMs } from '@sentry/utils'; @@ -93,7 +93,7 @@ export class IdleTransaction extends Transaction { // We set the transaction here on the scope so error events pick up the trace // context and attach it to the error. logger.log(`Setting idle transaction on scope. Span ID: ${this.spanId}`); - _idleHub.configureScope(scope => scope.setSpan(this)); + configureHubScope(_idleHub, scope => setScopeSpan(scope, this)); } this._initTimeout = setTimeout(() => { @@ -276,11 +276,11 @@ export class IdleTransaction extends Transaction { */ function clearActiveTransaction(hub?: Hub): void { if (hub) { - const scope = hub.getScope(); + const scope = getHubScope(hub); if (scope) { - const transaction = scope.getTransaction(); + const transaction = getScopeTransaction(scope); if (transaction) { - scope.setSpan(undefined); + setScopeSpan(scope, undefined); } } } diff --git a/packages/tracing/src/integrations/node/mongo.ts b/packages/tracing/src/integrations/node/mongo.ts index f19ae2a10429..6b3261eb481c 100644 --- a/packages/tracing/src/integrations/node/mongo.ts +++ b/packages/tracing/src/integrations/node/mongo.ts @@ -1,4 +1,4 @@ -import { Hub } from '@sentry/hub'; +import { getHubScope, getScopeSpan, Hub } from '@sentry/hub'; import { EventProcessor, Integration, SpanContext } from '@sentry/types'; import { fill, isThenable, loadModule, logger } from '@sentry/utils'; @@ -148,8 +148,8 @@ export class Mongo implements Integration { fill(collection.prototype, operation, function(orig: () => void | Promise) { return function(this: unknown, ...args: unknown[]) { const lastArg = args[args.length - 1]; - const scope = getCurrentHub().getScope(); - const parentSpan = scope?.getSpan(); + const scope = getHubScope(getCurrentHub()); + const parentSpan = scope && getScopeSpan(scope); // Check if the operation was passed a callback. (mapReduce requires a different check, as // its (non-callback) arguments can also be functions.) diff --git a/packages/tracing/src/integrations/node/mysql.ts b/packages/tracing/src/integrations/node/mysql.ts index 2d53f7dfc61a..635ead77a1fe 100644 --- a/packages/tracing/src/integrations/node/mysql.ts +++ b/packages/tracing/src/integrations/node/mysql.ts @@ -1,4 +1,4 @@ -import { Hub } from '@sentry/hub'; +import { getHubScope, getScopeSpan, Hub } from '@sentry/hub'; import { EventProcessor, Integration } from '@sentry/types'; import { fill, loadModule, logger } from '@sentry/utils'; @@ -35,8 +35,8 @@ export class Mysql implements Integration { // function (options, values, callback) => void fill(pkg, 'createQuery', function(orig: () => void) { return function(this: unknown, options: unknown, values: unknown, callback: unknown) { - const scope = getCurrentHub().getScope(); - const parentSpan = scope?.getSpan(); + const scope = getHubScope(getCurrentHub()); + const parentSpan = scope && getScopeSpan(scope); const span = parentSpan?.startChild({ description: typeof options === 'string' ? options : (options as { sql: string }).sql, op: `db`, diff --git a/packages/tracing/src/integrations/node/postgres.ts b/packages/tracing/src/integrations/node/postgres.ts index b345f8a2876b..c5a9a6a12f33 100644 --- a/packages/tracing/src/integrations/node/postgres.ts +++ b/packages/tracing/src/integrations/node/postgres.ts @@ -1,4 +1,4 @@ -import { Hub } from '@sentry/hub'; +import { getHubScope, getScopeSpan, Hub } from '@sentry/hub'; import { EventProcessor, Integration } from '@sentry/types'; import { fill, isThenable, loadModule, logger } from '@sentry/utils'; @@ -57,8 +57,8 @@ export class Postgres implements Integration { */ fill(Client.prototype, 'query', function(orig: () => void | Promise) { return function(this: unknown, config: unknown, values: unknown, callback: unknown) { - const scope = getCurrentHub().getScope(); - const parentSpan = scope?.getSpan(); + const scope = getHubScope(getCurrentHub()); + const parentSpan = scope && getScopeSpan(scope); const span = parentSpan?.startChild({ description: typeof config === 'string' ? config : (config as { text: string }).text, op: `db`, diff --git a/packages/tracing/src/transaction.ts b/packages/tracing/src/transaction.ts index 5143881ab159..648a9956a9e9 100644 --- a/packages/tracing/src/transaction.ts +++ b/packages/tracing/src/transaction.ts @@ -1,4 +1,4 @@ -import { getCurrentHub, Hub } from '@sentry/hub'; +import { captureHubEvent, getCurrentHub, getHubClient, Hub } from '@sentry/hub'; import { Event, Measurements, @@ -103,7 +103,7 @@ export class Transaction extends SpanClass implements TransactionInterface { // At this point if `sampled !== true` we want to discard the transaction. logger.log('[Tracing] Discarding transaction because its trace was not chosen to be sampled.'); - const client = this._hub.getClient(); + const client = getHubClient(this._hub); const transport = client && client.getTransport && client.getTransport(); if (transport && transport.recordLostEvent) { transport.recordLostEvent('sample_rate', 'transaction'); @@ -144,7 +144,7 @@ export class Transaction extends SpanClass implements TransactionInterface { logger.log(`[Tracing] Finishing ${this.op} transaction: ${this.name}.`); - return this._hub.captureEvent(transaction); + return captureHubEvent(this._hub, transaction); } /** diff --git a/packages/tracing/src/utils.ts b/packages/tracing/src/utils.ts index 0e2ea9ed694b..abf85a74f310 100644 --- a/packages/tracing/src/utils.ts +++ b/packages/tracing/src/utils.ts @@ -1,4 +1,4 @@ -import { getCurrentHub, Hub } from '@sentry/hub'; +import { getCurrentHub, getHubClient, getHubScope, getScopeTransaction, Hub } from '@sentry/hub'; import { Options, TraceparentData, Transaction } from '@sentry/types'; export const TRACEPARENT_REGEXP = new RegExp( @@ -15,7 +15,7 @@ export const TRACEPARENT_REGEXP = new RegExp( * Tracing is enabled when at least one of `tracesSampleRate` and `tracesSampler` is defined in the SDK config. */ export function hasTracingEnabled(maybeOptions?: Options | undefined): boolean { - const client = getCurrentHub().getClient(); + const client = getHubClient(getCurrentHub()); const options = maybeOptions || (client && client.getOptions()); return !!options && ('tracesSampleRate' in options || 'tracesSampler' in options); } @@ -48,8 +48,8 @@ export function extractTraceparentData(traceparent: string): TraceparentData | u /** Grabs active transaction off scope, if any */ export function getActiveTransaction(maybeHub?: Hub): T | undefined { const hub = maybeHub || getCurrentHub(); - const scope = hub.getScope(); - return scope && (scope.getTransaction() as T | undefined); + const scope = getHubScope(hub); + return scope && (getScopeTransaction(scope) as T | undefined); } /** diff --git a/packages/types/src/hub.ts b/packages/types/src/hub.ts index 6dac7c47e545..2ce9790fd537 100644 --- a/packages/types/src/hub.ts +++ b/packages/types/src/hub.ts @@ -1,231 +1,5 @@ -import { Breadcrumb, BreadcrumbHint } from './breadcrumb'; -import { Client } from './client'; -import { Event, EventHint } from './event'; -import { Extra, Extras } from './extra'; -import { Integration, IntegrationClass } from './integration'; -import { Primitive } from './misc'; -import { Scope } from './scope'; -import { Session, SessionContext } from './session'; -import { SeverityLevel } from './severity'; -import { Span, SpanContext } from './span'; -import { CustomSamplingContext, Transaction, TransactionContext } from './transaction'; -import { User } from './user'; - /** * Internal class used to make sure we always have the latest internal functions * working in case we have a version conflict. */ -export interface Hub { - /** - * Checks if this hub's version is older than the given version. - * - * @param version A version number to compare to. - * @return True if the given version is newer; otherwise false. - * - * @hidden - */ - isOlderThan(version: number): boolean; - - /** - * This binds the given client to the current scope. - * @param client An SDK client (client) instance. - */ - bindClient(client?: Client): void; - - /** - * Create a new scope to store context information. - * - * The scope will be layered on top of the current one. It is isolated, i.e. all - * breadcrumbs and context information added to this scope will be removed once - * the scope ends. Be sure to always remove this scope with {@link this.popScope} - * when the operation finishes or throws. - * - * @returns Scope, the new cloned scope - */ - pushScope(): Scope; - - /** - * Removes a previously pushed scope from the stack. - * - * This restores the state before the scope was pushed. All breadcrumbs and - * context information added since the last call to {@link this.pushScope} are - * discarded. - */ - popScope(): boolean; - - /** - * Creates a new scope with and executes the given operation within. - * The scope is automatically removed once the operation - * finishes or throws. - * - * This is essentially a convenience function for: - * - * pushScope(); - * callback(); - * popScope(); - * - * @param callback that will be enclosed into push/popScope. - */ - withScope(callback: (scope: Scope) => void): void; - - /** Returns the client of the top stack. */ - getClient(): Client | undefined; - - /** - * Captures an exception event and sends it to Sentry. - * - * @param exception An exception-like object. - * @param hint May contain additional information about the original exception. - * @returns The generated eventId. - */ - captureException(exception: any, hint?: EventHint): string; - - /** - * Captures a message event and sends it to Sentry. - * - * @param message The message to send to Sentry. - * @param level Define the level of the message. - * @param hint May contain additional information about the original exception. - * @returns The generated eventId. - */ - captureMessage(message: string, level?: SeverityLevel, hint?: EventHint): string; - - /** - * Captures a manually created event and sends it to Sentry. - * - * @param event The event to send to Sentry. - * @param hint May contain additional information about the original exception. - */ - captureEvent(event: Event, hint?: EventHint): string; - - /** - * This is the getter for lastEventId. - * - * @returns The last event id of a captured event. - */ - lastEventId(): string | undefined; - - /** - * Records a new breadcrumb which will be attached to future events. - * - * Breadcrumbs will be added to subsequent events to provide more context on - * user's actions prior to an error or crash. - * - * @param breadcrumb The breadcrumb to record. - * @param hint May contain additional information about the original breadcrumb. - */ - addBreadcrumb(breadcrumb: Breadcrumb, hint?: BreadcrumbHint): void; - - /** - * Updates user context information for future events. - * - * @param user User context object to be set in the current context. Pass `null` to unset the user. - */ - setUser(user: User | null): void; - - /** - * Set an object that will be merged sent as tags data with the event. - * - * @param tags Tags context object to merge into current context. - */ - setTags(tags: { [key: string]: Primitive }): void; - - /** - * Set key:value that will be sent as tags data with the event. - * - * Can also be used to unset a tag, by passing `undefined`. - * - * @param key String key of tag - * @param value Value of tag - */ - setTag(key: string, value: Primitive): void; - - /** - * Set key:value that will be sent as extra data with the event. - * @param key String of extra - * @param extra Any kind of data. This data will be normalized. - */ - setExtra(key: string, extra: Extra): void; - - /** - * Set an object that will be merged sent as extra data with the event. - * @param extras Extras object to merge into current context. - */ - setExtras(extras: Extras): void; - - /** - * Sets context data with the given name. - * @param name of the context - * @param context Any kind of data. This data will be normalized. - */ - setContext(name: string, context: { [key: string]: any } | null): void; - - /** - * Callback to set context information onto the scope. - * - * @param callback Callback function that receives Scope. - */ - configureScope(callback: (scope: Scope) => void): void; - - /** - * For the duration of the callback, this hub will be set as the global current Hub. - * This function is useful if you want to run your own client and hook into an already initialized one - * e.g.: Reporting issues to your own sentry when running in your component while still using the users configuration. - */ - run(callback: (hub: Hub) => void): void; - - /** Returns the integration if installed on the current client. */ - getIntegration(integration: IntegrationClass): T | null; - - /** Returns all trace headers that are currently on the top scope. */ - traceHeaders(): { [key: string]: string }; - - /** - * @deprecated No longer does anything. Use use {@link Transaction.startChild} instead. - */ - startSpan(context: SpanContext): Span; - - /** - * Starts a new `Transaction` and returns it. This is the entry point to manual tracing instrumentation. - * - * A tree structure can be built by adding child spans to the transaction, and child spans to other spans. To start a - * new child span within the transaction or any span, call the respective `.startChild()` method. - * - * Every child span must be finished before the transaction is finished, otherwise the unfinished spans are discarded. - * - * The transaction must be finished with a call to its `.finish()` method, at which point the transaction with all its - * finished child spans will be sent to Sentry. - * - * @param context Properties of the new `Transaction`. - * @param customSamplingContext Information given to the transaction sampling function (along with context-dependent - * default values). See {@link Options.tracesSampler}. - * - * @returns The transaction which was just started - */ - startTransaction(context: TransactionContext, customSamplingContext?: CustomSamplingContext): Transaction; - - /** - * Starts a new `Session`, sets on the current scope and returns it. - * - * To finish a `session`, it has to be passed directly to `client.captureSession`, which is done automatically - * when using `hub.endSession()` for the session currently stored on the scope. - * - * When there's already an existing session on the scope, it'll be automatically ended. - * - * @param context Optional properties of the new `Session`. - * - * @returns The session which was just started - */ - startSession(context?: SessionContext): Session; - - /** - * Ends the session that lives on the current scope and sends it to Sentry - */ - endSession(): void; - - /** - * Sends the current session on the scope to Sentry - * @param endSession If set the session will be marked as exited and removed from the scope - */ - captureSession(endSession?: boolean): void; -} +export interface Hub {} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index a6268e586a8d..0a44c33d8755 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -22,14 +22,13 @@ export { CaptureContext, Scope, ScopeContext } from './scope'; export { SdkInfo } from './sdkinfo'; export { SdkMetadata } from './sdkmetadata'; export { + Session, SessionAggregates, AggregationCounts, - Session, SessionContext, SessionStatus, RequestSession, RequestSessionStatus, - SessionFlusherLike, } from './session'; /* eslint-disable-next-line deprecation/deprecation */ diff --git a/packages/types/src/integration.ts b/packages/types/src/integration.ts index 251c135973fd..a6c4967af09e 100644 --- a/packages/types/src/integration.ts +++ b/packages/types/src/integration.ts @@ -22,5 +22,5 @@ export interface Integration { * Sets the integration up only once. * This takes no options on purpose, options should be passed in the constructor */ - setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void; + setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub | any): void; // TODO: fix any just to make the compiler happy for now } diff --git a/packages/types/src/scope.ts b/packages/types/src/scope.ts index 00014e0735d8..f20c4d922dfe 100644 --- a/packages/types/src/scope.ts +++ b/packages/types/src/scope.ts @@ -1,16 +1,13 @@ -import { Breadcrumb } from './breadcrumb'; -import { Context, Contexts } from './context'; -import { EventProcessor } from './eventprocessor'; -import { Extra, Extras } from './extra'; +import { Contexts } from './context'; +import { Extras } from './extra'; import { Primitive } from './misc'; -import { RequestSession, Session } from './session'; +import { RequestSession } from './session'; import { SeverityLevel } from './severity'; -import { Span } from './span'; -import { Transaction } from './transaction'; import { User } from './user'; /** JSDocs */ -export type CaptureContext = Scope | Partial | ((scope: Scope) => Scope); +export type CaptureContextCallback = (scope: Scope) => Scope; +export type CaptureContext = Scope | Partial | CaptureContextCallback; /** JSDocs */ export interface ScopeContext { @@ -23,136 +20,4 @@ export interface ScopeContext { requestSession: RequestSession; } -/** - * Holds additional event information. {@link Scope.applyToEvent} will be - * called by the client before an event will be sent. - */ -export interface Scope { - /** Add new event processor that will be called after {@link applyToEvent}. */ - addEventProcessor(callback: EventProcessor): this; - - /** - * Updates user context information for future events. - * - * @param user User context object to be set in the current context. Pass `null` to unset the user. - */ - setUser(user: User | null): this; - - /** - * Returns the `User` if there is one - */ - getUser(): User | undefined; - - /** - * Set an object that will be merged sent as tags data with the event. - * @param tags Tags context object to merge into current context. - */ - setTags(tags: { [key: string]: Primitive }): this; - - /** - * Set key:value that will be sent as tags data with the event. - * - * Can also be used to unset a tag by passing `undefined`. - * - * @param key String key of tag - * @param value Value of tag - */ - setTag(key: string, value: Primitive): this; - - /** - * Set an object that will be merged sent as extra data with the event. - * @param extras Extras object to merge into current context. - */ - setExtras(extras: Extras): this; - - /** - * Set key:value that will be sent as extra data with the event. - * @param key String of extra - * @param extra Any kind of data. This data will be normalized. - */ - setExtra(key: string, extra: Extra): this; - - /** - * Sets the fingerprint on the scope to send with the events. - * @param fingerprint string[] to group events in Sentry. - */ - setFingerprint(fingerprint: string[]): this; - - /** - * Sets the level on the scope for future events. - * @param level string {@link Severity} - */ - setLevel(level: SeverityLevel): this; - - /** - * Sets the transaction name on the scope for future events. - */ - setTransactionName(name?: string): this; - - /** - * Sets context data with the given name. - * @param name of the context - * @param context an object containing context data. This data will be normalized. Pass `null` to unset the context. - */ - setContext(name: string, context: Context | null): this; - - /** - * Sets the Span on the scope. - * @param span Span - */ - setSpan(span?: Span): this; - - /** - * Returns the `Span` if there is one - */ - getSpan(): Span | undefined; - - /** - * Returns the `Transaction` attached to the scope (if there is one) - */ - getTransaction(): Transaction | undefined; - - /** - * Returns the `Session` if there is one - */ - getSession(): Session | undefined; - - /** - * Sets the `Session` on the scope - */ - setSession(session?: Session): this; - - /** - * Returns the `RequestSession` if there is one - */ - getRequestSession(): RequestSession | undefined; - - /** - * Sets the `RequestSession` on the scope - */ - setRequestSession(requestSession?: RequestSession): this; - - /** - * Updates the scope with provided data. Can work in three variations: - * - plain object containing updatable attributes - * - Scope instance that'll extract the attributes from - * - callback function that'll receive the current scope as an argument and allow for modifications - * @param captureContext scope modifier to be used - */ - update(captureContext?: CaptureContext): this; - - /** Clears the current scope and resets its properties. */ - clear(): this; - - /** - * Sets the breadcrumbs in the scope - * @param breadcrumbs Breadcrumb - * @param maxBreadcrumbs number of max breadcrumbs to merged into event. - */ - addBreadcrumb(breadcrumb: Breadcrumb, maxBreadcrumbs?: number): this; - - /** - * Clears all currently set Breadcrumbs. - */ - clearBreadcrumbs(): this; -} +export type Scope = any; diff --git a/packages/types/src/session.ts b/packages/types/src/session.ts index 552dda002531..509c3a794dd7 100644 --- a/packages/types/src/session.ts +++ b/packages/types/src/session.ts @@ -1,34 +1,5 @@ import { User } from './user'; -/** - * @inheritdoc - */ -export interface Session extends SessionContext { - /** JSDoc */ - update(context?: SessionContext): void; - - /** JSDoc */ - close(status?: SessionStatus): void; - - /** JSDoc */ - toJSON(): { - init: boolean; - sid: string; - did?: string; - timestamp: string; - started: string; - duration?: number; - status: SessionStatus; - errors: number; - attrs?: { - release?: string; - environment?: string; - user_agent?: string; - ip_address?: string; - }; - }; -} - export interface RequestSession { status?: RequestSessionStatus; } @@ -67,26 +38,11 @@ export interface SessionAggregates { aggregates: Array; } -export interface SessionFlusherLike { - /** - * Increments the Session Status bucket in SessionAggregates Object corresponding to the status of the session - * captured - */ - incrementSessionStatusCount(): void; - - /** Submits the aggregates request mode sessions to Sentry */ - sendSessionAggregates(sessionAggregates: SessionAggregates): void; - - /** Empties Aggregate Buckets and Sends them to Transport Buffer */ - flush(): void; - - /** Clears setInterval and calls flush */ - close(): void; -} - export interface AggregationCounts { started: string; errored?: number; exited?: number; crashed?: number; } + +export type Session = any; diff --git a/packages/vue/package.json b/packages/vue/package.json index b9bf16d240a7..56b094b60724 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -16,6 +16,7 @@ "access": "public" }, "dependencies": { + "@sentry/hub": "6.17.0-beta.0", "@sentry/browser": "6.17.0-beta.0", "@sentry/core": "6.17.0-beta.0", "@sentry/minimal": "6.17.0-beta.0", diff --git a/packages/vue/src/errorhandler.ts b/packages/vue/src/errorhandler.ts index 2343e351cd72..08f9250b3094 100644 --- a/packages/vue/src/errorhandler.ts +++ b/packages/vue/src/errorhandler.ts @@ -1,4 +1,5 @@ import { getCurrentHub } from '@sentry/browser'; +import { captureHubException, setScopeContext, withHubScope } from '@sentry/hub'; import { formatComponentName, generateComponentTrace } from './components'; import { Options, ViewModel, Vue } from './types'; @@ -25,9 +26,9 @@ export const attachErrorHandler = (app: Vue, options: Options): void => { // Capture exception in the next event loop, to make sure that all breadcrumbs are recorded in time. setTimeout(() => { - getCurrentHub().withScope(scope => { - scope.setContext('vue', metadata); - getCurrentHub().captureException(error); + withHubScope(getCurrentHub(), scope => { + setScopeContext(scope, 'vue', metadata); + captureHubException(getCurrentHub(), error); }); }); diff --git a/packages/vue/src/tracing.ts b/packages/vue/src/tracing.ts index deb14eb848f6..685c8caacccf 100644 --- a/packages/vue/src/tracing.ts +++ b/packages/vue/src/tracing.ts @@ -1,4 +1,5 @@ import { getCurrentHub } from '@sentry/browser'; +import { getHubScope, getScopeTransaction } from '@sentry/hub'; import { Span, Transaction } from '@sentry/types'; import { logger, timestampInSeconds } from '@sentry/utils'; @@ -30,9 +31,8 @@ const HOOKS: { [key in Operation]: Hook[] } = { /** Grabs active transaction off scope, if any */ function getActiveTransaction(): Transaction | undefined { - return getCurrentHub() - .getScope() - ?.getTransaction(); + const scope = getHubScope(getCurrentHub()); + return scope ? getScopeTransaction(scope) : undefined; } /** Finish top-level span and activity with a debounce configured using `timeout` option */