diff --git a/packages/browser/src/client.ts b/packages/browser/src/client.ts index 8666eaebe8d5..a889d7f69d06 100644 --- a/packages/browser/src/client.ts +++ b/packages/browser/src/client.ts @@ -1,5 +1,5 @@ import { BaseClient, NewTransport, Scope, SDK_VERSION } from '@sentry/core'; -import { Event, EventHint, Options, Severity, SeverityLevel, Transport } from '@sentry/types'; +import { ClientOptions, Event, EventHint, Options, Severity, SeverityLevel, Transport } from '@sentry/types'; import { getGlobalObject, logger, stackParserFromOptions } from '@sentry/utils'; import { eventFromException, eventFromMessage } from './eventbuilder'; @@ -7,11 +7,7 @@ import { IS_DEBUG_BUILD } from './flags'; import { injectReportDialog, ReportDialogOptions } from './helpers'; import { Breadcrumbs } from './integrations'; -/** - * Configuration options for the Sentry Browser SDK. - * @see BrowserClient for more information. - */ -export interface BrowserOptions extends Options { +export interface BaseBrowserOptions { /** * A pattern for error URLs which should exclusively be sent to Sentry. * This is the opposite of {@link Options.denyUrls}. @@ -27,19 +23,31 @@ export interface BrowserOptions extends Options { denyUrls?: Array; } +/** + * Configuration options for the Sentry Browser SDK. + * @see @sentry/types Options for more information. + */ +export interface BrowserOptions extends Options, BaseBrowserOptions {} + +/** + * Configuration options for the Sentry Browser SDK Client class + * @see BrowserClient for more information. + */ +export interface BrowserClientOptions extends ClientOptions, BaseBrowserOptions {} + /** * The Sentry Browser SDK Client. * * @see BrowserOptions for documentation on configuration options. * @see SentryClient for usage documentation. */ -export class BrowserClient extends BaseClient { +export class BrowserClient extends BaseClient { /** * Creates a new Browser SDK instance. * * @param options Configuration options for this SDK. */ - public constructor(options: BrowserOptions = {}, transport: Transport, newTransport?: NewTransport) { + public constructor(options: BrowserClientOptions, transport: Transport, newTransport?: NewTransport) { options._metadata = options._metadata || {}; options._metadata.sdk = options._metadata.sdk || { name: 'sentry.javascript.browser', @@ -51,7 +59,6 @@ export class BrowserClient extends BaseClient { ], version: SDK_VERSION, }; - super(options, transport, newTransport); } diff --git a/packages/browser/src/sdk.ts b/packages/browser/src/sdk.ts index 94ec87cb4279..05d9d9c34462 100644 --- a/packages/browser/src/sdk.ts +++ b/packages/browser/src/sdk.ts @@ -1,12 +1,20 @@ -import { getCurrentHub, initAndBind, Integrations as CoreIntegrations } from '@sentry/core'; +import { getCurrentHub, getIntegrationsToSetup, initAndBind, Integrations as CoreIntegrations } from '@sentry/core'; import { Hub } from '@sentry/types'; -import { addInstrumentationHandler, getGlobalObject, logger, resolvedSyncPromise } from '@sentry/utils'; +import { + addInstrumentationHandler, + getGlobalObject, + logger, + resolvedSyncPromise, + stackParserFromOptions, + supportsFetch, +} from '@sentry/utils'; -import { BrowserClient, BrowserOptions } from './client'; +import { BrowserClient, BrowserClientOptions, BrowserOptions } from './client'; import { IS_DEBUG_BUILD } from './flags'; import { ReportDialogOptions, wrap as internalWrap } from './helpers'; import { Breadcrumbs, Dedupe, GlobalHandlers, LinkedErrors, TryCatch, UserAgent } from './integrations'; import { defaultStackParsers } from './stack-parsers'; +import { FetchTransport, XHRTransport } from './transports'; import { setupBrowserTransport } from './transports/setup'; export const defaultIntegrations = [ @@ -97,9 +105,17 @@ export function init(options: BrowserOptions = {}): void { if (options.stackParser === undefined) { options.stackParser = defaultStackParsers; } - const { transport, newTransport } = setupBrowserTransport(options); - initAndBind(BrowserClient, options, transport, newTransport); + + const clientOptions: BrowserClientOptions = { + ...options, + stackParser: stackParserFromOptions(options), + integrations: getIntegrationsToSetup(options), + // TODO(v7): get rid of transport being passed down below + transport: options.transport || (supportsFetch() ? FetchTransport : XHRTransport), + }; + + initAndBind(BrowserClient, clientOptions, transport, newTransport); if (options.autoSessionTracking) { startSessionTracking(); diff --git a/packages/browser/src/transports/setup.ts b/packages/browser/src/transports/setup.ts index 0af6aad90676..f72365e7dc94 100644 --- a/packages/browser/src/transports/setup.ts +++ b/packages/browser/src/transports/setup.ts @@ -31,7 +31,10 @@ export interface BrowserTransportOptions extends BaseTransportOptions { * this function will return a ready to use `NewTransport`. */ // TODO(v7): Adjust return value when NewTransport is the default -export function setupBrowserTransport(options: BrowserOptions): { transport: Transport; newTransport?: NewTransport } { +export function setupBrowserTransport(options: BrowserOptions): { + transport: Transport; + newTransport?: NewTransport; +} { if (!options.dsn) { // We return the noop transport here in case there is no Dsn. return { transport: new NoopTransport() }; diff --git a/packages/browser/test/unit/helper/browser-client-options.ts b/packages/browser/test/unit/helper/browser-client-options.ts new file mode 100644 index 000000000000..aa763a5de06b --- /dev/null +++ b/packages/browser/test/unit/helper/browser-client-options.ts @@ -0,0 +1,12 @@ +import { NoopTransport } from '@sentry/core'; + +import { BrowserClientOptions } from '../../../src/client'; + +export function getDefaultBrowserClientOptions(options: Partial = {}): BrowserClientOptions { + return { + integrations: [], + transport: NoopTransport, + stackParser: () => [], + ...options, + }; +} diff --git a/packages/browser/test/unit/index.test.ts b/packages/browser/test/unit/index.test.ts index 9371e60073c1..434dea98977a 100644 --- a/packages/browser/test/unit/index.test.ts +++ b/packages/browser/test/unit/index.test.ts @@ -16,6 +16,7 @@ import { showReportDialog, wrap, } from '../../src'; +import { getDefaultBrowserClientOptions } from './helper/browser-client-options'; import { SimpleTransport } from './mocks/simpletransport'; const dsn = 'https://53039209a22b4ec1bcc296a3c9fdecd6@sentry.io/4291'; @@ -75,7 +76,8 @@ describe('SentryBrowser', () => { describe('showReportDialog', () => { describe('user', () => { const EX_USER = { email: 'test@example.com' }; - const client = new BrowserClient({ dsn }, new SimpleTransport({ dsn })); + const options = getDefaultBrowserClientOptions({ dsn }); + const client = new BrowserClient(options, new SimpleTransport({ dsn })); const reportDialogSpy = jest.spyOn(client, 'showReportDialog'); beforeEach(() => { @@ -139,53 +141,41 @@ describe('SentryBrowser', () => { }); it('should capture a message', done => { - getCurrentHub().bindClient( - new BrowserClient( - { - beforeSend: (event: Event): Event | null => { - expect(event.message).toBe('test'); - expect(event.exception).toBeUndefined(); - done(); - return event; - }, - dsn, - }, - new SimpleTransport({ dsn }), - ), - ); + const options = getDefaultBrowserClientOptions({ + beforeSend: (event: Event): Event | null => { + expect(event.message).toBe('test'); + expect(event.exception).toBeUndefined(); + done(); + return event; + }, + dsn, + }); + getCurrentHub().bindClient(new BrowserClient(options, new SimpleTransport({ dsn }))); captureMessage('test'); }); it('should capture an event', done => { - getCurrentHub().bindClient( - new BrowserClient( - { - beforeSend: (event: Event): Event | null => { - expect(event.message).toBe('event'); - expect(event.exception).toBeUndefined(); - done(); - return event; - }, - dsn, - }, - new SimpleTransport({ dsn }), - ), - ); + const options = getDefaultBrowserClientOptions({ + beforeSend: (event: Event): Event | null => { + expect(event.message).toBe('event'); + expect(event.exception).toBeUndefined(); + done(); + return event; + }, + dsn, + }); + getCurrentHub().bindClient(new BrowserClient(options, new SimpleTransport({ dsn }))); captureEvent({ message: 'event' }); }); it('should not dedupe an event on bound client', async () => { const localBeforeSend = jest.fn(); - getCurrentHub().bindClient( - new BrowserClient( - { - beforeSend: localBeforeSend, - dsn, - integrations: [], - }, - new SimpleTransport({ dsn }), - ), - ); + const options = getDefaultBrowserClientOptions({ + beforeSend: localBeforeSend, + dsn, + integrations: [], + }); + getCurrentHub().bindClient(new BrowserClient(options, new SimpleTransport({ dsn }))); captureMessage('event222'); captureMessage('event222'); @@ -197,16 +187,12 @@ describe('SentryBrowser', () => { it('should use inboundfilter rules of bound client', async () => { const localBeforeSend = jest.fn(); - getCurrentHub().bindClient( - new BrowserClient( - { - beforeSend: localBeforeSend, - dsn, - integrations: [new Integrations.InboundFilters({ ignoreErrors: ['capture'] })], - }, - new SimpleTransport({ dsn }), - ), - ); + const options = getDefaultBrowserClientOptions({ + beforeSend: localBeforeSend, + dsn, + integrations: [new Integrations.InboundFilters({ ignoreErrors: ['capture'] })], + }); + getCurrentHub().bindClient(new BrowserClient(options, new SimpleTransport({ dsn }))); captureMessage('capture'); @@ -267,7 +253,8 @@ describe('SentryBrowser initialization', () => { }); it('should set SDK data when instantiating a client directly', () => { - const client = new BrowserClient({ dsn }, new SimpleTransport({ dsn })); + const options = getDefaultBrowserClientOptions({ dsn }); + const client = new BrowserClient(options, new SimpleTransport({ dsn })); const sdkData = (client.getTransport() as any)._api.metadata?.sdk; @@ -309,20 +296,16 @@ describe('SentryBrowser initialization', () => { describe('wrap()', () => { it('should wrap and call function while capturing error', done => { - getCurrentHub().bindClient( - new BrowserClient( - { - beforeSend: (event: Event): Event | null => { - expect(event.exception!.values![0].type).toBe('TypeError'); - expect(event.exception!.values![0].value).toBe('mkey'); - done(); - return null; - }, - dsn, - }, - new SimpleTransport({ dsn }), - ), - ); + const options = getDefaultBrowserClientOptions({ + beforeSend: (event: Event): Event | null => { + expect(event.exception!.values![0].type).toBe('TypeError'); + expect(event.exception!.values![0].value).toBe('mkey'); + done(); + return null; + }, + dsn, + }); + getCurrentHub().bindClient(new BrowserClient(options, new SimpleTransport({ dsn }))); try { wrap(() => { diff --git a/packages/browser/test/unit/integrations/linkederrors.test.ts b/packages/browser/test/unit/integrations/linkederrors.test.ts index 6ccd4b5975a3..4b862705bccc 100644 --- a/packages/browser/test/unit/integrations/linkederrors.test.ts +++ b/packages/browser/test/unit/integrations/linkederrors.test.ts @@ -5,6 +5,7 @@ import { BrowserClient } from '../../../src/client'; import * as LinkedErrorsModule from '../../../src/integrations/linkederrors'; import { defaultStackParsers } from '../../../src/stack-parsers'; import { setupBrowserTransport } from '../../../src/transports'; +import { getDefaultBrowserClientOptions } from '../helper/browser-client-options'; const parser = createStackParser(...defaultStackParsers); @@ -45,7 +46,7 @@ describe('LinkedErrors', () => { one.cause = two; const originalException = one; - const options = { stackParser: parser }; + const options = getDefaultBrowserClientOptions({ stackParser: parser }); const client = new BrowserClient(options, setupBrowserTransport(options).transport); return client.eventFromException(originalException).then(event => { const result = LinkedErrorsModule._handler(parser, 'cause', 5, event, { @@ -76,7 +77,7 @@ describe('LinkedErrors', () => { one.reason = two; const originalException = one; - const options = { stackParser: parser }; + const options = getDefaultBrowserClientOptions({ stackParser: parser }); const client = new BrowserClient(options, setupBrowserTransport(options).transport); return client.eventFromException(originalException).then(event => { const result = LinkedErrorsModule._handler(parser, 'reason', 5, event, { @@ -103,7 +104,7 @@ describe('LinkedErrors', () => { one.cause = two; two.cause = three; - const options = { stackParser: parser }; + const options = getDefaultBrowserClientOptions({ stackParser: parser }); const client = new BrowserClient(options, setupBrowserTransport(options).transport); const originalException = one; return client.eventFromException(originalException).then(event => { diff --git a/packages/browser/test/unit/sdk.test.ts b/packages/browser/test/unit/sdk.test.ts new file mode 100644 index 000000000000..8814329f16e9 --- /dev/null +++ b/packages/browser/test/unit/sdk.test.ts @@ -0,0 +1,124 @@ +/* eslint-disable @typescript-eslint/unbound-method */ +import { NoopTransport, Scope } from '@sentry/core'; +import { MockIntegration } from '@sentry/core/test/lib/sdk.test'; +import { Client, Integration } from '@sentry/types'; + +import { BrowserOptions } from '../../src'; +import { init } from '../../src/sdk'; +// eslint-disable-next-line no-var +declare var global: any; + +const PUBLIC_DSN = 'https://username@domain/123'; + +function getDefaultBrowserOptions(options: Partial = {}): BrowserOptions { + return { + integrations: [], + transport: NoopTransport, + stackParser: () => [], + ...options, + }; +} + +jest.mock('@sentry/hub', () => { + const original = jest.requireActual('@sentry/hub'); + return { + ...original, + getCurrentHub(): { + bindClient(client: Client): boolean; + getClient(): boolean; + getScope(): Scope; + } { + return { + getClient(): boolean { + return false; + }, + getScope(): Scope { + return new Scope(); + }, + bindClient(client: Client): boolean { + client.setupIntegrations(); + return true; + }, + }; + }, + }; +}); + +describe('init', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterAll(() => { + jest.resetAllMocks(); + }); + + test('installs default integrations', () => { + const DEFAULT_INTEGRATIONS: Integration[] = [ + new MockIntegration('MockIntegration 0.1'), + new MockIntegration('MockIntegration 0.2'), + ]; + const options = getDefaultBrowserOptions({ dsn: PUBLIC_DSN, defaultIntegrations: DEFAULT_INTEGRATIONS }); + + init(options); + + expect(DEFAULT_INTEGRATIONS[0].setupOnce as jest.Mock).toHaveBeenCalledTimes(1); + expect(DEFAULT_INTEGRATIONS[1].setupOnce as jest.Mock).toHaveBeenCalledTimes(1); + }); + + test("doesn't install default integrations if told not to", () => { + const DEFAULT_INTEGRATIONS: Integration[] = [ + new MockIntegration('MockIntegration 0.3'), + new MockIntegration('MockIntegration 0.4'), + ]; + const options = getDefaultBrowserOptions({ dsn: PUBLIC_DSN, defaultIntegrations: false }); + init(options); + + expect(DEFAULT_INTEGRATIONS[0].setupOnce as jest.Mock).toHaveBeenCalledTimes(0); + expect(DEFAULT_INTEGRATIONS[1].setupOnce as jest.Mock).toHaveBeenCalledTimes(0); + }); + + it('installs merged default integrations, with overrides provided through options', () => { + const DEFAULT_INTEGRATIONS = [ + new MockIntegration('MockIntegration 1.1'), + new MockIntegration('MockIntegration 1.2'), + ]; + + const integrations = [new MockIntegration('MockIntegration 1.1'), new MockIntegration('MockIntegration 1.3')]; + const options = getDefaultBrowserOptions({ + dsn: PUBLIC_DSN, + defaultIntegrations: DEFAULT_INTEGRATIONS, + integrations, + }); + + init(options); + // 'MockIntegration 1' should be overridden by the one with the same name provided through options + expect(DEFAULT_INTEGRATIONS[0].setupOnce as jest.Mock).toHaveBeenCalledTimes(0); + expect(DEFAULT_INTEGRATIONS[1].setupOnce as jest.Mock).toHaveBeenCalledTimes(1); + expect(integrations[0].setupOnce as jest.Mock).toHaveBeenCalledTimes(1); + expect(integrations[1].setupOnce as jest.Mock).toHaveBeenCalledTimes(1); + }); + + it('installs integrations returned from a callback function', () => { + const DEFAULT_INTEGRATIONS = [ + new MockIntegration('MockIntegration 2.1'), + new MockIntegration('MockIntegration 2.2'), + ]; + + const newIntegration = new MockIntegration('MockIntegration 2.3'); + const options = getDefaultBrowserOptions({ + defaultIntegrations: DEFAULT_INTEGRATIONS, + dsn: PUBLIC_DSN, + integrations: (integrations: Integration[]) => { + const t = integrations.slice(0, 1).concat(newIntegration); + return t; + }, + }); + + init(options); + + expect(DEFAULT_INTEGRATIONS[0].setupOnce as jest.Mock).toHaveBeenCalledTimes(1); + expect(newIntegration.setupOnce as jest.Mock).toHaveBeenCalledTimes(1); + expect(DEFAULT_INTEGRATIONS[1].setupOnce as jest.Mock).toHaveBeenCalledTimes(0); + }); +}); diff --git a/packages/browser/test/unit/transports/setup.test.ts b/packages/browser/test/unit/transports/setup.test.ts index 2683a0619aea..41b361d684d5 100644 --- a/packages/browser/test/unit/transports/setup.test.ts +++ b/packages/browser/test/unit/transports/setup.test.ts @@ -7,6 +7,7 @@ import { setupBrowserTransport, XHRTransport, } from '../../../src/transports'; +import { getDefaultBrowserClientOptions } from '../helper/browser-client-options'; import { SimpleTransport } from '../mocks/simpletransport'; const DSN = 'https://username@domain/123'; @@ -64,7 +65,7 @@ describe('setupBrowserTransport', () => { }); it('returns the instantiated transport passed via the options', () => { - const options = { dsn: DSN, transport: SimpleTransport }; + const options = getDefaultBrowserClientOptions({ dsn: DSN, transport: SimpleTransport }); const { transport, newTransport } = setupBrowserTransport(options); expect(transport).toBeDefined(); @@ -73,7 +74,8 @@ describe('setupBrowserTransport', () => { }); it('returns fetchTransports if fetch is supported', () => { - const options = { dsn: DSN }; + const options = getDefaultBrowserClientOptions({ dsn: DSN }); + delete options.transport; const { transport, newTransport } = setupBrowserTransport(options); expect(transport).toBeDefined(); @@ -86,7 +88,8 @@ describe('setupBrowserTransport', () => { it('returns xhrTransports if fetch is not supported', () => { fetchSupported = false; - const options = { dsn: DSN }; + const options = getDefaultBrowserClientOptions({ dsn: DSN }); + delete options.transport; const { transport, newTransport } = setupBrowserTransport(options); expect(transport).toBeDefined(); diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index 2888e9853f5e..3d82d829cead 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -2,12 +2,12 @@ import { Scope, Session } from '@sentry/hub'; import { Client, + ClientOptions, DsnComponents, Event, EventHint, Integration, IntegrationClass, - Options, Severity, SeverityLevel, Transport, @@ -68,7 +68,7 @@ const ALREADY_SEEN_ERROR = "Not capturing exception because it's already been ca * // ... * } */ -export abstract class BaseClient implements Client { +export abstract class BaseClient implements Client { /** Options passed to the SDK. */ protected readonly _options: O; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index ccf2aa519775..da68917c936e 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -37,6 +37,7 @@ export { initAndBind } from './sdk'; export { NoopTransport } from './transports/noop'; export { createTransport } from './transports/base'; export { SDK_VERSION } from './version'; +export { getIntegrationsToSetup } from './integration'; import * as Integrations from './integrations'; diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts index cc694c815289..b4f35e7d5b1e 100644 --- a/packages/core/src/integration.ts +++ b/packages/core/src/integration.ts @@ -1,5 +1,5 @@ import { addGlobalEventProcessor, getCurrentHub } from '@sentry/hub'; -import { Integration, Options } from '@sentry/types'; +import { ClientOptions, Integration, Options } from '@sentry/types'; import { addNonEnumerableProperty, logger } from '@sentry/utils'; import { IS_DEBUG_BUILD } from './flags'; @@ -70,9 +70,9 @@ export function setupIntegration(integration: Integration): void { * @param integrations array of integration instances * @param withDefault should enable default integrations */ -export function setupIntegrations(options: O): IntegrationIndex { +export function setupIntegrations(options: O): IntegrationIndex { const integrations: IntegrationIndex = {}; - getIntegrationsToSetup(options).forEach(integration => { + options.integrations.forEach(integration => { integrations[integration.name] = integration; setupIntegration(integration); }); diff --git a/packages/core/src/sdk.ts b/packages/core/src/sdk.ts index 97c8b349a235..c7f7bb4916a3 100644 --- a/packages/core/src/sdk.ts +++ b/packages/core/src/sdk.ts @@ -1,12 +1,12 @@ import { getCurrentHub } from '@sentry/hub'; -import { Client, Options, Transport } from '@sentry/types'; +import { Client, ClientOptions, Transport } from '@sentry/types'; import { logger } from '@sentry/utils'; import { IS_DEBUG_BUILD } from './flags'; import { NewTransport } from './transports/base'; /** A class object that can instantiate Client objects. */ -export type ClientClass = new ( +export type ClientClass = new ( options: O, transport: Transport, newTransport?: NewTransport, @@ -19,7 +19,7 @@ export type ClientClass = new ( * @param clientClass The client class to instantiate. * @param options Options to pass to the client. */ -export function initAndBind( +export function initAndBind( clientClass: ClientClass, options: O, transport: Transport, diff --git a/packages/core/test/lib/base.test.ts b/packages/core/test/lib/base.test.ts index 1bd2750e5378..47ec8ae70266 100644 --- a/packages/core/test/lib/base.test.ts +++ b/packages/core/test/lib/base.test.ts @@ -4,7 +4,7 @@ import { dsnToString, logger, SentryError, SyncPromise } from '@sentry/utils'; import * as integrationModule from '../../src/integration'; import { NoopTransport } from '../../src/transports/noop'; -import { setupTestTransport, TestClient } from '../mocks/client'; +import { getDefaultTestClientOptions, setupTestTransport, TestClient } from '../mocks/client'; import { TestIntegration } from '../mocks/integration'; import { FakeTransport } from '../mocks/transport'; @@ -67,7 +67,7 @@ describe('BaseClient', () => { test('returns the Dsn', () => { expect.assertions(1); - const options = { dsn: PUBLIC_DSN }; + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); const client = new TestClient(options, setupTestTransport(options).transport); expect(dsnToString(client.getDsn()!)).toBe(PUBLIC_DSN); }); @@ -75,7 +75,7 @@ describe('BaseClient', () => { test('allows missing Dsn', () => { expect.assertions(1); - const options = {}; + const options = getDefaultTestClientOptions(); const client = new TestClient(options, setupTestTransport(options).transport); expect(client.getDsn()).toBeUndefined(); @@ -84,7 +84,7 @@ describe('BaseClient', () => { test('throws with invalid Dsn', () => { expect.assertions(1); - const options = { dsn: 'abc' }; + const options = getDefaultTestClientOptions({ dsn: 'abc' }); expect(() => new TestClient(options, setupTestTransport(options).transport)).toThrow(SentryError); }); }); @@ -93,7 +93,7 @@ describe('BaseClient', () => { test('returns the options', () => { expect.assertions(1); - const options = { dsn: PUBLIC_DSN, test: true }; + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, test: true }); const client = new TestClient(options, setupTestTransport(options).transport); expect(client.getOptions()).toEqual(options); @@ -104,7 +104,7 @@ describe('BaseClient', () => { test('returns the transport from client', () => { expect.assertions(2); - const options = { dsn: PUBLIC_DSN, transport: FakeTransport }; + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, transport: FakeTransport }); const client = new TestClient(options, new FakeTransport()); expect(client.getTransport()).toBeInstanceOf(FakeTransport); @@ -114,7 +114,7 @@ describe('BaseClient', () => { test('retruns NoopTransport when no transport is passed', () => { expect.assertions(2); - const options = { dsn: PUBLIC_DSN }; + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); const client = new TestClient(options, setupTestTransport(options).transport); expect(client.getTransport()).toBeInstanceOf(NoopTransport); @@ -126,7 +126,7 @@ describe('BaseClient', () => { test('adds a breadcrumb', () => { expect.assertions(1); - const options = {}; + const options = getDefaultTestClientOptions({}); const client = new TestClient(options, setupTestTransport(options).transport); const scope = new Scope(); const hub = new Hub(client, scope); @@ -140,7 +140,7 @@ describe('BaseClient', () => { test('adds a timestamp to new breadcrumbs', () => { expect.assertions(1); - const options = {}; + const options = getDefaultTestClientOptions({}); const client = new TestClient(options, setupTestTransport(options).transport); const scope = new Scope(); const hub = new Hub(client, scope); @@ -154,7 +154,7 @@ describe('BaseClient', () => { test('discards breadcrumbs beyond maxBreadcrumbs', () => { expect.assertions(2); - const options = { maxBreadcrumbs: 1 }; + const options = getDefaultTestClientOptions({ maxBreadcrumbs: 1 }); const client = new TestClient(options, setupTestTransport(options).transport); const scope = new Scope(); const hub = new Hub(client, scope); @@ -169,7 +169,7 @@ describe('BaseClient', () => { test('allows concurrent updates', () => { expect.assertions(1); - const options = {}; + const options = getDefaultTestClientOptions({}); const client = new TestClient(options, setupTestTransport(options).transport); const scope = new Scope(); const hub = new Hub(client, scope); @@ -184,7 +184,7 @@ describe('BaseClient', () => { expect.assertions(1); const beforeBreadcrumb = jest.fn(breadcrumb => breadcrumb); - const options = { beforeBreadcrumb }; + const options = getDefaultTestClientOptions({ beforeBreadcrumb }); const client = new TestClient(options, setupTestTransport(options).transport); const scope = new Scope(); const hub = new Hub(client, scope); @@ -198,7 +198,7 @@ describe('BaseClient', () => { expect.assertions(1); const beforeBreadcrumb = jest.fn(() => ({ message: 'changed' })); - const options = { beforeBreadcrumb }; + const options = getDefaultTestClientOptions({ beforeBreadcrumb }); const client = new TestClient(options, setupTestTransport(options).transport); const scope = new Scope(); const hub = new Hub(client, scope); @@ -212,7 +212,7 @@ describe('BaseClient', () => { expect.assertions(1); const beforeBreadcrumb = jest.fn(() => null); - const options = { beforeBreadcrumb }; + const options = getDefaultTestClientOptions({ beforeBreadcrumb }); const client = new TestClient(options, setupTestTransport(options).transport); const scope = new Scope(); const hub = new Hub(client, scope); @@ -226,7 +226,7 @@ describe('BaseClient', () => { expect.assertions(2); const beforeBreadcrumb = jest.fn((breadcrumb, hint) => ({ ...breadcrumb, data: hint.data })); - const options = { beforeBreadcrumb }; + const options = getDefaultTestClientOptions({ beforeBreadcrumb }); const client = new TestClient(options, setupTestTransport(options).transport); const scope = new Scope(); const hub = new Hub(client, scope); @@ -240,7 +240,7 @@ describe('BaseClient', () => { describe('captureException', () => { test('captures and sends exceptions', () => { - const options = { dsn: PUBLIC_DSN }; + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); const client = new TestClient(options, setupTestTransport(options).transport); client.captureException(new Error('test exception')); @@ -263,7 +263,7 @@ describe('BaseClient', () => { }); test('allows for providing explicit scope', () => { - const options = { dsn: PUBLIC_DSN }; + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); const client = new TestClient(options, setupTestTransport(options).transport); const scope = new Scope(); scope.setExtra('foo', 'wat'); @@ -291,7 +291,7 @@ describe('BaseClient', () => { }); test('allows for clearing data from existing scope if explicit one does so in a callback function', () => { - const options = { dsn: PUBLIC_DSN }; + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); const client = new TestClient(options, setupTestTransport(options).transport); const scope = new Scope(); scope.setExtra('foo', 'wat'); @@ -326,7 +326,7 @@ describe('BaseClient', () => { // already-seen check to work . Any primitive which is passed without being wrapped will be captured each time it // is encountered, so this test doesn't apply. ])("doesn't capture the same exception twice - %s", (_name: string, thrown: any) => { - const options = { dsn: PUBLIC_DSN }; + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); const client = new TestClient(options, setupTestTransport(options).transport); expect(thrown.__sentry_captured__).toBeUndefined(); @@ -345,7 +345,7 @@ describe('BaseClient', () => { describe('captureMessage', () => { test('captures and sends messages', () => { - const options = { dsn: PUBLIC_DSN }; + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); const client = new TestClient(options, setupTestTransport(options).transport); client.captureMessage('test message'); @@ -362,7 +362,7 @@ describe('BaseClient', () => { }); test('should call eventFromException if input to captureMessage is not a primitive', () => { - const options = { dsn: PUBLIC_DSN }; + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); const client = new TestClient(options, setupTestTransport(options).transport); const spy = jest.spyOn(TestClient.instance!, 'eventFromException'); @@ -381,7 +381,7 @@ describe('BaseClient', () => { }); test('allows for providing explicit scope', () => { - const options = { dsn: PUBLIC_DSN }; + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); const client = new TestClient(options, setupTestTransport(options).transport); const scope = new Scope(); scope.setExtra('foo', 'wat'); @@ -415,7 +415,7 @@ describe('BaseClient', () => { test('skips when disabled', () => { expect.assertions(1); - const options = { enabled: false, dsn: PUBLIC_DSN }; + const options = getDefaultTestClientOptions({ enabled: false, dsn: PUBLIC_DSN }); const client = new TestClient(options, setupTestTransport(options).transport); const scope = new Scope(); @@ -427,7 +427,7 @@ describe('BaseClient', () => { test('skips without a Dsn', () => { expect.assertions(1); - const options = {}; + const options = getDefaultTestClientOptions({}); const client = new TestClient(options, setupTestTransport(options).transport); const scope = new Scope(); @@ -445,7 +445,7 @@ describe('BaseClient', () => { // already-seen check to work . Any primitive which is passed without being wrapped will be captured each time it // is encountered, so this test doesn't apply. ])("doesn't capture an event wrapping the same exception twice - %s", (_name: string, thrown: any) => { - const options = { dsn: PUBLIC_DSN }; + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); // Note: this is the same test as in `describe(captureException)`, except with the exception already wrapped in a // hint and accompanying an event. Duplicated here because some methods skip `captureException` and go straight to // `captureEvent`. @@ -469,7 +469,7 @@ describe('BaseClient', () => { test('sends an event', () => { expect.assertions(2); - const options = { dsn: PUBLIC_DSN }; + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); const client = new TestClient(options, setupTestTransport(options).transport); const scope = new Scope(); @@ -489,7 +489,7 @@ describe('BaseClient', () => { test('does not overwrite existing timestamp', () => { expect.assertions(2); - const options = { dsn: PUBLIC_DSN }; + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); const client = new TestClient(options, setupTestTransport(options).transport); const scope = new Scope(); @@ -509,7 +509,7 @@ describe('BaseClient', () => { test('adds event_id from hint if available', () => { expect.assertions(1); - const options = { dsn: PUBLIC_DSN }; + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); const client = new TestClient(options, setupTestTransport(options).transport); const scope = new Scope(); @@ -528,9 +528,7 @@ describe('BaseClient', () => { test('sets default environment to `production` if none provided', () => { expect.assertions(1); - const options = { - dsn: PUBLIC_DSN, - }; + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); const client = new TestClient(options, setupTestTransport(options).transport); const scope = new Scope(); @@ -549,10 +547,7 @@ describe('BaseClient', () => { test('adds the configured environment', () => { expect.assertions(1); - const options = { - dsn: PUBLIC_DSN, - environment: 'env', - }; + const options = getDefaultTestClientOptions({ environment: 'env', dsn: PUBLIC_DSN }); const client = new TestClient(options, setupTestTransport(options).transport); const scope = new Scope(); @@ -571,10 +566,7 @@ describe('BaseClient', () => { test('allows for environment to be explicitly set to falsy value', () => { expect.assertions(1); - const options = { - dsn: PUBLIC_DSN, - environment: undefined, - }; + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, environment: undefined }); const client = new TestClient(options, setupTestTransport(options).transport); const scope = new Scope(); @@ -593,10 +585,7 @@ describe('BaseClient', () => { test('adds the configured release', () => { expect.assertions(1); - const options = { - dsn: PUBLIC_DSN, - release: 'v1.0.0', - }; + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, release: 'v1.0.0' }); const client = new TestClient(options, setupTestTransport(options).transport); const scope = new Scope(); @@ -616,7 +605,7 @@ describe('BaseClient', () => { test('adds breadcrumbs', () => { expect.assertions(4); - const options = { dsn: PUBLIC_DSN }; + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); const client = new TestClient(options, setupTestTransport(options).transport); const scope = new Scope(); scope.addBreadcrumb({ message: 'breadcrumb' }, 100); @@ -632,7 +621,7 @@ describe('BaseClient', () => { test('limits previously saved breadcrumbs', () => { expect.assertions(2); - const options = { dsn: PUBLIC_DSN, maxBreadcrumbs: 1 }; + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, maxBreadcrumbs: 1 }); const client = new TestClient(options, setupTestTransport(options).transport); const scope = new Scope(); const hub = new Hub(client, scope); @@ -648,7 +637,7 @@ describe('BaseClient', () => { test('adds context data', () => { expect.assertions(1); - const options = { dsn: PUBLIC_DSN }; + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); const client = new TestClient(options, setupTestTransport(options).transport); const scope = new Scope(); scope.setExtra('b', 'b'); @@ -673,7 +662,7 @@ describe('BaseClient', () => { test('adds fingerprint', () => { expect.assertions(1); - const options = { dsn: PUBLIC_DSN }; + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); const client = new TestClient(options, setupTestTransport(options).transport); const scope = new Scope(); scope.setFingerprint(['abcd']); @@ -692,7 +681,7 @@ describe('BaseClient', () => { }); test('adds installed integrations to sdk info', () => { - const options = { dsn: PUBLIC_DSN, integrations: [new TestIntegration()] }; + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, integrations: [new TestIntegration()] }); const client = new TestClient(options, setupTestTransport(options).transport); client.setupIntegrations(); @@ -706,7 +695,7 @@ describe('BaseClient', () => { test('normalizes event with default depth of 3', () => { expect.assertions(1); - const options = { dsn: PUBLIC_DSN }; + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); const client = new TestClient(options, setupTestTransport(options).transport); const fourLevelsObject = { a: { @@ -758,10 +747,7 @@ describe('BaseClient', () => { test('normalization respects `normalizeDepth` option', () => { expect.assertions(1); - const options = { - dsn: PUBLIC_DSN, - normalizeDepth: 2, - }; + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, normalizeDepth: 2 }); const client = new TestClient(options, setupTestTransport(options).transport); const fourLevelsObject = { a: { @@ -810,10 +796,7 @@ describe('BaseClient', () => { test('skips normalization when `normalizeDepth: 0`', () => { expect.assertions(1); - const options = { - dsn: PUBLIC_DSN, - normalizeDepth: 0, - }; + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, normalizeDepth: 0 }); const client = new TestClient(options, setupTestTransport(options).transport); const fourLevelsObject = { a: { @@ -867,7 +850,7 @@ describe('BaseClient', () => { test('normalization applies to Transaction and Span consistently', () => { expect.assertions(1); - const options = { dsn: PUBLIC_DSN }; + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); const client = new TestClient(options, setupTestTransport(options).transport); const transaction: Event = { contexts: { @@ -942,7 +925,7 @@ describe('BaseClient', () => { expect.assertions(1); const beforeSend = jest.fn(event => event); - const options = { dsn: PUBLIC_DSN, beforeSend }; + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, beforeSend }); const client = new TestClient(options, setupTestTransport(options).transport); client.captureEvent({ message: 'hello' }); @@ -954,7 +937,7 @@ describe('BaseClient', () => { expect.assertions(1); const beforeSend = jest.fn(() => ({ message: 'changed1' })); - const options = { dsn: PUBLIC_DSN, beforeSend }; + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, beforeSend }); const client = new TestClient(options, setupTestTransport(options).transport); client.captureEvent({ message: 'hello' }); @@ -966,7 +949,8 @@ describe('BaseClient', () => { expect.assertions(3); const beforeSend = jest.fn(() => null); - const client = new TestClient({ dsn: PUBLIC_DSN, beforeSend }, setupTestTransport({ dsn: PUBLIC_DSN }).transport); + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, beforeSend }); + const client = new TestClient(options, setupTestTransport(options).transport); const captureExceptionSpy = jest.spyOn(client, 'captureException'); const loggerErrorSpy = jest.spyOn(logger, 'error'); @@ -983,9 +967,9 @@ describe('BaseClient', () => { for (const val of invalidValues) { const beforeSend = jest.fn(() => val); - const options = { dsn: PUBLIC_DSN, beforeSend }; // @ts-ignore we need to test regular-js behavior - const client = new TestClient(options, setupTestTransport({ dsn: PUBLIC_DSN }).transport); + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, beforeSend }); + const client = new TestClient(options, setupTestTransport(options).transport); const loggerErrorSpy = jest.spyOn(logger, 'error'); client.captureEvent({ message: 'hello' }); @@ -1009,7 +993,7 @@ describe('BaseClient', () => { }, 1); }), ); - const options = { dsn: PUBLIC_DSN, beforeSend }; + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, beforeSend }); const client = new TestClient(options, setupTestTransport(options).transport); client.captureEvent({ message: 'hello' }); @@ -1038,7 +1022,7 @@ describe('BaseClient', () => { }, 1); }), ); - const options = { dsn: PUBLIC_DSN, beforeSend }; + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, beforeSend }); const client = new TestClient(options, setupTestTransport(options).transport); client.captureEvent({ message: 'hello' }); @@ -1067,7 +1051,7 @@ describe('BaseClient', () => { }); }), ); - const options = { dsn: PUBLIC_DSN, beforeSend }; + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, beforeSend }); const client = new TestClient(options, setupTestTransport(options).transport); client.captureEvent({ message: 'hello' }); @@ -1080,7 +1064,7 @@ describe('BaseClient', () => { expect.assertions(2); const beforeSend = jest.fn((event, hint) => ({ ...event, data: hint.data })); - const options = { dsn: PUBLIC_DSN, beforeSend }; + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, beforeSend }); const client = new TestClient(options, setupTestTransport(options).transport); client.captureEvent({ message: 'hello' }, { data: 'someRandomThing' }); @@ -1093,13 +1077,13 @@ describe('BaseClient', () => { expect.assertions(1); const client = new TestClient( - { + getDefaultTestClientOptions({ dsn: PUBLIC_DSN, beforeSend() { return null; }, - }, - setupTestTransport({ dsn: PUBLIC_DSN }).transport, + }), + setupTestTransport(getDefaultTestClientOptions({ dsn: PUBLIC_DSN })).transport, ); const recordLostEventSpy = jest.fn(); jest.spyOn(client, 'getTransport').mockImplementationOnce( @@ -1117,7 +1101,10 @@ describe('BaseClient', () => { test('eventProcessor can drop the even when it returns null', () => { expect.assertions(3); - const client = new TestClient({ dsn: PUBLIC_DSN }, setupTestTransport({ dsn: PUBLIC_DSN }).transport); + const client = new TestClient( + getDefaultTestClientOptions({ dsn: PUBLIC_DSN }), + setupTestTransport(getDefaultTestClientOptions({ dsn: PUBLIC_DSN })).transport, + ); const captureExceptionSpy = jest.spyOn(client, 'captureException'); const loggerErrorSpy = jest.spyOn(logger, 'error'); const scope = new Scope(); @@ -1133,7 +1120,7 @@ describe('BaseClient', () => { test('eventProcessor records dropped events', () => { expect.assertions(1); - const options = { dsn: PUBLIC_DSN }; + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); const client = new TestClient(options, setupTestTransport(options).transport); const recordLostEventSpy = jest.fn(); @@ -1155,7 +1142,7 @@ describe('BaseClient', () => { test('eventProcessor sends an event and logs when it crashes', () => { expect.assertions(3); - const options = { dsn: PUBLIC_DSN }; + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); const client = new TestClient(options, setupTestTransport(options).transport); const captureExceptionSpy = jest.spyOn(client, 'captureException'); const loggerErrorSpy = jest.spyOn(logger, 'error'); @@ -1184,10 +1171,7 @@ describe('BaseClient', () => { test('records events dropped due to sampleRate', () => { expect.assertions(1); - const options = { - dsn: PUBLIC_DSN, - sampleRate: 0, - }; + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, sampleRate: 0 }); const client = new TestClient(options, setupTestTransport(options).transport); const recordLostEventSpy = jest.fn(); @@ -1211,10 +1195,7 @@ describe('BaseClient', () => { test('setup each one of them on setupIntegration call', () => { expect.assertions(2); - const options = { - dsn: PUBLIC_DSN, - integrations: [new TestIntegration()], - }; + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, integrations: [new TestIntegration()] }); const client = new TestClient(options, setupTestTransport(options).transport); client.setupIntegrations(); @@ -1225,9 +1206,7 @@ describe('BaseClient', () => { test('skips installation if DSN is not provided', () => { expect.assertions(2); - const options = { - integrations: [new TestIntegration()], - }; + const options = getDefaultTestClientOptions({ integrations: [new TestIntegration()] }); const client = new TestClient(options, setupTestTransport(options).transport); client.setupIntegrations(); @@ -1238,11 +1217,11 @@ describe('BaseClient', () => { test('skips installation if enabled is set to false', () => { expect.assertions(2); - const options = { + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, enabled: false, integrations: [new TestIntegration()], - }; + }); const client = new TestClient(options, setupTestTransport(options).transport); client.setupIntegrations(); @@ -1253,10 +1232,7 @@ describe('BaseClient', () => { test('skips installation if integrations are already installed', () => { expect.assertions(4); - const options = { - dsn: PUBLIC_DSN, - integrations: [new TestIntegration()], - }; + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, integrations: [new TestIntegration()] }); const client = new TestClient(options, setupTestTransport(options).transport); // note: not the `Client` method `setupIntegrations`, but the free-standing function which that method calls const setupIntegrationsHelper = jest.spyOn(integrationModule, 'setupIntegrations'); @@ -1281,11 +1257,11 @@ describe('BaseClient', () => { expect.assertions(5); const client = new TestClient( - { + getDefaultTestClientOptions({ dsn: PUBLIC_DSN, enableSend: true, transport: FakeTransport, - }, + }), new FakeTransport(), ); @@ -1310,11 +1286,11 @@ describe('BaseClient', () => { expect.assertions(5); const client = new TestClient( - { + getDefaultTestClientOptions({ dsn: PUBLIC_DSN, enableSend: true, transport: FakeTransport, - }, + }), new FakeTransport(), ); @@ -1349,11 +1325,11 @@ describe('BaseClient', () => { expect.assertions(2); const client = new TestClient( - { + getDefaultTestClientOptions({ dsn: PUBLIC_DSN, enableSend: true, transport: FakeTransport, - }, + }), new FakeTransport(), ); @@ -1373,7 +1349,7 @@ describe('BaseClient', () => { jest.useRealTimers(); expect.assertions(3); - const options = { dsn: PUBLIC_DSN }; + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); const client = new TestClient(options, setupTestTransport(options).transport); return Promise.all([ @@ -1394,7 +1370,7 @@ describe('BaseClient', () => { test('sends sessions to the client', () => { expect.assertions(1); - const options = { dsn: PUBLIC_DSN }; + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); const client = new TestClient(options, setupTestTransport(options).transport); const session = new Session({ release: 'test' }); @@ -1406,7 +1382,7 @@ describe('BaseClient', () => { test('skips when disabled', () => { expect.assertions(1); - const options = { enabled: false, dsn: PUBLIC_DSN }; + const options = getDefaultTestClientOptions({ enabled: false, dsn: PUBLIC_DSN }); const client = new TestClient(options, setupTestTransport(options).transport); const session = new Session({ release: 'test' }); diff --git a/packages/core/test/lib/sdk.test.ts b/packages/core/test/lib/sdk.test.ts index 578361a97511..7dd3229c5c7e 100644 --- a/packages/core/test/lib/sdk.test.ts +++ b/packages/core/test/lib/sdk.test.ts @@ -3,7 +3,7 @@ import { Client, Integration } from '@sentry/types'; import { installedIntegrations } from '../../src/integration'; import { initAndBind } from '../../src/sdk'; -import { setupTestTransport, TestClient, TestOptions } from '../mocks/client'; +import { getDefaultTestClientOptions, setupTestTransport, TestClient } from '../mocks/client'; // eslint-disable-next-line no-var declare var global: any; @@ -35,7 +35,7 @@ jest.mock('@sentry/hub', () => { }; }); -class MockIntegration implements Integration { +export class MockIntegration implements Integration { public name: string; public setupOnce: () => void = jest.fn(); public constructor(name: string) { @@ -50,72 +50,15 @@ describe('SDK', () => { }); describe('initAndBind', () => { - test('installs default integrations', () => { - const DEFAULT_INTEGRATIONS: Integration[] = [ - new MockIntegration('MockIntegration 1'), - new MockIntegration('MockIntegration 2'), - ]; - const options = { dsn: PUBLIC_DSN, defaultIntegrations: DEFAULT_INTEGRATIONS }; - initAndBind(TestClient, options, setupTestTransport(options).transport); - expect((DEFAULT_INTEGRATIONS[0].setupOnce as jest.Mock).mock.calls.length).toBe(1); - expect((DEFAULT_INTEGRATIONS[1].setupOnce as jest.Mock).mock.calls.length).toBe(1); - }); - - test("doesn't install default integrations if told not to", () => { - const DEFAULT_INTEGRATIONS: Integration[] = [ - new MockIntegration('MockIntegration 1'), - new MockIntegration('MockIntegration 2'), - ]; - const options: TestOptions = { dsn: PUBLIC_DSN, defaultIntegrations: false }; - initAndBind(TestClient, options, setupTestTransport(options).transport); - expect((DEFAULT_INTEGRATIONS[0].setupOnce as jest.Mock).mock.calls.length).toBe(0); - expect((DEFAULT_INTEGRATIONS[1].setupOnce as jest.Mock).mock.calls.length).toBe(0); - }); - test('installs integrations provided through options', () => { const integrations: Integration[] = [ new MockIntegration('MockIntegration 1'), new MockIntegration('MockIntegration 2'), ]; - const options = { dsn: PUBLIC_DSN, integrations }; - initAndBind(TestClient, options, setupTestTransport(options).transport); - expect((integrations[0].setupOnce as jest.Mock).mock.calls.length).toBe(1); - expect((integrations[1].setupOnce as jest.Mock).mock.calls.length).toBe(1); - }); - - test('installs merged default integrations, with overrides provided through options', () => { - const DEFAULT_INTEGRATIONS: Integration[] = [ - new MockIntegration('MockIntegration 1'), - new MockIntegration('MockIntegration 2'), - ]; - const integrations: Integration[] = [ - new MockIntegration('MockIntegration 1'), - new MockIntegration('MockIntegration 3'), - ]; - const options = { dsn: PUBLIC_DSN, defaultIntegrations: DEFAULT_INTEGRATIONS, integrations }; + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, integrations }); initAndBind(TestClient, options, setupTestTransport(options).transport); - // 'MockIntegration 1' should be overridden by the one with the same name provided through options - expect((DEFAULT_INTEGRATIONS[0].setupOnce as jest.Mock).mock.calls.length).toBe(0); - expect((DEFAULT_INTEGRATIONS[1].setupOnce as jest.Mock).mock.calls.length).toBe(1); expect((integrations[0].setupOnce as jest.Mock).mock.calls.length).toBe(1); expect((integrations[1].setupOnce as jest.Mock).mock.calls.length).toBe(1); }); - - test('installs integrations returned from a callback function', () => { - const DEFAULT_INTEGRATIONS: Integration[] = [ - new MockIntegration('MockIntegration 1'), - new MockIntegration('MockIntegration 2'), - ]; - const newIntegration = new MockIntegration('MockIntegration 3'); - const options = { - defaultIntegrations: DEFAULT_INTEGRATIONS, - dsn: PUBLIC_DSN, - integrations: (integrations: Integration[]) => integrations.slice(0, 1).concat(newIntegration), - }; - initAndBind(TestClient, options, setupTestTransport(options).transport); - expect((DEFAULT_INTEGRATIONS[0].setupOnce as jest.Mock).mock.calls.length).toBe(1); - expect((newIntegration.setupOnce as jest.Mock).mock.calls.length).toBe(1); - expect((DEFAULT_INTEGRATIONS[1].setupOnce as jest.Mock).mock.calls.length).toBe(0); - }); }); }); diff --git a/packages/core/test/mocks/client.ts b/packages/core/test/mocks/client.ts index 707513ff91b1..5778dcf5e193 100644 --- a/packages/core/test/mocks/client.ts +++ b/packages/core/test/mocks/client.ts @@ -1,26 +1,36 @@ import { Session } from '@sentry/hub'; -import { Event, Integration, Options, Severity, SeverityLevel, Transport } from '@sentry/types'; +import { ClientOptions, Event, Integration, Severity, SeverityLevel, Transport } from '@sentry/types'; import { resolvedSyncPromise } from '@sentry/utils'; import { BaseClient } from '../../src/baseclient'; import { initAndBind } from '../../src/sdk'; import { NewTransport } from '../../src/transports/base'; import { NoopTransport } from '../../src/transports/noop'; -export interface TestOptions extends Options { + +export function getDefaultTestClientOptions(options: Partial = {}): TestClientOptions { + return { + integrations: [], + transport: NoopTransport, + stackParser: () => [], + ...options, + }; +} + +export interface TestClientOptions extends ClientOptions { test?: boolean; mockInstallFailure?: boolean; enableSend?: boolean; defaultIntegrations?: Integration[] | false; } -export class TestClient extends BaseClient { +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, transport: Transport, newTransport?: NewTransport) { + public constructor(options: TestClientOptions, transport: Transport, newTransport?: NewTransport) { super(options, transport, newTransport); TestClient.instance = this; } @@ -64,11 +74,11 @@ export class TestClient extends BaseClient { } } -export function init(options: TestOptions, transport: Transport, newTransport?: NewTransport): void { +export function init(options: TestClientOptions, transport: Transport, newTransport?: NewTransport): void { initAndBind(TestClient, options, transport, newTransport); } -export function setupTestTransport(options: TestOptions): { transport: Transport; newTransport?: NewTransport } { +export function setupTestTransport(options: TestClientOptions): { transport: Transport; newTransport?: NewTransport } { const noop = { transport: new NoopTransport() }; if (!options.dsn) { diff --git a/packages/node/src/client.ts b/packages/node/src/client.ts index e660c6c7b421..7864fd1b7ccc 100644 --- a/packages/node/src/client.ts +++ b/packages/node/src/client.ts @@ -5,22 +5,22 @@ import { logger, resolvedSyncPromise, stackParserFromOptions } from '@sentry/uti import { eventFromMessage, eventFromUnknownInput } from './eventbuilder'; import { IS_DEBUG_BUILD } from './flags'; -import { NodeOptions } from './types'; +import { NodeClientOptions } from './types'; /** * The Sentry Node SDK Client. * - * @see NodeOptions for documentation on configuration options. + * @see NodeClientOptions for documentation on configuration options. * @see SentryClient for usage documentation. */ -export class NodeClient extends BaseClient { +export class NodeClient extends BaseClient { protected _sessionFlusher: SessionFlusher | undefined; /** * Creates a new Node SDK instance. * @param options Configuration options for this SDK. */ - public constructor(options: NodeOptions, transport: Transport, newTransport?: NewTransport) { + public constructor(options: NodeClientOptions, transport: Transport, newTransport?: NewTransport) { options._metadata = options._metadata || {}; options._metadata.sdk = options._metadata.sdk || { name: 'sentry.javascript.node', diff --git a/packages/node/src/sdk.ts b/packages/node/src/sdk.ts index 0b7fb9bd5671..fa4b22a12066 100644 --- a/packages/node/src/sdk.ts +++ b/packages/node/src/sdk.ts @@ -1,15 +1,15 @@ -import { getCurrentHub, initAndBind, Integrations as CoreIntegrations } from '@sentry/core'; +import { getCurrentHub, getIntegrationsToSetup, initAndBind, Integrations as CoreIntegrations } from '@sentry/core'; import { getMainCarrier, setHubOnCarrier } from '@sentry/hub'; import { SessionStatus } from '@sentry/types'; -import { getGlobalObject, logger } from '@sentry/utils'; +import { getGlobalObject, logger, stackParserFromOptions } from '@sentry/utils'; import * as domain from 'domain'; import { NodeClient } from './client'; import { IS_DEBUG_BUILD } from './flags'; import { Console, ContextLines, Http, LinkedErrors, OnUncaughtException, OnUnhandledRejection } from './integrations'; import { nodeStackParser } from './stack-parser'; -import { setupNodeTransport } from './transports'; -import { NodeOptions } from './types'; +import { HTTPSTransport, HTTPTransport, setupNodeTransport } from './transports'; +import { NodeClientOptions, NodeOptions } from './types'; export const defaultIntegrations = [ // Common @@ -132,7 +132,17 @@ export function init(options: NodeOptions = {}): void { } const { transport, newTransport } = setupNodeTransport(options); - initAndBind(NodeClient, options, transport, newTransport); + + // TODO(v7): Refactor this to reduce the logic above + const clientOptions: NodeClientOptions = { + ...options, + stackParser: stackParserFromOptions(options), + integrations: getIntegrationsToSetup(options), + // TODO(v7): Fix me when we switch to new transports entirely. + transport: options.transport || (transport instanceof HTTPTransport ? HTTPTransport : HTTPSTransport), + }; + + initAndBind(NodeClient, clientOptions, transport, newTransport); if (options.autoSessionTracking) { startSessionTracking(); @@ -189,7 +199,7 @@ export function isAutoSessionTrackingEnabled(client?: NodeClient): boolean { if (client === undefined) { return false; } - const clientOptions: NodeOptions = client && client.getOptions(); + const clientOptions = client && client.getOptions(); if (clientOptions && clientOptions.autoSessionTracking !== undefined) { return clientOptions.autoSessionTracking; } diff --git a/packages/node/src/types.ts b/packages/node/src/types.ts index 055006a47e9e..a2311a4698f9 100644 --- a/packages/node/src/types.ts +++ b/packages/node/src/types.ts @@ -1,16 +1,9 @@ -import { Options } from '@sentry/types'; +import { ClientOptions, Options } from '@sentry/types'; -/** - * Configuration options for the Sentry Node SDK. - * @see NodeClient for more information. - */ -export interface NodeOptions extends Options { +export interface BaseNodeOptions { /** Sets an optional server name (device name) */ serverName?: string; - /** Maximum time in milliseconds to wait to drain the request queue, before the process is allowed to exit. */ - shutdownTimeout?: number; - /** Set a HTTP proxy that should be used for outbound requests. */ httpProxy?: string; @@ -23,3 +16,15 @@ export interface NodeOptions extends Options { /** Callback that is executed when a fatal global error occurs. */ onFatalError?(error: Error): void; } + +/** + * Configuration options for the Sentry Node SDK + * @see @sentry/types Options for more information. + */ +export interface NodeOptions extends Options, BaseNodeOptions {} + +/** + * Configuration options for the Sentry Node SDK Client class + * @see NodeClient for more information. + */ +export interface NodeClientOptions extends ClientOptions, BaseNodeOptions {} diff --git a/packages/node/test/client.test.ts b/packages/node/test/client.test.ts index 1cddd0e85e3f..bb3d13ca4122 100644 --- a/packages/node/test/client.test.ts +++ b/packages/node/test/client.test.ts @@ -2,6 +2,7 @@ import { Scope, SessionFlusher } from '@sentry/hub'; import { NodeClient } from '../src'; import { setupNodeTransport } from '../src/transports'; +import { getDefaultNodeClientOptions } from './helper/node-client-options'; const PUBLIC_DSN = 'https://username@domain/123'; @@ -15,7 +16,7 @@ describe('NodeClient', () => { describe('captureException', () => { test('when autoSessionTracking is enabled, and requestHandler is not used -> requestStatus should not be set', () => { - const options = { dsn: PUBLIC_DSN, autoSessionTracking: true, release: '1.4' }; + const options = getDefaultNodeClientOptions({ dsn: PUBLIC_DSN, autoSessionTracking: true, release: '1.4' }); client = new NodeClient(options, setupNodeTransport(options).transport); const scope = new Scope(); scope.setRequestSession({ status: 'ok' }); @@ -26,7 +27,7 @@ describe('NodeClient', () => { expect(requestSession!.status).toEqual('ok'); }); test('when autoSessionTracking is disabled -> requestStatus should not be set', () => { - const options = { dsn: PUBLIC_DSN, autoSessionTracking: false, release: '1.4' }; + const options = getDefaultNodeClientOptions({ dsn: PUBLIC_DSN, autoSessionTracking: false, release: '1.4' }); client = new NodeClient(options, setupNodeTransport(options).transport); // It is required to initialise SessionFlusher to capture Session Aggregates (it is usually initialised // by the`requestHandler`) @@ -41,7 +42,7 @@ describe('NodeClient', () => { expect(requestSession!.status).toEqual('ok'); }); test('when autoSessionTracking is enabled + requestSession status is Crashed -> requestStatus should not be overridden', () => { - const options = { dsn: PUBLIC_DSN, autoSessionTracking: true, release: '1.4' }; + const options = getDefaultNodeClientOptions({ dsn: PUBLIC_DSN, autoSessionTracking: true, release: '1.4' }); client = new NodeClient(options, setupNodeTransport(options).transport); // It is required to initialise SessionFlusher to capture Session Aggregates (it is usually initialised // by the`requestHandler`) @@ -56,7 +57,7 @@ describe('NodeClient', () => { expect(requestSession!.status).toEqual('crashed'); }); test('when autoSessionTracking is enabled + error occurs within request bounds -> requestStatus should be set to Errored', () => { - const options = { dsn: PUBLIC_DSN, autoSessionTracking: true, release: '1.4' }; + const options = getDefaultNodeClientOptions({ dsn: PUBLIC_DSN, autoSessionTracking: true, release: '1.4' }); client = new NodeClient(options, setupNodeTransport(options).transport); // It is required to initialise SessionFlusher to capture Session Aggregates (it is usually initialised // by the`requestHandler`) @@ -71,7 +72,7 @@ describe('NodeClient', () => { expect(requestSession!.status).toEqual('errored'); }); test('when autoSessionTracking is enabled + error occurs outside of request bounds -> requestStatus should not be set to Errored', () => { - const options = { dsn: PUBLIC_DSN, autoSessionTracking: true, release: '1.4' }; + const options = getDefaultNodeClientOptions({ dsn: PUBLIC_DSN, autoSessionTracking: true, release: '1.4' }); client = new NodeClient(options, setupNodeTransport(options).transport); // It is required to initialise SessionFlusher to capture Session Aggregates (it is usually initialised // by the`requestHandler`) @@ -88,7 +89,7 @@ describe('NodeClient', () => { describe('captureEvent()', () => { test('If autoSessionTracking is disabled, requestSession status should not be set', () => { - const options = { dsn: PUBLIC_DSN, autoSessionTracking: false, release: '1.4' }; + const options = getDefaultNodeClientOptions({ dsn: PUBLIC_DSN, autoSessionTracking: false, release: '1.4' }); client = new NodeClient(options, setupNodeTransport(options).transport); // It is required to initialise SessionFlusher to capture Session Aggregates (it is usually initialised // by the`requestHandler`) @@ -107,7 +108,7 @@ describe('NodeClient', () => { }); test('When captureEvent is called with an exception, requestSession status should be set to Errored', () => { - const options = { dsn: PUBLIC_DSN, autoSessionTracking: true, release: '2.2' }; + const options = getDefaultNodeClientOptions({ dsn: PUBLIC_DSN, autoSessionTracking: true, release: '2.2' }); client = new NodeClient(options, setupNodeTransport(options).transport); // It is required to initialise SessionFlusher to capture Session Aggregates (it is usually initialised // by the`requestHandler`) @@ -123,7 +124,7 @@ describe('NodeClient', () => { }); test('When captureEvent is called without an exception, requestSession status should not be set to Errored', () => { - const options = { dsn: PUBLIC_DSN, autoSessionTracking: true, release: '2.2' }; + const options = getDefaultNodeClientOptions({ dsn: PUBLIC_DSN, autoSessionTracking: true, release: '2.2' }); client = new NodeClient(options, setupNodeTransport(options).transport); // It is required to initialise SessionFlusher to capture Session Aggregates (it is usually initialised // by the`requestHandler`) @@ -139,7 +140,7 @@ describe('NodeClient', () => { }); test('When captureEvent is called with an exception but outside of a request, then requestStatus should not be set', () => { - const options = { dsn: PUBLIC_DSN, autoSessionTracking: true, release: '2.2' }; + const options = getDefaultNodeClientOptions({ dsn: PUBLIC_DSN, autoSessionTracking: true, release: '2.2' }); client = new NodeClient(options, setupNodeTransport(options).transport); // It is required to initialise SessionFlusher to capture Session Aggregates (it is usually initialised // by the`requestHandler`) @@ -157,7 +158,7 @@ describe('NodeClient', () => { }); test('When captureEvent is called with a transaction, then requestSession status should not be set', () => { - const options = { dsn: PUBLIC_DSN, autoSessionTracking: true, release: '1.3' }; + const options = getDefaultNodeClientOptions({ dsn: PUBLIC_DSN, autoSessionTracking: true, release: '1.3' }); client = new NodeClient(options, setupNodeTransport(options).transport); // It is required to initialise SessionFlusher to capture Session Aggregates (it is usually initialised // by the`requestHandler`) @@ -172,7 +173,7 @@ describe('NodeClient', () => { }); test('When captureEvent is called with an exception but requestHandler is not used, then requestSession status should not be set', () => { - const options = { dsn: PUBLIC_DSN, autoSessionTracking: true, release: '1.3' }; + const options = getDefaultNodeClientOptions({ dsn: PUBLIC_DSN, autoSessionTracking: true, release: '1.3' }); client = new NodeClient(options, setupNodeTransport(options).transport); const scope = new Scope(); @@ -192,11 +193,11 @@ describe('NodeClient', () => { describe('flush/close', () => { test('client close function disables _sessionFlusher', async () => { jest.useRealTimers(); - const options = { + const options = getDefaultNodeClientOptions({ dsn: PUBLIC_DSN, autoSessionTracking: true, release: '1.1', - }; + }); const client = new NodeClient(options, setupNodeTransport(options).transport); client.initSessionFlusher(); // Clearing interval is important here to ensure that the flush function later on is called by the `client.close()` diff --git a/packages/node/test/handlers.test.ts b/packages/node/test/handlers.test.ts index d7d47fda053d..b857e1b74bf5 100644 --- a/packages/node/test/handlers.test.ts +++ b/packages/node/test/handlers.test.ts @@ -1,6 +1,6 @@ import * as sentryCore from '@sentry/core'; -import { Hub } from '@sentry/hub'; import * as sentryHub from '@sentry/hub'; +import { Hub } from '@sentry/hub'; import { Transaction } from '@sentry/tracing'; import { Runtime } from '@sentry/types'; import { SentryError } from '@sentry/utils'; @@ -19,6 +19,7 @@ import { } from '../src/handlers'; import * as SDK from '../src/sdk'; import { setupNodeTransport } from '../src/transports'; +import { getDefaultNodeClientOptions } from './helper/node-client-options'; describe('parseRequest', () => { let mockReq: { [key: string]: any }; @@ -224,7 +225,7 @@ describe('requestHandler', () => { }); it('autoSessionTracking is enabled, sets requestSession status to ok, when handling a request', () => { - const options = { autoSessionTracking: true, release: '1.2' }; + const options = getDefaultNodeClientOptions({ autoSessionTracking: true, release: '1.2' }); client = new NodeClient(options, setupNodeTransport(options).transport); const hub = new Hub(client); @@ -237,7 +238,7 @@ describe('requestHandler', () => { }); it('autoSessionTracking is disabled, does not set requestSession, when handling a request', () => { - const options = { autoSessionTracking: false, release: '1.2' }; + const options = getDefaultNodeClientOptions({ autoSessionTracking: false, release: '1.2' }); client = new NodeClient(options, setupNodeTransport(options).transport); const hub = new Hub(client); @@ -250,7 +251,7 @@ describe('requestHandler', () => { }); it('autoSessionTracking is enabled, calls _captureRequestSession, on response finish', done => { - const options = { autoSessionTracking: true, release: '1.2' }; + const options = getDefaultNodeClientOptions({ autoSessionTracking: true, release: '1.2' }); client = new NodeClient(options, setupNodeTransport(options).transport); const hub = new Hub(client); @@ -271,7 +272,7 @@ describe('requestHandler', () => { }); it('autoSessionTracking is disabled, does not call _captureRequestSession, on response finish', done => { - const options = { autoSessionTracking: false, release: '1.2' }; + const options = getDefaultNodeClientOptions({ autoSessionTracking: false, release: '1.2' }); client = new NodeClient(options, setupNodeTransport(options).transport); const hub = new Hub(client); jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub); @@ -372,7 +373,7 @@ describe('tracingHandler', () => { it('extracts request data for sampling context', () => { const tracesSampler = jest.fn(); - const options = { tracesSampler }; + const options = getDefaultNodeClientOptions({ tracesSampler }); const hub = new Hub(new NodeClient(options, setupNodeTransport(options).transport)); // we need to mock both of these because the tracing handler relies on `@sentry/core` while the sampler relies on // `@sentry/hub`, and mocking breaks the link between the two @@ -395,7 +396,7 @@ describe('tracingHandler', () => { }); it('puts its transaction on the scope', () => { - const options = { tracesSampleRate: 1.0 }; + const options = getDefaultNodeClientOptions({ tracesSampleRate: 1.0 }); const hub = new Hub(new NodeClient(options, setupNodeTransport(options).transport)); // we need to mock both of these because the tracing handler relies on `@sentry/core` while the sampler relies on // `@sentry/hub`, and mocking breaks the link between the two @@ -727,7 +728,7 @@ describe('errorHandler()', () => { jest.restoreAllMocks(); }); it('when autoSessionTracking is disabled, does not set requestSession status on Crash', () => { - const options = { autoSessionTracking: false, release: '3.3' }; + const options = getDefaultNodeClientOptions({ autoSessionTracking: false, release: '3.3' }); client = new NodeClient(options, setupNodeTransport(options).transport); // It is required to initialise SessionFlusher to capture Session Aggregates (it is usually initialised // by the`requestHandler`) @@ -747,7 +748,7 @@ describe('errorHandler()', () => { }); it('autoSessionTracking is enabled + requestHandler is not used -> does not set requestSession status on Crash', () => { - const options = { autoSessionTracking: false, release: '3.3' }; + const options = getDefaultNodeClientOptions({ autoSessionTracking: false, release: '3.3' }); client = new NodeClient(options, setupNodeTransport(options).transport); const scope = sentryCore.getCurrentHub().getScope(); @@ -764,7 +765,7 @@ describe('errorHandler()', () => { }); it('when autoSessionTracking is enabled, should set requestSession status to Crashed when an unhandled error occurs within the bounds of a request', () => { - const options = { autoSessionTracking: true, release: '1.1' }; + const options = getDefaultNodeClientOptions({ autoSessionTracking: true, release: '1.1' }); client = new NodeClient(options, setupNodeTransport(options).transport); // It is required to initialise SessionFlusher to capture Session Aggregates (it is usually initialised // by the`requestHandler`) @@ -783,7 +784,7 @@ describe('errorHandler()', () => { }); it('when autoSessionTracking is enabled, should not set requestSession status on Crash when it occurs outside the bounds of a request', () => { - const options = { autoSessionTracking: true, release: '2.2' }; + const options = getDefaultNodeClientOptions({ autoSessionTracking: true, release: '2.2' }); client = new NodeClient(options, setupNodeTransport(options).transport); // It is required to initialise SessionFlusher to capture Session Aggregates (it is usually initialised // by the`requestHandler`) diff --git a/packages/node/test/helper/node-client-options.ts b/packages/node/test/helper/node-client-options.ts new file mode 100644 index 000000000000..c2bb1d42a871 --- /dev/null +++ b/packages/node/test/helper/node-client-options.ts @@ -0,0 +1,12 @@ +import { NoopTransport } from '@sentry/core'; + +import { NodeClientOptions } from '../../src/types'; + +export function getDefaultNodeClientOptions(options: Partial = {}): NodeClientOptions { + return { + integrations: [], + transport: NoopTransport, + stackParser: () => [], + ...options, + }; +} diff --git a/packages/node/test/index.test.ts b/packages/node/test/index.test.ts index 02eaad72b2c0..2bdf8497097c 100644 --- a/packages/node/test/index.test.ts +++ b/packages/node/test/index.test.ts @@ -19,6 +19,7 @@ import { import { ContextLines, LinkedErrors } from '../src/integrations'; import { nodeStackParser } from '../src/stack-parser'; import { setupNodeTransport } from '../src/transports'; +import { getDefaultNodeClientOptions } from './helper/node-client-options'; const stackParser = createStackParser(nodeStackParser); @@ -90,7 +91,7 @@ describe('SentryNode', () => { }); test('record auto breadcrumbs', done => { - const options = { + const options = getDefaultNodeClientOptions({ beforeSend: (event: Event) => { // TODO: It should be 3, but we don't capture a breadcrumb // for our own captureMessage/captureException calls yet @@ -100,7 +101,7 @@ describe('SentryNode', () => { }, dsn, stackParser, - }; + }); const client = new NodeClient(options, setupNodeTransport(options).transport); getCurrentHub().bindClient(client); addBreadcrumb({ message: 'test1' }); @@ -122,7 +123,7 @@ describe('SentryNode', () => { test('capture an exception', done => { expect.assertions(6); - const options = { + const options = getDefaultNodeClientOptions({ stackParser, beforeSend: (event: Event) => { expect(event.tags).toEqual({ test: '1' }); @@ -135,7 +136,7 @@ describe('SentryNode', () => { return null; }, dsn, - }; + }); getCurrentHub().bindClient(new NodeClient(options, setupNodeTransport(options).transport)); configureScope((scope: Scope) => { scope.setTag('test', '1'); @@ -149,7 +150,7 @@ describe('SentryNode', () => { test('capture a string exception', done => { expect.assertions(6); - const options = { + const options = getDefaultNodeClientOptions({ stackParser, beforeSend: (event: Event) => { expect(event.tags).toEqual({ test: '1' }); @@ -162,7 +163,7 @@ describe('SentryNode', () => { return null; }, dsn, - }; + }); getCurrentHub().bindClient(new NodeClient(options, setupNodeTransport(options).transport)); configureScope((scope: Scope) => { scope.setTag('test', '1'); @@ -176,7 +177,7 @@ describe('SentryNode', () => { test('capture an exception with pre/post context', done => { expect.assertions(10); - const options = { + const options = getDefaultNodeClientOptions({ stackParser, beforeSend: (event: Event) => { expect(event.tags).toEqual({ test: '1' }); @@ -193,7 +194,7 @@ describe('SentryNode', () => { return null; }, dsn, - }; + }); getCurrentHub().bindClient(new NodeClient(options, setupNodeTransport(options).transport)); configureScope((scope: Scope) => { scope.setTag('test', '1'); @@ -207,7 +208,7 @@ describe('SentryNode', () => { test('capture a linked exception with pre/post context', done => { expect.assertions(15); - const options = { + const options = getDefaultNodeClientOptions({ stackParser, integrations: [new ContextLines(), new LinkedErrors()], beforeSend: (event: Event) => { @@ -231,7 +232,7 @@ describe('SentryNode', () => { return null; }, dsn, - }; + }); getCurrentHub().bindClient(new NodeClient(options, setupNodeTransport(options).transport)); try { throw new Error('test'); @@ -247,7 +248,7 @@ describe('SentryNode', () => { test('capture a message', done => { expect.assertions(2); - const options = { + const options = getDefaultNodeClientOptions({ stackParser, beforeSend: (event: Event) => { expect(event.message).toBe('test'); @@ -256,14 +257,14 @@ describe('SentryNode', () => { return null; }, dsn, - }; + }); getCurrentHub().bindClient(new NodeClient(options, setupNodeTransport(options).transport)); captureMessage('test'); }); test('capture an event', done => { expect.assertions(2); - const options = { + const options = getDefaultNodeClientOptions({ stackParser, beforeSend: (event: Event) => { expect(event.message).toBe('test event'); @@ -272,7 +273,7 @@ describe('SentryNode', () => { return null; }, dsn, - }; + }); getCurrentHub().bindClient(new NodeClient(options, setupNodeTransport(options).transport)); captureEvent({ message: 'test event' }); }); @@ -280,7 +281,7 @@ describe('SentryNode', () => { test('capture an event in a domain', done => { const d = domain.create(); - const options = { + const options = getDefaultNodeClientOptions({ stackParser, beforeSend: (event: Event) => { expect(event.message).toBe('test domain'); @@ -289,7 +290,7 @@ describe('SentryNode', () => { return null; }, dsn, - }; + }); const client = new NodeClient(options, setupNodeTransport(options).transport); d.run(() => { @@ -301,7 +302,7 @@ describe('SentryNode', () => { test('stacktrace order', done => { expect.assertions(1); - const options = { + const options = getDefaultNodeClientOptions({ stackParser, beforeSend: (event: Event) => { expect( @@ -312,7 +313,7 @@ describe('SentryNode', () => { return null; }, dsn, - }; + }); getCurrentHub().bindClient(new NodeClient(options, setupNodeTransport(options).transport)); try { // @ts-ignore allow function declarations in strict mode @@ -376,7 +377,7 @@ describe('SentryNode initialization', () => { }); it('should set SDK data when instantiating a client directly', () => { - const options = { dsn }; + const options = getDefaultNodeClientOptions({ dsn }); const client = new NodeClient(options, setupNodeTransport(options).transport); // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/node/test/integrations/http.test.ts b/packages/node/test/integrations/http.test.ts index f66f847cd298..30a923524a3e 100644 --- a/packages/node/test/integrations/http.test.ts +++ b/packages/node/test/integrations/http.test.ts @@ -1,6 +1,6 @@ import * as sentryCore from '@sentry/core'; -import { Hub } from '@sentry/hub'; import * as hubModule from '@sentry/hub'; +import { Hub } from '@sentry/hub'; import { addExtensionMethods, Span, TRACEPARENT_REGEXP, Transaction } from '@sentry/tracing'; import { parseSemver } from '@sentry/utils'; import * as http from 'http'; @@ -12,16 +12,17 @@ import { Breadcrumb } from '../../src'; import { NodeClient } from '../../src/client'; import { Http as HttpIntegration } from '../../src/integrations/http'; import { setupNodeTransport } from '../../src/transports'; +import { getDefaultNodeClientOptions } from '../helper/node-client-options'; const NODE_VERSION = parseSemver(process.versions.node); describe('tracing', () => { function createTransactionOnScope() { - const options = { + const options = getDefaultNodeClientOptions({ dsn: 'https://dogsarebadatkeepingsecrets@squirrelchasers.ingest.sentry.io/12312012', tracesSampleRate: 1.0, integrations: [new HttpIntegration({ tracing: true })], - }; + }); const hub = new Hub(new NodeClient(options, setupNodeTransport(options).transport)); addExtensionMethods(); @@ -97,7 +98,7 @@ describe('default protocols', () => { const p = new Promise(r => { resolve = r; }); - const options = { + const options = getDefaultNodeClientOptions({ dsn: 'https://dogsarebadatkeepingsecrets@squirrelchasers.ingest.sentry.io/12312012', integrations: [new HttpIntegration({ breadcrumbs: true })], beforeBreadcrumb: (b: Breadcrumb) => { @@ -106,7 +107,7 @@ describe('default protocols', () => { } return b; }, - }; + }); hub.bindClient(new NodeClient(options, setupNodeTransport(options).transport)); return p; diff --git a/packages/node/test/integrations/linkederrors.test.ts b/packages/node/test/integrations/linkederrors.test.ts index 2c2957bf6a69..4d18707be904 100644 --- a/packages/node/test/integrations/linkederrors.test.ts +++ b/packages/node/test/integrations/linkederrors.test.ts @@ -5,6 +5,7 @@ import { Event, NodeClient } from '../../src'; import { LinkedErrors } from '../../src/integrations/linkederrors'; import { nodeStackParser } from '../../src/stack-parser'; import { setupNodeTransport } from '../../src/transports'; +import { getDefaultNodeClientOptions } from '../helper/node-client-options'; const stackParser = createStackParser(nodeStackParser); @@ -32,7 +33,7 @@ describe('LinkedErrors', () => { expect.assertions(2); const spy = jest.spyOn(linkedErrors, '_walkErrorTree'); const one = new Error('originalException'); - const options = { stackParser }; + const options = getDefaultNodeClientOptions({ stackParser }); const client = new NodeClient(options, setupNodeTransport(options).transport); let event: Event | undefined; return client @@ -56,7 +57,7 @@ describe('LinkedErrors', () => { }), ); const one = new Error('originalException'); - const options = { stackParser }; + const options = getDefaultNodeClientOptions({ stackParser }); const client = new NodeClient(options, setupNodeTransport(options).transport); return client.eventFromException(one).then(event => linkedErrors @@ -77,7 +78,7 @@ describe('LinkedErrors', () => { one.cause = two; two.cause = three; - const options = { stackParser }; + const options = getDefaultNodeClientOptions({ stackParser }); const client = new NodeClient(options, setupNodeTransport(options).transport); return client.eventFromException(one).then(event => linkedErrors @@ -111,7 +112,7 @@ describe('LinkedErrors', () => { one.reason = two; two.reason = three; - const options = { stackParser }; + const options = getDefaultNodeClientOptions({ stackParser }); const client = new NodeClient(options, setupNodeTransport(options).transport); return client.eventFromException(one).then(event => linkedErrors @@ -145,7 +146,7 @@ describe('LinkedErrors', () => { one.cause = two; two.cause = three; - const options = { stackParser }; + const options = getDefaultNodeClientOptions({ stackParser }); const client = new NodeClient(options, setupNodeTransport(options).transport); return client.eventFromException(one).then(event => linkedErrors diff --git a/packages/node/test/sdk.test.ts b/packages/node/test/sdk.test.ts new file mode 100644 index 000000000000..48e5accee439 --- /dev/null +++ b/packages/node/test/sdk.test.ts @@ -0,0 +1,92 @@ +import { Integration } from '@sentry/types'; + +import { init } from '../src/sdk'; +import * as sdk from '../src/sdk'; + +// eslint-disable-next-line no-var +declare var global: any; + +const PUBLIC_DSN = 'https://username@domain/123'; + +class MockIntegration implements Integration { + public name: string; + public setupOnce: jest.Mock = jest.fn(); + public constructor(name: string) { + this.name = name; + } +} + +const defaultIntegrationsBackup = sdk.defaultIntegrations; + +describe('init()', () => { + beforeEach(() => { + global.__SENTRY__ = {}; + }); + + afterEach(() => { + // @ts-ignore - Reset the default integrations of node sdk to original + sdk.defaultIntegrations = defaultIntegrationsBackup; + }); + + it("doesn't install default integrations if told not to", () => { + const mockDefaultIntegrations = [ + new MockIntegration('Mock integration 1.1'), + new MockIntegration('Mock integration 1.2'), + ]; + + // @ts-ignore - Replace default integrations with mock integrations, needs ts-ignore because imports are readonly + sdk.defaultIntegrations = mockDefaultIntegrations; + + init({ dsn: PUBLIC_DSN, defaultIntegrations: false }); + + expect(mockDefaultIntegrations[0].setupOnce as jest.Mock).toHaveBeenCalledTimes(0); + expect(mockDefaultIntegrations[1].setupOnce as jest.Mock).toHaveBeenCalledTimes(0); + }); + + it('installs merged default integrations, with overrides provided through options', () => { + const mockDefaultIntegrations = [ + new MockIntegration('Some mock integration 2.1'), + new MockIntegration('Some mock integration 2.2'), + ]; + + // @ts-ignore - Replace default integrations with mock integrations, needs ts-ignore because imports are readonly + sdk.defaultIntegrations = mockDefaultIntegrations; + + const mockIntegrations = [ + new MockIntegration('Some mock integration 2.1'), + new MockIntegration('Some mock integration 2.3'), + ]; + + init({ dsn: PUBLIC_DSN, integrations: mockIntegrations }); + + expect(mockDefaultIntegrations[0].setupOnce as jest.Mock).toHaveBeenCalledTimes(0); + expect(mockDefaultIntegrations[1].setupOnce as jest.Mock).toHaveBeenCalledTimes(1); + expect(mockIntegrations[0].setupOnce as jest.Mock).toHaveBeenCalledTimes(1); + expect(mockIntegrations[1].setupOnce as jest.Mock).toHaveBeenCalledTimes(1); + }); + + it('installs integrations returned from a callback function', () => { + const mockDefaultIntegrations = [ + new MockIntegration('Some mock integration 3.1'), + new MockIntegration('Some mock integration 3.2'), + ]; + + // @ts-ignore - Replace default integrations with mock integrations, needs ts-ignore because imports are readonly + sdk.defaultIntegrations = mockDefaultIntegrations; + + const newIntegration = new MockIntegration('Some mock integration 3.3'); + + init({ + dsn: PUBLIC_DSN, + integrations: integrations => { + const newIntegrations = [...integrations]; + newIntegrations[1] = newIntegration; + return newIntegrations; + }, + }); + + expect(mockDefaultIntegrations[0].setupOnce as jest.Mock).toHaveBeenCalledTimes(1); + expect(mockDefaultIntegrations[1].setupOnce as jest.Mock).toHaveBeenCalledTimes(0); + expect(newIntegration.setupOnce as jest.Mock).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/node/test/transports/setup.test.ts b/packages/node/test/transports/setup.test.ts index 6864ac89ea1e..38f99a4c95f3 100644 --- a/packages/node/test/transports/setup.test.ts +++ b/packages/node/test/transports/setup.test.ts @@ -3,6 +3,7 @@ import { FakeTransport } from '@sentry/core/test/mocks/transport'; import { HTTPSTransport, HTTPTransport, setupNodeTransport } from '@sentry/node/src/transports'; import { makeNodeTransport } from '../../src/transports/new'; +import { getDefaultNodeClientOptions } from '../helper/node-client-options'; jest.mock('../../src/transports/new', () => { const original = jest.requireActual('../../src/transports/new'); @@ -31,7 +32,7 @@ describe('setupNodeTransport', () => { }); it('returns the instantiated transport passed via the options', () => { - const options = { dsn: DSN, transport: FakeTransport }; + const options = getDefaultNodeClientOptions({ dsn: DSN, transport: FakeTransport }); const { transport, newTransport } = setupNodeTransport(options); expect(transport).toBeDefined(); diff --git a/packages/tracing/src/hubextensions.ts b/packages/tracing/src/hubextensions.ts index bd38b270cc22..80bd7a5783c8 100644 --- a/packages/tracing/src/hubextensions.ts +++ b/packages/tracing/src/hubextensions.ts @@ -1,5 +1,6 @@ import { getMainCarrier, Hub } from '@sentry/hub'; import { + ClientOptions, CustomSamplingContext, Integration, IntegrationClass, @@ -41,7 +42,11 @@ function traceHeaders(this: Hub): { [key: string]: string } { * * @returns The given transaction with its `sampled` value set */ -function sample(transaction: T, options: Options, samplingContext: SamplingContext): T { +function sample( + transaction: T, + options: Pick, + samplingContext: SamplingContext, +): T { // nothing to do if tracing is not enabled if (!hasTracingEnabled(options)) { transaction.sampled = false; @@ -171,7 +176,7 @@ function _startTransaction( customSamplingContext?: CustomSamplingContext, ): Transaction { const client = this.getClient(); - const options = (client && client.getOptions()) || {}; + const options: Partial = (client && client.getOptions()) || {}; let transaction = new Transaction(transactionContext, this); transaction = sample(transaction, options, { @@ -196,7 +201,7 @@ export function startIdleTransaction( customSamplingContext?: CustomSamplingContext, ): IdleTransaction { const client = hub.getClient(); - const options = (client && client.getOptions()) || {}; + const options: Partial = (client && client.getOptions()) || {}; let transaction = new IdleTransaction(transactionContext, hub, idleTimeout, onScope); transaction = sample(transaction, options, { diff --git a/packages/tracing/src/utils.ts b/packages/tracing/src/utils.ts index 63598c00ac58..9d999df486fb 100644 --- a/packages/tracing/src/utils.ts +++ b/packages/tracing/src/utils.ts @@ -20,7 +20,9 @@ export { TRACEPARENT_REGEXP, extractTraceparentData } from '@sentry/utils'; * * Tracing is enabled when at least one of `tracesSampleRate` and `tracesSampler` is defined in the SDK config. */ -export function hasTracingEnabled(maybeOptions?: Options | undefined): boolean { +export function hasTracingEnabled( + maybeOptions?: Pick | undefined, +): boolean { const client = getCurrentHub().getClient(); const options = maybeOptions || (client && client.getOptions()); return !!options && ('tracesSampleRate' in options || 'tracesSampler' in options); diff --git a/packages/tracing/test/browser/backgroundtab.test.ts b/packages/tracing/test/browser/backgroundtab.test.ts index 440eb785a609..29612b410322 100644 --- a/packages/tracing/test/browser/backgroundtab.test.ts +++ b/packages/tracing/test/browser/backgroundtab.test.ts @@ -1,5 +1,6 @@ import { BrowserClient } from '@sentry/browser'; import { setupBrowserTransport } from '@sentry/browser/src/transports'; +import { getDefaultBrowserClientOptions } from '@sentry/browser/test/unit/helper/browser-client-options'; import { Hub, makeMain } from '@sentry/hub'; import { JSDOM } from 'jsdom'; @@ -14,7 +15,7 @@ describe('registerBackgroundTabDetection', () => { // @ts-ignore need to override global document global.document = dom.window.document; - const options = { tracesSampleRate: 1 }; + const options = getDefaultBrowserClientOptions({ tracesSampleRate: 1 }); hub = new Hub(new BrowserClient(options, setupBrowserTransport(options).transport)); makeMain(hub); diff --git a/packages/tracing/test/browser/browsertracing.test.ts b/packages/tracing/test/browser/browsertracing.test.ts index dfa4e0436b8c..76ccaf947271 100644 --- a/packages/tracing/test/browser/browsertracing.test.ts +++ b/packages/tracing/test/browser/browsertracing.test.ts @@ -1,5 +1,6 @@ import { BrowserClient } from '@sentry/browser'; import { setupBrowserTransport } from '@sentry/browser/src/transports'; +import { getDefaultBrowserClientOptions } from '@sentry/browser/test/unit/helper/browser-client-options'; import { Hub, makeMain } from '@sentry/hub'; import { getGlobalObject, InstrumentHandlerCallback, InstrumentHandlerType } from '@sentry/utils'; import { JSDOM } from 'jsdom'; @@ -52,7 +53,7 @@ describe('BrowserTracing', () => { let hub: Hub; beforeEach(() => { jest.useFakeTimers(); - const options = { tracesSampleRate: 1 }; + const options = getDefaultBrowserClientOptions({ tracesSampleRate: 1 }); hub = new Hub(new BrowserClient(options, setupBrowserTransport(options).transport)); makeMain(hub); document.head.innerHTML = ''; @@ -474,7 +475,7 @@ describe('BrowserTracing', () => { getGlobalObject().location = dogParkLocation as any; const tracesSampler = jest.fn(); - const options = { tracesSampler }; + const options = getDefaultBrowserClientOptions({ tracesSampler }); hub.bindClient(new BrowserClient(options, setupBrowserTransport(options).transport)); // setting up the BrowserTracing integration automatically starts a pageload transaction createBrowserTracing(true); @@ -491,7 +492,7 @@ describe('BrowserTracing', () => { getGlobalObject().location = dogParkLocation as any; const tracesSampler = jest.fn(); - const options = { tracesSampler }; + const options = getDefaultBrowserClientOptions({ tracesSampler }); hub.bindClient(new BrowserClient(options, setupBrowserTransport(options).transport)); // setting up the BrowserTracing integration normally automatically starts a pageload transaction, but that's not // what we're testing here diff --git a/packages/tracing/test/browser/request.test.ts b/packages/tracing/test/browser/request.test.ts index 65823e293351..4ead6fe5bb9b 100644 --- a/packages/tracing/test/browser/request.test.ts +++ b/packages/tracing/test/browser/request.test.ts @@ -1,5 +1,6 @@ import { BrowserClient } from '@sentry/browser'; import { setupBrowserTransport } from '@sentry/browser/src/transports'; +import { getDefaultBrowserClientOptions } from '@sentry/browser/test/unit/helper/browser-client-options'; import { Hub, makeMain } from '@sentry/hub'; import * as utils from '@sentry/utils'; @@ -73,7 +74,7 @@ describe('callbacks', () => { }; beforeAll(() => { - const options = { tracesSampleRate: 1 }; + const options = getDefaultBrowserClientOptions({ tracesSampleRate: 1 }); hub = new Hub(new BrowserClient(options, setupBrowserTransport(options).transport)); makeMain(hub); }); diff --git a/packages/tracing/test/errors.test.ts b/packages/tracing/test/errors.test.ts index c15825fd5480..8dbcb2454539 100644 --- a/packages/tracing/test/errors.test.ts +++ b/packages/tracing/test/errors.test.ts @@ -1,5 +1,6 @@ import { BrowserClient } from '@sentry/browser'; import { setupBrowserTransport } from '@sentry/browser/src/transports'; +import { NoopTransport } from '@sentry/core/src/transports/noop'; import { Hub, makeMain } from '@sentry/hub'; import { InstrumentHandlerCallback, InstrumentHandlerType } from '@sentry/utils'; @@ -35,7 +36,7 @@ describe('registerErrorHandlers()', () => { let hub: Hub; beforeEach(() => { mockAddInstrumentationHandler.mockClear(); - const options = { tracesSampleRate: 1 }; + const options = { tracesSampleRate: 1, transport: NoopTransport, integrations: [], stackParser: () => [] }; hub = new Hub(new BrowserClient(options, setupBrowserTransport(options).transport)); makeMain(hub); }); diff --git a/packages/tracing/test/hub.test.ts b/packages/tracing/test/hub.test.ts index 045d9fa96fb1..6df8bad175c1 100644 --- a/packages/tracing/test/hub.test.ts +++ b/packages/tracing/test/hub.test.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/unbound-method */ import { BrowserClient } from '@sentry/browser'; import { setupBrowserTransport } from '@sentry/browser/src/transports'; +import { getDefaultBrowserClientOptions } from '@sentry/browser/test/unit/helper/browser-client-options'; import { Hub, makeMain } from '@sentry/hub'; import * as utilsModule from '@sentry/utils'; // for mocking import { logger } from '@sentry/utils'; @@ -33,7 +34,7 @@ describe('Hub', () => { describe('getTransaction()', () => { it('should find a transaction which has been set on the scope if sampled = true', () => { - const options = { tracesSampleRate: 1 }; + const options = getDefaultBrowserClientOptions({ tracesSampleRate: 1 }); const hub = new Hub(new BrowserClient(options, setupBrowserTransport(options).transport)); makeMain(hub); const transaction = hub.startTransaction({ name: 'dogpark' }); @@ -47,7 +48,7 @@ describe('Hub', () => { }); it('should find a transaction which has been set on the scope if sampled = false', () => { - const options = { tracesSampleRate: 1 }; + const options = getDefaultBrowserClientOptions({ tracesSampleRate: 1 }); const hub = new Hub(new BrowserClient(options, setupBrowserTransport(options).transport)); makeMain(hub); const transaction = hub.startTransaction({ name: 'dogpark', sampled: false }); @@ -60,7 +61,7 @@ describe('Hub', () => { }); it("should not find an open transaction if it's not on the scope", () => { - const options = { tracesSampleRate: 1 }; + const options = getDefaultBrowserClientOptions({ tracesSampleRate: 1 }); const hub = new Hub(new BrowserClient(options, setupBrowserTransport(options).transport)); makeMain(hub); hub.startTransaction({ name: 'dogpark' }); @@ -73,7 +74,7 @@ describe('Hub', () => { describe('default sample context', () => { it('should add transaction context data to default sample context', () => { const tracesSampler = jest.fn(); - const options = { tracesSampler }; + const options = getDefaultBrowserClientOptions({ tracesSampler }); const hub = new Hub(new BrowserClient(options, setupBrowserTransport(options).transport)); makeMain(hub); @@ -90,7 +91,7 @@ describe('Hub', () => { it("should add parent's sampling decision to default sample context", () => { const tracesSampler = jest.fn(); - const options = { tracesSampler }; + const options = getDefaultBrowserClientOptions({ tracesSampler }); const hub = new Hub(new BrowserClient(options, setupBrowserTransport(options).transport)); makeMain(hub); const parentSamplingDecsion = false; @@ -109,7 +110,7 @@ describe('Hub', () => { describe('sample()', () => { it('should set sampled = false when tracing is disabled', () => { - const options = {}; + const options = getDefaultBrowserClientOptions({}); // neither tracesSampleRate nor tracesSampler is defined -> tracing disabled const hub = new Hub(new BrowserClient(options, setupBrowserTransport(options).transport)); makeMain(hub); @@ -119,7 +120,7 @@ describe('Hub', () => { }); it('should set sampled = false if tracesSampleRate is 0', () => { - const options = { tracesSampleRate: 0 }; + const options = getDefaultBrowserClientOptions({ tracesSampleRate: 0 }); const hub = new Hub(new BrowserClient(options, setupBrowserTransport(options).transport)); makeMain(hub); const transaction = hub.startTransaction({ name: 'dogpark' }); @@ -128,7 +129,7 @@ describe('Hub', () => { }); it('should set sampled = true if tracesSampleRate is 1', () => { - const options = { tracesSampleRate: 1 }; + const options = getDefaultBrowserClientOptions({ tracesSampleRate: 1 }); const hub = new Hub(new BrowserClient(options, setupBrowserTransport(options).transport)); makeMain(hub); const transaction = hub.startTransaction({ name: 'dogpark' }); @@ -137,7 +138,7 @@ describe('Hub', () => { }); it('should set sampled = true if tracesSampleRate is 1 (without global hub)', () => { - const options = { tracesSampleRate: 1 }; + const options = getDefaultBrowserClientOptions({ tracesSampleRate: 1 }); const hub = new Hub(new BrowserClient(options, setupBrowserTransport(options).transport)); const transaction = hub.startTransaction({ name: 'dogpark' }); @@ -146,7 +147,7 @@ describe('Hub', () => { it("should call tracesSampler if it's defined", () => { const tracesSampler = jest.fn(); - const options = { tracesSampler }; + const options = getDefaultBrowserClientOptions({ tracesSampler }); const hub = new Hub(new BrowserClient(options, setupBrowserTransport(options).transport)); makeMain(hub); hub.startTransaction({ name: 'dogpark' }); @@ -156,7 +157,7 @@ describe('Hub', () => { it('should set sampled = false if tracesSampler returns 0', () => { const tracesSampler = jest.fn().mockReturnValue(0); - const options = { tracesSampler }; + const options = getDefaultBrowserClientOptions({ tracesSampler }); const hub = new Hub(new BrowserClient(options, setupBrowserTransport(options).transport)); makeMain(hub); const transaction = hub.startTransaction({ name: 'dogpark' }); @@ -167,7 +168,7 @@ describe('Hub', () => { it('should set sampled = true if tracesSampler returns 1', () => { const tracesSampler = jest.fn().mockReturnValue(1); - const options = { tracesSampler }; + const options = getDefaultBrowserClientOptions({ tracesSampler }); const hub = new Hub(new BrowserClient(options, setupBrowserTransport(options).transport)); makeMain(hub); const transaction = hub.startTransaction({ name: 'dogpark' }); @@ -178,7 +179,7 @@ describe('Hub', () => { it('should set sampled = true if tracesSampler returns 1 (without global hub)', () => { const tracesSampler = jest.fn().mockReturnValue(1); - const options = { tracesSampler }; + const options = getDefaultBrowserClientOptions({ tracesSampler }); const hub = new Hub(new BrowserClient(options, setupBrowserTransport(options).transport)); const transaction = hub.startTransaction({ name: 'dogpark' }); @@ -189,7 +190,7 @@ describe('Hub', () => { it('should not try to override explicitly set positive sampling decision', () => { // so that the decision otherwise would be false const tracesSampler = jest.fn().mockReturnValue(0); - const options = { tracesSampler }; + const options = getDefaultBrowserClientOptions({ tracesSampler }); const hub = new Hub(new BrowserClient(options, setupBrowserTransport(options).transport)); makeMain(hub); const transaction = hub.startTransaction({ name: 'dogpark', sampled: true }); @@ -200,7 +201,7 @@ describe('Hub', () => { it('should not try to override explicitly set negative sampling decision', () => { // so that the decision otherwise would be true const tracesSampler = jest.fn().mockReturnValue(1); - const options = { tracesSampler }; + const options = getDefaultBrowserClientOptions({ tracesSampler }); const hub = new Hub(new BrowserClient(options, setupBrowserTransport(options).transport)); makeMain(hub); const transaction = hub.startTransaction({ name: 'dogpark', sampled: false }); @@ -211,7 +212,7 @@ describe('Hub', () => { it('should prefer tracesSampler to tracesSampleRate', () => { // make the two options do opposite things to prove precedence const tracesSampler = jest.fn().mockReturnValue(true); - const options = { tracesSampleRate: 0, tracesSampler }; + const options = getDefaultBrowserClientOptions({ tracesSampleRate: 0, tracesSampler }); const hub = new Hub(new BrowserClient(options, setupBrowserTransport(options).transport)); makeMain(hub); const transaction = hub.startTransaction({ name: 'dogpark' }); @@ -222,7 +223,7 @@ describe('Hub', () => { it('should tolerate tracesSampler returning a boolean', () => { const tracesSampler = jest.fn().mockReturnValue(true); - const options = { tracesSampler }; + const options = getDefaultBrowserClientOptions({ tracesSampler }); const hub = new Hub(new BrowserClient(options, setupBrowserTransport(options).transport)); makeMain(hub); const transaction = hub.startTransaction({ name: 'dogpark' }); @@ -233,7 +234,7 @@ describe('Hub', () => { it('should record sampling method when sampling decision is explicitly set', () => { const tracesSampler = jest.fn().mockReturnValue(0.1121); - const options = { tracesSampler }; + const options = getDefaultBrowserClientOptions({ tracesSampler }); const hub = new Hub(new BrowserClient(options, setupBrowserTransport(options).transport)); makeMain(hub); hub.startTransaction({ name: 'dogpark', sampled: true }); @@ -245,7 +246,7 @@ describe('Hub', () => { it('should record sampling method and rate when sampling decision comes from tracesSampler', () => { const tracesSampler = jest.fn().mockReturnValue(0.1121); - const options = { tracesSampler }; + const options = getDefaultBrowserClientOptions({ tracesSampler }); const hub = new Hub(new BrowserClient(options, setupBrowserTransport(options).transport)); makeMain(hub); hub.startTransaction({ name: 'dogpark' }); @@ -256,7 +257,7 @@ describe('Hub', () => { }); it('should record sampling method when sampling decision is inherited', () => { - const options = { tracesSampleRate: 0.1121 }; + const options = getDefaultBrowserClientOptions({ tracesSampleRate: 0.1121 }); const hub = new Hub(new BrowserClient(options, setupBrowserTransport(options).transport)); makeMain(hub); hub.startTransaction({ name: 'dogpark', parentSampled: true }); @@ -267,7 +268,7 @@ describe('Hub', () => { }); it('should record sampling method and rate when sampling decision comes from traceSampleRate', () => { - const options = { tracesSampleRate: 0.1121 }; + const options = getDefaultBrowserClientOptions({ tracesSampleRate: 0.1121 }); const hub = new Hub(new BrowserClient(options, setupBrowserTransport(options).transport)); makeMain(hub); hub.startTransaction({ name: 'dogpark' }); @@ -280,7 +281,7 @@ describe('Hub', () => { describe('isValidSampleRate()', () => { it("should reject tracesSampleRates which aren't numbers or booleans", () => { - const options = { tracesSampleRate: 'dogs!' as any }; + const options = getDefaultBrowserClientOptions({ tracesSampleRate: 'dogs!' as any }); const hub = new Hub(new BrowserClient(options, setupBrowserTransport(options).transport)); makeMain(hub); hub.startTransaction({ name: 'dogpark' }); @@ -289,7 +290,7 @@ describe('Hub', () => { }); it('should reject tracesSampleRates which are NaN', () => { - const options = { tracesSampleRate: 'dogs!' as any }; + const options = getDefaultBrowserClientOptions({ tracesSampleRate: 'dogs!' as any }); const hub = new Hub(new BrowserClient(options, setupBrowserTransport(options).transport)); makeMain(hub); hub.startTransaction({ name: 'dogpark' }); @@ -299,7 +300,7 @@ describe('Hub', () => { // the rate might be a boolean, but for our purposes, false is equivalent to 0 and true is equivalent to 1 it('should reject tracesSampleRates less than 0', () => { - const options = { tracesSampleRate: -26 }; + const options = getDefaultBrowserClientOptions({ tracesSampleRate: -26 }); const hub = new Hub(new BrowserClient(options, setupBrowserTransport(options).transport)); makeMain(hub); hub.startTransaction({ name: 'dogpark' }); @@ -309,7 +310,7 @@ describe('Hub', () => { // the rate might be a boolean, but for our purposes, false is equivalent to 0 and true is equivalent to 1 it('should reject tracesSampleRates greater than 1', () => { - const options = { tracesSampleRate: 26 }; + const options = getDefaultBrowserClientOptions({ tracesSampleRate: 26 }); const hub = new Hub(new BrowserClient(options, setupBrowserTransport(options).transport)); makeMain(hub); hub.startTransaction({ name: 'dogpark' }); @@ -319,7 +320,7 @@ describe('Hub', () => { it("should reject tracesSampler return values which aren't numbers or booleans", () => { const tracesSampler = jest.fn().mockReturnValue('dogs!'); - const options = { tracesSampler }; + const options = getDefaultBrowserClientOptions({ tracesSampler }); const hub = new Hub(new BrowserClient(options, setupBrowserTransport(options).transport)); makeMain(hub); hub.startTransaction({ name: 'dogpark' }); @@ -329,7 +330,7 @@ describe('Hub', () => { it('should reject tracesSampler return values which are NaN', () => { const tracesSampler = jest.fn().mockReturnValue(NaN); - const options = { tracesSampler }; + const options = getDefaultBrowserClientOptions({ tracesSampler }); const hub = new Hub(new BrowserClient(options, setupBrowserTransport(options).transport)); makeMain(hub); hub.startTransaction({ name: 'dogpark' }); @@ -340,7 +341,7 @@ describe('Hub', () => { // the rate might be a boolean, but for our purposes, false is equivalent to 0 and true is equivalent to 1 it('should reject tracesSampler return values less than 0', () => { const tracesSampler = jest.fn().mockReturnValue(-12); - const options = { tracesSampler }; + const options = getDefaultBrowserClientOptions({ tracesSampler }); const hub = new Hub(new BrowserClient(options, setupBrowserTransport(options).transport)); makeMain(hub); hub.startTransaction({ name: 'dogpark' }); @@ -351,7 +352,7 @@ describe('Hub', () => { // the rate might be a boolean, but for our purposes, false is equivalent to 0 and true is equivalent to 1 it('should reject tracesSampler return values greater than 1', () => { const tracesSampler = jest.fn().mockReturnValue(31); - const options = { tracesSampler }; + const options = getDefaultBrowserClientOptions({ tracesSampler }); const hub = new Hub(new BrowserClient(options, setupBrowserTransport(options).transport)); makeMain(hub); hub.startTransaction({ name: 'dogpark' }); @@ -361,7 +362,7 @@ describe('Hub', () => { }); it('should drop transactions with sampled = false', () => { - const options = { tracesSampleRate: 0 }; + const options = getDefaultBrowserClientOptions({ tracesSampleRate: 0 }); const client = new BrowserClient(options, setupBrowserTransport(options).transport); jest.spyOn(client, 'captureEvent'); @@ -379,7 +380,7 @@ describe('Hub', () => { describe('sampling inheritance', () => { it('should propagate sampling decision to child spans', () => { - const options = { tracesSampleRate: Math.random() }; + const options = getDefaultBrowserClientOptions({ tracesSampleRate: Math.random() }); const hub = new Hub(new BrowserClient(options, setupBrowserTransport(options).transport)); makeMain(hub); const transaction = hub.startTransaction({ name: 'dogpark' }); @@ -392,11 +393,11 @@ describe('Hub', () => { testOnlyIfNodeVersionAtLeast(10)( 'should propagate positive sampling decision to child transactions in XHR header', async () => { - const options = { + const options = getDefaultBrowserClientOptions({ dsn: 'https://1231@dogs.are.great/1121', tracesSampleRate: 1, integrations: [new BrowserTracing()], - }; + }); const hub = new Hub(new BrowserClient(options, setupBrowserTransport(options).transport)); makeMain(hub); @@ -433,11 +434,11 @@ describe('Hub', () => { testOnlyIfNodeVersionAtLeast(10)( 'should propagate negative sampling decision to child transactions in XHR header', async () => { - const options = { + const options = getDefaultBrowserClientOptions({ dsn: 'https://1231@dogs.are.great/1121', tracesSampleRate: 1, integrations: [new BrowserTracing()], - }; + }); const hub = new Hub(new BrowserClient(options, setupBrowserTransport(options).transport)); makeMain(hub); @@ -483,7 +484,7 @@ describe('Hub', () => { // sample rate), so make parent's decision the opposite to prove that inheritance takes precedence over // tracesSampleRate mathRandom.mockReturnValueOnce(1); - const options = { tracesSampleRate: 0.5 }; + const options = getDefaultBrowserClientOptions({ tracesSampleRate: 0.5 }); const hub = new Hub(new BrowserClient(options, setupBrowserTransport(options).transport)); makeMain(hub); const parentSamplingDecsion = true; @@ -498,7 +499,7 @@ describe('Hub', () => { }); it("should inherit parent's negative sampling decision if tracesSampler is undefined", () => { - const options = { tracesSampleRate: 1 }; + const options = getDefaultBrowserClientOptions({ tracesSampleRate: 1 }); // tracesSampleRate = 1 means every transaction should end up with sampled = true, so make parent's decision the // opposite to prove that inheritance takes precedence over tracesSampleRate const hub = new Hub(new BrowserClient(options, setupBrowserTransport(options).transport)); @@ -520,7 +521,7 @@ describe('Hub', () => { const tracesSampler = () => true; const parentSamplingDecsion = false; - const options = { tracesSampler }; + const options = getDefaultBrowserClientOptions({ tracesSampler }); const hub = new Hub(new BrowserClient(options, setupBrowserTransport(options).transport)); makeMain(hub); @@ -539,7 +540,7 @@ describe('Hub', () => { const tracesSampler = () => false; const parentSamplingDecsion = true; - const options = { tracesSampler }; + const options = getDefaultBrowserClientOptions({ tracesSampler }); const hub = new Hub(new BrowserClient(options, setupBrowserTransport(options).transport)); makeMain(hub); diff --git a/packages/tracing/test/idletransaction.test.ts b/packages/tracing/test/idletransaction.test.ts index b8029c6b92d7..b040f4adbd9b 100644 --- a/packages/tracing/test/idletransaction.test.ts +++ b/packages/tracing/test/idletransaction.test.ts @@ -1,4 +1,5 @@ import { BrowserClient, Transports } from '@sentry/browser'; +import { getDefaultBrowserClientOptions } from '@sentry/browser/test/unit/helper/browser-client-options'; import { Hub } from '@sentry/hub'; import { @@ -16,8 +17,8 @@ class SimpleTransport extends Transports.BaseTransport {} const dsn = 'https://123@sentry.io/42'; let hub: Hub; beforeEach(() => { - const options = { dsn, tracesSampleRate: 1, transport: SimpleTransport }; - hub = new Hub(new BrowserClient(options, new SimpleTransport(options))); + const options = getDefaultBrowserClientOptions({ dsn, tracesSampleRate: 1, transport: SimpleTransport }); + hub = new Hub(new BrowserClient(options, new SimpleTransport({ dsn }))); }); describe('IdleTransaction', () => { diff --git a/packages/tracing/test/span.test.ts b/packages/tracing/test/span.test.ts index fe11fbf143ee..13f47c51ae7b 100644 --- a/packages/tracing/test/span.test.ts +++ b/packages/tracing/test/span.test.ts @@ -1,5 +1,6 @@ import { BrowserClient } from '@sentry/browser'; import { setupBrowserTransport } from '@sentry/browser/src/transports'; +import { getDefaultBrowserClientOptions } from '@sentry/browser/test/unit/helper/browser-client-options'; import { Hub, makeMain, Scope } from '@sentry/hub'; import { Span, Transaction } from '../src'; @@ -10,7 +11,7 @@ describe('Span', () => { beforeEach(() => { const myScope = new Scope(); - const options = { tracesSampleRate: 1 }; + const options = getDefaultBrowserClientOptions({ tracesSampleRate: 1 }); hub = new Hub(new BrowserClient(options, setupBrowserTransport(options).transport), myScope); makeMain(hub); }); @@ -218,10 +219,10 @@ describe('Span', () => { }); test('maxSpans correctly limits number of spans', () => { - const options = { + const options = getDefaultBrowserClientOptions({ _experiments: { maxSpans: 3 }, tracesSampleRate: 1, - }; + }); const _hub = new Hub(new BrowserClient(options, setupBrowserTransport(options).transport)); const spy = jest.spyOn(_hub as any, 'captureEvent') as any; const transaction = _hub.startTransaction({ name: 'test' }); @@ -234,9 +235,9 @@ describe('Span', () => { }); test('no span recorder created if transaction.sampled is false', () => { - const options = { + const options = getDefaultBrowserClientOptions({ tracesSampleRate: 1, - }; + }); const _hub = new Hub(new BrowserClient(options, setupBrowserTransport(options).transport)); const spy = jest.spyOn(_hub as any, 'captureEvent') as any; const transaction = _hub.startTransaction({ name: 'test', sampled: false }); diff --git a/packages/types/src/client.ts b/packages/types/src/client.ts index c3f2a9920258..f0cdc8b3a988 100644 --- a/packages/types/src/client.ts +++ b/packages/types/src/client.ts @@ -1,7 +1,7 @@ import { DsnComponents } from './dsn'; import { Event, EventHint } from './event'; import { Integration, IntegrationClass } from './integration'; -import { Options } from './options'; +import { ClientOptions } from './options'; import { Scope } from './scope'; import { Session } from './session'; import { Severity, SeverityLevel } from './severity'; @@ -16,7 +16,7 @@ import { Transport } from './transport'; * there will only be one instance during runtime. * */ -export interface Client { +export interface Client { /** * Captures an exception event and sends it to Sentry. * diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 84430c1d3976..5fe4ced6375b 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -27,7 +27,7 @@ export type { Hub } from './hub'; export type { Integration, IntegrationClass } from './integration'; export type { Mechanism } from './mechanism'; export type { ExtractedNodeRequestData, Primitive, WorkerLocation } from './misc'; -export type { Options } from './options'; +export type { ClientOptions, Options } from './options'; export type { Package } from './package'; export type { QueryParams, Request, SentryRequest, SentryRequestType } from './request'; export type { Response } from './response'; diff --git a/packages/types/src/options.ts b/packages/types/src/options.ts index a7306acdd76f..8bd273752700 100644 --- a/packages/types/src/options.ts +++ b/packages/types/src/options.ts @@ -7,8 +7,7 @@ import { StackLineParser, StackParser } from './stacktrace'; import { SamplingContext } from './transaction'; import { Transport, TransportClass, TransportOptions } from './transport'; -/** Base configuration options for every SDK. */ -export interface Options { +export interface ClientOptions { /** * Enable debug functionality in the SDK itself */ @@ -20,6 +19,21 @@ export interface Options { */ enabled?: boolean; + /** Attaches stacktraces to pure capture message / log integrations */ + attachStacktrace?: boolean; + + /** + * A flag enabling Sessions Tracking feature. + * By default, Sessions Tracking is enabled. + */ + autoSessionTracking?: boolean; + + /** + * Send SDK Client Reports. + * By default, Client Reports are enabled. + */ + sendClientReports?: boolean; + /** * The Dsn used to connect to Sentry and identify the project. If omitted, the * SDK will not send any data to Sentry. @@ -27,29 +41,33 @@ export interface Options { dsn?: string; /** - * If this is set to false, default integrations will not be added, otherwise this will internally be set to the - * recommended default integrations. - * TODO: We should consider changing this to `boolean | Integration[]` + * The release identifier used when uploading respective source maps. Specify + * this value to allow Sentry to resolve the correct source maps when + * processing events. */ - defaultIntegrations?: false | Integration[]; + release?: string; + + /** The current environment of your application (e.g. "production"). */ + environment?: string; + + /** Sets the distribution for all events */ + dist?: string; /** * List of integrations that should be installed after SDK was initialized. - * Accepts either a list of integrations or a function that receives - * default integrations and returns a new, updated list. */ - integrations?: Integration[] | ((integrations: Integration[]) => Integration[]); + integrations: Integration[]; /** - * A pattern for error messages which should not be sent to Sentry. - * By default, all errors will be sent. + * Transport object that should be used to send events to Sentry */ - ignoreErrors?: Array; + transport: TransportClass; /** - * Transport object that should be used to send events to Sentry + * A stack parser implementation + * By default, a stack parser is supplied for all supported platforms */ - transport?: TransportClass; + stackParser: StackParser; /** * Options for the default transport that the SDK uses. @@ -57,24 +75,20 @@ export interface Options { transportOptions?: TransportOptions; /** - * A URL to an envelope tunnel endpoint. An envelope tunnel is an HTTP endpoint - * that accepts Sentry envelopes for forwarding. This can be used to force data - * through a custom server independent of the type of data. + * Sample rate to determine trace sampling. + * + * 0.0 = 0% chance of a given trace being sent (send no traces) 1.0 = 100% chance of a given trace being sent (send + * all traces) + * + * Tracing is enabled if either this or `tracesSampler` is defined. If both are defined, `tracesSampleRate` is + * ignored. */ - tunnel?: string; + tracesSampleRate?: number; /** - * The release identifier used when uploading respective source maps. Specify - * this value to allow Sentry to resolve the correct source maps when - * processing events. + * Initial data to populate scope. */ - release?: string; - - /** The current environment of your application (e.g. "production"). */ - environment?: string; - - /** Sets the distribution for all events */ - dist?: string; + initialScope?: CaptureContext; /** * The maximum number of breadcrumbs sent with events. Defaults to 100. @@ -85,9 +99,6 @@ export interface Options { /** A global sample rate to apply to all events (0 - 1). */ sampleRate?: number; - /** Attaches stacktraces to pure capture message / log integrations */ - attachStacktrace?: boolean; - /** Maximum number of chars a single value can have before it will be truncated. */ maxValueLength?: number; @@ -123,38 +134,17 @@ export interface Options { shutdownTimeout?: number; /** - * Sample rate to determine trace sampling. - * - * 0.0 = 0% chance of a given trace being sent (send no traces) 1.0 = 100% chance of a given trace being sent (send - * all traces) - * - * Tracing is enabled if either this or `tracesSampler` is defined. If both are defined, `tracesSampleRate` is - * ignored. - */ - tracesSampleRate?: number; - - /** - * A flag enabling Sessions Tracking feature. - * By default, Sessions Tracking is enabled. - */ - autoSessionTracking?: boolean; - - /** - * Send SDK Client Reports. - * By default, Client Reports are enabled. - */ - sendClientReports?: boolean; - - /** - * Initial data to populate scope. + * A pattern for error messages which should not be sent to Sentry. + * By default, all errors will be sent. */ - initialScope?: CaptureContext; + ignoreErrors?: Array; /** - * A stack parser implementation or an array of stack line parsers - * By default, a stack parser is supplied for all supported browsers + * A URL to an envelope tunnel endpoint. An envelope tunnel is an HTTP endpoint + * that accepts Sentry envelopes for forwarding. This can be used to force data + * through a custom server independent of the type of data. */ - stackParser?: StackParser | StackLineParser[]; + tunnel?: string; /** * Set of metadata about the SDK that can be internally used to enhance envelopes and events, @@ -210,3 +200,31 @@ export interface Options { */ beforeBreadcrumb?: (breadcrumb: Breadcrumb, hint?: BreadcrumbHint) => Breadcrumb | null; } + +/** Base configuration options for every SDK. */ +export interface Options extends Omit, 'integrations' | 'transport' | 'stackParser'> { + /** + * If this is set to false, default integrations will not be added, otherwise this will internally be set to the + * recommended default integrations. + * TODO: We should consider changing this to `boolean | Integration[]` + */ + defaultIntegrations?: false | Integration[]; + + /** + * List of integrations that should be installed after SDK was initialized. + * Accepts either a list of integrations or a function that receives + * default integrations and returns a new, updated list. + */ + integrations?: Integration[] | ((integrations: Integration[]) => Integration[]); + + /** + * Transport object that should be used to send events to Sentry + */ + transport?: TransportClass; + + /** + * A stack parser implementation or an array of stack line parsers + * By default, a stack parser is supplied for all supported browsers + */ + stackParser?: StackParser | StackLineParser[]; +} diff --git a/packages/utils/src/dsn.ts b/packages/utils/src/dsn.ts index 5c500c5ec654..56b864e2863b 100644 --- a/packages/utils/src/dsn.ts +++ b/packages/utils/src/dsn.ts @@ -98,8 +98,6 @@ function validateDsn(dsn: DsnComponents): boolean | void { /** The Sentry Dsn, identifying a Sentry instance and project. */ export function makeDsn(from: DsnLike): DsnComponents { const components = typeof from === 'string' ? dsnFromString(from) : dsnFromComponents(from); - validateDsn(components); - return components; }