From 7079520518f324ca4b782b03413f2dc8ba04f8e9 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 12 Apr 2022 10:12:48 +0200 Subject: [PATCH] ref(backend): Port functionality from Backend to Client (#4911) port all the functionality from the Backend classes to the Client classes. This includes: * Backend (interface) * BaseBackend * BrowserBackend * NodeBackend * TestBackend Additionally, fix the unit and integration tests in Core to spy on TestClient instead of TestBackend. --- packages/browser/src/backend.ts | 2 + packages/browser/src/client.ts | 60 ++++++++- packages/browser/src/exports.ts | 1 + packages/browser/test/unit/backend.test.ts | 2 + packages/browser/test/unit/index.test.ts | 6 +- .../unit/integrations/linkederrors.test.ts | 3 + packages/core/src/baseclient.ts | 106 ++++++++++++++-- packages/core/src/index.ts | 1 + packages/core/test/lib/base.test.ts | 116 ++++++++++-------- packages/core/test/mocks/backend.ts | 9 +- packages/core/test/mocks/client.ts | 68 +++++++++- packages/node/src/client.ts | 67 +++++++++- packages/node/src/index.ts | 1 + packages/node/test/index.test.ts | 11 +- .../test/integrations/linkederrors.test.ts | 5 + packages/types/src/client.ts | 20 ++- 16 files changed, 394 insertions(+), 84 deletions(-) diff --git a/packages/browser/src/backend.ts b/packages/browser/src/backend.ts index 89cf843d48ae..2543c6ae99ad 100644 --- a/packages/browser/src/backend.ts +++ b/packages/browser/src/backend.ts @@ -8,6 +8,8 @@ import { FetchTransport, makeNewFetchTransport, makeNewXHRTransport, XHRTranspor /** * Configuration options for the Sentry Browser SDK. * @see BrowserClient for more information. + * + * TODO(v7): move to client */ export interface BrowserOptions extends Options { /** diff --git a/packages/browser/src/client.ts b/packages/browser/src/client.ts index 75aab49db77b..b64997c0a23b 100644 --- a/packages/browser/src/client.ts +++ b/packages/browser/src/client.ts @@ -1,17 +1,20 @@ -import { BaseClient, Scope, SDK_VERSION } from '@sentry/core'; -import { Event, EventHint } from '@sentry/types'; -import { getGlobalObject, logger } from '@sentry/utils'; +import { BaseClient, getEnvelopeEndpointWithUrlEncodedAuth, initAPIDetails, Scope, SDK_VERSION } from '@sentry/core'; +import { Event, EventHint, Severity, Transport, TransportOptions } from '@sentry/types'; +import { getGlobalObject, logger, supportsFetch } from '@sentry/utils'; import { BrowserBackend, BrowserOptions } from './backend'; +import { eventFromException, eventFromMessage } from './eventbuilder'; import { IS_DEBUG_BUILD } from './flags'; import { injectReportDialog, ReportDialogOptions } from './helpers'; import { Breadcrumbs } from './integrations'; +import { FetchTransport, makeNewFetchTransport, makeNewXHRTransport, XHRTransport } from './transports'; /** * The Sentry Browser SDK Client. * * @see BrowserOptions for documentation on configuration options. * @see SentryClient for usage documentation. + * TODO(v7): remove BrowserBackend */ export class BrowserClient extends BaseClient { /** @@ -32,6 +35,7 @@ export class BrowserClient extends BaseClient { version: SDK_VERSION, }; + // TODO(v7): remove BrowserBackend param super(BrowserBackend, options); } @@ -58,6 +62,20 @@ export class BrowserClient extends BaseClient { }); } + /** + * @inheritDoc + */ + public eventFromException(exception: unknown, hint?: EventHint): PromiseLike { + return eventFromException(exception, hint, this._options.attachStacktrace); + } + + /** + * @inheritDoc + */ + public eventFromMessage(message: string, level: Severity = Severity.Info, hint?: EventHint): PromiseLike { + return eventFromMessage(message, level, hint, this._options.attachStacktrace); + } + /** * @inheritDoc */ @@ -76,4 +94,40 @@ export class BrowserClient extends BaseClient { } super._sendEvent(event); } + + /** + * @inheritDoc + */ + protected _setupTransport(): Transport { + if (!this._options.dsn) { + // We return the noop transport here in case there is no Dsn. + return super._setupTransport(); + } + + const transportOptions: TransportOptions = { + ...this._options.transportOptions, + dsn: this._options.dsn, + tunnel: this._options.tunnel, + sendClientReports: this._options.sendClientReports, + _metadata: this._options._metadata, + }; + + const api = initAPIDetails(transportOptions.dsn, transportOptions._metadata, transportOptions.tunnel); + const url = getEnvelopeEndpointWithUrlEncodedAuth(api.dsn, api.tunnel); + + if (this._options.transport) { + return new this._options.transport(transportOptions); + } + if (supportsFetch()) { + const requestOptions: RequestInit = { ...transportOptions.fetchParameters }; + this._newTransport = makeNewFetchTransport({ requestOptions, url }); + return new FetchTransport(transportOptions); + } + + this._newTransport = makeNewXHRTransport({ + url, + headers: transportOptions.headers, + }); + return new XHRTransport(transportOptions); + } } diff --git a/packages/browser/src/exports.ts b/packages/browser/src/exports.ts index b4dd4374a7b3..2a62e8e46fbe 100644 --- a/packages/browser/src/exports.ts +++ b/packages/browser/src/exports.ts @@ -41,6 +41,7 @@ export { withScope, } from '@sentry/core'; +// TODO(v7): refactor to use client here! export { BrowserOptions } from './backend'; export { BrowserClient } from './client'; export { injectReportDialog, ReportDialogOptions } from './helpers'; diff --git a/packages/browser/test/unit/backend.test.ts b/packages/browser/test/unit/backend.test.ts index 71ffec4d75b1..83f95307a8c7 100644 --- a/packages/browser/test/unit/backend.test.ts +++ b/packages/browser/test/unit/backend.test.ts @@ -2,6 +2,8 @@ import { BrowserBackend } from '../../src/backend'; let backend: BrowserBackend; +// TODO(v7): remove when deleting Backend + describe('BrowserBackend', () => { describe('sendEvent()', () => { it('should use NoopTransport', () => { diff --git a/packages/browser/test/unit/index.test.ts b/packages/browser/test/unit/index.test.ts index 83c0053d3b66..707354fe7d1d 100644 --- a/packages/browser/test/unit/index.test.ts +++ b/packages/browser/test/unit/index.test.ts @@ -246,7 +246,7 @@ describe('SentryBrowser initialization', () => { it('should set SDK data when Sentry.init() is called', () => { init({ dsn }); - const sdkData = (getCurrentHub().getClient() as any)._backend._transport._api.metadata?.sdk; + const sdkData = (getCurrentHub().getClient() as any).getTransport()._api.metadata?.sdk; expect(sdkData.name).toBe('sentry.javascript.browser'); expect(sdkData.packages[0].name).toBe('npm:@sentry/browser'); @@ -257,7 +257,7 @@ describe('SentryBrowser initialization', () => { it('should set SDK data when instantiating a client directly', () => { const client = new BrowserClient({ dsn }); - const sdkData = (client as any)._backend._transport._api.metadata?.sdk; + const sdkData = (client as any).getTransport()._api.metadata?.sdk; expect(sdkData.name).toBe('sentry.javascript.browser'); expect(sdkData.packages[0].name).toBe('npm:@sentry/browser'); @@ -285,7 +285,7 @@ describe('SentryBrowser initialization', () => { }, }); - const sdkData = (getCurrentHub().getClient() as any)._backend._transport._api.metadata?.sdk; + const sdkData = (getCurrentHub().getClient() as any).getTransport()._api.metadata?.sdk; expect(sdkData.name).toBe('sentry.javascript.angular'); expect(sdkData.packages[0].name).toBe('npm:@sentry/angular'); diff --git a/packages/browser/test/unit/integrations/linkederrors.test.ts b/packages/browser/test/unit/integrations/linkederrors.test.ts index 1531aa6f77ed..35f54f4b2d87 100644 --- a/packages/browser/test/unit/integrations/linkederrors.test.ts +++ b/packages/browser/test/unit/integrations/linkederrors.test.ts @@ -34,6 +34,7 @@ describe('LinkedErrors', () => { one.cause = two; const originalException = one; + // TODO(v7): refactor to use client here! const backend = new BrowserBackend({}); return backend.eventFromException(originalException).then(event => { const result = LinkedErrorsModule._handler('cause', 5, event, { @@ -65,6 +66,7 @@ describe('LinkedErrors', () => { const originalException = one; const backend = new BrowserBackend({}); + // TODO(v7): refactor to use client here! return backend.eventFromException(originalException).then(event => { const result = LinkedErrorsModule._handler('reason', 5, event, { originalException, @@ -92,6 +94,7 @@ describe('LinkedErrors', () => { const backend = new BrowserBackend({}); const originalException = one; + // TODO(v7): refactor to use client here! return backend.eventFromException(originalException).then(event => { const result = LinkedErrorsModule._handler('cause', 2, event, { originalException, diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index b7fef7f2164f..59145508ca9d 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -28,15 +28,21 @@ import { uuid4, } from '@sentry/utils'; +import { initAPIDetails } from './api'; import { Backend, BackendClass } from './basebackend'; import { IS_DEBUG_BUILD } from './flags'; import { IntegrationIndex, setupIntegrations } from './integration'; +import { createEventEnvelope, createSessionEnvelope } from './request'; +import { NewTransport } from './transports/base'; +import { NoopTransport } from './transports/noop'; const ALREADY_SEEN_ERROR = "Not capturing exception because it's already been captured."; /** * Base implementation for all JavaScript SDK clients. * + * TODO(v7): refactor doc w.r.t. Backend + * * Call the constructor with the corresponding backend constructor and options * specific to the client subclass. To access these options later, use * {@link Client.getOptions}. Also, the Backend instance is available via @@ -71,6 +77,7 @@ export abstract class BaseClient implement * The backend used to physically interact in the environment. Usually, this * will correspond to the client. When composing SDKs, however, the Backend * from the root SDK will be used. + * TODO(v7): DELETE */ protected readonly _backend: B; @@ -86,6 +93,12 @@ export abstract class BaseClient implement /** Number of calls being processed */ protected _numProcessing: number = 0; + /** Cached transport used internally. */ + protected _transport: Transport; + + /** New v7 Transport that is initialized alongside the old one */ + protected _newTransport?: NewTransport; + /** * Initializes this client instance. * @@ -93,12 +106,17 @@ export abstract class BaseClient implement * @param options Options for the client. */ protected constructor(backendClass: BackendClass, options: O) { + // TODO(v7): Delete this._backend = new backendClass(options); this._options = options; if (options.dsn) { this._dsn = makeDsn(options.dsn); + } else { + IS_DEBUG_BUILD && logger.warn('No DSN provided, client will not do anything.'); } + + this._transport = this._setupTransport(); } /** @@ -115,8 +133,7 @@ export abstract class BaseClient implement let eventId: string | undefined = hint && hint.event_id; this._process( - this._getBackend() - .eventFromException(exception, hint) + this.eventFromException(exception, hint) .then(event => this._captureEvent(event, hint, scope)) .then(result => { eventId = result; @@ -133,8 +150,8 @@ export abstract class BaseClient implement let eventId: string | undefined = hint && hint.event_id; const promisedEvent = isPrimitive(message) - ? this._getBackend().eventFromMessage(String(message), level, hint) - : this._getBackend().eventFromException(message, hint); + ? this.eventFromMessage(String(message), level, hint) + : this.eventFromException(message, hint); this._process( promisedEvent @@ -204,7 +221,7 @@ export abstract class BaseClient implement * @inheritDoc */ public getTransport(): Transport { - return this._getBackend().getTransport(); + return this._transport; } /** @@ -249,6 +266,57 @@ export abstract class BaseClient implement } } + /** + * @inheritDoc + */ + public sendEvent(event: Event): void { + // TODO(v7): Remove the if-else + if ( + this._newTransport && + this._options.dsn && + this._options._experiments && + this._options._experiments.newTransport + ) { + const api = initAPIDetails(this._options.dsn, this._options._metadata, this._options.tunnel); + const env = createEventEnvelope(event, api); + void this._newTransport.send(env).then(null, reason => { + IS_DEBUG_BUILD && logger.error('Error while sending event:', reason); + }); + } else { + void this._transport.sendEvent(event).then(null, reason => { + IS_DEBUG_BUILD && logger.error('Error while sending event:', reason); + }); + } + } + + /** + * @inheritDoc + */ + public sendSession(session: Session): void { + if (!this._transport.sendSession) { + IS_DEBUG_BUILD && logger.warn("Dropping session because custom transport doesn't implement sendSession"); + return; + } + + // TODO(v7): Remove the if-else + if ( + this._newTransport && + this._options.dsn && + this._options._experiments && + this._options._experiments.newTransport + ) { + const api = initAPIDetails(this._options.dsn, this._options._metadata, this._options.tunnel); + const [env] = createSessionEnvelope(session, api); + void this._newTransport.send(env).then(null, reason => { + IS_DEBUG_BUILD && logger.error('Error while sending session:', reason); + }); + } else { + void this._transport.sendSession(session).then(null, reason => { + IS_DEBUG_BUILD && logger.error('Error while sending session:', reason); + }); + } + } + /** Updates existing session based on the provided event */ protected _updateSessionFromEvent(session: Session, event: Event): void { let crashed = false; @@ -283,8 +351,9 @@ export abstract class BaseClient implement } /** Deliver captured session to Sentry */ + // TODO(v7): should this be deleted? protected _sendSession(session: Session): void { - this._getBackend().sendSession(session); + this.sendSession(session); } /** @@ -317,7 +386,9 @@ export abstract class BaseClient implement }); } - /** Returns the current backend. */ + /** Returns the current backend. + * TODO(v7): DELETE + */ protected _getBackend(): B { return this._backend; } @@ -490,8 +561,9 @@ export abstract class BaseClient implement * Tells the backend to send this event * @param event The Sentry event to send */ + // TODO(v7): refactor: get rid of method? protected _sendEvent(event: Event): void { - this._getBackend().sendEvent(event); + this.sendEvent(event); } /** @@ -618,6 +690,24 @@ export abstract class BaseClient implement }, ); } + + /** + * Sets up the transport so it can be used later to send requests. + */ + protected _setupTransport(): Transport { + return new NoopTransport(); + } + + /** + * @inheritDoc + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types + public abstract eventFromException(_exception: any, _hint?: EventHint): PromiseLike; + + /** + * @inheritDoc + */ + public abstract eventFromMessage(_message: string, _level?: Severity, _hint?: EventHint): PromiseLike; } /** diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index b066724ce099..77018c2b4437 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -23,6 +23,7 @@ export { getReportDialogEndpoint, } from './api'; export { BaseClient } from './baseclient'; +// TODO(v7): Delete! export { BackendClass, BaseBackend } from './basebackend'; export { eventToSentryRequest, sessionToSentryRequest } from './request'; export { initAndBind, ClientClass } from './sdk'; diff --git a/packages/core/test/lib/base.test.ts b/packages/core/test/lib/base.test.ts index 5c978b0ee530..8a26bd51f116 100644 --- a/packages/core/test/lib/base.test.ts +++ b/packages/core/test/lib/base.test.ts @@ -3,7 +3,7 @@ import { Event, Span, Transport } from '@sentry/types'; import { dsnToString, logger, SentryError, SyncPromise } from '@sentry/utils'; import * as integrationModule from '../../src/integration'; -import { TestBackend } from '../mocks/backend'; +import { NoopTransport } from '../../src/transports/noop'; import { TestClient } from '../mocks/client'; import { TestIntegration } from '../mocks/integration'; import { FakeTransport } from '../mocks/transport'; @@ -12,7 +12,7 @@ const PUBLIC_DSN = 'https://username@domain/123'; // eslint-disable-next-line no-var declare var global: any; -const backendEventFromException = jest.spyOn(TestBackend.prototype, 'eventFromException'); +const clientEventFromException = jest.spyOn(TestClient.prototype, 'eventFromException'); const clientProcess = jest.spyOn(TestClient.prototype as any, '_process'); jest.mock('@sentry/utils', () => { @@ -55,8 +55,8 @@ jest.mock('@sentry/utils', () => { describe('BaseClient', () => { beforeEach(() => { - TestBackend.sendEventCalled = undefined; - TestBackend.instance = undefined; + TestClient.sendEventCalled = undefined; + TestClient.instance = undefined; }); afterEach(() => { @@ -98,14 +98,24 @@ describe('BaseClient', () => { }); describe('getTransport()', () => { - test('returns the transport from backend', () => { + test('returns the transport from client', () => { expect.assertions(2); const options = { dsn: PUBLIC_DSN, transport: FakeTransport }; const client = new TestClient(options); expect(client.getTransport()).toBeInstanceOf(FakeTransport); - expect(TestBackend.instance!.getTransport()).toBe(client.getTransport()); + expect(TestClient.instance!.getTransport()).toBe(client.getTransport()); + }); + + test('retruns NoopTransport when no transport is passed', () => { + expect.assertions(2); + + const options = { dsn: PUBLIC_DSN }; + const client = new TestClient(options); + + expect(client.getTransport()).toBeInstanceOf(NoopTransport); + expect(TestClient.instance!.getTransport()).toBe(client.getTransport()); }); }); @@ -223,7 +233,7 @@ describe('BaseClient', () => { client.captureException(new Error('test exception')); - expect(TestBackend.instance!.event).toEqual( + expect(TestClient.instance!.event).toEqual( expect.objectContaining({ environment: 'production', event_id: '42', @@ -257,7 +267,7 @@ describe('BaseClient', () => { scope, ); - expect(TestBackend.instance!.event).toEqual( + expect(TestClient.instance!.event).toEqual( expect.objectContaining({ extra: { bar: 'wat', @@ -284,7 +294,7 @@ describe('BaseClient', () => { scope, ); - expect(TestBackend.instance!.event).toEqual( + expect(TestClient.instance!.event).toEqual( expect.objectContaining({ extra: { bar: 'wat', @@ -309,12 +319,12 @@ describe('BaseClient', () => { client.captureException(thrown); expect(thrown.__sentry_captured__).toBe(true); - expect(backendEventFromException).toHaveBeenCalledTimes(1); + expect(clientEventFromException).toHaveBeenCalledTimes(1); client.captureException(thrown); // `captureException` should bail right away this second time around and not get as far as calling this again - expect(backendEventFromException).toHaveBeenCalledTimes(1); + expect(clientEventFromException).toHaveBeenCalledTimes(1); }); }); @@ -324,7 +334,7 @@ describe('BaseClient', () => { client.captureMessage('test message'); - expect(TestBackend.instance!.event).toEqual( + expect(TestClient.instance!.event).toEqual( expect.objectContaining({ environment: 'production', event_id: '42', @@ -337,7 +347,7 @@ describe('BaseClient', () => { test('should call eventFromException if input to captureMessage is not a primitive', () => { const client = new TestClient({ dsn: PUBLIC_DSN }); - const spy = jest.spyOn(TestBackend.instance!, 'eventFromException'); + const spy = jest.spyOn(TestClient.instance!, 'eventFromException'); client.captureMessage('foo'); client.captureMessage(null as any); @@ -371,7 +381,7 @@ describe('BaseClient', () => { scope, ); - expect(TestBackend.instance!.event).toEqual( + expect(TestClient.instance!.event).toEqual( expect.objectContaining({ extra: { bar: 'wat', @@ -392,7 +402,7 @@ describe('BaseClient', () => { client.captureEvent({}, undefined, scope); - expect(TestBackend.instance!.event).toBeUndefined(); + expect(TestClient.instance!.event).toBeUndefined(); }); test('skips without a Dsn', () => { @@ -403,7 +413,7 @@ describe('BaseClient', () => { client.captureEvent({}, undefined, scope); - expect(TestBackend.instance!.event).toBeUndefined(); + expect(TestClient.instance!.event).toBeUndefined(); }); test.each([ @@ -443,8 +453,8 @@ describe('BaseClient', () => { client.captureEvent({ message: 'message' }, undefined, scope); - expect(TestBackend.instance!.event!.message).toBe('message'); - expect(TestBackend.instance!.event).toEqual( + expect(TestClient.instance!.event!.message).toBe('message'); + expect(TestClient.instance!.event).toEqual( expect.objectContaining({ environment: 'production', event_id: '42', @@ -462,8 +472,8 @@ describe('BaseClient', () => { client.captureEvent({ message: 'message', timestamp: 1234 }, undefined, scope); - expect(TestBackend.instance!.event!.message).toBe('message'); - expect(TestBackend.instance!.event).toEqual( + expect(TestClient.instance!.event!.message).toBe('message'); + expect(TestClient.instance!.event).toEqual( expect.objectContaining({ environment: 'production', event_id: '42', @@ -481,7 +491,7 @@ describe('BaseClient', () => { client.captureEvent({ message: 'message' }, { event_id: 'wat' }, scope); - expect(TestBackend.instance!.event!).toEqual( + expect(TestClient.instance!.event!).toEqual( expect.objectContaining({ environment: 'production', event_id: 'wat', @@ -501,7 +511,7 @@ describe('BaseClient', () => { client.captureEvent({ message: 'message' }, undefined, scope); - expect(TestBackend.instance!.event!).toEqual( + expect(TestClient.instance!.event!).toEqual( expect.objectContaining({ environment: 'production', event_id: '42', @@ -522,7 +532,7 @@ describe('BaseClient', () => { client.captureEvent({ message: 'message' }, undefined, scope); - expect(TestBackend.instance!.event!).toEqual( + expect(TestClient.instance!.event!).toEqual( expect.objectContaining({ environment: 'env', event_id: '42', @@ -543,7 +553,7 @@ describe('BaseClient', () => { client.captureEvent({ message: 'message' }, undefined, scope); - expect(TestBackend.instance!.event!).toEqual( + expect(TestClient.instance!.event!).toEqual( expect.objectContaining({ environment: undefined, event_id: '42', @@ -564,7 +574,7 @@ describe('BaseClient', () => { client.captureEvent({ message: 'message' }, undefined, scope); - expect(TestBackend.instance!.event!).toEqual( + expect(TestClient.instance!.event!).toEqual( expect.objectContaining({ environment: 'production', event_id: '42', @@ -584,10 +594,10 @@ describe('BaseClient', () => { client.captureEvent({ message: 'message' }, undefined, scope); - expect(TestBackend.instance!.event!).toHaveProperty('event_id', '42'); - expect(TestBackend.instance!.event!).toHaveProperty('message', 'message'); - expect(TestBackend.instance!.event!).toHaveProperty('breadcrumbs'); - expect(TestBackend.instance!.event!.breadcrumbs![0]).toHaveProperty('message', 'breadcrumb'); + expect(TestClient.instance!.event!).toHaveProperty('event_id', '42'); + expect(TestClient.instance!.event!).toHaveProperty('message', 'message'); + expect(TestClient.instance!.event!).toHaveProperty('breadcrumbs'); + expect(TestClient.instance!.event!.breadcrumbs![0]).toHaveProperty('message', 'breadcrumb'); }); test('limits previously saved breadcrumbs', () => { @@ -601,8 +611,8 @@ describe('BaseClient', () => { client.captureEvent({ message: 'message' }, undefined, scope); - expect(TestBackend.instance!.event!.breadcrumbs).toHaveLength(1); - expect(TestBackend.instance!.event!.breadcrumbs![0].message).toEqual('2'); + expect(TestClient.instance!.event!.breadcrumbs).toHaveLength(1); + expect(TestClient.instance!.event!.breadcrumbs![0].message).toEqual('2'); }); test('adds context data', () => { @@ -616,7 +626,7 @@ describe('BaseClient', () => { client.captureEvent({ message: 'message' }, undefined, scope); - expect(TestBackend.instance!.event!).toEqual( + expect(TestClient.instance!.event!).toEqual( expect.objectContaining({ environment: 'production', event_id: '42', @@ -638,7 +648,7 @@ describe('BaseClient', () => { client.captureEvent({ message: 'message' }, undefined, scope); - expect(TestBackend.instance!.event!).toEqual( + expect(TestClient.instance!.event!).toEqual( expect.objectContaining({ environment: 'production', event_id: '42', @@ -655,7 +665,7 @@ describe('BaseClient', () => { client.captureEvent({ message: 'message' }); - expect(TestBackend.instance!.event!.sdk).toEqual({ + expect(TestClient.instance!.event!.sdk).toEqual({ integrations: ['TestIntegration'], }); }); @@ -698,7 +708,7 @@ describe('BaseClient', () => { user: fourLevelsObject, }); - expect(TestBackend.instance!.event!).toEqual( + expect(TestClient.instance!.event!).toEqual( expect.objectContaining({ breadcrumbs: [normalizedBreadcrumb, normalizedBreadcrumb, normalizedBreadcrumb], contexts: normalizedObject, @@ -749,7 +759,7 @@ describe('BaseClient', () => { user: fourLevelsObject, }); - expect(TestBackend.instance!.event!).toEqual( + expect(TestClient.instance!.event!).toEqual( expect.objectContaining({ breadcrumbs: [normalizedBreadcrumb, normalizedBreadcrumb, normalizedBreadcrumb], contexts: normalizedObject, @@ -805,7 +815,7 @@ describe('BaseClient', () => { user: fourLevelsObject, }); - expect(TestBackend.instance!.event!).toEqual( + expect(TestClient.instance!.event!).toEqual( expect.objectContaining({ breadcrumbs: [normalizedBreadcrumb, normalizedBreadcrumb, normalizedBreadcrumb], contexts: normalizedObject, @@ -871,7 +881,7 @@ describe('BaseClient', () => { // event. The code can be restored to its original form (the commented-out line below) once that hack is // removed. See https://github.com/getsentry/sentry-javascript/pull/4425 and // https://github.com/getsentry/sentry-javascript/pull/4574 - const capturedEvent = TestBackend.instance!.event!; + const capturedEvent = TestClient.instance!.event!; if (capturedEvent.sdkProcessingMetadata?.normalizeDepth) { if (Object.keys(capturedEvent.sdkProcessingMetadata).length === 1) { delete capturedEvent.sdkProcessingMetadata; @@ -899,7 +909,7 @@ describe('BaseClient', () => { client.captureEvent({ message: 'hello' }); - expect(TestBackend.instance!.event!.message).toBe('hello'); + expect(TestClient.instance!.event!.message).toBe('hello'); }); test('calls beforeSend and uses the new one', () => { @@ -910,7 +920,7 @@ describe('BaseClient', () => { client.captureEvent({ message: 'hello' }); - expect(TestBackend.instance!.event!.message).toBe('changed1'); + expect(TestClient.instance!.event!.message).toBe('changed1'); }); test('calls beforeSend and discards the event', () => { @@ -923,7 +933,7 @@ describe('BaseClient', () => { client.captureEvent({ message: 'hello' }); - expect(TestBackend.instance!.event).toBeUndefined(); + expect(TestClient.instance!.event).toBeUndefined(); expect(captureExceptionSpy).not.toBeCalled(); expect(loggerErrorSpy).toBeCalledWith(new SentryError('`beforeSend` returned `null`, will not send event.')); }); @@ -940,7 +950,7 @@ describe('BaseClient', () => { client.captureEvent({ message: 'hello' }); - expect(TestBackend.instance!.event).toBeUndefined(); + expect(TestClient.instance!.event).toBeUndefined(); expect(loggerErrorSpy).toBeCalledWith( new SentryError('`beforeSend` method has to return `null` or a valid event.'), ); @@ -964,7 +974,7 @@ describe('BaseClient', () => { client.captureEvent({ message: 'hello' }); jest.runOnlyPendingTimers(); - TestBackend.sendEventCalled = (event: Event) => { + TestClient.sendEventCalled = (event: Event) => { expect(event.message).toBe('hello'); }; @@ -992,7 +1002,7 @@ describe('BaseClient', () => { client.captureEvent({ message: 'hello' }); jest.runOnlyPendingTimers(); - TestBackend.sendEventCalled = (event: Event) => { + TestClient.sendEventCalled = (event: Event) => { expect(event.message).toBe('changed2'); }; @@ -1020,7 +1030,7 @@ describe('BaseClient', () => { client.captureEvent({ message: 'hello' }); jest.runAllTimers(); - expect(TestBackend.instance!.event).toBeUndefined(); + expect(TestClient.instance!.event).toBeUndefined(); }); test('beforeSend gets access to a hint as a second argument', () => { @@ -1031,8 +1041,8 @@ describe('BaseClient', () => { client.captureEvent({ message: 'hello' }, { data: 'someRandomThing' }); - expect(TestBackend.instance!.event!.message).toBe('hello'); - expect((TestBackend.instance!.event! as any).data).toBe('someRandomThing'); + expect(TestClient.instance!.event!.message).toBe('hello'); + expect((TestClient.instance!.event! as any).data).toBe('someRandomThing'); }); test('beforeSend records dropped events', () => { @@ -1068,7 +1078,7 @@ describe('BaseClient', () => { client.captureEvent({ message: 'hello' }, {}, scope); - expect(TestBackend.instance!.event).toBeUndefined(); + expect(TestClient.instance!.event).toBeUndefined(); expect(captureExceptionSpy).not.toBeCalled(); expect(loggerErrorSpy).toBeCalledWith(new SentryError('An event processor returned null, will not send event.')); }); @@ -1108,7 +1118,7 @@ describe('BaseClient', () => { client.captureEvent({ message: 'hello' }, {}, scope); - expect(TestBackend.instance!.event!.exception!.values![0]).toStrictEqual({ type: 'Error', value: 'sorry' }); + expect(TestClient.instance!.event!.exception!.values![0]).toStrictEqual({ type: 'Error', value: 'sorry' }); expect(captureExceptionSpy).toBeCalledWith(exception, { data: { __sentry__: true, @@ -1249,7 +1259,7 @@ describe('BaseClient', () => { }); const delay = 300; - const spy = jest.spyOn(TestBackend.instance!, 'eventFromMessage'); + const spy = jest.spyOn(TestClient.instance!, 'eventFromMessage'); spy.mockImplementationOnce( (message, level) => new SyncPromise(resolve => { @@ -1317,7 +1327,7 @@ describe('BaseClient', () => { }); describe('captureSession()', () => { - test('sends sessions to the backend', () => { + test('sends sessions to the client', () => { expect.assertions(1); const client = new TestClient({ dsn: PUBLIC_DSN }); @@ -1325,7 +1335,7 @@ describe('BaseClient', () => { client.captureSession(session); - expect(TestBackend.instance!.session).toEqual(session); + expect(TestClient.instance!.session).toEqual(session); }); test('skips when disabled', () => { @@ -1336,7 +1346,7 @@ describe('BaseClient', () => { client.captureSession(session); - expect(TestBackend.instance!.session).toBeUndefined(); + expect(TestClient.instance!.session).toBeUndefined(); }); }); }); diff --git a/packages/core/test/mocks/backend.ts b/packages/core/test/mocks/backend.ts index 48ddb0d9cc0c..a201bc8c0b68 100644 --- a/packages/core/test/mocks/backend.ts +++ b/packages/core/test/mocks/backend.ts @@ -1,14 +1,11 @@ import { Session } from '@sentry/hub'; -import { Event, Options, Severity, Transport } from '@sentry/types'; +import { Event, Severity, Transport } from '@sentry/types'; import { resolvedSyncPromise } from '@sentry/utils'; import { BaseBackend } from '../../src/basebackend'; +import { TestOptions } from './client'; -export interface TestOptions extends Options { - test?: boolean; - mockInstallFailure?: boolean; - enableSend?: boolean; -} +// TODO: Delete whole file (?) export class TestBackend extends BaseBackend { public static instance?: TestBackend; diff --git a/packages/core/test/mocks/client.ts b/packages/core/test/mocks/client.ts index 810328818eae..8c02ca30f5f3 100644 --- a/packages/core/test/mocks/client.ts +++ b/packages/core/test/mocks/client.ts @@ -1,14 +1,80 @@ +import { Session } from '@sentry/hub'; +import { Event, Options, Severity, Transport } from '@sentry/types'; +import { resolvedSyncPromise } from '@sentry/utils'; + import { BaseClient } from '../../src/baseclient'; import { initAndBind } from '../../src/sdk'; -import { TestBackend, TestOptions } from './backend'; +import { TestBackend } from './backend'; +export interface TestOptions extends Options { + test?: boolean; + mockInstallFailure?: boolean; + enableSend?: boolean; +} +// TODO(v7): remove TestBackend export class TestClient extends BaseClient { public static instance?: TestClient; + public static sendEventCalled?: (event: Event) => void; + + public event?: Event; + public session?: Session; public constructor(options: TestOptions) { + // TODO(v7): remove TestBackend param super(TestBackend, options); TestClient.instance = this; } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types + public eventFromException(exception: any): PromiseLike { + return resolvedSyncPromise({ + exception: { + values: [ + { + /* eslint-disable @typescript-eslint/no-unsafe-member-access */ + type: exception.name, + value: exception.message, + /* eslint-enable @typescript-eslint/no-unsafe-member-access */ + }, + ], + }, + }); + } + + public eventFromMessage(message: string, level: Severity = Severity.Info): PromiseLike { + return resolvedSyncPromise({ message, level }); + } + + public sendEvent(event: Event): void { + this.event = event; + if (this._options.enableSend) { + super.sendEvent(event); + return; + } + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + TestClient.sendEventCalled && TestClient.sendEventCalled(event); + } + + public sendSession(session: Session): void { + this.session = session; + } + + protected _setupTransport(): Transport { + if (!this._options.dsn) { + // We return the noop transport here in case there is no Dsn. + return super._setupTransport(); + } + + const transportOptions = this._options.transportOptions + ? this._options.transportOptions + : { dsn: this._options.dsn }; + + if (this._options.transport) { + return new this._options.transport(transportOptions); + } + + return super._setupTransport(); + } } export function init(options: TestOptions): void { diff --git a/packages/node/src/client.ts b/packages/node/src/client.ts index dde7f8941b74..0b2b585edc5d 100644 --- a/packages/node/src/client.ts +++ b/packages/node/src/client.ts @@ -1,10 +1,12 @@ -import { BaseClient, Scope, SDK_VERSION } from '@sentry/core'; +import { BaseClient, getEnvelopeEndpointWithUrlEncodedAuth, initAPIDetails, Scope, SDK_VERSION } from '@sentry/core'; import { SessionFlusher } from '@sentry/hub'; -import { Event, EventHint } from '@sentry/types'; -import { logger } from '@sentry/utils'; +import { Event, EventHint, Severity, Transport, TransportOptions } from '@sentry/types'; +import { logger, makeDsn, resolvedSyncPromise } from '@sentry/utils'; import { NodeBackend } from './backend'; +import { eventFromMessage, eventFromUnknownInput } from './eventbuilder'; import { IS_DEBUG_BUILD } from './flags'; +import { HTTPSTransport, HTTPTransport, makeNodeTransport } from './transports'; import { NodeOptions } from './types'; /** @@ -12,6 +14,8 @@ import { NodeOptions } from './types'; * * @see NodeOptions for documentation on configuration options. * @see SentryClient for usage documentation. + * + * TODO(v7): remove NodeBackend */ export class NodeClient extends BaseClient { protected _sessionFlusher: SessionFlusher | undefined; @@ -33,6 +37,7 @@ export class NodeClient extends BaseClient { version: SDK_VERSION, }; + // TODO(v7): remove NodeBackend param super(NodeBackend, options); } @@ -106,6 +111,21 @@ export class NodeClient extends BaseClient { } } + /** + * @inheritDoc + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types + public eventFromException(exception: any, hint?: EventHint): PromiseLike { + return resolvedSyncPromise(eventFromUnknownInput(exception, hint)); + } + + /** + * @inheritDoc + */ + public eventFromMessage(message: string, level: Severity = Severity.Info, hint?: EventHint): PromiseLike { + return resolvedSyncPromise(eventFromMessage(message, level, hint, this._options.attachStacktrace)); + } + /** * @inheritDoc */ @@ -128,4 +148,45 @@ export class NodeClient extends BaseClient { this._sessionFlusher.incrementSessionStatusCount(); } } + + /** + * @inheritDoc + */ + protected _setupTransport(): Transport { + if (!this._options.dsn) { + // We return the noop transport here in case there is no Dsn. + return super._setupTransport(); + } + + const dsn = makeDsn(this._options.dsn); + + const transportOptions: TransportOptions = { + ...this._options.transportOptions, + ...(this._options.httpProxy && { httpProxy: this._options.httpProxy }), + ...(this._options.httpsProxy && { httpsProxy: this._options.httpsProxy }), + ...(this._options.caCerts && { caCerts: this._options.caCerts }), + dsn: this._options.dsn, + tunnel: this._options.tunnel, + _metadata: this._options._metadata, + }; + + if (this._options.transport) { + return new this._options.transport(transportOptions); + } + + const api = initAPIDetails(transportOptions.dsn, transportOptions._metadata, transportOptions.tunnel); + const url = getEnvelopeEndpointWithUrlEncodedAuth(api.dsn, api.tunnel); + + this._newTransport = makeNodeTransport({ + url, + headers: transportOptions.headers, + proxy: transportOptions.httpProxy, + caCerts: transportOptions.caCerts, + }); + + if (dsn.protocol === 'http') { + return new HTTPTransport(transportOptions); + } + return new HTTPSTransport(transportOptions); + } } diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 9f65c3f1a1a8..48bb88a24872 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -42,6 +42,7 @@ export { } from '@sentry/core'; export { NodeOptions } from './types'; +// TODO(v7): delete! export { NodeBackend } from './backend'; export { NodeClient } from './client'; export { defaultIntegrations, init, lastEventId, flush, close, getSentryRelease } from './sdk'; diff --git a/packages/node/test/index.test.ts b/packages/node/test/index.test.ts index 0af38dae80fb..687465802a94 100644 --- a/packages/node/test/index.test.ts +++ b/packages/node/test/index.test.ts @@ -15,7 +15,6 @@ import { NodeClient, Scope, } from '../src'; -import { NodeBackend } from '../src/backend'; import { ContextLines, LinkedErrors } from '../src/integrations'; jest.mock('@sentry/core', () => { @@ -78,7 +77,7 @@ describe('SentryNode', () => { let s: jest.SpyInstance; beforeEach(() => { - s = jest.spyOn(NodeBackend.prototype, 'sendEvent').mockImplementation(async () => Promise.resolve({ code: 200 })); + s = jest.spyOn(NodeClient.prototype, 'sendEvent').mockImplementation(async () => Promise.resolve({ code: 200 })); }); afterEach(() => { @@ -107,7 +106,7 @@ describe('SentryNode', () => { let s: jest.SpyInstance; beforeEach(() => { - s = jest.spyOn(NodeBackend.prototype, 'sendEvent').mockImplementation(async () => Promise.resolve({ code: 200 })); + s = jest.spyOn(NodeClient.prototype, 'sendEvent').mockImplementation(async () => Promise.resolve({ code: 200 })); }); afterEach(() => { @@ -360,7 +359,7 @@ describe('SentryNode initialization', () => { init({ dsn }); // eslint-disable-next-line @typescript-eslint/no-explicit-any - const sdkData = (getCurrentHub().getClient() as any)._backend._transport._api.metadata?.sdk; + const sdkData = (getCurrentHub().getClient() as any).getTransport()._api.metadata?.sdk; expect(sdkData.name).toEqual('sentry.javascript.node'); expect(sdkData.packages[0].name).toEqual('npm:@sentry/node'); @@ -372,7 +371,7 @@ describe('SentryNode initialization', () => { const client = new NodeClient({ dsn }); // eslint-disable-next-line @typescript-eslint/no-explicit-any - const sdkData = (client as any)._backend._transport._api.metadata?.sdk; + const sdkData = (client as any).getTransport()._api.metadata?.sdk; expect(sdkData.name).toEqual('sentry.javascript.node'); expect(sdkData.packages[0].name).toEqual('npm:@sentry/node'); @@ -401,7 +400,7 @@ describe('SentryNode initialization', () => { }); // eslint-disable-next-line @typescript-eslint/no-explicit-any - const sdkData = (getCurrentHub().getClient() as any)._backend._transport._api.metadata?.sdk; + const sdkData = (getCurrentHub().getClient() as any).getTransport()._api.metadata?.sdk; expect(sdkData.name).toEqual('sentry.javascript.serverless'); expect(sdkData.packages[0].name).toEqual('npm:@sentry/serverless'); diff --git a/packages/node/test/integrations/linkederrors.test.ts b/packages/node/test/integrations/linkederrors.test.ts index 7dcaf077e4c6..8363bd177359 100644 --- a/packages/node/test/integrations/linkederrors.test.ts +++ b/packages/node/test/integrations/linkederrors.test.ts @@ -28,6 +28,7 @@ describe('LinkedErrors', () => { expect.assertions(2); const spy = jest.spyOn(linkedErrors, '_walkErrorTree'); const one = new Error('originalException'); + // TODO(v7): refactor to use client here! const backend = new NodeBackend({}); let event: Event | undefined; return backend @@ -52,6 +53,7 @@ describe('LinkedErrors', () => { ); const one = new Error('originalException'); const backend = new NodeBackend({}); + // TODO(v7): refactor to use client here! return backend.eventFromException(one).then(event => linkedErrors ._handler(event, { @@ -72,6 +74,7 @@ describe('LinkedErrors', () => { two.cause = three; const backend = new NodeBackend({}); + // TODO(v7): refactor to use client here! return backend.eventFromException(one).then(event => linkedErrors ._handler(event, { @@ -105,6 +108,7 @@ describe('LinkedErrors', () => { two.reason = three; const backend = new NodeBackend({}); + // TODO(v7): refactor to use client here! return backend.eventFromException(one).then(event => linkedErrors ._handler(event, { @@ -138,6 +142,7 @@ describe('LinkedErrors', () => { two.cause = three; const backend = new NodeBackend({}); + // TODO(v7): refactor to use client here! return backend.eventFromException(one).then(event => linkedErrors ._handler(event, { diff --git a/packages/types/src/client.ts b/packages/types/src/client.ts index 69f7298105b2..e11950487fa1 100644 --- a/packages/types/src/client.ts +++ b/packages/types/src/client.ts @@ -60,7 +60,12 @@ export interface Client { /** Returns the current options. */ getOptions(): O; - /** Returns clients transport. */ + /** + * Returns the transport that is used by the client. + * Please note that the transport gets lazy initialized so it will only be there once the first event has been sent. + * + * @returns The transport. + */ getTransport?(): Transport; /** @@ -88,4 +93,17 @@ export interface Client { /** This is an internal function to setup all integrations that should run on the client */ setupIntegrations(): void; + + /** Creates an {@link Event} from all inputs to `captureException` and non-primitive inputs to `captureMessage`. */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + eventFromException(exception: any, hint?: EventHint): PromiseLike; + + /** Creates an {@link Event} from primitive inputs to `captureMessage`. */ + eventFromMessage(message: string, level?: Severity, hint?: EventHint): PromiseLike; + + /** Submits the event to Sentry */ + sendEvent(event: Event): void; + + /** Submits the session to Sentry */ + sendSession(session: Session): void; }