From 39df58623a01bb03bd6ef2b5268c925ae425a8a2 Mon Sep 17 00:00:00 2001 From: HazA Date: Wed, 10 Oct 2018 15:27:58 +0200 Subject: [PATCH 01/11] feat: Add setupIntegrations, Update Scope & Hub --- .../browser/src/integrations/breadcrumbs.ts | 2 +- .../src/integrations/globalhandlers.ts | 6 +- .../browser/src/integrations/linkederrors.ts | 8 ++- packages/browser/src/integrations/trycatch.ts | 2 +- .../browser/src/integrations/useragent.ts | 36 ++++++------ packages/browser/src/sdk.ts | 7 ++- packages/core/src/baseclient.ts | 14 +++++ packages/core/src/integrations/dedupe.ts | 11 ++-- .../core/src/integrations/functiontostring.ts | 2 +- .../core/src/integrations/inboundfilters.ts | 21 ++++--- packages/core/src/integrations/index.ts | 56 +++++++++++++++++++ .../core/src/integrations/pluggable/debug.ts | 41 +++++++------- .../integrations/pluggable/rewriteframes.ts | 8 ++- packages/core/src/interfaces.ts | 8 ++- packages/core/src/sdk.ts | 29 +--------- packages/hub/src/hub.ts | 7 ++- packages/hub/src/index.ts | 2 +- packages/hub/src/scope.ts | 22 ++++++-- packages/node/src/sdk.ts | 7 ++- packages/types/src/index.ts | 4 +- 20 files changed, 189 insertions(+), 104 deletions(-) diff --git a/packages/browser/src/integrations/breadcrumbs.ts b/packages/browser/src/integrations/breadcrumbs.ts index 819289b2c98a..1cb9473e4e9d 100644 --- a/packages/browser/src/integrations/breadcrumbs.ts +++ b/packages/browser/src/integrations/breadcrumbs.ts @@ -455,7 +455,7 @@ export class Breadcrumbs implements Integration { * - Fetch API * - History API */ - public install(options: BrowserOptions = {}): void { + public setupOnce(options: BrowserOptions = {}): void { const filterUrl = options.dsn && new API(options.dsn).getStoreEndpoint(); if (this.options.console) { diff --git a/packages/browser/src/integrations/globalhandlers.ts b/packages/browser/src/integrations/globalhandlers.ts index 1bb371f1ce3f..ebefa8cc9014 100644 --- a/packages/browser/src/integrations/globalhandlers.ts +++ b/packages/browser/src/integrations/globalhandlers.ts @@ -36,7 +36,7 @@ export class GlobalHandlers implements Integration { /** * @inheritDoc */ - public install(): void { + public setupOnce(): void { subscribe((stack: TraceKitStackTrace, _: boolean, error: Error) => { // TODO: use stack.context to get a valuable information from TraceKit, eg. // [ @@ -55,7 +55,9 @@ export class GlobalHandlers implements Integration { if (shouldIgnoreOnError()) { return; } - getCurrentHub().captureEvent(this.eventFromGlobalHandler(stack), { originalException: error, data: { stack } }); + if (getCurrentHub().getIntegration(this.name)) { + getCurrentHub().captureEvent(this.eventFromGlobalHandler(stack), { originalException: error, data: { stack } }); + } }); if (this.options.onerror) { diff --git a/packages/browser/src/integrations/linkederrors.ts b/packages/browser/src/integrations/linkederrors.ts index 27b8c41b2c16..db598fc9c6a8 100644 --- a/packages/browser/src/integrations/linkederrors.ts +++ b/packages/browser/src/integrations/linkederrors.ts @@ -1,4 +1,4 @@ -import { configureScope } from '@sentry/core'; +import { configureScope, getCurrentHub } from '@sentry/core'; import { Integration, SentryEvent, SentryEventHint, SentryException } from '@sentry/types'; import { exceptionFromStacktrace } from '../parsers'; import { computeStackTrace } from '../tracekit'; @@ -41,8 +41,10 @@ export class LinkedErrors implements Integration { /** * @inheritDoc */ - public install(): void { - configureScope(scope => scope.addEventProcessor(this.handler.bind(this))); + public setupOnce(): void { + if (getCurrentHub().getIntegration(this.name)) { + configureScope(scope => scope.addEventProcessor(this.handler.bind(this))); + } } /** diff --git a/packages/browser/src/integrations/trycatch.ts b/packages/browser/src/integrations/trycatch.ts index e6831e15ff05..d27b8821c709 100644 --- a/packages/browser/src/integrations/trycatch.ts +++ b/packages/browser/src/integrations/trycatch.ts @@ -162,7 +162,7 @@ export class TryCatch implements Integration { * Wrap timer functions and event targets to catch errors * and provide better metadata. */ - public install(): void { + public setupOnce(): void { this.ignoreOnError = this.ignoreOnError; const global = getGlobalObject(); diff --git a/packages/browser/src/integrations/useragent.ts b/packages/browser/src/integrations/useragent.ts index 4d5fbf3a50f6..d0ab23472e64 100644 --- a/packages/browser/src/integrations/useragent.ts +++ b/packages/browser/src/integrations/useragent.ts @@ -1,4 +1,4 @@ -import { configureScope } from '@sentry/core'; +import { configureScope, getCurrentHub } from '@sentry/core'; import { Integration, SentryEvent } from '@sentry/types'; import { getGlobalObject } from '@sentry/utils/misc'; @@ -14,24 +14,26 @@ export class UserAgent implements Integration { /** * @inheritDoc */ - public install(): void { - configureScope(scope => { - scope.addEventProcessor(async (event: SentryEvent) => { - if (!global.navigator || !global.location) { - return event; - } + public setupOnce(): void { + if (getCurrentHub().getIntegration(this.name)) { + configureScope(scope => { + scope.addEventProcessor(async (event: SentryEvent) => { + if (!global.navigator || !global.location) { + return event; + } - // HTTP Interface: https://docs.sentry.io/clientdev/interfaces/http/?platform=javascript - const request = event.request || {}; - request.url = request.url || global.location.href; - request.headers = request.headers || {}; - request.headers['User-Agent'] = global.navigator.userAgent; + // HTTP Interface: https://docs.sentry.io/clientdev/interfaces/http/?platform=javascript + const request = event.request || {}; + request.url = request.url || global.location.href; + request.headers = request.headers || {}; + request.headers['User-Agent'] = global.navigator.userAgent; - return { - ...event, - request, - }; + return { + ...event, + request, + }; + }); }); - }); + } } } diff --git a/packages/browser/src/sdk.ts b/packages/browser/src/sdk.ts index ec7bd7c08892..e2d4cd086b4b 100644 --- a/packages/browser/src/sdk.ts +++ b/packages/browser/src/sdk.ts @@ -62,8 +62,11 @@ export const defaultIntegrations = [ * * @see BrowserOptions for documentation on configuration options. */ -export function init(options: BrowserOptions): void { - initAndBind(BrowserClient, options, defaultIntegrations); +export function init(options: BrowserOptions = {}): void { + if (options.defaultIntegrations === undefined) { + options.defaultIntegrations = defaultIntegrations; + } + initAndBind(BrowserClient, options); } /** diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index 3d978ee3f110..1210c38c6a2d 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -1,6 +1,7 @@ import { Scope } from '@sentry/hub'; import { Breadcrumb, + Integration, SentryBreadcrumbHint, SentryEvent, SentryEventHint, @@ -14,6 +15,7 @@ import { getGlobalObject, uuid4 } from '@sentry/utils/misc'; import { truncate } from '@sentry/utils/string'; import { BackendClass } from './basebackend'; import { Dsn } from './dsn'; +import { IntegrationIndex, setupIntegrations } from './integrations'; import { Backend, Client, Options } from './interfaces'; import { logger } from './logger'; @@ -122,6 +124,9 @@ export abstract class BaseClient implement */ private installed?: boolean; + /** Array of used integrations. */ + private integrations?: IntegrationIndex; + /** * Initializes this client instance. * @@ -135,6 +140,8 @@ export abstract class BaseClient implement if (options.dsn) { this.dsn = new Dsn(options.dsn); } + + this.integrations = setupIntegrations(options); } /** @@ -410,4 +417,11 @@ export abstract class BaseClient implement .getBuffer() .drain(timeout); } + + /** + * @inheritDoc + */ + public getIntegration(name: string): Integration | null { + return (this.integrations && this.integrations[name]) || null; + } } diff --git a/packages/core/src/integrations/dedupe.ts b/packages/core/src/integrations/dedupe.ts index 7df511ca8c95..1c7103204b4b 100644 --- a/packages/core/src/integrations/dedupe.ts +++ b/packages/core/src/integrations/dedupe.ts @@ -1,4 +1,4 @@ -import { configureScope } from '@sentry/minimal'; +import { addGlobalEventProcessor, getCurrentHub } from '@sentry/hub'; import { Integration, SentryEvent, SentryException, StackFrame } from '@sentry/types'; import { getEventDescription } from '@sentry/utils/misc'; import { logger } from '../logger'; @@ -18,9 +18,9 @@ export class Dedupe implements Integration { /** * @inheritDoc */ - public install(): void { - configureScope(scope => { - scope.addEventProcessor(async (currentEvent: SentryEvent) => { + public setupOnce(): void { + addGlobalEventProcessor(async (currentEvent: SentryEvent) => { + if (getCurrentHub().getIntegration(this.name)) { // Juuust in case something goes wrong try { if (this.shouldDropEvent(currentEvent, this.previousEvent)) { @@ -31,7 +31,8 @@ export class Dedupe implements Integration { } return (this.previousEvent = currentEvent); - }); + } + return currentEvent; }); } diff --git a/packages/core/src/integrations/functiontostring.ts b/packages/core/src/integrations/functiontostring.ts index 8b6228ad781c..d039bbd6c04c 100644 --- a/packages/core/src/integrations/functiontostring.ts +++ b/packages/core/src/integrations/functiontostring.ts @@ -11,7 +11,7 @@ export class FunctionToString implements Integration { /** * @inheritDoc */ - public install(): void { + public setupOnce(): void { originalFunctionToString = Function.prototype.toString; Function.prototype.toString = function(this: SentryWrappedFunction, ...args: any[]): string { diff --git a/packages/core/src/integrations/inboundfilters.ts b/packages/core/src/integrations/inboundfilters.ts index f186cf610380..e172bbc957c8 100644 --- a/packages/core/src/integrations/inboundfilters.ts +++ b/packages/core/src/integrations/inboundfilters.ts @@ -1,3 +1,4 @@ +import { getCurrentHub } from '@sentry/hub'; import { configureScope } from '@sentry/minimal'; import { Integration, SentryEvent } from '@sentry/types'; import { isRegExp } from '@sentry/utils/is'; @@ -32,17 +33,19 @@ export class InboundFilters implements Integration { /** * @inheritDoc */ - public install(options: InboundFiltersOptions = {}): void { - this.configureOptions(options); + public setupOnce(options: InboundFiltersOptions = {}): void { + if (getCurrentHub().getIntegration(this.name)) { + this.configureOptions(options); - configureScope(scope => { - scope.addEventProcessor(async (event: SentryEvent) => { - if (this.shouldDropEvent(event)) { - return null; - } - return event; + configureScope(scope => { + scope.addEventProcessor(async (event: SentryEvent) => { + if (this.shouldDropEvent(event)) { + return null; + } + return event; + }); }); - }); + } } /** JSDoc */ diff --git a/packages/core/src/integrations/index.ts b/packages/core/src/integrations/index.ts index 52a16507d399..46189cd368e5 100644 --- a/packages/core/src/integrations/index.ts +++ b/packages/core/src/integrations/index.ts @@ -1,3 +1,7 @@ +import { Integration } from '@sentry/types'; +import { Options } from '../interfaces'; +import { logger } from '../logger'; + export { Dedupe } from './dedupe'; export { FunctionToString } from './functiontostring'; export { SDKInformation } from './sdkinformation'; @@ -5,3 +9,55 @@ export { InboundFilters } from './inboundfilters'; export { Debug } from './pluggable/debug'; export { RewriteFrames } from './pluggable/rewriteframes'; + +const installedIntegrations: string[] = []; + +export interface IntegrationIndex { + [key: string]: Integration; +} + +/** + * Given a list of integration instances this installs them all. When `withDefaults` is set to `true` then all default + * integrations are added unless they were already provided before. + * @param integrations array of integration instances + * @param withDefault should enable default integrations + */ +export function setupIntegrations(options: O): IntegrationIndex { + const integrations: IntegrationIndex = {}; + let integrationsToInstall = options.defaultIntegrations === false ? [] : [...options.defaultIntegrations]; + if (Array.isArray(integrationsToInstall)) { + const providedIntegrationsNames = integrationsToInstall.map(i => i.name); + integrationsToInstall = [ + // Leave only unique integrations, that were not overridden with provided integrations with the same name + ...integrationsToInstall.filter(integration => providedIntegrationsNames.indexOf(integration.name) === -1), + ...options.integrations, + ]; + } else if (typeof options.integrations === 'function') { + integrationsToInstall = options.integrations(integrationsToInstall); + } + + // Just in case someone will return non-array from a `itegrations` callback + if (Array.isArray(integrationsToInstall)) { + integrationsToInstall.forEach(integration => { + if (installedIntegrations.indexOf(integration.name) !== -1) { + return; + } + integrations[name] = integration; + try { + if (integration.setupOnce) { + // TODO remove + integration.setupOnce(options); + } + } catch (_Oo) { + logger.warn(`Integration ${integration.name}: The install method is deprecated. Use "setupOnce".`); + if (integration.install) { + // TODO remove if + integration.install(options); + } + } + installedIntegrations.push(integration.name); + logger.log(`Integration installed: ${integration.name}`); + }); + } + return integrations; +} diff --git a/packages/core/src/integrations/pluggable/debug.ts b/packages/core/src/integrations/pluggable/debug.ts index 642eca1cce52..82f093b7b75f 100644 --- a/packages/core/src/integrations/pluggable/debug.ts +++ b/packages/core/src/integrations/pluggable/debug.ts @@ -1,3 +1,4 @@ +import { getCurrentHub } from '@sentry/hub'; import { configureScope } from '@sentry/minimal'; import { Integration, SentryEvent, SentryEventHint } from '@sentry/types'; @@ -32,28 +33,30 @@ export class Debug implements Integration { * @inheritDoc */ public install(): void { - configureScope(scope => { - scope.addEventProcessor(async (event: SentryEvent, hint?: SentryEventHint) => { - // tslint:disable:no-console - // tslint:disable:no-debugger + if (getCurrentHub().getIntegration(this.name)) { + configureScope(scope => { + scope.addEventProcessor(async (event: SentryEvent, hint?: SentryEventHint) => { + // tslint:disable:no-console + // tslint:disable:no-debugger - if (this.options.debugger) { - debugger; - } - - if (this.options.stringify) { - console.log(JSON.stringify(event, null, 2)); - if (hint) { - console.log(JSON.stringify(hint, null, 2)); + if (this.options.debugger) { + debugger; } - } else { - console.log(event); - if (hint) { - console.log(hint); + + if (this.options.stringify) { + console.log(JSON.stringify(event, null, 2)); + if (hint) { + console.log(JSON.stringify(hint, null, 2)); + } + } else { + console.log(event); + if (hint) { + console.log(hint); + } } - } - return event; + return event; + }); }); - }); + } } } diff --git a/packages/core/src/integrations/pluggable/rewriteframes.ts b/packages/core/src/integrations/pluggable/rewriteframes.ts index 0a1f9cfa3aa8..5bf2cf9df9b9 100644 --- a/packages/core/src/integrations/pluggable/rewriteframes.ts +++ b/packages/core/src/integrations/pluggable/rewriteframes.ts @@ -43,9 +43,11 @@ export class RewriteFrames implements Integration { * @inheritDoc */ public install(): void { - getCurrentHub().configureScope((scope: Scope) => { - scope.addEventProcessor(async event => this.process(event)); - }); + if (getCurrentHub().getIntegration(this.name)) { + getCurrentHub().configureScope((scope: Scope) => { + scope.addEventProcessor(async event => this.process(event)); + }); + } } /** JSDoc */ diff --git a/packages/core/src/interfaces.ts b/packages/core/src/interfaces.ts index e0d639ddb9a4..0bb1fe365ba4 100644 --- a/packages/core/src/interfaces.ts +++ b/packages/core/src/interfaces.ts @@ -47,9 +47,10 @@ export interface Options { dsn?: string; /** - * If this is set to false, default integrations will not be added. + * If this is set to false, default integrations will not be added, otherwise this will internally be set to the + * recommended default integrations. */ - defaultIntegrations?: boolean; + defaultIntegrations?: boolean | Integration[]; /** * List of integrations that should be installed after SDK was initialized. @@ -212,6 +213,9 @@ export interface Client { * @param timeout Maximum time in ms the client should wait. */ close(timeout?: number): Promise; + + /** Returns an array of installed integrations on the client. */ + getIntegration(name: string): Integration | null; } /** diff --git a/packages/core/src/sdk.ts b/packages/core/src/sdk.ts index e686854ade1e..b086d7f72d04 100644 --- a/packages/core/src/sdk.ts +++ b/packages/core/src/sdk.ts @@ -1,5 +1,4 @@ import { getCurrentHub } from '@sentry/hub'; -import { Integration } from '@sentry/types'; import { Client, Options } from './interfaces'; import { logger } from './logger'; @@ -16,11 +15,7 @@ export interface ClientClass { * @param options Options to pass to the client. * @returns The installed and bound client instance. */ -export function initAndBind( - clientClass: ClientClass, - options: O, - defaultIntegrations: Integration[] = [], -): void { +export function initAndBind(clientClass: ClientClass, options: O): void { if (options.debug) { logger.enable(); } @@ -32,27 +27,5 @@ export function initAndBind( const client = new clientClass(options); client.install(); - // This should happen here if any integration uses {@link Hub.addEventProcessor} - // there needs to be a client on the hub already. getCurrentHub().bindClient(client); - - let integrations = options.defaultIntegrations === false ? [] : [...defaultIntegrations]; - if (Array.isArray(options.integrations)) { - const providedIntegrationsNames = options.integrations.map(i => i.name); - integrations = [ - // Leave only unique integrations, that were not overridden with provided integrations with the same name - ...integrations.filter(integration => providedIntegrationsNames.indexOf(integration.name) === -1), - ...options.integrations, - ]; - } else if (typeof options.integrations === 'function') { - integrations = options.integrations(integrations); - } - - // Just in case someone will return non-array from a `itegrations` callback - if (Array.isArray(integrations)) { - integrations.forEach(integration => { - integration.install(options); - logger.log(`Integration installed: ${integration.name}`); - }); - } } diff --git a/packages/hub/src/hub.ts b/packages/hub/src/hub.ts index 4d88ed352554..f550164baa3c 100644 --- a/packages/hub/src/hub.ts +++ b/packages/hub/src/hub.ts @@ -1,4 +1,4 @@ -import { Breadcrumb, SentryBreadcrumbHint, SentryEvent, SentryEventHint, Severity } from '@sentry/types'; +import { Breadcrumb, Integration, SentryBreadcrumbHint, SentryEvent, SentryEventHint, Severity } from '@sentry/types'; import { getGlobalObject, uuid4 } from '@sentry/utils/misc'; import { Carrier, Layer } from './interfaces'; import { Scope } from './scope'; @@ -264,6 +264,11 @@ export class Hub { makeMain(oldHub); } } + + /** Returns the integration if installed on the current client. */ + public getIntegration(name: string): Integration | null { + return this.getClient().getIntegration(name); + } } /** Returns the global shim registry. */ diff --git a/packages/hub/src/index.ts b/packages/hub/src/index.ts index 08bf34f1d7a2..85edb7077622 100644 --- a/packages/hub/src/index.ts +++ b/packages/hub/src/index.ts @@ -1,3 +1,3 @@ export { Carrier, Layer } from './interfaces'; -export { Scope } from './scope'; +export { addGlobalEventProcessor, Scope } from './scope'; export { getCurrentHub, getHubFromCarrier, Hub } from './hub'; diff --git a/packages/hub/src/scope.ts b/packages/hub/src/scope.ts index b7f31bf41d47..82d8f86989a3 100644 --- a/packages/hub/src/scope.ts +++ b/packages/hub/src/scope.ts @@ -1,6 +1,9 @@ import { Breadcrumb, SentryEvent, SentryEventHint, Severity, User } from '@sentry/types'; +import { getGlobalObject } from '@sentry/utils/misc'; import { assign } from '@sentry/utils/object'; +export type EventProcessor = (event: SentryEvent, hint?: SentryEventHint) => Promise; + /** * Holds additional event information. {@link Scope.applyToEvent} will be * called by the client before an event will be sent. @@ -13,7 +16,7 @@ export class Scope { protected scopeListeners: Array<(scope: Scope) => void> = []; /** Callback list that will be called after {@link applyToEvent}. */ - protected eventProcessors: Array<(scope: SentryEvent, hint?: SentryEventHint) => Promise> = []; + protected eventProcessors: EventProcessor[] = []; /** Array of breadcrumbs. */ protected breadcrumbs: Breadcrumb[] = []; @@ -39,9 +42,7 @@ export class Scope { } /** Add new event processor that will be called after {@link applyToEvent}. */ - public addEventProcessor( - callback: (scope: SentryEvent, hint?: SentryEventHint) => Promise, - ): Scope { + public addEventProcessor(callback: EventProcessor): Scope { this.eventProcessors.push(callback); return this; } @@ -66,7 +67,7 @@ export class Scope { */ protected async notifyEventProcessors(event: SentryEvent, hint?: SentryEventHint): Promise { let processedEvent: SentryEvent | null = event; - for (const processor of this.eventProcessors) { + for (const processor of [...getGlobalEventProcessors(), ...this.eventProcessors]) { try { processedEvent = await processor({ ...processedEvent }, hint); if (processedEvent === null) { @@ -229,7 +230,6 @@ export class Scope { if (this.level) { event.level = this.level; } - const hasNoBreadcrumbs = !event.breadcrumbs || event.breadcrumbs.length === 0; if (hasNoBreadcrumbs && this.breadcrumbs.length > 0) { event.breadcrumbs = @@ -241,3 +241,13 @@ export class Scope { return this.notifyEventProcessors(event, hint); } } + +function getGlobalEventProcessors(): EventProcessor[] { + const global: any = getGlobalObject(); + global.__SENTRY__.globalEventProcessors = global.__SENTRY__.globalEventProcessors || []; + return global.__SENTRY__.globalEventProcessors; +} + +export function addGlobalEventProcessor(callback: EventProcessor): void { + getGlobalEventProcessors().push(callback); +} diff --git a/packages/node/src/sdk.ts b/packages/node/src/sdk.ts index be12d9741fba..e276d86082a6 100644 --- a/packages/node/src/sdk.ts +++ b/packages/node/src/sdk.ts @@ -62,6 +62,9 @@ export const defaultIntegrations = [ * * @see NodeOptions for documentation on configuration options. */ -export function init(options: NodeOptions): void { - initAndBind(NodeClient, options, defaultIntegrations); +export function init(options: NodeOptions = {}): void { + if (options.defaultIntegrations === undefined) { + options.defaultIntegrations = defaultIntegrations; + } + initAndBind(NodeClient, options); } diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index de00322b32fb..00525bb6f0d2 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -196,7 +196,9 @@ export interface Mechanism { /** JSDoc */ export interface Integration { name: string; - install(options?: object): void; + /** @deprecated */ + install?(options?: object): void; + setupOnce?(options?: object): void; // TODO: make not optional } /** JSDoc */ From 1e9bd9fc4b4f2f76b65991edfd10618f750fb431 Mon Sep 17 00:00:00 2001 From: HazA Date: Wed, 10 Oct 2018 17:35:55 +0200 Subject: [PATCH 02/11] fix: Call setupOnce in integrations --- .../browser/src/integrations/breadcrumbs.ts | 23 ++++++++++++------- .../src/integrations/reportingobserver.ts | 5 +++- .../browser/src/integrations/useragent.ts | 11 +++++---- packages/core/src/integrations/index.ts | 8 +++---- .../core/src/integrations/pluggable/debug.ts | 13 +++++------ .../integrations/pluggable/rewriteframes.ts | 11 +++++---- 6 files changed, 42 insertions(+), 29 deletions(-) diff --git a/packages/browser/src/integrations/breadcrumbs.ts b/packages/browser/src/integrations/breadcrumbs.ts index 1cb9473e4e9d..a84af0cc7271 100644 --- a/packages/browser/src/integrations/breadcrumbs.ts +++ b/packages/browser/src/integrations/breadcrumbs.ts @@ -1,5 +1,5 @@ import { API, getCurrentHub, logger } from '@sentry/core'; -import { Integration, Severity } from '@sentry/types'; +import { Breadcrumb, Integration, SentryBreadcrumbHint, Severity } from '@sentry/types'; import { isFunction, isString } from '@sentry/utils/is'; import { getEventDescription, getGlobalObject, parseUrl } from '@sentry/utils/misc'; import { deserialize, fill } from '@sentry/utils/object'; @@ -27,7 +27,7 @@ function addSentryBreadcrumb(serializedData: string): void { try { const event: { [key: string]: any } = deserialize(serializedData); - getCurrentHub().addBreadcrumb( + Breadcrumbs.addBreadcrumb( { category: 'sentry', event_id: event.event_id, @@ -113,7 +113,7 @@ export class Breadcrumbs implements Integration { breadcrumbData.level = Severity.Error; } - getCurrentHub().addBreadcrumb(breadcrumbData, { + Breadcrumbs.addBreadcrumb(breadcrumbData, { input: args, result, }); @@ -156,7 +156,7 @@ export class Breadcrumbs implements Integration { } } - getCurrentHub().addBreadcrumb(breadcrumbData, { + Breadcrumbs.addBreadcrumb(breadcrumbData, { input: args, level, }); @@ -230,7 +230,7 @@ export class Breadcrumbs implements Integration { .apply(global, args) .then((response: Response) => { fetchData.status_code = response.status; - getCurrentHub().addBreadcrumb( + Breadcrumbs.addBreadcrumb( { category: 'fetch', data: fetchData, @@ -244,7 +244,7 @@ export class Breadcrumbs implements Integration { return response; }) .catch((error: Error) => { - getCurrentHub().addBreadcrumb( + Breadcrumbs.addBreadcrumb( { category: 'fetch', data: fetchData, @@ -295,7 +295,7 @@ export class Breadcrumbs implements Integration { from = parsedFrom.relative; } - getCurrentHub().addBreadcrumb({ + Breadcrumbs.addBreadcrumb({ category: 'navigation', data: { from, @@ -404,7 +404,7 @@ export class Breadcrumbs implements Integration { } catch (e) { /* do nothing */ } - getCurrentHub().addBreadcrumb( + Breadcrumbs.addBreadcrumb( { category: 'xhr', data: xhr.__sentry_xhr__, @@ -447,6 +447,13 @@ export class Breadcrumbs implements Integration { }, ); } + + public static addBreadcrumb(breadcrumb: Breadcrumb, hint?: SentryBreadcrumbHint): void { + if (getCurrentHub().getIntegration('Breadcrumbs')) { + getCurrentHub().addBreadcrumb(breadcrumb, hint); + } + } + /** * Instrument browser built-ins w/ breadcrumb capturing * - Console API diff --git a/packages/browser/src/integrations/reportingobserver.ts b/packages/browser/src/integrations/reportingobserver.ts index 23c5743a9802..398a0c363b91 100644 --- a/packages/browser/src/integrations/reportingobserver.ts +++ b/packages/browser/src/integrations/reportingobserver.ts @@ -1,4 +1,4 @@ -import { captureMessage, withScope } from '@sentry/core'; +import { captureMessage, getCurrentHub, withScope } from '@sentry/core'; import { Integration } from '@sentry/types'; import { getGlobalObject } from '@sentry/utils/misc'; import { supportsReportingObserver } from '@sentry/utils/supports'; @@ -92,6 +92,9 @@ export class ReportingObserver implements Integration { * @inheritDoc */ public handler(reports: Report[]): void { + if (!getCurrentHub().getIntegration(this.name)) { + return; + } for (const report of reports) { withScope(scope => { scope.setExtra('url', report.url); diff --git a/packages/browser/src/integrations/useragent.ts b/packages/browser/src/integrations/useragent.ts index d0ab23472e64..8da8e9f3eda1 100644 --- a/packages/browser/src/integrations/useragent.ts +++ b/packages/browser/src/integrations/useragent.ts @@ -15,9 +15,9 @@ export class UserAgent implements Integration { * @inheritDoc */ public setupOnce(): void { - if (getCurrentHub().getIntegration(this.name)) { - configureScope(scope => { - scope.addEventProcessor(async (event: SentryEvent) => { + configureScope(scope => { + scope.addEventProcessor(async (event: SentryEvent) => { + if (getCurrentHub().getIntegration(this.name)) { if (!global.navigator || !global.location) { return event; } @@ -32,8 +32,9 @@ export class UserAgent implements Integration { ...event, request, }; - }); + } + return event; }); - } + }); } } diff --git a/packages/core/src/integrations/index.ts b/packages/core/src/integrations/index.ts index 46189cd368e5..2f43a5545b73 100644 --- a/packages/core/src/integrations/index.ts +++ b/packages/core/src/integrations/index.ts @@ -24,9 +24,9 @@ export interface IntegrationIndex { */ export function setupIntegrations(options: O): IntegrationIndex { const integrations: IntegrationIndex = {}; - let integrationsToInstall = options.defaultIntegrations === false ? [] : [...options.defaultIntegrations]; - if (Array.isArray(integrationsToInstall)) { - const providedIntegrationsNames = integrationsToInstall.map(i => i.name); + let integrationsToInstall = (options.defaultIntegrations && [...options.defaultIntegrations]) || []; + if (Array.isArray(options.integrations)) { + const providedIntegrationsNames = options.integrations.map(i => i.name); integrationsToInstall = [ // Leave only unique integrations, that were not overridden with provided integrations with the same name ...integrationsToInstall.filter(integration => providedIntegrationsNames.indexOf(integration.name) === -1), @@ -39,10 +39,10 @@ export function setupIntegrations(options: O): IntegrationInd // Just in case someone will return non-array from a `itegrations` callback if (Array.isArray(integrationsToInstall)) { integrationsToInstall.forEach(integration => { + integrations[name] = integration; if (installedIntegrations.indexOf(integration.name) !== -1) { return; } - integrations[name] = integration; try { if (integration.setupOnce) { // TODO remove diff --git a/packages/core/src/integrations/pluggable/debug.ts b/packages/core/src/integrations/pluggable/debug.ts index 82f093b7b75f..f16dbf32f672 100644 --- a/packages/core/src/integrations/pluggable/debug.ts +++ b/packages/core/src/integrations/pluggable/debug.ts @@ -33,12 +33,11 @@ export class Debug implements Integration { * @inheritDoc */ public install(): void { - if (getCurrentHub().getIntegration(this.name)) { - configureScope(scope => { - scope.addEventProcessor(async (event: SentryEvent, hint?: SentryEventHint) => { + configureScope(scope => { + scope.addEventProcessor(async (event: SentryEvent, hint?: SentryEventHint) => { + if (getCurrentHub().getIntegration(this.name)) { // tslint:disable:no-console // tslint:disable:no-debugger - if (this.options.debugger) { debugger; } @@ -54,9 +53,9 @@ export class Debug implements Integration { console.log(hint); } } - return event; - }); + } + return event; }); - } + }); } } diff --git a/packages/core/src/integrations/pluggable/rewriteframes.ts b/packages/core/src/integrations/pluggable/rewriteframes.ts index 5bf2cf9df9b9..2867fcd837e1 100644 --- a/packages/core/src/integrations/pluggable/rewriteframes.ts +++ b/packages/core/src/integrations/pluggable/rewriteframes.ts @@ -43,11 +43,14 @@ export class RewriteFrames implements Integration { * @inheritDoc */ public install(): void { - if (getCurrentHub().getIntegration(this.name)) { - getCurrentHub().configureScope((scope: Scope) => { - scope.addEventProcessor(async event => this.process(event)); + getCurrentHub().configureScope((scope: Scope) => { + scope.addEventProcessor(async event => { + if (getCurrentHub().getIntegration(this.name)) { + return this.process(event); + } + return event; }); - } + }); } /** JSDoc */ From 7c4e3f5ae1e13a34fecba55d8d0889454873d8c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Og=C3=B3rek?= Date: Thu, 11 Oct 2018 15:04:01 +0200 Subject: [PATCH 03/11] feat: Update all integrations, Update tests --- CHANGELOG.md | 3 + .../browser/src/integrations/breadcrumbs.ts | 56 +++-- .../src/integrations/globalhandlers.ts | 10 +- .../browser/src/integrations/linkederrors.ts | 17 +- .../src/integrations/pluggable/ember.ts | 38 +-- .../browser/src/integrations/pluggable/vue.ts | 40 +-- .../src/integrations/reportingobserver.ts | 8 +- packages/browser/src/integrations/trycatch.ts | 5 + .../browser/src/integrations/useragent.ts | 41 ++-- packages/browser/test/index.test.ts | 103 +++++--- packages/browser/test/integration/init.js | 2 +- packages/core/src/baseclient.ts | 19 +- packages/core/src/index.ts | 2 +- packages/core/src/integrations/dedupe.ts | 14 +- .../core/src/integrations/inboundfilters.ts | 48 ++-- packages/core/src/integrations/index.ts | 114 ++++++--- .../core/src/integrations/pluggable/debug.ts | 49 ++-- .../integrations/pluggable/rewriteframes.ts | 22 +- .../core/src/integrations/sdkinformation.ts | 2 +- packages/core/src/interfaces.ts | 3 +- packages/core/src/sdk.ts | 3 +- packages/core/test/lib/base.test.ts | 14 +- .../lib/integrations/inboundfilters.test.ts | 53 ++-- .../core/test/lib/integrations/index.test.ts | 124 ++++++++++ .../lib/integrations/rewriteframes.test.ts | 2 +- packages/core/test/lib/sdk.test.ts | 70 +++--- packages/core/test/mocks/integration.ts | 24 ++ packages/hub/src/hub.ts | 14 +- packages/hub/src/scope.ts | 8 + packages/hub/test/{lib => }/global.test.ts | 2 +- packages/hub/test/{lib => }/hub.test.ts | 2 +- packages/hub/test/{lib => }/scope.test.ts | 2 +- packages/node/package.json | 1 - packages/node/src/backend.ts | 3 +- packages/node/src/handlers.ts | 71 +----- packages/node/src/integrations/console.ts | 65 ++--- packages/node/src/integrations/http.ts | 51 ++-- .../node/src/integrations/linkederrors.ts | 16 +- .../src/integrations/onuncaughtexception.ts | 85 ++++++- .../src/integrations/onunhandledrejection.ts | 12 +- .../src/integrations/pluggable/modules.ts | 18 +- .../src/integrations/pluggable/transaction.ts | 17 +- packages/node/test/domain.test.ts | 12 +- packages/types/src/index.ts | 20 +- yarn.lock | 231 +----------------- 45 files changed, 858 insertions(+), 658 deletions(-) create mode 100644 packages/core/test/lib/integrations/index.test.ts create mode 100644 packages/core/test/mocks/integration.ts rename packages/hub/test/{lib => }/global.test.ts (90%) rename packages/hub/test/{lib => }/hub.test.ts (99%) rename packages/hub/test/{lib => }/scope.test.ts (99%) diff --git a/CHANGELOG.md b/CHANGELOG.md index be77e2563ed6..f8ae0030756d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ - [browser] fix: Make `addBreadcrumb` sync internally, `beforeBreadcrumb` is now only sync - [browser] fix: Remove internal `console` guard in `beforeBreadcrumb` +- [core]: Integrations now live on the `Client`. This means that when binding a new Client to the `Hub` the client + itself can decide which integration should run. It shouldn't break anything, Integrations just internally work + differently. ## 4.1.1 diff --git a/packages/browser/src/integrations/breadcrumbs.ts b/packages/browser/src/integrations/breadcrumbs.ts index a84af0cc7271..d8c626b38f32 100644 --- a/packages/browser/src/integrations/breadcrumbs.ts +++ b/packages/browser/src/integrations/breadcrumbs.ts @@ -21,28 +21,6 @@ export interface SentryWrappedXMLHttpRequest extends XMLHttpRequest { }; } -/** JSDoc */ -function addSentryBreadcrumb(serializedData: string): void { - // There's always something that can go wrong with deserialization... - try { - const event: { [key: string]: any } = deserialize(serializedData); - - Breadcrumbs.addBreadcrumb( - { - category: 'sentry', - event_id: event.event_id, - level: event.level || Severity.fromString('error'), - message: getEventDescription(event), - }, - { - event, - }, - ); - } catch (_oO) { - logger.error('Error while adding sentry type breadcrumb'); - } -} - /** JSDoc */ interface BreadcrumbIntegrations { beacon?: boolean; @@ -61,6 +39,11 @@ export class Breadcrumbs implements Integration { */ public name: string = 'Breadcrumbs'; + /** + * @inheritDoc + */ + public static id: string = 'Breadcrumbs'; + /** JSDoc */ private readonly options: BreadcrumbIntegrations; @@ -448,8 +431,13 @@ export class Breadcrumbs implements Integration { ); } + /** + * Helper that checks if integration is enabled on the client. + * @param breadcrumb Breadcrumb + * @param hint SentryBreadcrumbHint + */ public static addBreadcrumb(breadcrumb: Breadcrumb, hint?: SentryBreadcrumbHint): void { - if (getCurrentHub().getIntegration('Breadcrumbs')) { + if (getCurrentHub().getIntegration(Breadcrumbs)) { getCurrentHub().addBreadcrumb(breadcrumb, hint); } } @@ -485,3 +473,25 @@ export class Breadcrumbs implements Integration { } } } + +/** JSDoc */ +function addSentryBreadcrumb(serializedData: string): void { + // There's always something that can go wrong with deserialization... + try { + const event: { [key: string]: any } = deserialize(serializedData); + + Breadcrumbs.addBreadcrumb( + { + category: 'sentry', + event_id: event.event_id, + level: event.level || Severity.fromString('error'), + message: getEventDescription(event), + }, + { + event, + }, + ); + } catch (_oO) { + logger.error('Error while adding sentry type breadcrumb'); + } +} diff --git a/packages/browser/src/integrations/globalhandlers.ts b/packages/browser/src/integrations/globalhandlers.ts index ebefa8cc9014..ca8a1c1ec745 100644 --- a/packages/browser/src/integrations/globalhandlers.ts +++ b/packages/browser/src/integrations/globalhandlers.ts @@ -22,6 +22,11 @@ export class GlobalHandlers implements Integration { */ public name: string = 'GlobalHandlers'; + /** + * @inheritDoc + */ + public static id: string = 'GlobalHandlers'; + /** JSDoc */ private readonly options: GlobalHandlersIntegrations; @@ -55,8 +60,9 @@ export class GlobalHandlers implements Integration { if (shouldIgnoreOnError()) { return; } - if (getCurrentHub().getIntegration(this.name)) { - getCurrentHub().captureEvent(this.eventFromGlobalHandler(stack), { originalException: error, data: { stack } }); + const self = getCurrentHub().getIntegration(GlobalHandlers); + if (self) { + getCurrentHub().captureEvent(self.eventFromGlobalHandler(stack), { originalException: error, data: { stack } }); } }); diff --git a/packages/browser/src/integrations/linkederrors.ts b/packages/browser/src/integrations/linkederrors.ts index db598fc9c6a8..a950d4543177 100644 --- a/packages/browser/src/integrations/linkederrors.ts +++ b/packages/browser/src/integrations/linkederrors.ts @@ -1,4 +1,4 @@ -import { configureScope, getCurrentHub } from '@sentry/core'; +import { addGlobalEventProcessor, getCurrentHub } from '@sentry/core'; import { Integration, SentryEvent, SentryEventHint, SentryException } from '@sentry/types'; import { exceptionFromStacktrace } from '../parsers'; import { computeStackTrace } from '../tracekit'; @@ -20,6 +20,11 @@ export class LinkedErrors implements Integration { */ public readonly name: string = 'LinkedErrors'; + /** + * @inheritDoc + */ + public static id: string = 'LinkedErrors'; + /** * @inheritDoc */ @@ -42,9 +47,13 @@ export class LinkedErrors implements Integration { * @inheritDoc */ public setupOnce(): void { - if (getCurrentHub().getIntegration(this.name)) { - configureScope(scope => scope.addEventProcessor(this.handler.bind(this))); - } + addGlobalEventProcessor(async (event: SentryEvent, hint?: SentryEventHint) => { + const self = getCurrentHub().getIntegration(LinkedErrors); + if (self) { + return self.handler(event, hint); + } + return event; + }); } /** diff --git a/packages/browser/src/integrations/pluggable/ember.ts b/packages/browser/src/integrations/pluggable/ember.ts index 2469611a3f17..a463a13675d3 100644 --- a/packages/browser/src/integrations/pluggable/ember.ts +++ b/packages/browser/src/integrations/pluggable/ember.ts @@ -8,6 +8,10 @@ export class Ember implements Integration { * @inheritDoc */ public name: string = 'Ember'; + /** + * @inheritDoc + */ + public static id: string = 'Ember'; /** * @inheritDoc @@ -28,7 +32,7 @@ export class Ember implements Integration { /** * @inheritDoc */ - public install(): void { + public setupOnce(): void { if (!this.Ember) { return; } @@ -36,10 +40,12 @@ export class Ember implements Integration { const oldOnError = this.Ember.onerror; this.Ember.onerror = (error: Error): void => { - withScope(scope => { - this.addIntegrationToSdkInfo(scope); - getCurrentHub().captureException(error, { originalException: error }); - }); + if (getCurrentHub().getIntegration(Ember)) { + withScope(scope => { + this.addIntegrationToSdkInfo(scope); + getCurrentHub().captureException(error, { originalException: error }); + }); + } if (typeof oldOnError === 'function') { oldOnError.call(this.Ember, error); @@ -49,17 +55,19 @@ export class Ember implements Integration { this.Ember.RSVP.on( 'error', (reason: any): void => { - const scope = getCurrentHub().pushScope(); - if (reason instanceof Error) { - scope.setExtra('context', 'Unhandled Promise error detected'); - this.addIntegrationToSdkInfo(scope); - getCurrentHub().captureException(reason, { originalException: reason }); - } else { - scope.setExtra('reason', reason); - this.addIntegrationToSdkInfo(scope); - captureMessage('Unhandled Promise error detected'); + if (getCurrentHub().getIntegration(Ember)) { + const scope = getCurrentHub().pushScope(); + if (reason instanceof Error) { + scope.setExtra('context', 'Unhandled Promise error detected'); + this.addIntegrationToSdkInfo(scope); + getCurrentHub().captureException(reason, { originalException: reason }); + } else { + scope.setExtra('reason', reason); + this.addIntegrationToSdkInfo(scope); + captureMessage('Unhandled Promise error detected'); + } + getCurrentHub().popScope(); } - getCurrentHub().popScope(); }, ); } diff --git a/packages/browser/src/integrations/pluggable/vue.ts b/packages/browser/src/integrations/pluggable/vue.ts index f1f4dffe5b42..387594775181 100644 --- a/packages/browser/src/integrations/pluggable/vue.ts +++ b/packages/browser/src/integrations/pluggable/vue.ts @@ -19,6 +19,10 @@ export class Vue implements Integration { * @inheritDoc */ public name: string = 'Vue'; + /** + * @inheritDoc + */ + public static id: string = 'Vue'; /** * @inheritDoc @@ -51,7 +55,7 @@ export class Vue implements Integration { /** * @inheritDoc */ - public install(): void { + public setupOnce(): void { if (!this.Vue || !this.Vue.config) { return; } @@ -70,24 +74,26 @@ export class Vue implements Integration { metadata.lifecycleHook = info; } - withScope(scope => { - Object.keys(metadata).forEach(key => { - scope.setExtra(key, metadata[key]); - }); + if (getCurrentHub().getIntegration(Vue)) { + withScope(scope => { + Object.keys(metadata).forEach(key => { + scope.setExtra(key, metadata[key]); + }); - scope.addEventProcessor(async (event: SentryEvent) => { - if (event.sdk) { - const integrations = event.sdk.integrations || []; - event.sdk = { - ...event.sdk, - integrations: [...integrations, 'vue'], - }; - } - return event; - }); + scope.addEventProcessor(async (event: SentryEvent) => { + if (event.sdk) { + const integrations = event.sdk.integrations || []; + event.sdk = { + ...event.sdk, + integrations: [...integrations, 'vue'], + }; + } + return event; + }); - getCurrentHub().captureException(error, { originalException: error }); - }); + getCurrentHub().captureException(error, { originalException: error }); + }); + } if (typeof oldOnError === 'function') { oldOnError.call(this.Vue, error, vm, info); diff --git a/packages/browser/src/integrations/reportingobserver.ts b/packages/browser/src/integrations/reportingobserver.ts index 398a0c363b91..bcf1bfdd7ba6 100644 --- a/packages/browser/src/integrations/reportingobserver.ts +++ b/packages/browser/src/integrations/reportingobserver.ts @@ -58,6 +58,10 @@ export class ReportingObserver implements Integration { * @inheritDoc */ public readonly name: string = 'ReportingObserver'; + /** + * @inheritDoc + */ + public static id: string = 'ReportingObserver'; /** * @inheritDoc @@ -73,7 +77,7 @@ export class ReportingObserver implements Integration { /** * @inheritDoc */ - public install(): void { + public setupOnce(): void { if (!supportsReportingObserver()) { return; } @@ -92,7 +96,7 @@ export class ReportingObserver implements Integration { * @inheritDoc */ public handler(reports: Report[]): void { - if (!getCurrentHub().getIntegration(this.name)) { + if (!getCurrentHub().getIntegration(ReportingObserver)) { return; } for (const report of reports) { diff --git a/packages/browser/src/integrations/trycatch.ts b/packages/browser/src/integrations/trycatch.ts index d27b8821c709..f9cf7a12299d 100644 --- a/packages/browser/src/integrations/trycatch.ts +++ b/packages/browser/src/integrations/trycatch.ts @@ -13,6 +13,11 @@ export class TryCatch implements Integration { */ public name: string = 'TryCatch'; + /** + * @inheritDoc + */ + public static id: string = 'TryCatch'; + /** JSDoc */ private wrapTimeFunction(original: () => void): () => number { return function(this: any, ...args: any[]): number { diff --git a/packages/browser/src/integrations/useragent.ts b/packages/browser/src/integrations/useragent.ts index 8da8e9f3eda1..3864a4e95dae 100644 --- a/packages/browser/src/integrations/useragent.ts +++ b/packages/browser/src/integrations/useragent.ts @@ -1,4 +1,4 @@ -import { configureScope, getCurrentHub } from '@sentry/core'; +import { addGlobalEventProcessor, getCurrentHub } from '@sentry/core'; import { Integration, SentryEvent } from '@sentry/types'; import { getGlobalObject } from '@sentry/utils/misc'; @@ -11,30 +11,33 @@ export class UserAgent implements Integration { */ public name: string = 'UserAgent'; + /** + * @inheritDoc + */ + public static id: string = 'UserAgent'; + /** * @inheritDoc */ public setupOnce(): void { - configureScope(scope => { - scope.addEventProcessor(async (event: SentryEvent) => { - if (getCurrentHub().getIntegration(this.name)) { - if (!global.navigator || !global.location) { - return event; - } + addGlobalEventProcessor(async (event: SentryEvent) => { + if (getCurrentHub().getIntegration(UserAgent)) { + if (!global.navigator || !global.location) { + return event; + } - // HTTP Interface: https://docs.sentry.io/clientdev/interfaces/http/?platform=javascript - const request = event.request || {}; - request.url = request.url || global.location.href; - request.headers = request.headers || {}; - request.headers['User-Agent'] = global.navigator.userAgent; + // HTTP Interface: https://docs.sentry.io/clientdev/interfaces/http/?platform=javascript + const request = event.request || {}; + request.url = request.url || global.location.href; + request.headers = request.headers || {}; + request.headers['User-Agent'] = global.navigator.userAgent; - return { - ...event, - request, - }; - } - return event; - }); + return { + ...event, + request, + }; + } + return event; }); } } diff --git a/packages/browser/test/index.test.ts b/packages/browser/test/index.test.ts index 7c9ae1f80e30..729afaadec59 100644 --- a/packages/browser/test/index.test.ts +++ b/packages/browser/test/index.test.ts @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import { stub } from 'sinon'; +import { SinonSpy, spy, stub } from 'sinon'; import { addBreadcrumb, BrowserBackend, @@ -10,6 +10,7 @@ import { configureScope, getCurrentHub, init, + Integrations, Scope, SentryEvent, Status, @@ -20,8 +21,13 @@ const dsn = 'https://53039209a22b4ec1bcc296a3c9fdecd6@sentry.io/4291'; declare var global: any; describe('SentryBrowser', () => { + const beforeSend: SinonSpy = spy(); + before(() => { - init({ dsn }); + init({ + beforeSend, + dsn, + }); }); beforeEach(() => { @@ -30,6 +36,7 @@ describe('SentryBrowser', () => { afterEach(() => { getCurrentHub().popScope(); + beforeSend.resetHistory(); }); describe('getContext() / setContext()', () => { @@ -72,24 +79,13 @@ describe('SentryBrowser', () => { s.restore(); }); - it('should record auto breadcrumbs', done => { - getCurrentHub().pushScope(); - getCurrentHub().bindClient( - new BrowserClient({ - beforeSend: (event: SentryEvent) => { - expect(event.breadcrumbs!).to.have.lengthOf(2); - done(); - return event; - }, - dsn, - }), - ); - + it('should record breadcrumbs', async () => { addBreadcrumb({ message: 'test1' }); addBreadcrumb({ message: 'test2' }); captureMessage('event'); - getCurrentHub().popScope(); + await (getCurrentHub().getClient() as BrowserClient).close(2000); + expect(beforeSend.args[0][0].breadcrumbs).to.have.lengthOf(2); }); }); @@ -104,32 +100,24 @@ describe('SentryBrowser', () => { s.restore(); }); - it('should capture an exception', done => { - getCurrentHub().pushScope(); - getCurrentHub().bindClient( - new BrowserClient({ - beforeSend: (event: SentryEvent) => { - expect(event.exception).to.not.be.undefined; - expect(event.exception!.values![0]).to.not.be.undefined; - expect(event.exception!.values![0].type).to.equal('Error'); - expect(event.exception!.values![0].value).to.equal('test'); - expect(event.exception!.values![0].stacktrace).to.not.be.empty; - done(); - return event; - }, - dsn, - }), - ); + it('should capture an exception', async () => { try { throw new Error('test'); } catch (e) { captureException(e); } - getCurrentHub().popScope(); + + await (getCurrentHub().getClient() as BrowserClient).close(2000); + + const event = beforeSend.args[0][0]; + expect(event.exception).to.not.be.undefined; + expect(event.exception.values[0]).to.not.be.undefined; + expect(event.exception.values[0].type).to.equal('Error'); + expect(event.exception.values[0].value).to.equal('test'); + expect(event.exception.values[0].stacktrace).to.not.be.empty; }); it('should capture a message', done => { - getCurrentHub().pushScope(); getCurrentHub().bindClient( new BrowserClient({ beforeSend: (event: SentryEvent) => { @@ -142,11 +130,9 @@ describe('SentryBrowser', () => { }), ); captureMessage('test'); - getCurrentHub().popScope(); }); it('should capture an event', done => { - getCurrentHub().pushScope(); getCurrentHub().bindClient( new BrowserClient({ beforeSend: (event: SentryEvent) => { @@ -159,7 +145,50 @@ describe('SentryBrowser', () => { }), ); captureEvent({ message: 'event' }); - getCurrentHub().popScope(); + }); + + it('should dedupe an event', async () => { + captureMessage('event222'); + captureMessage('event222'); + + await (getCurrentHub().getClient() as BrowserClient).close(2000); + + expect(beforeSend.calledOnce).to.be.true; + }); + + it('should not dedupe an event on bound client', async () => { + const localBeforeSend = spy(); + getCurrentHub().bindClient( + new BrowserClient({ + beforeSend: localBeforeSend, + dsn, + integrations: [], + }), + ); + + captureMessage('event222'); + captureMessage('event222'); + + await (getCurrentHub().getClient() as BrowserClient).close(2000); + + expect(localBeforeSend.calledTwice).to.be.true; + }); + + it('should use inboundfilter rules of bound client', async () => { + const localBeforeSend = spy(); + getCurrentHub().bindClient( + new BrowserClient({ + beforeSend: localBeforeSend, + dsn, + integrations: [new Integrations.InboundFilters({ ignoreErrors: ['capture'] })], + }), + ); + + captureMessage('capture'); + + await (getCurrentHub().getClient() as BrowserClient).close(2000); + + expect(localBeforeSend.called).to.be.false; }); }); }); diff --git a/packages/browser/test/integration/init.js b/packages/browser/test/integration/init.js index 44e248c9b4e7..bda34ee9eeda 100644 --- a/packages/browser/test/integration/init.js +++ b/packages/browser/test/integration/init.js @@ -33,7 +33,7 @@ DummyTransport.prototype.captureEvent = function(event) { Sentry.init({ dsn: 'https://public@example.com/1', - // debug: true, + debug: true, attachStacktrace: true, transport: DummyTransport, // integrations: function(old) { diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index 1210c38c6a2d..338f2ea5fd61 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -2,6 +2,7 @@ import { Scope } from '@sentry/hub'; import { Breadcrumb, Integration, + IntegrationClass, SentryBreadcrumbHint, SentryEvent, SentryEventHint, @@ -125,7 +126,7 @@ export abstract class BaseClient implement private installed?: boolean; /** Array of used integrations. */ - private integrations?: IntegrationIndex; + private readonly integrations: IntegrationIndex; /** * Initializes this client instance. @@ -140,8 +141,9 @@ export abstract class BaseClient implement if (options.dsn) { this.dsn = new Dsn(options.dsn); } - - this.integrations = setupIntegrations(options); + // We have to setup the integrations in the constructor since we do not want + // that anyone needs to call client.install(); + this.integrations = setupIntegrations(this.options); } /** @@ -421,7 +423,14 @@ export abstract class BaseClient implement /** * @inheritDoc */ - public getIntegration(name: string): Integration | null { - return (this.integrations && this.integrations[name]) || null; + public getIntegrations(): IntegrationIndex { + return this.integrations || {}; + } + + /** + * @inheritDoc + */ + public getIntegration(integration: IntegrationClass): T | null { + return (this.integrations[integration.id] as T) || null; } } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 51ad5255d370..a067ce76d6b1 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -7,7 +7,7 @@ export { configureScope, withScope, } from '@sentry/minimal'; -export { getCurrentHub, Hub, getHubFromCarrier, Scope } from '@sentry/hub'; +export { addGlobalEventProcessor, getCurrentHub, Hub, getHubFromCarrier, Scope } from '@sentry/hub'; export { API } from './api'; export { BaseClient } from './baseclient'; export { BackendClass, BaseBackend } from './basebackend'; diff --git a/packages/core/src/integrations/dedupe.ts b/packages/core/src/integrations/dedupe.ts index 1c7103204b4b..44d76c8b7504 100644 --- a/packages/core/src/integrations/dedupe.ts +++ b/packages/core/src/integrations/dedupe.ts @@ -15,22 +15,28 @@ export class Dedupe implements Integration { */ public name: string = 'Dedupe'; + /** + * @inheritDoc + */ + public static id: string = 'Dedupe'; + /** * @inheritDoc */ public setupOnce(): void { addGlobalEventProcessor(async (currentEvent: SentryEvent) => { - if (getCurrentHub().getIntegration(this.name)) { + const self = getCurrentHub().getIntegration(Dedupe); + if (self) { // Juuust in case something goes wrong try { - if (this.shouldDropEvent(currentEvent, this.previousEvent)) { + if (self.shouldDropEvent(currentEvent, self.previousEvent)) { return null; } } catch (_oO) { - return (this.previousEvent = currentEvent); + return (self.previousEvent = currentEvent); } - return (this.previousEvent = currentEvent); + return (self.previousEvent = currentEvent); } return currentEvent; }); diff --git a/packages/core/src/integrations/inboundfilters.ts b/packages/core/src/integrations/inboundfilters.ts index e172bbc957c8..007c51bd6037 100644 --- a/packages/core/src/integrations/inboundfilters.ts +++ b/packages/core/src/integrations/inboundfilters.ts @@ -1,5 +1,4 @@ -import { getCurrentHub } from '@sentry/hub'; -import { configureScope } from '@sentry/minimal'; +import { addGlobalEventProcessor, getCurrentHub } from '@sentry/hub'; import { Integration, SentryEvent } from '@sentry/types'; import { isRegExp } from '@sentry/utils/is'; import { getEventDescription } from '@sentry/utils/misc'; @@ -33,19 +32,26 @@ export class InboundFilters implements Integration { /** * @inheritDoc */ - public setupOnce(options: InboundFiltersOptions = {}): void { - if (getCurrentHub().getIntegration(this.name)) { - this.configureOptions(options); + public static id: string = 'InboundFilters'; - configureScope(scope => { - scope.addEventProcessor(async (event: SentryEvent) => { - if (this.shouldDropEvent(event)) { - return null; - } - return event; - }); - }); - } + public constructor(private readonly options: InboundFiltersOptions = {}) { + this.configureOptions(); + } + + /** + * @inheritDoc + */ + public setupOnce(): void { + addGlobalEventProcessor(async (event: SentryEvent) => { + const self = getCurrentHub().getIntegration(InboundFilters); + if (self) { + self.configureOptions(); + if (self.shouldDropEvent(event)) { + return null; + } + } + return event; + }); } /** JSDoc */ @@ -119,15 +125,15 @@ export class InboundFilters implements Integration { } /** JSDoc */ - private configureOptions(options: InboundFiltersOptions): void { - if (options.ignoreErrors) { - this.ignoreErrors = [...DEFAULT_IGNORE_ERRORS, ...options.ignoreErrors]; + private configureOptions(): void { + if (this.options.ignoreErrors) { + this.ignoreErrors = [...DEFAULT_IGNORE_ERRORS, ...this.options.ignoreErrors]; } - if (options.blacklistUrls) { - this.blacklistUrls = [...options.blacklistUrls]; + if (this.options.blacklistUrls) { + this.blacklistUrls = [...this.options.blacklistUrls]; } - if (options.whitelistUrls) { - this.whitelistUrls = [...options.whitelistUrls]; + if (this.options.whitelistUrls) { + this.whitelistUrls = [...this.options.whitelistUrls]; } } diff --git a/packages/core/src/integrations/index.ts b/packages/core/src/integrations/index.ts index 2f43a5545b73..731859a6b197 100644 --- a/packages/core/src/integrations/index.ts +++ b/packages/core/src/integrations/index.ts @@ -1,3 +1,4 @@ +// tslint:disable:deprecation import { Integration } from '@sentry/types'; import { Options } from '../interfaces'; import { logger } from '../logger'; @@ -10,12 +11,72 @@ export { InboundFilters } from './inboundfilters'; export { Debug } from './pluggable/debug'; export { RewriteFrames } from './pluggable/rewriteframes'; -const installedIntegrations: string[] = []; +export const installedIntegrations: string[] = []; +/** Map of integrations assigned to a client */ export interface IntegrationIndex { [key: string]: Integration; } +/** Gets integration to install */ +export function getIntegrationsToSetup(options: Options): Integration[] { + const defaultIntegrations = (options.defaultIntegrations && [...options.defaultIntegrations]) || []; + const userIntegrations = options.integrations; + let integrations: Integration[] = []; + if (Array.isArray(userIntegrations)) { + const userIntegrationsNames = userIntegrations.map(i => i.name); + const pickedIntegrationsNames = []; + + // Leave only unique default integrations, that were not overridden with provided user integrations + for (const defaultIntegration of defaultIntegrations) { + if ( + userIntegrationsNames.indexOf(getIntegrationName(defaultIntegration)) === -1 && + pickedIntegrationsNames.indexOf(getIntegrationName(defaultIntegration)) === -1 + ) { + integrations.push(defaultIntegration); + pickedIntegrationsNames.push(getIntegrationName(defaultIntegration)); + } + } + + // Don't add same user integration twice + for (const userIntegration of userIntegrations) { + if (pickedIntegrationsNames.indexOf(getIntegrationName(userIntegration)) === -1) { + integrations.push(userIntegration); + pickedIntegrationsNames.push(getIntegrationName(userIntegration)); + } + } + } else if (typeof userIntegrations === 'function') { + integrations = userIntegrations(defaultIntegrations); + integrations = Array.isArray(integrations) ? integrations : [integrations]; + } else { + return [...defaultIntegrations]; + } + + return integrations; +} + +/** Setup given integration */ +export function setupIntegration(integration: Integration, options: Options): void { + if (installedIntegrations.indexOf(getIntegrationName(integration)) !== -1) { + return; + } + + try { + integration.setupOnce(); + } catch (_Oo) { + /** @deprecated */ + // TODO: Remove in v5 + logger.warn(`Integration ${getIntegrationName(integration)}: The install method is deprecated. Use "setupOnce".`); + + if (integration.install) { + integration.install(options); + } + } + + installedIntegrations.push(getIntegrationName(integration)); + logger.log(`Integration installed: ${getIntegrationName(integration)}`); +} + /** * Given a list of integration instances this installs them all. When `withDefaults` is set to `true` then all default * integrations are added unless they were already provided before. @@ -24,40 +85,21 @@ export interface IntegrationIndex { */ export function setupIntegrations(options: O): IntegrationIndex { const integrations: IntegrationIndex = {}; - let integrationsToInstall = (options.defaultIntegrations && [...options.defaultIntegrations]) || []; - if (Array.isArray(options.integrations)) { - const providedIntegrationsNames = options.integrations.map(i => i.name); - integrationsToInstall = [ - // Leave only unique integrations, that were not overridden with provided integrations with the same name - ...integrationsToInstall.filter(integration => providedIntegrationsNames.indexOf(integration.name) === -1), - ...options.integrations, - ]; - } else if (typeof options.integrations === 'function') { - integrationsToInstall = options.integrations(integrationsToInstall); - } - - // Just in case someone will return non-array from a `itegrations` callback - if (Array.isArray(integrationsToInstall)) { - integrationsToInstall.forEach(integration => { - integrations[name] = integration; - if (installedIntegrations.indexOf(integration.name) !== -1) { - return; - } - try { - if (integration.setupOnce) { - // TODO remove - integration.setupOnce(options); - } - } catch (_Oo) { - logger.warn(`Integration ${integration.name}: The install method is deprecated. Use "setupOnce".`); - if (integration.install) { - // TODO remove if - integration.install(options); - } - } - installedIntegrations.push(integration.name); - logger.log(`Integration installed: ${integration.name}`); - }); - } + getIntegrationsToSetup(options).forEach(integration => { + integrations[getIntegrationName(integration)] = integration; + setupIntegration(integration, options); + }); return integrations; } + +/** + * Returns the integration static id. + * @param integration Integration to retrieve id + */ +function getIntegrationName(integration: Integration): string { + /** + * @depracted + */ + // tslint:disable-next-line:no-unsafe-any + return (integration as any).constructor.id || integration.name; +} diff --git a/packages/core/src/integrations/pluggable/debug.ts b/packages/core/src/integrations/pluggable/debug.ts index f16dbf32f672..d11f5388f5c0 100644 --- a/packages/core/src/integrations/pluggable/debug.ts +++ b/packages/core/src/integrations/pluggable/debug.ts @@ -1,5 +1,4 @@ -import { getCurrentHub } from '@sentry/hub'; -import { configureScope } from '@sentry/minimal'; +import { addGlobalEventProcessor, getCurrentHub } from '@sentry/hub'; import { Integration, SentryEvent, SentryEventHint } from '@sentry/types'; /** JSDoc */ @@ -15,6 +14,11 @@ export class Debug implements Integration { */ public name: string = 'Debug'; + /** + * @inheritDoc + */ + public static id: string = 'Debug'; + /** JSDoc */ private readonly options: DebugOptions; @@ -32,30 +36,29 @@ export class Debug implements Integration { /** * @inheritDoc */ - public install(): void { - configureScope(scope => { - scope.addEventProcessor(async (event: SentryEvent, hint?: SentryEventHint) => { - if (getCurrentHub().getIntegration(this.name)) { - // tslint:disable:no-console - // tslint:disable:no-debugger - if (this.options.debugger) { - debugger; - } + public setupOnce(): void { + addGlobalEventProcessor(async (event: SentryEvent, hint?: SentryEventHint) => { + const self = getCurrentHub().getIntegration(Debug); + if (self) { + // tslint:disable:no-console + // tslint:disable:no-debugger + if (self.options.debugger) { + debugger; + } - if (this.options.stringify) { - console.log(JSON.stringify(event, null, 2)); - if (hint) { - console.log(JSON.stringify(hint, null, 2)); - } - } else { - console.log(event); - if (hint) { - console.log(hint); - } + if (self.options.stringify) { + console.log(JSON.stringify(event, null, 2)); + if (hint) { + console.log(JSON.stringify(hint, null, 2)); + } + } else { + console.log(event); + if (hint) { + console.log(hint); } } - return event; - }); + } + return event; }); } } diff --git a/packages/core/src/integrations/pluggable/rewriteframes.ts b/packages/core/src/integrations/pluggable/rewriteframes.ts index 2867fcd837e1..cf1454166e86 100644 --- a/packages/core/src/integrations/pluggable/rewriteframes.ts +++ b/packages/core/src/integrations/pluggable/rewriteframes.ts @@ -1,4 +1,4 @@ -import { getCurrentHub, Scope } from '@sentry/hub'; +import { addGlobalEventProcessor, getCurrentHub } from '@sentry/hub'; import { Integration, SentryEvent, StackFrame } from '@sentry/types'; import { basename, relative } from '@sentry/utils/path'; @@ -11,6 +11,11 @@ export class RewriteFrames implements Integration { */ public name: string = 'RewriteFrames'; + /** + * @inheritDoc + */ + public static id: string = 'RewriteFrames'; + /** * @inheritDoc */ @@ -42,14 +47,13 @@ export class RewriteFrames implements Integration { /** * @inheritDoc */ - public install(): void { - getCurrentHub().configureScope((scope: Scope) => { - scope.addEventProcessor(async event => { - if (getCurrentHub().getIntegration(this.name)) { - return this.process(event); - } - return event; - }); + public setupOnce(): void { + addGlobalEventProcessor(async event => { + const self = getCurrentHub().getIntegration(RewriteFrames); + if (self) { + return self.process(event); + } + return event; }); } diff --git a/packages/core/src/integrations/sdkinformation.ts b/packages/core/src/integrations/sdkinformation.ts index 5f2ea9683f08..d6bb30785ebb 100644 --- a/packages/core/src/integrations/sdkinformation.ts +++ b/packages/core/src/integrations/sdkinformation.ts @@ -16,7 +16,7 @@ export class SDKInformation implements Integration { /** * @inheritDoc */ - public install(): void { + public setupOnce(): void { logger.warn( "SDKInformation Integration is deprecated and can be safely removed. It's functionality has been merged into the SDK's core.", ); diff --git a/packages/core/src/interfaces.ts b/packages/core/src/interfaces.ts index 0bb1fe365ba4..6bdf6864a73d 100644 --- a/packages/core/src/interfaces.ts +++ b/packages/core/src/interfaces.ts @@ -2,6 +2,7 @@ import { Scope } from '@sentry/hub'; import { Breadcrumb, Integration, + IntegrationClass, SentryBreadcrumbHint, SentryEvent, SentryEventHint, @@ -215,7 +216,7 @@ export interface Client { close(timeout?: number): Promise; /** Returns an array of installed integrations on the client. */ - getIntegration(name: string): Integration | null; + getIntegration(integartion: IntegrationClass): T | null; } /** diff --git a/packages/core/src/sdk.ts b/packages/core/src/sdk.ts index b086d7f72d04..02c9c48fee25 100644 --- a/packages/core/src/sdk.ts +++ b/packages/core/src/sdk.ts @@ -25,7 +25,6 @@ export function initAndBind(clientClass: Cl } const client = new clientClass(options); - client.install(); - getCurrentHub().bindClient(client); + client.install(); } diff --git a/packages/core/test/lib/base.test.ts b/packages/core/test/lib/base.test.ts index 03673c56c2b3..48e853acd28c 100644 --- a/packages/core/test/lib/base.test.ts +++ b/packages/core/test/lib/base.test.ts @@ -1,8 +1,9 @@ import { Scope } from '@sentry/hub'; -import { Breadcrumb, Status } from '@sentry/types'; +import { Breadcrumb, SentryEvent, Status } from '@sentry/types'; import { SentryError } from '../../src/error'; import { TestBackend } from '../mocks/backend'; import { TestClient } from '../mocks/client'; +import { TestIntegration } from '../mocks/integration'; const PUBLIC_DSN = 'https://username@domain/path'; @@ -364,4 +365,15 @@ describe('BaseClient', () => { // TODO: Test rate limiting queues here }); }); + + describe('integrations', () => { + test('setup each one of them on ctor', () => { + const client = new TestClient({ + dsn: PUBLIC_DSN, + integrations: [new TestIntegration()], + }); + expect(Object.keys(client.getIntegrations()).length).toBe(1); + expect(client.getIntegration(TestIntegration)).toBeTruthy(); + }); + }); }); diff --git a/packages/core/test/lib/integrations/inboundfilters.test.ts b/packages/core/test/lib/integrations/inboundfilters.test.ts index 8576cf0a63af..98c146995fac 100644 --- a/packages/core/test/lib/integrations/inboundfilters.test.ts +++ b/packages/core/test/lib/integrations/inboundfilters.test.ts @@ -65,30 +65,34 @@ describe('InboundFilters', () => { }; it('string filter with partial match', () => { - inboundFilters.install({ + inboundFilters = new InboundFilters({ ignoreErrors: ['capture'], }); + inboundFilters.setupOnce(); expect(inboundFilters.isIgnoredError(messageEvent)).toBe(true); }); it('string filter with exact match', () => { - inboundFilters.install({ + inboundFilters = new InboundFilters({ ignoreErrors: ['captureMessage'], }); + inboundFilters.setupOnce(); expect(inboundFilters.isIgnoredError(messageEvent)).toBe(true); }); it('regexp filter with partial match', () => { - inboundFilters.install({ + inboundFilters = new InboundFilters({ ignoreErrors: [/capture/], }); + inboundFilters.setupOnce(); expect(inboundFilters.isIgnoredError(messageEvent)).toBe(true); }); it('regexp filter with exact match', () => { - inboundFilters.install({ + inboundFilters = new InboundFilters({ ignoreErrors: [/^captureMessage$/], }); + inboundFilters.setupOnce(); expect(inboundFilters.isIgnoredError(messageEvent)).toBe(true); expect( inboundFilters.isIgnoredError({ @@ -98,9 +102,10 @@ describe('InboundFilters', () => { }); it('uses message when both, message and exception are available', () => { - inboundFilters.install({ + inboundFilters = new InboundFilters({ ignoreErrors: [/captureMessage/], }); + inboundFilters.setupOnce(); expect( inboundFilters.isIgnoredError({ ...exceptionEvent, @@ -110,15 +115,16 @@ describe('InboundFilters', () => { }); it('can use multiple filters', () => { - inboundFilters.install({ + inboundFilters = new InboundFilters({ ignoreErrors: ['captureMessage', /SyntaxError/], }); + inboundFilters.setupOnce(); expect(inboundFilters.isIgnoredError(messageEvent)).toBe(true); expect(inboundFilters.isIgnoredError(exceptionEvent)).toBe(true); }); it('uses default filters', () => { - inboundFilters.install(); + inboundFilters.setupOnce(); expect( inboundFilters.isIgnoredError({ exception: { @@ -135,23 +141,26 @@ describe('InboundFilters', () => { describe('on exception', () => { it('uses exceptions data when message is unavailable', () => { - inboundFilters.install({ + inboundFilters = new InboundFilters({ ignoreErrors: ['SyntaxError: unidentified ? at line 1337'], }); + inboundFilters.setupOnce(); expect(inboundFilters.isIgnoredError(exceptionEvent)).toBe(true); }); it('can match on exception value', () => { - inboundFilters.install({ + inboundFilters = new InboundFilters({ ignoreErrors: [/unidentified \?/], }); + inboundFilters.setupOnce(); expect(inboundFilters.isIgnoredError(exceptionEvent)).toBe(true); }); it('can match on exception type', () => { - inboundFilters.install({ + inboundFilters = new InboundFilters({ ignoreErrors: [/^SyntaxError/], }); + inboundFilters.setupOnce(); expect(inboundFilters.isIgnoredError(exceptionEvent)).toBe(true); }); }); @@ -179,27 +188,30 @@ describe('InboundFilters', () => { }; it('should filter captured message based on its stack trace using string filter', () => { - inboundFilters.install({ + inboundFilters = new InboundFilters({ blacklistUrls: ['https://awesome-analytics.io'], whitelistUrls: ['https://awesome-analytics.io'], }); + inboundFilters.setupOnce(); expect(inboundFilters.isBlacklistedUrl(messageEvent)).toBe(true); expect(inboundFilters.isWhitelistedUrl(messageEvent)).toBe(true); }); it('should filter captured message based on its stack trace using regexp filter', () => { - inboundFilters.install({ + inboundFilters = new InboundFilters({ blacklistUrls: [/awesome-analytics\.io/], }); + inboundFilters.setupOnce(); expect(inboundFilters.isBlacklistedUrl(messageEvent)).toBe(true); expect(inboundFilters.isWhitelistedUrl(messageEvent)).toBe(true); }); it('should not filter captured messages with no stacktraces', () => { - inboundFilters.install({ + inboundFilters = new InboundFilters({ blacklistUrls: ['https://awesome-analytics.io'], whitelistUrls: ['https://awesome-analytics.io'], }); + inboundFilters.setupOnce(); expect( inboundFilters.isBlacklistedUrl({ message: 'any', @@ -213,37 +225,41 @@ describe('InboundFilters', () => { }); it('should filter captured exception based on its stack trace using string filter', () => { - inboundFilters.install({ + inboundFilters = new InboundFilters({ blacklistUrls: ['https://awesome-analytics.io'], whitelistUrls: ['https://awesome-analytics.io'], }); + inboundFilters.setupOnce(); expect(inboundFilters.isBlacklistedUrl(exceptionEvent)).toBe(true); expect(inboundFilters.isWhitelistedUrl(exceptionEvent)).toBe(true); }); it('should filter captured exceptions based on its stack trace using regexp filter', () => { - inboundFilters.install({ + inboundFilters = new InboundFilters({ blacklistUrls: [/awesome-analytics\.io/], whitelistUrls: [/awesome-analytics\.io/], }); + inboundFilters.setupOnce(); expect(inboundFilters.isBlacklistedUrl(exceptionEvent)).toBe(true); expect(inboundFilters.isWhitelistedUrl(exceptionEvent)).toBe(true); }); it('should not filter events that doesnt pass the test', () => { - inboundFilters.install({ + inboundFilters = new InboundFilters({ blacklistUrls: ['some-other-domain.com'], whitelistUrls: ['some-other-domain.com'], }); + inboundFilters.setupOnce(); expect(inboundFilters.isBlacklistedUrl(exceptionEvent)).toBe(false); expect(inboundFilters.isWhitelistedUrl(exceptionEvent)).toBe(false); }); it('should be able to use multiple filters', () => { - inboundFilters.install({ + inboundFilters = new InboundFilters({ blacklistUrls: ['some-other-domain.com', /awesome-analytics\.io/], whitelistUrls: ['some-other-domain.com', /awesome-analytics\.io/], }); + inboundFilters.setupOnce(); expect(inboundFilters.isBlacklistedUrl(exceptionEvent)).toBe(true); expect(inboundFilters.isWhitelistedUrl(exceptionEvent)).toBe(true); }); @@ -254,10 +270,11 @@ describe('InboundFilters', () => { frames: undefined, }, }; - inboundFilters.install({ + inboundFilters = new InboundFilters({ blacklistUrls: ['https://awesome-analytics.io'], whitelistUrls: ['https://awesome-analytics.io'], }); + inboundFilters.setupOnce(); expect(inboundFilters.isBlacklistedUrl(malformedEvent)).toBe(false); expect(inboundFilters.isWhitelistedUrl(malformedEvent)).toBe(true); }); diff --git a/packages/core/test/lib/integrations/index.test.ts b/packages/core/test/lib/integrations/index.test.ts new file mode 100644 index 000000000000..531e4cfe536f --- /dev/null +++ b/packages/core/test/lib/integrations/index.test.ts @@ -0,0 +1,124 @@ +// tslint:disable:deprecation +import { Integration } from '@sentry/types'; +import { getIntegrationsToSetup } from '../../../src/integrations'; + +/** JSDoc */ +class MockIntegration implements Integration { + public constructor(name: string) { + this.name = name; + } + public name: string; + public setupOnce(): void { + // noop + } +} + +describe('getIntegrationsToSetup', () => { + it('works with empty array', () => { + const integrations = getIntegrationsToSetup({ + integrations: [], + }); + + expect(integrations.map(i => i.name)).toEqual([]); + }); + + it('works with single item', () => { + const integrations = getIntegrationsToSetup({ + integrations: [new MockIntegration('foo')], + }); + + expect(integrations.map(i => i.name)).toEqual(['foo']); + }); + + it('works with multiple items', () => { + const integrations = getIntegrationsToSetup({ + integrations: [new MockIntegration('foo'), new MockIntegration('bar')], + }); + + expect(integrations.map(i => i.name)).toEqual(['foo', 'bar']); + }); + + it('filter duplicated items', () => { + const integrations = getIntegrationsToSetup({ + integrations: [new MockIntegration('foo'), new MockIntegration('foo'), new MockIntegration('bar')], + }); + + expect(integrations.map(i => i.name)).toEqual(['foo', 'bar']); + }); + + it('filter duplicated items and always let first win', () => { + const first = new MockIntegration('foo'); + (first as any).order = 'first'; + const second = new MockIntegration('foo'); + (second as any).order = 'second'; + + const integrations = getIntegrationsToSetup({ + integrations: [first, second, new MockIntegration('bar')], + }); + + expect(integrations.map(i => i.name)).toEqual(['foo', 'bar']); + expect((integrations[0] as any).order).toEqual('first'); + }); + + it('work with empty defaults', () => { + const integrations = getIntegrationsToSetup({ + defaultIntegrations: [], + }); + + expect(integrations.map(i => i.name)).toEqual([]); + }); + + it('work with single defaults', () => { + const integrations = getIntegrationsToSetup({ + defaultIntegrations: [new MockIntegration('foo')], + }); + + expect(integrations.map(i => i.name)).toEqual(['foo']); + }); + + it('work with multiple defaults', () => { + const integrations = getIntegrationsToSetup({ + defaultIntegrations: [new MockIntegration('foo'), new MockIntegration('bar')], + }); + + expect(integrations.map(i => i.name)).toEqual(['foo', 'bar']); + }); + + it('work with user integrations and defaults and pick defaults first', () => { + const integrations = getIntegrationsToSetup({ + defaultIntegrations: [new MockIntegration('foo')], + integrations: [new MockIntegration('bar')], + }); + + expect(integrations.map(i => i.name)).toEqual(['foo', 'bar']); + }); + + it('work with user integrations and defaults and filter duplicates', () => { + const integrations = getIntegrationsToSetup({ + defaultIntegrations: [new MockIntegration('foo'), new MockIntegration('foo')], + integrations: [new MockIntegration('bar'), new MockIntegration('bar')], + }); + + expect(integrations.map(i => i.name)).toEqual(['foo', 'bar']); + }); + + it('user integrations override defaults', () => { + const firstDefault = new MockIntegration('foo'); + (firstDefault as any).order = 'firstDefault'; + const secondDefault = new MockIntegration('bar'); + (secondDefault as any).order = 'secondDefault'; + const firstUser = new MockIntegration('foo'); + (firstUser as any).order = 'firstUser'; + const secondUser = new MockIntegration('bar'); + (secondUser as any).order = 'secondUser'; + + const integrations = getIntegrationsToSetup({ + defaultIntegrations: [firstDefault, secondDefault], + integrations: [firstUser, secondUser], + }); + + expect(integrations.map(i => i.name)).toEqual(['foo', 'bar']); + expect((integrations[0] as any).order).toEqual('firstUser'); + expect((integrations[1] as any).order).toEqual('secondUser'); + }); +}); diff --git a/packages/core/test/lib/integrations/rewriteframes.test.ts b/packages/core/test/lib/integrations/rewriteframes.test.ts index 8b928f52b3af..e524d835448a 100644 --- a/packages/core/test/lib/integrations/rewriteframes.test.ts +++ b/packages/core/test/lib/integrations/rewriteframes.test.ts @@ -5,7 +5,7 @@ let rewriteFrames: RewriteFrames; let messageEvent: SentryEvent; let exceptionEvent: SentryEvent; -describe.only('RewriteFrames', () => { +describe('RewriteFrames', () => { beforeEach(() => { messageEvent = { stacktrace: { diff --git a/packages/core/test/lib/sdk.test.ts b/packages/core/test/lib/sdk.test.ts index 1a2d2592fc7b..795dc19b51ae 100644 --- a/packages/core/test/lib/sdk.test.ts +++ b/packages/core/test/lib/sdk.test.ts @@ -1,23 +1,38 @@ import { Integration } from '@sentry/types'; +import { installedIntegrations } from '../../src/integrations'; import { initAndBind } from '../../src/sdk'; import { TestClient } from '../mocks/client'; declare var global: any; +jest.mock('@sentry/hub', () => ({ + getCurrentHub(): { + bindClient(): boolean; + getClient(): boolean; + } { + return { + getClient(): boolean { + return false; + }, + bindClient(): boolean { + return true; + }, + }; + }, +})); + class MockIntegration implements Integration { public constructor(name: string) { this.name = name; } public name: string; - public handler: () => void = jest.fn(); - public install: () => void = () => { - this.handler(); - }; + public setupOnce: () => void = jest.fn(); } describe('SDK', () => { beforeEach(() => { global.__SENTRY__ = {}; + installedIntegrations.splice(0); }); describe('initAndBind', () => { @@ -26,9 +41,9 @@ describe('SDK', () => { new MockIntegration('MockIntegration 1'), new MockIntegration('MockIntegration 2'), ]; - initAndBind(TestClient, {}, DEFAULT_INTEGRATIONS); - expect(DEFAULT_INTEGRATIONS[0].handler.mock.calls.length).toBe(1); - expect(DEFAULT_INTEGRATIONS[1].handler.mock.calls.length).toBe(1); + initAndBind(TestClient, { defaultIntegrations: DEFAULT_INTEGRATIONS }); + 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('not installs default integrations', () => { @@ -36,9 +51,9 @@ describe('SDK', () => { new MockIntegration('MockIntegration 1'), new MockIntegration('MockIntegration 2'), ]; - initAndBind(TestClient, { defaultIntegrations: false }, DEFAULT_INTEGRATIONS); - expect(DEFAULT_INTEGRATIONS[0].handler.mock.calls.length).toBe(0); - expect(DEFAULT_INTEGRATIONS[1].handler.mock.calls.length).toBe(0); + initAndBind(TestClient, { defaultIntegrations: false }); + 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', () => { @@ -46,9 +61,9 @@ describe('SDK', () => { new MockIntegration('MockIntegration 1'), new MockIntegration('MockIntegration 2'), ]; - initAndBind(TestClient, { integrations }, []); - expect(integrations[0].handler.mock.calls.length).toBe(1); - expect(integrations[1].handler.mock.calls.length).toBe(1); + initAndBind(TestClient, { integrations }); + 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', () => { @@ -60,12 +75,12 @@ describe('SDK', () => { new MockIntegration('MockIntegration 1'), new MockIntegration('MockIntegration 3'), ]; - initAndBind(TestClient, { integrations }, DEFAULT_INTEGRATIONS); + initAndBind(TestClient, { defaultIntegrations: DEFAULT_INTEGRATIONS, integrations }); // 'MockIntegration 1' should be overridden by the one with the same name provided through options - expect(DEFAULT_INTEGRATIONS[0].handler.mock.calls.length).toBe(0); - expect(DEFAULT_INTEGRATIONS[1].handler.mock.calls.length).toBe(1); - expect(integrations[0].handler.mock.calls.length).toBe(1); - expect(integrations[1].handler.mock.calls.length).toBe(1); + 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', () => { @@ -74,17 +89,14 @@ describe('SDK', () => { new MockIntegration('MockIntegration 2'), ]; const newIntegration = new MockIntegration('MockIntegration 3'); - initAndBind( - TestClient, - { - // Take only the first one and add a new one to it - integrations: (integrations: Integration[]) => integrations.slice(0, 1).concat(newIntegration), - }, - DEFAULT_INTEGRATIONS, - ); - expect(DEFAULT_INTEGRATIONS[0].handler.mock.calls.length).toBe(1); - expect(newIntegration.handler.mock.calls.length).toBe(1); - expect(DEFAULT_INTEGRATIONS[1].handler.mock.calls.length).toBe(0); + initAndBind(TestClient, { + // Take only the first one and add a new one to it + defaultIntegrations: DEFAULT_INTEGRATIONS, + integrations: (integrations: Integration[]) => integrations.slice(0, 1).concat(newIntegration), + }); + 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/integration.ts b/packages/core/test/mocks/integration.ts new file mode 100644 index 000000000000..4f89a04fd78f --- /dev/null +++ b/packages/core/test/mocks/integration.ts @@ -0,0 +1,24 @@ +import { getCurrentHub } from '@sentry/hub'; +import { configureScope } from '@sentry/minimal'; +import { Integration, SentryEvent } from '@sentry/types'; + +export class TestIntegration implements Integration { + public name: string = 'TestIntegration'; + public static id: string = 'TestIntegration'; + + public setupOnce(): void { + configureScope(scope => { + scope.addEventProcessor(async (event: SentryEvent) => { + if (!getCurrentHub().getIntegration(TestIntegration)) { + return event; + } + + if (true) { + return null; + } + + // return event; + }); + }); + } +} diff --git a/packages/hub/src/hub.ts b/packages/hub/src/hub.ts index f550164baa3c..5a5f1c5891f5 100644 --- a/packages/hub/src/hub.ts +++ b/packages/hub/src/hub.ts @@ -1,4 +1,12 @@ -import { Breadcrumb, Integration, SentryBreadcrumbHint, SentryEvent, SentryEventHint, Severity } from '@sentry/types'; +import { + Breadcrumb, + Integration, + IntegrationClass, + SentryBreadcrumbHint, + SentryEvent, + SentryEventHint, + Severity, +} from '@sentry/types'; import { getGlobalObject, uuid4 } from '@sentry/utils/misc'; import { Carrier, Layer } from './interfaces'; import { Scope } from './scope'; @@ -266,8 +274,8 @@ export class Hub { } /** Returns the integration if installed on the current client. */ - public getIntegration(name: string): Integration | null { - return this.getClient().getIntegration(name); + public getIntegration(integartion: IntegrationClass): T | null { + return this.getClient().getIntegration(integartion); } } diff --git a/packages/hub/src/scope.ts b/packages/hub/src/scope.ts index 82d8f86989a3..233c5cef04be 100644 --- a/packages/hub/src/scope.ts +++ b/packages/hub/src/scope.ts @@ -242,12 +242,20 @@ export class Scope { } } +/** + * Retruns the global event processors. + */ function getGlobalEventProcessors(): EventProcessor[] { const global: any = getGlobalObject(); + global.__SENTRY__ = global.__SENTRY__ || {}; global.__SENTRY__.globalEventProcessors = global.__SENTRY__.globalEventProcessors || []; return global.__SENTRY__.globalEventProcessors; } +/** + * Add a EventProcessor to be kept globally. + * @param callback EventProcessor to add + */ export function addGlobalEventProcessor(callback: EventProcessor): void { getGlobalEventProcessors().push(callback); } diff --git a/packages/hub/test/lib/global.test.ts b/packages/hub/test/global.test.ts similarity index 90% rename from packages/hub/test/lib/global.test.ts rename to packages/hub/test/global.test.ts index 3c9f70016799..e25ffb6361d6 100644 --- a/packages/hub/test/lib/global.test.ts +++ b/packages/hub/test/global.test.ts @@ -1,4 +1,4 @@ -import { getCurrentHub, getHubFromCarrier, Hub } from '../../src'; +import { getCurrentHub, getHubFromCarrier, Hub } from '../src'; describe('global', () => { test('getGlobalHub', () => { diff --git a/packages/hub/test/lib/hub.test.ts b/packages/hub/test/hub.test.ts similarity index 99% rename from packages/hub/test/lib/hub.test.ts rename to packages/hub/test/hub.test.ts index b6145ef99285..32d564b50908 100644 --- a/packages/hub/test/lib/hub.test.ts +++ b/packages/hub/test/hub.test.ts @@ -1,5 +1,5 @@ import { SentryEvent } from '@sentry/types'; -import { getCurrentHub, Hub, Scope } from '../../src'; +import { getCurrentHub, Hub, Scope } from '../src'; const clientFn = jest.fn(); const asyncClientFn = async () => Promise.reject('error'); diff --git a/packages/hub/test/lib/scope.test.ts b/packages/hub/test/scope.test.ts similarity index 99% rename from packages/hub/test/lib/scope.test.ts rename to packages/hub/test/scope.test.ts index 9115ca52770a..b890cb815832 100644 --- a/packages/hub/test/lib/scope.test.ts +++ b/packages/hub/test/scope.test.ts @@ -1,5 +1,5 @@ import { SentryEvent, SentryEventHint, Severity } from '@sentry/types'; -import { Scope } from '../../src'; +import { Scope } from '../src'; describe('Scope', () => { afterEach(() => { diff --git a/packages/node/package.json b/packages/node/package.json index 5019252ed8b5..b63fbf0e4ad9 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -16,7 +16,6 @@ }, "dependencies": { "@sentry/core": "4.1.1", - "@sentry/hub": "4.1.1", "@sentry/types": "4.1.0", "@sentry/utils": "4.1.1", "cookie": "0.3.1", diff --git a/packages/node/src/backend.ts b/packages/node/src/backend.ts index 30b60da73121..3f4e5434e2d4 100644 --- a/packages/node/src/backend.ts +++ b/packages/node/src/backend.ts @@ -1,5 +1,4 @@ -import { BaseBackend, Dsn, Options, SentryError } from '@sentry/core'; -import { getCurrentHub } from '@sentry/hub'; +import { BaseBackend, Dsn, getCurrentHub, Options, SentryError } from '@sentry/core'; import { SentryEvent, SentryEventHint, SentryResponse, Severity } from '@sentry/types'; import { isError, isPlainObject } from '@sentry/utils/is'; import { limitObjectDepthToSize, serializeKeysToEventMessage } from '@sentry/utils/object'; diff --git a/packages/node/src/handlers.ts b/packages/node/src/handlers.ts index 88a4bc2f4102..7733b4ede068 100644 --- a/packages/node/src/handlers.ts +++ b/packages/node/src/handlers.ts @@ -1,6 +1,5 @@ -import { logger } from '@sentry/core'; -import { getCurrentHub, Scope } from '@sentry/hub'; -import { SentryEvent, Severity } from '@sentry/types'; +import { getCurrentHub, logger } from '@sentry/core'; +import { SentryEvent } from '@sentry/types'; import { forget } from '@sentry/utils/async'; import { serialize } from '@sentry/utils/object'; import * as cookie from 'cookie'; @@ -285,69 +284,3 @@ export function defaultOnFatalError(error: Error): void { }), ); } - -/** JSDoc */ -export function makeErrorHandler( - onFatalError: (firstError: Error, secondError?: Error) => void = defaultOnFatalError, -): (error: Error) => void { - const timeout = 2000; - let caughtFirstError: boolean = false; - let caughtSecondError: boolean = false; - let calledFatalError: boolean = false; - let firstError: Error; - - return (error: Error): void => { - if (!caughtFirstError) { - // this is the first uncaught error and the ultimate reason for shutting down - // we want to do absolutely everything possible to ensure it gets captured - // also we want to make sure we don't go recursion crazy if more errors happen after this one - firstError = error; - caughtFirstError = true; - - getCurrentHub().withScope(async () => { - getCurrentHub().configureScope((scope: Scope) => { - scope.addEventProcessor(async (event: SentryEvent) => ({ - ...event, - level: Severity.Fatal, - })); - }); - - getCurrentHub().captureException(error, { originalException: error }); - - if (!calledFatalError) { - calledFatalError = true; - onFatalError(error); - } - }); - } else if (calledFatalError) { - // we hit an error *after* calling onFatalError - pretty boned at this point, just shut it down - logger.warn('uncaught exception after calling fatal error shutdown callback - this is bad! forcing shutdown'); - defaultOnFatalError(error); - } else if (!caughtSecondError) { - // two cases for how we can hit this branch: - // - capturing of first error blew up and we just caught the exception from that - // - quit trying to capture, proceed with shutdown - // - a second independent error happened while waiting for first error to capture - // - want to avoid causing premature shutdown before first error capture finishes - // it's hard to immediately tell case 1 from case 2 without doing some fancy/questionable domain stuff - // so let's instead just delay a bit before we proceed with our action here - // in case 1, we just wait a bit unnecessarily but ultimately do the same thing - // in case 2, the delay hopefully made us wait long enough for the capture to finish - // two potential nonideal outcomes: - // nonideal case 1: capturing fails fast, we sit around for a few seconds unnecessarily before proceeding correctly by calling onFatalError - // nonideal case 2: case 2 happens, 1st error is captured but slowly, timeout completes before capture and we treat second error as the sendErr of (nonexistent) failure from trying to capture first error - // note that after hitting this branch, we might catch more errors where (caughtSecondError && !calledFatalError) - // we ignore them - they don't matter to us, we're just waiting for the second error timeout to finish - caughtSecondError = true; - setTimeout(() => { - if (!calledFatalError) { - // it was probably case 1, let's treat err as the sendErr and call onFatalError - calledFatalError = true; - onFatalError(firstError, error); - } else { - // it was probably case 2, our first error finished capturing while we waited, cool, do nothing - } - }, timeout); // capturing could take at least sendTimeout to fail, plus an arbitrary second for how long it takes to collect surrounding source etc - } - }; -} diff --git a/packages/node/src/integrations/console.ts b/packages/node/src/integrations/console.ts index 3b7773f70d51..d7e2f8de7ad0 100644 --- a/packages/node/src/integrations/console.ts +++ b/packages/node/src/integrations/console.ts @@ -1,8 +1,30 @@ -import { getCurrentHub } from '@sentry/hub'; +import { getCurrentHub } from '@sentry/core'; import { Integration, Severity } from '@sentry/types'; import { fill } from '@sentry/utils/object'; import * as util from 'util'; +/** Console module integration */ +export class Console implements Integration { + /** + * @inheritDoc + */ + public name: string = 'Console'; + /** + * @inheritDoc + */ + public static id: string = 'Console'; + + /** + * @inheritDoc + */ + public setupOnce(): void { + const nativeModule = require('module'); + fill(nativeModule, '_load', loadWrapper(nativeModule)); + // special case: since console is built-in and app-level code won't require() it, do that here + require('console'); + } +} + /** * Wrapper function for internal _load calls within `require` */ @@ -56,37 +78,22 @@ function consoleWrapper(originalModule: any): any { } return function(): any { - getCurrentHub().addBreadcrumb( - { - category: 'console', - level: sentryLevel, - message: util.format.apply(undefined, arguments), - }, - { - input: [...arguments], - level, - }, - ); + if (getCurrentHub().getIntegration(Console)) { + getCurrentHub().addBreadcrumb( + { + category: 'console', + level: sentryLevel, + message: util.format.apply(undefined, arguments), + }, + { + input: [...arguments], + level, + }, + ); + } originalConsoleLevel.apply(originalModule, arguments); }; }); }; } - -/** Console module integration */ -export class Console implements Integration { - /** - * @inheritDoc - */ - public name: string = 'Console'; - /** - * @inheritDoc - */ - public install(): void { - const nativeModule = require('module'); - fill(nativeModule, '_load', loadWrapper(nativeModule)); - // special case: since console is built-in and app-level code won't require() it, do that here - require('console'); - } -} diff --git a/packages/node/src/integrations/http.ts b/packages/node/src/integrations/http.ts index 62eee762b16e..45d5432c8814 100644 --- a/packages/node/src/integrations/http.ts +++ b/packages/node/src/integrations/http.ts @@ -1,4 +1,4 @@ -import { getCurrentHub } from '@sentry/hub'; +import { getCurrentHub } from '@sentry/core'; import { Integration } from '@sentry/types'; import { fill } from '@sentry/utils/object'; import * as http from 'http'; @@ -14,6 +14,32 @@ interface SentryRequest extends http.IncomingMessage { __ravenBreadcrumbUrl?: string; } +/** http module integration */ +export class Http implements Integration { + /** + * @inheritDoc + */ + public name: string = 'Http'; + /** + * @inheritDoc + */ + public static id: string = 'Http'; + + /** + * @inheritDoc + */ + public setupOnce(): void { + const nativeModule = require('module'); + fill(nativeModule, '_load', loadWrapper(nativeModule)); + // observation: when the https module does its own require('http'), it *does not* hit our hooked require to instrument http on the fly + // but if we've previously instrumented http, https *does* get our already-instrumented version + // this is because raven's transports are required before this instrumentation takes place, which loads https (and http) + // so module cache will have uninstrumented http; proactively loading it here ensures instrumented version is in module cache + // alternatively we could refactor to load our transports later, but this is easier and doesn't have much drawback + require('http'); + } +} + /** * Function that can combine together a url that'll be used for our breadcrumbs. * @@ -119,7 +145,7 @@ function emitWrapper(origEmit: EventListener): (event: string, response: http.Se const isInterestingEvent = event === 'response' || event === 'error'; const isNotSentryRequest = dsn && this.__ravenBreadcrumbUrl && !this.__ravenBreadcrumbUrl.includes(dsn.host); - if (isInterestingEvent && isNotSentryRequest) { + if (isInterestingEvent && isNotSentryRequest && getCurrentHub().getIntegration(Http)) { getCurrentHub().addBreadcrumb( { category: 'http', @@ -141,24 +167,3 @@ function emitWrapper(origEmit: EventListener): (event: string, response: http.Se return origEmit.apply(this, arguments); }; } - -/** http module integration */ -export class Http implements Integration { - /** - * @inheritDoc - */ - public name: string = 'Http'; - /** - * @inheritDoc - */ - public install(): void { - const nativeModule = require('module'); - fill(nativeModule, '_load', loadWrapper(nativeModule)); - // observation: when the https module does its own require('http'), it *does not* hit our hooked require to instrument http on the fly - // but if we've previously instrumented http, https *does* get our already-instrumented version - // this is because raven's transports are required before this instrumentation takes place, which loads https (and http) - // so module cache will have uninstrumented http; proactively loading it here ensures instrumented version is in module cache - // alternatively we could refactor to load our transports later, but this is easier and doesn't have much drawback - require('http'); - } -} diff --git a/packages/node/src/integrations/linkederrors.ts b/packages/node/src/integrations/linkederrors.ts index 06084d8f95fd..c3d19431a721 100644 --- a/packages/node/src/integrations/linkederrors.ts +++ b/packages/node/src/integrations/linkederrors.ts @@ -1,4 +1,4 @@ -import { getCurrentHub } from '@sentry/hub'; +import { addGlobalEventProcessor, getCurrentHub } from '@sentry/core'; import { Integration, SentryEvent, SentryEventHint, SentryException } from '@sentry/types'; import { getExceptionFromError } from '../parsers'; @@ -18,6 +18,10 @@ export class LinkedErrors implements Integration { * @inheritDoc */ public readonly name: string = 'LinkedErrors'; + /** + * @inheritDoc + */ + public static id: string = 'LinkedErrors'; /** * @inheritDoc @@ -40,9 +44,13 @@ export class LinkedErrors implements Integration { /** * @inheritDoc */ - public install(): void { - getCurrentHub().configureScope(scope => { - scope.addEventProcessor(this.handler.bind(this)); + public setupOnce(): void { + addGlobalEventProcessor(async (event: SentryEvent, hint?: SentryEventHint) => { + const self = getCurrentHub().getIntegration(LinkedErrors); + if (self) { + return self.handler(event, hint); + } + return event; }); } diff --git a/packages/node/src/integrations/onuncaughtexception.ts b/packages/node/src/integrations/onuncaughtexception.ts index 0237ee262fd4..8d2d68610992 100644 --- a/packages/node/src/integrations/onuncaughtexception.ts +++ b/packages/node/src/integrations/onuncaughtexception.ts @@ -1,5 +1,6 @@ -import { Integration } from '@sentry/types'; -import { makeErrorHandler } from '../handlers'; +import { getCurrentHub, logger, Scope } from '@sentry/core'; +import { Integration, SentryEvent, Severity } from '@sentry/types'; +import { defaultOnFatalError } from '../handlers'; /** Global Promise Rejection handler */ export class OnUncaughtException implements Integration { @@ -7,6 +8,11 @@ export class OnUncaughtException implements Integration { * @inheritDoc */ public name: string = 'OnUncaughtException'; + /** + * @inheritDoc + */ + public static id: string = 'OnUncaughtException'; + /** * @inheritDoc */ @@ -25,7 +31,80 @@ export class OnUncaughtException implements Integration { /** * @inheritDoc */ - public install(): void { + public setupOnce(): void { global.process.on('uncaughtException', this.handler); } } + +/** JSDoc */ +export function makeErrorHandler( + onFatalError: (firstError: Error, secondError?: Error) => void = defaultOnFatalError, +): (error: Error) => void { + const timeout = 2000; + let caughtFirstError: boolean = false; + let caughtSecondError: boolean = false; + let calledFatalError: boolean = false; + let firstError: Error; + + return (error: Error): void => { + if (!caughtFirstError) { + // this is the first uncaught error and the ultimate reason for shutting down + // we want to do absolutely everything possible to ensure it gets captured + // also we want to make sure we don't go recursion crazy if more errors happen after this one + firstError = error; + caughtFirstError = true; + + if (getCurrentHub().getIntegration(OnUncaughtException)) { + getCurrentHub().withScope(async () => { + getCurrentHub().configureScope((scope: Scope) => { + scope.addEventProcessor(async (event: SentryEvent) => ({ + ...event, + level: Severity.Fatal, + })); + }); + + getCurrentHub().captureException(error, { originalException: error }); + + if (!calledFatalError) { + calledFatalError = true; + onFatalError(error); + } + }); + } else { + if (!calledFatalError) { + calledFatalError = true; + onFatalError(error); + } + } + } else if (calledFatalError) { + // we hit an error *after* calling onFatalError - pretty boned at this point, just shut it down + logger.warn('uncaught exception after calling fatal error shutdown callback - this is bad! forcing shutdown'); + defaultOnFatalError(error); + } else if (!caughtSecondError) { + // two cases for how we can hit this branch: + // - capturing of first error blew up and we just caught the exception from that + // - quit trying to capture, proceed with shutdown + // - a second independent error happened while waiting for first error to capture + // - want to avoid causing premature shutdown before first error capture finishes + // it's hard to immediately tell case 1 from case 2 without doing some fancy/questionable domain stuff + // so let's instead just delay a bit before we proceed with our action here + // in case 1, we just wait a bit unnecessarily but ultimately do the same thing + // in case 2, the delay hopefully made us wait long enough for the capture to finish + // two potential nonideal outcomes: + // nonideal case 1: capturing fails fast, we sit around for a few seconds unnecessarily before proceeding correctly by calling onFatalError + // nonideal case 2: case 2 happens, 1st error is captured but slowly, timeout completes before capture and we treat second error as the sendErr of (nonexistent) failure from trying to capture first error + // note that after hitting this branch, we might catch more errors where (caughtSecondError && !calledFatalError) + // we ignore them - they don't matter to us, we're just waiting for the second error timeout to finish + caughtSecondError = true; + setTimeout(() => { + if (!calledFatalError) { + // it was probably case 1, let's treat err as the sendErr and call onFatalError + calledFatalError = true; + onFatalError(firstError, error); + } else { + // it was probably case 2, our first error finished capturing while we waited, cool, do nothing + } + }, timeout); // capturing could take at least sendTimeout to fail, plus an arbitrary second for how long it takes to collect surrounding source etc + } + }; +} diff --git a/packages/node/src/integrations/onunhandledrejection.ts b/packages/node/src/integrations/onunhandledrejection.ts index bb7162ea05a7..99728aaefe9e 100644 --- a/packages/node/src/integrations/onunhandledrejection.ts +++ b/packages/node/src/integrations/onunhandledrejection.ts @@ -1,4 +1,4 @@ -import { getCurrentHub } from '@sentry/hub'; +import { getCurrentHub } from '@sentry/core'; import { Integration } from '@sentry/types'; /** Global Promise Rejection handler */ @@ -10,7 +10,12 @@ export class OnUnhandledRejection implements Integration { /** * @inheritDoc */ - public install(): void { + public static id: string = 'OnUnhandledRejection'; + + /** + * @inheritDoc + */ + public setupOnce(): void { global.process.on('unhandledRejection', this.sendUnhandledPromise.bind(this)); } @@ -20,6 +25,9 @@ export class OnUnhandledRejection implements Integration { * @param promise promise */ public sendUnhandledPromise(reason: any, promise: any): void { + if (!getCurrentHub().getIntegration(OnUnhandledRejection)) { + return; + } const context = (promise.domain && promise.domain.sentryContext) || {}; getCurrentHub().withScope(() => { getCurrentHub().configureScope(scope => { diff --git a/packages/node/src/integrations/pluggable/modules.ts b/packages/node/src/integrations/pluggable/modules.ts index 806310c36f3f..a76e47b0c117 100644 --- a/packages/node/src/integrations/pluggable/modules.ts +++ b/packages/node/src/integrations/pluggable/modules.ts @@ -1,7 +1,6 @@ -import { getCurrentHub, Scope } from '@sentry/hub'; +import { addGlobalEventProcessor, getCurrentHub } from '@sentry/core'; import { Integration } from '@sentry/types'; import * as lsmod from 'lsmod'; -import { NodeOptions } from '../../backend'; let moduleCache: { [key: string]: string }; @@ -11,16 +10,23 @@ export class Modules implements Integration { * @inheritDoc */ public name: string = 'Modules'; + /** + * @inheritDoc + */ + public static id: string = 'Modules'; /** * @inheritDoc */ - public install(_: NodeOptions = {}): void { - getCurrentHub().configureScope((scope: Scope) => { - scope.addEventProcessor(async event => ({ + public setupOnce(): void { + addGlobalEventProcessor(async event => { + if (!getCurrentHub().getIntegration(Modules)) { + return event; + } + return { ...event, modules: this.getModules(), - })); + }; }); } diff --git a/packages/node/src/integrations/pluggable/transaction.ts b/packages/node/src/integrations/pluggable/transaction.ts index 0a262cc93992..090fec63b88d 100644 --- a/packages/node/src/integrations/pluggable/transaction.ts +++ b/packages/node/src/integrations/pluggable/transaction.ts @@ -1,6 +1,5 @@ -import { getCurrentHub, Scope } from '@sentry/hub'; +import { addGlobalEventProcessor, getCurrentHub } from '@sentry/core'; import { Integration, SentryEvent, StackFrame } from '@sentry/types'; -import { NodeOptions } from '../../backend'; /** Add node transaction to the event */ export class Transaction implements Integration { @@ -8,13 +7,21 @@ export class Transaction implements Integration { * @inheritDoc */ public name: string = 'Transaction'; + /** + * @inheritDoc + */ + public static id: string = 'Transaction'; /** * @inheritDoc */ - public install(_: NodeOptions = {}): void { - getCurrentHub().configureScope((scope: Scope) => { - scope.addEventProcessor(async event => this.process(event)); + public setupOnce(): void { + addGlobalEventProcessor(async event => { + const self = getCurrentHub().getIntegration(Transaction); + if (self) { + return self.process(event); + } + return event; }); } diff --git a/packages/node/test/domain.test.ts b/packages/node/test/domain.test.ts index 80ce410df2f2..b4ac33e3aeec 100644 --- a/packages/node/test/domain.test.ts +++ b/packages/node/test/domain.test.ts @@ -1,14 +1,7 @@ -import { getCurrentHub, Hub } from '@sentry/hub'; +import { getCurrentHub, Hub } from '@sentry/core'; import * as domain from 'domain'; describe('domains', () => { - afterEach(() => { - if (domain.active) { - domain.active.exit(); - } - jest.resetAllMocks(); - }); - test('without domain', () => { expect(domain.active).toBeFalsy(); const hub = getCurrentHub(); @@ -33,6 +26,7 @@ describe('domains', () => { const d = domain.create(); d.run(() => { expect(getCurrentHub()).toBe(getCurrentHub()); + d.exit(); }); }); @@ -47,6 +41,7 @@ describe('domains', () => { setTimeout(() => { expect(getCurrentHub().getStack()[1]).toEqual({ client: 'process' }); + d1.exit(); }, 50); }); @@ -57,6 +52,7 @@ describe('domains', () => { setTimeout(() => { expect(getCurrentHub().getStack()[1]).toEqual({ client: 'local' }); + d2.exit(); done(); }, 100); }); diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 00525bb6f0d2..567bf59b2de0 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -193,12 +193,28 @@ export interface Mechanism { }; } -/** JSDoc */ +/** Integration interface */ export interface Integration { + // TODO: Remove with v5 + /** + * @deprecated Use {@link IntegrationClass.id} instead + */ name: string; + // TODO: Remove with v5 /** @deprecated */ install?(options?: object): void; - setupOnce?(options?: object): void; // TODO: make not optional + + // This takes no options on purpose, options should be passed in the constructor + setupOnce(): void; // TODO: make not optional +} + +/** Integration Class Interface */ +export interface IntegrationClass { + new (): T; + /** + * Property that holds the integration name + */ + id: string; } /** JSDoc */ diff --git a/yarn.lock b/yarn.lock index d2b3cf3aa6fb..50047f65fc7f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -731,12 +731,6 @@ abbrev@1.0.x: version "1.0.9" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135" -abstract-leveldown@~0.12.0, abstract-leveldown@~0.12.1: - version "0.12.4" - resolved "https://registry.yarnpkg.com/abstract-leveldown/-/abstract-leveldown-0.12.4.tgz#29e18e632e60e4e221d5810247852a63d7b2e410" - dependencies: - xtend "~3.0.0" - accepts@~1.3.4, accepts@~1.3.5: version "1.3.5" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.5.tgz#eb777df6011723a3b14e8a72c0805c8e86746bd2" @@ -1718,12 +1712,6 @@ bl@^1.0.0: readable-stream "^2.3.5" safe-buffer "^5.1.1" -bl@~0.8.1: - version "0.8.2" - resolved "http://registry.npmjs.org/bl/-/bl-0.8.2.tgz#c9b6bca08d1bc2ea00fc8afb4f1a5fd1e1c66e4e" - dependencies: - readable-stream "~1.0.26" - bl@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/bl/-/bl-1.1.2.tgz#fdca871a99713aa00d19e3bbba41c44787a65398" @@ -1894,14 +1882,6 @@ browserify-des@^1.0.0: des.js "^1.0.0" inherits "^2.0.1" -browserify-fs@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/browserify-fs/-/browserify-fs-1.0.0.tgz#f075aa8a729d4d1716d066620e386fcc1311a96f" - dependencies: - level-filesystem "^1.0.1" - level-js "^2.1.3" - levelup "^0.18.2" - browserify-rsa@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524" @@ -1963,10 +1943,6 @@ buffer-equal-constant-time@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" -buffer-es6@^4.9.2: - version "4.9.3" - resolved "https://registry.yarnpkg.com/buffer-es6/-/buffer-es6-4.9.3.tgz#f26347b82df76fd37e18bcb5288c4970cfd5c404" - buffer-fill@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c" @@ -2297,10 +2273,6 @@ clone@^1.0.2: version "1.0.4" resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" -clone@~0.1.9: - version "0.1.19" - resolved "https://registry.yarnpkg.com/clone/-/clone-0.1.19.tgz#613fb68639b26a494ac53253e15b1a6bd88ada85" - cmd-shim@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/cmd-shim/-/cmd-shim-2.0.2.tgz#6fcbda99483a8fd15d7d30a196ca69d688a2efdb" @@ -2465,7 +2437,7 @@ concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" -concat-stream@^1.4.4, concat-stream@^1.5.0, concat-stream@^1.6.0: +concat-stream@^1.5.0, concat-stream@^1.6.0: version "1.6.2" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" dependencies: @@ -2742,7 +2714,7 @@ cryptiles@3.x.x: dependencies: boom "5.x.x" -crypto-browserify@^3.11.0, crypto-browserify@^3.11.1: +crypto-browserify@^3.11.1: version "3.12.0" resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" dependencies: @@ -2957,12 +2929,6 @@ defaults@^1.0.3: dependencies: clone "^1.0.2" -deferred-leveldown@~0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/deferred-leveldown/-/deferred-leveldown-0.2.0.tgz#2cef1f111e1c57870d8bbb8af2650e587cd2f5b4" - dependencies: - abstract-leveldown "~0.12.1" - define-properties@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.2.tgz#83a73f2fea569898fb737193c8f873caf6d45c94" @@ -3226,12 +3192,6 @@ err-code@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/err-code/-/err-code-1.1.2.tgz#06e0116d3028f6aef4806849eb0ea6a748ae6960" -errno@^0.1.1, errno@~0.1.1: - version "0.1.7" - resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618" - dependencies: - prr "~1.0.1" - error-ex@^1.2.0, error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" @@ -3685,7 +3645,7 @@ for-own@^0.1.4: dependencies: for-in "^1.0.1" -foreach@^2.0.5, foreach@~2.0.1: +foreach@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" @@ -3830,12 +3790,6 @@ function-bind@^1.0.2, function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" -fwd-stream@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/fwd-stream/-/fwd-stream-1.0.4.tgz#ed281cabed46feecf921ee32dc4c50b372ac7cfa" - dependencies: - readable-stream "~1.0.26-4" - gauge@~2.7.3: version "2.7.4" resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" @@ -4442,10 +4396,6 @@ iconv-lite@^0.4.24, iconv-lite@~0.4.13: dependencies: safer-buffer ">= 2.1.2 < 3" -idb-wrapper@^1.5.0: - version "1.7.2" - resolved "https://registry.yarnpkg.com/idb-wrapper/-/idb-wrapper-1.7.2.tgz#8251afd5e77fe95568b1c16152eb44b396767ea2" - ieee754@^1.1.4: version "1.1.12" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.12.tgz#50bf24e5b9c8bb98af4964c941cdb0918da7b60b" @@ -4489,7 +4439,7 @@ indent-string@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289" -indexof@0.0.1, indexof@~0.0.1: +indexof@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" @@ -4773,10 +4723,6 @@ is-obj@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" -is-object@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/is-object/-/is-object-0.1.2.tgz#00efbc08816c33cfc4ac8251d132e10dc65098d7" - is-odd@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-odd/-/is-odd-2.0.0.tgz#7646624671fd7ea558ccd9a2795182f2958f1b24" @@ -4865,10 +4811,6 @@ is@^3.0.1, is@^3.2.0: version "3.2.1" resolved "https://registry.yarnpkg.com/is/-/is-3.2.1.tgz#d0ac2ad55eb7b0bec926a5266f6c662aaa83dca5" -is@~0.2.6: - version "0.2.7" - resolved "http://registry.npmjs.org/is/-/is-0.2.7.tgz#3b34a2c48f359972f35042849193ae7264b63562" - isarray@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" @@ -4885,10 +4827,6 @@ isbinaryfile@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-3.0.2.tgz#4a3e974ec0cba9004d3fc6cde7209ea69368a621" -isbuffer@~0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/isbuffer/-/isbuffer-0.0.0.tgz#38c146d9df528b8bf9b0701c3d43cf12df3fc39b" - isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -5652,82 +5590,6 @@ lerna@3.4.0: import-local "^1.0.0" npmlog "^4.1.2" -level-blobs@^0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/level-blobs/-/level-blobs-0.1.7.tgz#9ab9b97bb99f1edbf9f78a3433e21ed56386bdaf" - dependencies: - level-peek "1.0.6" - once "^1.3.0" - readable-stream "^1.0.26-4" - -level-filesystem@^1.0.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/level-filesystem/-/level-filesystem-1.2.0.tgz#a00aca9919c4a4dfafdca6a8108d225aadff63b3" - dependencies: - concat-stream "^1.4.4" - errno "^0.1.1" - fwd-stream "^1.0.4" - level-blobs "^0.1.7" - level-peek "^1.0.6" - level-sublevel "^5.2.0" - octal "^1.0.0" - once "^1.3.0" - xtend "^2.2.0" - -level-fix-range@2.0: - version "2.0.0" - resolved "http://registry.npmjs.org/level-fix-range/-/level-fix-range-2.0.0.tgz#c417d62159442151a19d9a2367868f1724c2d548" - dependencies: - clone "~0.1.9" - -level-fix-range@~1.0.2: - version "1.0.2" - resolved "http://registry.npmjs.org/level-fix-range/-/level-fix-range-1.0.2.tgz#bf15b915ae36d8470c821e883ddf79cd16420828" - -"level-hooks@>=4.4.0 <5": - version "4.5.0" - resolved "http://registry.npmjs.org/level-hooks/-/level-hooks-4.5.0.tgz#1b9ae61922930f3305d1a61fc4d83c8102c0dd93" - dependencies: - string-range "~1.2" - -level-js@^2.1.3: - version "2.2.4" - resolved "https://registry.yarnpkg.com/level-js/-/level-js-2.2.4.tgz#bc055f4180635d4489b561c9486fa370e8c11697" - dependencies: - abstract-leveldown "~0.12.0" - idb-wrapper "^1.5.0" - isbuffer "~0.0.0" - ltgt "^2.1.2" - typedarray-to-buffer "~1.0.0" - xtend "~2.1.2" - -level-peek@1.0.6, level-peek@^1.0.6: - version "1.0.6" - resolved "http://registry.npmjs.org/level-peek/-/level-peek-1.0.6.tgz#bec51c72a82ee464d336434c7c876c3fcbcce77f" - dependencies: - level-fix-range "~1.0.2" - -level-sublevel@^5.2.0: - version "5.2.3" - resolved "http://registry.npmjs.org/level-sublevel/-/level-sublevel-5.2.3.tgz#744c12c72d2e72be78dde3b9b5cd84d62191413a" - dependencies: - level-fix-range "2.0" - level-hooks ">=4.4.0 <5" - string-range "~1.2.1" - xtend "~2.0.4" - -levelup@^0.18.2: - version "0.18.6" - resolved "https://registry.yarnpkg.com/levelup/-/levelup-0.18.6.tgz#e6a01cb089616c8ecc0291c2a9bd3f0c44e3e5eb" - dependencies: - bl "~0.8.1" - deferred-leveldown "~0.2.0" - errno "~0.1.1" - prr "~0.0.0" - readable-stream "~1.0.26" - semver "~2.3.1" - xtend "~3.0.0" - leven@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/leven/-/leven-2.1.0.tgz#c2e7a9f772094dee9d34202ae8acce4687875580" @@ -5967,10 +5829,6 @@ lsmod@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/lsmod/-/lsmod-1.0.0.tgz#9a00f76dca36eb23fa05350afe1b585d4299e64b" -ltgt@^2.1.2: - version "2.2.1" - resolved "https://registry.yarnpkg.com/ltgt/-/ltgt-2.2.1.tgz#f35ca91c493f7b73da0e07495304f17b31f87ee5" - macos-release@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-1.1.0.tgz#831945e29365b470aa8724b0ab36c8f8959d10fb" @@ -6751,18 +6609,6 @@ object-keys@^1.0.8: version "1.0.12" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.12.tgz#09c53855377575310cca62f55bb334abff7b3ed2" -object-keys@~0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-0.2.0.tgz#cddec02998b091be42bf1035ae32e49f1cb6ea67" - dependencies: - foreach "~2.0.1" - indexof "~0.0.1" - is "~0.2.6" - -object-keys@~0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-0.4.0.tgz#28a6aae7428dd2c3a92f3d95f21335dd204e0336" - object-visit@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" @@ -6789,10 +6635,6 @@ object.pick@^1.3.0: dependencies: isobject "^3.0.1" -octal@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/octal/-/octal-1.0.0.tgz#63e7162a68efbeb9e213588d58e989d1e5c4530b" - on-finished@~2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" @@ -7294,10 +7136,6 @@ private@^0.1.6, private@^0.1.8: version "0.1.8" resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" -process-es6@^0.11.2: - version "0.11.6" - resolved "https://registry.yarnpkg.com/process-es6/-/process-es6-0.11.6.tgz#c6bb389f9a951f82bd4eb169600105bd2ff9c778" - process-nextick-args@~1.0.6: version "1.0.7" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" @@ -7364,14 +7202,6 @@ proxy-from-env@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee" -prr@~0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/prr/-/prr-0.0.0.tgz#1a84b85908325501411853d0081ee3fa86e2926a" - -prr@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" - ps-tree@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/ps-tree/-/ps-tree-1.1.0.tgz#b421b24140d6203f1ed3c76996b4427b08e8c014" @@ -7592,7 +7422,7 @@ read@1, read@~1.0.1: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@1.1.x, "readable-stream@1.x >=1.1.9", readable-stream@^1.0.26-4, readable-stream@^1.1.7: +readable-stream@1.1.x, "readable-stream@1.x >=1.1.9", readable-stream@^1.1.7: version "1.1.14" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" dependencies: @@ -7601,15 +7431,6 @@ readable-stream@1.1.x, "readable-stream@1.x >=1.1.9", readable-stream@^1.0.26-4, isarray "0.0.1" string_decoder "~0.10.x" -readable-stream@~1.0.26, readable-stream@~1.0.26-4: - version "1.0.34" - resolved "http://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.1" - isarray "0.0.1" - string_decoder "~0.10.x" - readable-stream@~2.0.0, readable-stream@~2.0.5: version "2.0.6" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e" @@ -8026,15 +7847,6 @@ rollup-plugin-license@^0.6.0: mkdirp "0.5.1" moment "2.21.0" -rollup-plugin-node-builtins@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/rollup-plugin-node-builtins/-/rollup-plugin-node-builtins-2.1.2.tgz#24a1fed4a43257b6b64371d8abc6ce1ab14597e9" - dependencies: - browserify-fs "^1.0.0" - buffer-es6 "^4.9.2" - crypto-browserify "^3.11.0" - process-es6 "^0.11.2" - rollup-plugin-node-resolve@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-3.3.0.tgz#c26d110a36812cbefa7ce117cadcd3439aa1c713" @@ -8165,10 +7977,6 @@ semver@^5.0.1, semver@^5.0.3, semver@^5.1.0: version "5.5.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.1.tgz#7dfdd8814bdb7cabc7be0fb1d734cfb66c940477" -semver@~2.3.1: - version "2.3.2" - resolved "https://registry.yarnpkg.com/semver/-/semver-2.3.2.tgz#b9848f25d6cf36333073ec9ef8856d42f1233e52" - semver@~5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" @@ -8679,10 +8487,6 @@ string-length@^2.0.0: astral-regex "^1.0.0" strip-ansi "^4.0.0" -string-range@~1.2, string-range@~1.2.1: - version "1.2.2" - resolved "http://registry.npmjs.org/string-range/-/string-range-1.2.2.tgz#a893ed347e72299bc83befbbf2a692a8d239d5dd" - string-similarity@1.1.0: version "1.1.0" resolved "http://registry.npmjs.org/string-similarity/-/string-similarity-1.1.0.tgz#3c66498858a465ec7c40c7d81739bbd995904914" @@ -9134,10 +8938,6 @@ type-is@~1.6.16: media-typer "0.3.0" mime-types "~2.1.18" -typedarray-to-buffer@~1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-1.0.4.tgz#9bb8ba0e841fb3f4cf1fe7c245e9f3fa8a5fe99c" - typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" @@ -9629,31 +9429,10 @@ xregexp@4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-4.0.0.tgz#e698189de49dd2a18cc5687b05e17c8e43943020" -xtend@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-2.2.0.tgz#eef6b1f198c1c8deafad8b1765a04dad4a01c5a9" - xtend@^4.0.0, xtend@~4.0.0, xtend@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" -xtend@~2.0.4: - version "2.0.6" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-2.0.6.tgz#5ea657a6dba447069c2e59c58a1138cb0c5e6cee" - dependencies: - is-object "~0.1.2" - object-keys "~0.2.0" - -xtend@~2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-2.1.2.tgz#6efecc2a4dad8e6962c4901b337ce7ba87b5d28b" - dependencies: - object-keys "~0.4.0" - -xtend@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-3.0.0.tgz#5cce7407baf642cba7becda568111c493f59665a" - y18n@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" From 06ef1681bb1ebaaafaa2059f7437772bc38fc996 Mon Sep 17 00:00:00 2001 From: HazA Date: Fri, 12 Oct 2018 14:21:15 +0200 Subject: [PATCH 04/11] fix: Browser Breadcrumb integration --- .../browser/src/integrations/breadcrumbs.ts | 66 +++++++++++-------- packages/browser/test/integration/init.js | 2 +- 2 files changed, 41 insertions(+), 27 deletions(-) diff --git a/packages/browser/src/integrations/breadcrumbs.ts b/packages/browser/src/integrations/breadcrumbs.ts index d8c626b38f32..85fe4389e612 100644 --- a/packages/browser/src/integrations/breadcrumbs.ts +++ b/packages/browser/src/integrations/breadcrumbs.ts @@ -5,7 +5,7 @@ import { getEventDescription, getGlobalObject, parseUrl } from '@sentry/utils/mi import { deserialize, fill } from '@sentry/utils/object'; import { includes, safeJoin } from '@sentry/utils/string'; import { supportsBeacon, supportsHistory, supportsNativeFetch } from '@sentry/utils/supports'; -import { BrowserOptions } from '../backend'; +import { BrowserClient } from '../client'; import { breadcrumbEventHandler, keypressEventHandler, wrap } from './helpers'; const global = getGlobalObject() as Window; @@ -64,7 +64,7 @@ export class Breadcrumbs implements Integration { } /** JSDoc */ - private instrumentBeacon(options: { filterUrl?: string }): void { + private instrumentBeacon(): void { if (!supportsBeacon()) { return; } @@ -78,11 +78,16 @@ export class Breadcrumbs implements Integration { // https://developer.mozilla.org/en-US/docs/Web/API/Beacon_API/Using_the_Beacon_API const result = originalBeaconFunction.apply(this, args); - // if Sentry key appears in URL, don't capture it as a request - // but rather as our own 'sentry' type breadcrumb - if (options.filterUrl && includes(url, options.filterUrl)) { - addSentryBreadcrumb(data); - return result; + const client = getCurrentHub().getClient() as BrowserClient; + const dsn = client && client.getDsn(); + if (dsn) { + const filterUrl = new API(dsn).getStoreEndpoint(); + // if Sentry key appears in URL, don't capture it as a request + // but rather as our own 'sentry' type breadcrumb + if (filterUrl && includes(url, filterUrl)) { + addSentryBreadcrumb(data); + return result; + } } // What is wrong with you TypeScript... @@ -165,7 +170,7 @@ export class Breadcrumbs implements Integration { } /** JSDoc */ - private instrumentFetch(options: { filterUrl?: string }): void { + private instrumentFetch(): void { if (!supportsNativeFetch()) { return; } @@ -191,13 +196,18 @@ export class Breadcrumbs implements Integration { method = args[1].method; } - // if Sentry key appears in URL, don't capture it as a request - // but rather as our own 'sentry' type breadcrumb - if (options.filterUrl && includes(url, options.filterUrl)) { - if (method === 'POST' && args[1] && args[1].body) { - addSentryBreadcrumb(args[1].body); + const client = getCurrentHub().getClient() as BrowserClient; + const dsn = client && client.getDsn(); + if (dsn) { + const filterUrl = new API(dsn).getStoreEndpoint(); + // if Sentry key appears in URL, don't capture it as a request + // but rather as our own 'sentry' type breadcrumb + if (filterUrl && includes(url, filterUrl)) { + if (method === 'POST' && args[1] && args[1].body) { + addSentryBreadcrumb(args[1].body); + } + return originalFetch.apply(global, args); } - return originalFetch.apply(global, args); } const fetchData: { @@ -317,7 +327,7 @@ export class Breadcrumbs implements Integration { } /** JSDoc */ - private instrumentXHR(options: { filterUrl?: string }): void { + private instrumentXHR(): void { if (!('XMLHttpRequest' in global)) { return; } @@ -352,11 +362,18 @@ export class Breadcrumbs implements Integration { method: args[0], url: args[1], }; - // if Sentry key appears in URL, don't capture it as a request - // but rather as our own 'sentry' type breadcrumb - if (isString(url) && (options.filterUrl && includes(url, options.filterUrl))) { - this.__sentry_own_request__ = true; + + const client = getCurrentHub().getClient() as BrowserClient; + const dsn = client && client.getDsn(); + if (dsn) { + const filterUrl = new API(dsn).getStoreEndpoint(); + // if Sentry key appears in URL, don't capture it as a request + // but rather as our own 'sentry' type breadcrumb + if (isString(url) && (filterUrl && includes(url, filterUrl))) { + this.__sentry_own_request__ = true; + } } + return originalOpen.apply(this, args); }, ); @@ -450,9 +467,7 @@ export class Breadcrumbs implements Integration { * - Fetch API * - History API */ - public setupOnce(options: BrowserOptions = {}): void { - const filterUrl = options.dsn && new API(options.dsn).getStoreEndpoint(); - + public setupOnce(): void { if (this.options.console) { this.instrumentConsole(); } @@ -460,13 +475,13 @@ export class Breadcrumbs implements Integration { this.instrumentDOM(); } if (this.options.xhr) { - this.instrumentXHR({ filterUrl }); + this.instrumentXHR(); } if (this.options.fetch) { - this.instrumentFetch({ filterUrl }); + this.instrumentFetch(); } if (this.options.beacon) { - this.instrumentBeacon({ filterUrl }); + this.instrumentBeacon(); } if (this.options.history) { this.instrumentHistory(); @@ -479,7 +494,6 @@ function addSentryBreadcrumb(serializedData: string): void { // There's always something that can go wrong with deserialization... try { const event: { [key: string]: any } = deserialize(serializedData); - Breadcrumbs.addBreadcrumb( { category: 'sentry', diff --git a/packages/browser/test/integration/init.js b/packages/browser/test/integration/init.js index bda34ee9eeda..44e248c9b4e7 100644 --- a/packages/browser/test/integration/init.js +++ b/packages/browser/test/integration/init.js @@ -33,7 +33,7 @@ DummyTransport.prototype.captureEvent = function(event) { Sentry.init({ dsn: 'https://public@example.com/1', - debug: true, + // debug: true, attachStacktrace: true, transport: DummyTransport, // integrations: function(old) { From e0c7e059e513e9011b346ca4763844ff81cd3e82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Og=C3=B3rek?= Date: Fri, 12 Oct 2018 15:50:47 +0200 Subject: [PATCH 05/11] ES6 to ES5 in console-logs.js test file --- packages/browser/test/integration/console-logs.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/browser/test/integration/console-logs.js b/packages/browser/test/integration/console-logs.js index 6e6a39f00ff2..f0fc1bfe3d18 100644 --- a/packages/browser/test/integration/console-logs.js +++ b/packages/browser/test/integration/console-logs.js @@ -1,7 +1,7 @@ console.log('One'); console.warn('Two', { a: 1 }); console.error('Error 2'); -let a = () => { +function a() { throw new Error('Error thrown 3'); -}; +} a(); From 4a1b6112e27b157872fd6f9182657b2f8e9a220b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Og=C3=B3rek?= Date: Fri, 12 Oct 2018 16:23:08 +0200 Subject: [PATCH 06/11] Fix Android 4 breadcrumbs tests --- packages/browser/test/integration/init.js | 11 +++++++---- packages/browser/test/integration/test.js | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/browser/test/integration/init.js b/packages/browser/test/integration/init.js index 44e248c9b4e7..909ababd39ed 100644 --- a/packages/browser/test/integration/init.js +++ b/packages/browser/test/integration/init.js @@ -40,11 +40,9 @@ Sentry.init({ // return [new Sentry.Integrations.Debug({ stringify: true })].concat(old); // }, beforeBreadcrumb: function(breadcrumb) { - if (window.forceAllBreadcrumbs) { - return breadcrumb; - } // Filter console logs as we use them for debugging *a lot* and they are not *that* important - if (breadcrumb.category === 'console') { + // But allow then if we explicitly say so (for one of integration tests) + if (breadcrumb.category === 'console' && !window.allowConsoleBreadcrumbs) { return null; } @@ -59,6 +57,11 @@ Sentry.init({ return null; } + // Filter "refresh" like navigation which occurs in Mocha when testing on Android 4 + if (breadcrumb.category === 'navigation' && breadcrumb.data.from === breadcrumb.data.from) { + return null; + } + sentryBreadcrumbs.push(breadcrumb); return breadcrumb; diff --git a/packages/browser/test/integration/test.js b/packages/browser/test/integration/test.js index aa612bc1f1d5..fb5f5023668c 100644 --- a/packages/browser/test/integration/test.js +++ b/packages/browser/test/integration/test.js @@ -1378,7 +1378,7 @@ for (var idx in frames) { iframe, done, function() { - window.forceAllBreadcrumbs = true; + window.allowConsoleBreadcrumbs = true; var logs = document.createElement('script'); logs.src = 'console-logs.js'; logs.onload = function() { From c1aa17918dd262d606417b00946993b7ad139cd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Og=C3=B3rek?= Date: Fri, 12 Oct 2018 16:30:05 +0200 Subject: [PATCH 07/11] why... --- packages/browser/test/integration/init.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/browser/test/integration/init.js b/packages/browser/test/integration/init.js index 909ababd39ed..656dbf2e6b6b 100644 --- a/packages/browser/test/integration/init.js +++ b/packages/browser/test/integration/init.js @@ -58,7 +58,7 @@ Sentry.init({ } // Filter "refresh" like navigation which occurs in Mocha when testing on Android 4 - if (breadcrumb.category === 'navigation' && breadcrumb.data.from === breadcrumb.data.from) { + if (breadcrumb.category === 'navigation' && breadcrumb.data.to === breadcrumb.data.from) { return null; } From b00bf3f72a1800b232a187a3ac83a35b8277edf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Og=C3=B3rek?= Date: Mon, 15 Oct 2018 13:36:49 +0200 Subject: [PATCH 08/11] Create consoleSandbox util and make logger use it --- packages/browser/src/backend.ts | 3 +- .../browser/src/integrations/breadcrumbs.ts | 3 +- .../src/integrations/globalhandlers.ts | 3 +- packages/core/src/basebackend.ts | 2 +- packages/core/src/baseclient.ts | 47 +++------------ packages/core/src/index.ts | 1 - .../core/src/integrations/inboundfilters.ts | 2 +- packages/core/src/integrations/index.ts | 2 +- .../core/src/integrations/sdkinformation.ts | 2 +- packages/core/src/logger.ts | 14 +++-- packages/core/src/sdk.ts | 6 +- packages/core/test/lib/base.test.ts | 19 +++++- packages/hub/src/hub.ts | 12 +++- packages/hub/test/hub.test.ts | 8 +-- packages/node/src/handlers.ts | 4 +- .../src/integrations/onuncaughtexception.ts | 3 +- packages/utils/src/logger.ts | 59 +++++++++++++++++++ packages/utils/src/misc.ts | 38 +++++++++++- 18 files changed, 162 insertions(+), 66 deletions(-) create mode 100644 packages/utils/src/logger.ts diff --git a/packages/browser/src/backend.ts b/packages/browser/src/backend.ts index a2e07803d1dd..a85154c7f48c 100644 --- a/packages/browser/src/backend.ts +++ b/packages/browser/src/backend.ts @@ -1,6 +1,7 @@ -import { BaseBackend, logger, Options, SentryError } from '@sentry/core'; +import { BaseBackend, Options, SentryError } from '@sentry/core'; import { SentryEvent, SentryEventHint, SentryResponse, Severity, Status } from '@sentry/types'; import { isDOMError, isDOMException, isError, isErrorEvent, isPlainObject } from '@sentry/utils/is'; +import { logger } from '@sentry/utils/logger'; import { supportsBeacon, supportsFetch } from '@sentry/utils/supports'; import { eventFromPlainObject, eventFromStacktrace, prepareFramesForEvent } from './parsers'; import { computeStackTrace } from './tracekit'; diff --git a/packages/browser/src/integrations/breadcrumbs.ts b/packages/browser/src/integrations/breadcrumbs.ts index 85fe4389e612..776c9db5a81d 100644 --- a/packages/browser/src/integrations/breadcrumbs.ts +++ b/packages/browser/src/integrations/breadcrumbs.ts @@ -1,6 +1,7 @@ -import { API, getCurrentHub, logger } from '@sentry/core'; +import { API, getCurrentHub } from '@sentry/core'; import { Breadcrumb, Integration, SentryBreadcrumbHint, Severity } from '@sentry/types'; import { isFunction, isString } from '@sentry/utils/is'; +import { logger } from '@sentry/utils/logger'; import { getEventDescription, getGlobalObject, parseUrl } from '@sentry/utils/misc'; import { deserialize, fill } from '@sentry/utils/object'; import { includes, safeJoin } from '@sentry/utils/string'; diff --git a/packages/browser/src/integrations/globalhandlers.ts b/packages/browser/src/integrations/globalhandlers.ts index ca8a1c1ec745..01aa0e9c16b4 100644 --- a/packages/browser/src/integrations/globalhandlers.ts +++ b/packages/browser/src/integrations/globalhandlers.ts @@ -1,5 +1,6 @@ -import { getCurrentHub, logger } from '@sentry/core'; +import { getCurrentHub } from '@sentry/core'; import { Integration, SentryEvent } from '@sentry/types'; +import { logger } from '@sentry/utils/logger'; import { eventFromStacktrace } from '../parsers'; import { installGlobalHandler, diff --git a/packages/core/src/basebackend.ts b/packages/core/src/basebackend.ts index 5d21a0ee9d34..bec82c2579f9 100644 --- a/packages/core/src/basebackend.ts +++ b/packages/core/src/basebackend.ts @@ -1,8 +1,8 @@ import { Scope } from '@sentry/hub'; import { Breadcrumb, SentryEvent, SentryEventHint, SentryResponse, Severity, Transport } from '@sentry/types'; +import { logger } from '@sentry/utils/logger'; import { SentryError } from './error'; import { Backend, Options } from './interfaces'; -import { logger } from './logger'; import { RequestBuffer } from './requestbuffer'; /** A class object that can instanciate Backend objects. */ diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index 338f2ea5fd61..525553222df7 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -7,18 +7,17 @@ import { SentryEvent, SentryEventHint, SentryResponse, - SentryWrappedFunction, Severity, Status, } from '@sentry/types'; import { forget } from '@sentry/utils/async'; -import { getGlobalObject, uuid4 } from '@sentry/utils/misc'; +import { logger } from '@sentry/utils/logger'; +import { consoleSandbox, uuid4 } from '@sentry/utils/misc'; import { truncate } from '@sentry/utils/string'; import { BackendClass } from './basebackend'; import { Dsn } from './dsn'; import { IntegrationIndex, setupIntegrations } from './integrations'; import { Backend, Client, Options } from './interfaces'; -import { logger } from './logger'; /** * Default maximum number of breadcrumbs added to an event. Can be overwritten @@ -37,39 +36,6 @@ const MAX_BREADCRUMBS = 100; */ const MAX_URL_LENGTH = 250; -/** JSDoc */ -interface ExtensibleConsole extends Console { - [key: string]: any; -} -/** JSDoc */ -function beforeBreadcrumbConsoleLoopGuard(callback: () => Breadcrumb | null): Breadcrumb | null { - const global = getGlobalObject() as Window; - const levels = ['debug', 'info', 'warn', 'error', 'log']; - if (!('console' in global)) { - return callback(); - } - const originalConsole = global.console as ExtensibleConsole; - const wrappedLevels: { [key: string]: any } = {}; - - // Restore all wrapped console methods - levels.forEach(level => { - if (level in global.console && (originalConsole[level] as SentryWrappedFunction).__sentry__) { - wrappedLevels[level] = (originalConsole[level] as SentryWrappedFunction).__sentry_wrapped__; - originalConsole[level] = (originalConsole[level] as SentryWrappedFunction).__sentry_original__; - } - }); - - // Perform callback manipulations - const result = callback(); - - // Revert restoration to wrapped state - Object.keys(wrappedLevels).forEach(level => { - originalConsole[level] = wrappedLevels[level]; - }); - - return result; -} - /** * Base implementation for all JavaScript SDK clients. * @@ -228,7 +194,7 @@ export abstract class BaseClient implement const timestamp = new Date().getTime() / 1000; const mergedBreadcrumb = { timestamp, ...breadcrumb }; const finalBreadcrumb = beforeBreadcrumb - ? beforeBreadcrumbConsoleLoopGuard(() => beforeBreadcrumb(mergedBreadcrumb, hint)) + ? (consoleSandbox(() => beforeBreadcrumb(mergedBreadcrumb, hint)) as Breadcrumb | null) : mergedBreadcrumb; if (finalBreadcrumb === null) { @@ -431,6 +397,11 @@ export abstract class BaseClient implement * @inheritDoc */ public getIntegration(integration: IntegrationClass): T | null { - return (this.integrations[integration.id] as T) || null; + try { + return (this.integrations[integration.id] as T) || null; + } catch (_oO) { + logger.warn(`Cannot retrieve integration ${integration.id} from the current Client`); + return null; + } } } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index a067ce76d6b1..06592c02b159 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,4 +1,3 @@ -export { logger } from './logger'; export { addBreadcrumb, captureException, diff --git a/packages/core/src/integrations/inboundfilters.ts b/packages/core/src/integrations/inboundfilters.ts index 007c51bd6037..d3f440c3fe50 100644 --- a/packages/core/src/integrations/inboundfilters.ts +++ b/packages/core/src/integrations/inboundfilters.ts @@ -1,9 +1,9 @@ import { addGlobalEventProcessor, getCurrentHub } from '@sentry/hub'; import { Integration, SentryEvent } from '@sentry/types'; import { isRegExp } from '@sentry/utils/is'; +import { logger } from '@sentry/utils/logger'; import { getEventDescription } from '@sentry/utils/misc'; import { includes } from '@sentry/utils/string'; -import { logger } from '../logger'; // "Script error." is hard coded into browsers for errors that it can't read. // this is the result of a script being pulled in from an external domain and CORS. diff --git a/packages/core/src/integrations/index.ts b/packages/core/src/integrations/index.ts index 731859a6b197..b0bcbd8b4a37 100644 --- a/packages/core/src/integrations/index.ts +++ b/packages/core/src/integrations/index.ts @@ -1,7 +1,7 @@ // tslint:disable:deprecation import { Integration } from '@sentry/types'; +import { logger } from '@sentry/utils/logger'; import { Options } from '../interfaces'; -import { logger } from '../logger'; export { Dedupe } from './dedupe'; export { FunctionToString } from './functiontostring'; diff --git a/packages/core/src/integrations/sdkinformation.ts b/packages/core/src/integrations/sdkinformation.ts index d6bb30785ebb..58ce510a5651 100644 --- a/packages/core/src/integrations/sdkinformation.ts +++ b/packages/core/src/integrations/sdkinformation.ts @@ -1,5 +1,5 @@ import { Integration } from '@sentry/types'; -import { logger } from '../logger'; +import { logger } from '@sentry/utils/logger'; /** * @deprecated diff --git a/packages/core/src/logger.ts b/packages/core/src/logger.ts index 0797f8806b4d..83216baa6fc7 100644 --- a/packages/core/src/logger.ts +++ b/packages/core/src/logger.ts @@ -1,4 +1,4 @@ -import { getGlobalObject } from '@sentry/utils/misc'; +import { consoleSandbox, getGlobalObject } from '@sentry/utils/misc'; // TODO: Implement different loggers for different environments const global = getGlobalObject() as Window; @@ -28,21 +28,27 @@ class Logger { if (this.disabled) { return; } - this.console.log(`Sentry Logger [Log]: ${message}`); // tslint:disable-line:no-console + consoleSandbox(() => { + this.console.log(`Sentry Logger [Log]: ${message}`); // tslint:disable-line:no-console + }); } /** JSDoc */ public warn(message: any): void { if (this.disabled) { return; } - this.console.warn(`Sentry Logger [Warn]: ${message}`); // tslint:disable-line:no-console + consoleSandbox(() => { + this.console.warn(`Sentry Logger [Warn]: ${message}`); // tslint:disable-line:no-console + }); } /** JSDoc */ public error(message: any): void { if (this.disabled) { return; } - this.console.error(`Sentry Logger [Error]: ${message}`); // tslint:disable-line:no-console + consoleSandbox(() => { + this.console.error(`Sentry Logger [Error]: ${message}`); // tslint:disable-line:no-console + }); } } diff --git a/packages/core/src/sdk.ts b/packages/core/src/sdk.ts index 02c9c48fee25..da93afeefe2e 100644 --- a/packages/core/src/sdk.ts +++ b/packages/core/src/sdk.ts @@ -1,6 +1,6 @@ import { getCurrentHub } from '@sentry/hub'; +import { logger } from '@sentry/utils/logger'; import { Client, Options } from './interfaces'; -import { logger } from './logger'; /** A class object that can instanciate Client objects. */ export interface ClientClass { @@ -16,8 +16,8 @@ export interface ClientClass { * @returns The installed and bound client instance. */ export function initAndBind(clientClass: ClientClass, options: O): void { - if (options.debug) { - logger.enable(); + if (!options.debug) { + logger.disable(); } if (getCurrentHub().getClient()) { diff --git a/packages/core/test/lib/base.test.ts b/packages/core/test/lib/base.test.ts index 48e853acd28c..cb050dbd16ca 100644 --- a/packages/core/test/lib/base.test.ts +++ b/packages/core/test/lib/base.test.ts @@ -1,5 +1,5 @@ import { Scope } from '@sentry/hub'; -import { Breadcrumb, SentryEvent, Status } from '@sentry/types'; +import { SentryEvent, Status } from '@sentry/types'; import { SentryError } from '../../src/error'; import { TestBackend } from '../mocks/backend'; import { TestClient } from '../mocks/client'; @@ -12,7 +12,22 @@ jest.mock('@sentry/utils/misc', () => ({ return '42'; }, getGlobalObject(): object { - return {}; + return { + console: { + log(): void { + // no-empty + }, + warn(): void { + // no-empty + }, + error(): void { + // no-empty + }, + }, + }; + }, + consoleSandbox(cb: () => void): void { + cb(); }, })); diff --git a/packages/hub/src/hub.ts b/packages/hub/src/hub.ts index 5a5f1c5891f5..e8034bc2502b 100644 --- a/packages/hub/src/hub.ts +++ b/packages/hub/src/hub.ts @@ -7,6 +7,7 @@ import { SentryEventHint, Severity, } from '@sentry/types'; +import { logger } from '@sentry/utils/logger'; import { getGlobalObject, uuid4 } from '@sentry/utils/misc'; import { Carrier, Layer } from './interfaces'; import { Scope } from './scope'; @@ -66,7 +67,7 @@ export class Hub { const top = this.getStackTop(); if (top && top.client && top.client[method]) { top.client[method](...args, top.scope).catch((err: any) => { - console.error(err); + logger.error(err); }); } } @@ -274,8 +275,13 @@ export class Hub { } /** Returns the integration if installed on the current client. */ - public getIntegration(integartion: IntegrationClass): T | null { - return this.getClient().getIntegration(integartion); + public getIntegration(integration: IntegrationClass): T | null { + try { + return this.getClient().getIntegration(integration); + } catch (_oO) { + logger.warn(`Cannot retrieve integration ${integration.id} from the current Hub`); + return null; + } } } diff --git a/packages/hub/test/hub.test.ts b/packages/hub/test/hub.test.ts index 32d564b50908..020c9c4e2459 100644 --- a/packages/hub/test/hub.test.ts +++ b/packages/hub/test/hub.test.ts @@ -1,4 +1,5 @@ import { SentryEvent } from '@sentry/types'; +import { logger } from '@sentry/utils/logger'; import { getCurrentHub, Hub, Scope } from '../src'; const clientFn = jest.fn(); @@ -50,9 +51,7 @@ describe('Hub', () => { }); test('invoke client async catch error in case', done => { - const orig = global.console; - // @ts-ignore - global.console = { error: jest.fn() }; + jest.spyOn(logger, 'error'); const hub = new Hub({ asyncClientFn, clientFn, @@ -60,8 +59,7 @@ describe('Hub', () => { (hub as any).invokeClientAsync('asyncClientFn', true); setTimeout(() => { // tslint:disable-next-line - expect(console.error).toHaveBeenCalled(); - global.console = orig; + expect(logger.error).toHaveBeenCalled(); done(); }); }); diff --git a/packages/node/src/handlers.ts b/packages/node/src/handlers.ts index 7733b4ede068..cb2a64752808 100644 --- a/packages/node/src/handlers.ts +++ b/packages/node/src/handlers.ts @@ -1,6 +1,8 @@ -import { getCurrentHub, logger } from '@sentry/core'; +import { getCurrentHub } from '@sentry/core'; +import { Carrier, Hub } from '@sentry/hub'; import { SentryEvent } from '@sentry/types'; import { forget } from '@sentry/utils/async'; +import { logger } from '@sentry/utils/logger'; import { serialize } from '@sentry/utils/object'; import * as cookie from 'cookie'; import * as domain from 'domain'; diff --git a/packages/node/src/integrations/onuncaughtexception.ts b/packages/node/src/integrations/onuncaughtexception.ts index 8d2d68610992..f7f875c1ec8f 100644 --- a/packages/node/src/integrations/onuncaughtexception.ts +++ b/packages/node/src/integrations/onuncaughtexception.ts @@ -1,5 +1,6 @@ -import { getCurrentHub, logger, Scope } from '@sentry/core'; +import { getCurrentHub, Scope } from '@sentry/core'; import { Integration, SentryEvent, Severity } from '@sentry/types'; +import { logger } from '@sentry/utils/logger'; import { defaultOnFatalError } from '../handlers'; /** Global Promise Rejection handler */ diff --git a/packages/utils/src/logger.ts b/packages/utils/src/logger.ts new file mode 100644 index 000000000000..cb2182549bb3 --- /dev/null +++ b/packages/utils/src/logger.ts @@ -0,0 +1,59 @@ +import { consoleSandbox, getGlobalObject } from './misc'; + +// TODO: Implement different loggers for different environments +const global = getGlobalObject() as Window; + +/** JSDoc */ +class Logger { + /** JSDoc */ + private enabled: boolean; + + /** JSDoc */ + public constructor() { + this.enabled = true; + } + + /** JSDoc */ + public disable(): void { + this.enabled = false; + } + + /** JSDoc */ + public enable(): void { + this.enabled = true; + } + + /** JSDoc */ + public log(...args: any[]): void { + if (!this.enabled) { + return; + } + consoleSandbox(() => { + global.console.log(`Sentry Logger [Log]: ${args.join(' ')}`); // tslint:disable-line:no-console + }); + } + + /** JSDoc */ + public warn(...args: any[]): void { + if (!this.enabled) { + return; + } + consoleSandbox(() => { + global.console.warn(`Sentry Logger [Warn]: ${args.join(' ')}`); // tslint:disable-line:no-console + }); + } + + /** JSDoc */ + public error(...args: any[]): void { + if (!this.enabled) { + return; + } + consoleSandbox(() => { + global.console.error(`Sentry Logger [Error]: ${args.join(' ')}`); // tslint:disable-line:no-console + }); + } +} + +const logger = new Logger(); + +export { logger }; diff --git a/packages/utils/src/misc.ts b/packages/utils/src/misc.ts index f18bf998b29d..35f2556b2835 100644 --- a/packages/utils/src/misc.ts +++ b/packages/utils/src/misc.ts @@ -1,4 +1,4 @@ -import { SentryEvent } from '@sentry/types'; +import { SentryEvent, SentryWrappedFunction } from '@sentry/types'; import { isString } from './is'; /** @@ -204,3 +204,39 @@ export function getEventDescription(event: SentryEvent): string { return event.event_id || ''; } } + +/** JSDoc */ +interface ExtensibleConsole extends Console { + [key: string]: any; +} + +/** JSDoc */ +export function consoleSandbox(callback: () => any): any { + const global = getGlobalObject() as Window; + const levels = ['debug', 'info', 'warn', 'error', 'log']; + + if (!('console' in global)) { + return callback(); + } + + const originalConsole = global.console as ExtensibleConsole; + const wrappedLevels: { [key: string]: any } = {}; + + // Restore all wrapped console methods + levels.forEach(level => { + if (level in global.console && (originalConsole[level] as SentryWrappedFunction).__sentry__) { + wrappedLevels[level] = (originalConsole[level] as SentryWrappedFunction).__sentry_wrapped__; + originalConsole[level] = (originalConsole[level] as SentryWrappedFunction).__sentry_original__; + } + }); + + // Perform callback manipulations + const result = callback(); + + // Revert restoration to wrapped state + Object.keys(wrappedLevels).forEach(level => { + originalConsole[level] = wrappedLevels[level]; + }); + + return result; +} From 8740e239e877ef488667d22399442797e5906c82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Og=C3=B3rek?= Date: Mon, 15 Oct 2018 13:38:08 +0200 Subject: [PATCH 09/11] Simplify getCurrentHub and inherit hub from previous domain --- packages/core/test/lib/base.test.ts | 4 +- .../lib/integrations/inboundfilters.test.ts | 3 ++ packages/hub/src/hub.ts | 41 +++++++++++-------- packages/hub/src/index.ts | 2 +- packages/node/package.json | 1 + packages/node/src/handlers.ts | 1 - packages/node/src/sdk.ts | 8 +++- 7 files changed, 38 insertions(+), 22 deletions(-) diff --git a/packages/core/test/lib/base.test.ts b/packages/core/test/lib/base.test.ts index cb050dbd16ca..e90850e6edcf 100644 --- a/packages/core/test/lib/base.test.ts +++ b/packages/core/test/lib/base.test.ts @@ -26,8 +26,8 @@ jest.mock('@sentry/utils/misc', () => ({ }, }; }, - consoleSandbox(cb: () => void): void { - cb(); + consoleSandbox(cb: () => any): any { + return cb(); }, })); diff --git a/packages/core/test/lib/integrations/inboundfilters.test.ts b/packages/core/test/lib/integrations/inboundfilters.test.ts index 98c146995fac..66232ef8ce4a 100644 --- a/packages/core/test/lib/integrations/inboundfilters.test.ts +++ b/packages/core/test/lib/integrations/inboundfilters.test.ts @@ -1,5 +1,8 @@ +import { logger } from '@sentry/utils/logger'; import { InboundFilters } from '../../../src/integrations/inboundfilters'; +logger.disable(); + let inboundFilters: InboundFilters; describe('InboundFilters', () => { diff --git a/packages/hub/src/hub.ts b/packages/hub/src/hub.ts index e8034bc2502b..51ab54c95797 100644 --- a/packages/hub/src/hub.ts +++ b/packages/hub/src/hub.ts @@ -325,28 +325,21 @@ export function getCurrentHub(): Hub { registry.hub = new Hub(); } - let domain = null; try { - domain = process.domain as SentryDomain; - } catch (_Oo) { - // We do not have process - } + const domain = process.domain as SentryDomain; - if (!domain) { - return registry.hub; - } + if (!domain.__SENTRY__) { + return registry.hub; + } - let carrier = domain.__SENTRY__; - if (!carrier) { - domain.__SENTRY__ = carrier = {}; - } + if (!domain.__SENTRY__.hub || domain.__SENTRY__.hub.isOlderThan(API_VERSION)) { + domain.__SENTRY__.hub = new Hub(registry.hub.getStackTop().client, Scope.clone(registry.hub.getStackTop().scope)); + } - if (!carrier.hub) { - const top = registry.hub.getStackTop(); - carrier.hub = top ? new Hub(top.client, Scope.clone(top.scope)) : new Hub(); + return domain.__SENTRY__.hub; + } catch (_Oo) { + return registry.hub; } - - return carrier.hub; } /** @@ -363,3 +356,17 @@ export function getHubFromCarrier(carrier: any): Hub { return carrier.__SENTRY__.hub; } } + +/** + * This will set passed {@link Hub} on the passed object's __SENTRY__.hub attribute + * @param carrier object + * @param hub Hub + */ +export function setHubOnCarrier(carrier: any, hub: Hub): boolean { + if (!carrier) { + return false; + } + carrier.__SENTRY__ = carrier.__SENTRY__ || {}; + carrier.__SENTRY__.hub = hub; + return true; +} diff --git a/packages/hub/src/index.ts b/packages/hub/src/index.ts index 85edb7077622..743beb0302ef 100644 --- a/packages/hub/src/index.ts +++ b/packages/hub/src/index.ts @@ -1,3 +1,3 @@ export { Carrier, Layer } from './interfaces'; export { addGlobalEventProcessor, Scope } from './scope'; -export { getCurrentHub, getHubFromCarrier, Hub } from './hub'; +export { getCurrentHub, getHubFromCarrier, getMainCarrier, Hub, setHubOnCarrier } from './hub'; diff --git a/packages/node/package.json b/packages/node/package.json index b63fbf0e4ad9..5019252ed8b5 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -16,6 +16,7 @@ }, "dependencies": { "@sentry/core": "4.1.1", + "@sentry/hub": "4.1.1", "@sentry/types": "4.1.0", "@sentry/utils": "4.1.1", "cookie": "0.3.1", diff --git a/packages/node/src/handlers.ts b/packages/node/src/handlers.ts index cb2a64752808..fa91544a621c 100644 --- a/packages/node/src/handlers.ts +++ b/packages/node/src/handlers.ts @@ -1,5 +1,4 @@ import { getCurrentHub } from '@sentry/core'; -import { Carrier, Hub } from '@sentry/hub'; import { SentryEvent } from '@sentry/types'; import { forget } from '@sentry/utils/async'; import { logger } from '@sentry/utils/logger'; diff --git a/packages/node/src/sdk.ts b/packages/node/src/sdk.ts index e276d86082a6..bc92185e979b 100644 --- a/packages/node/src/sdk.ts +++ b/packages/node/src/sdk.ts @@ -1,4 +1,5 @@ -import { initAndBind, Integrations as CoreIntegrations } from '@sentry/core'; +import { getCurrentHub, initAndBind, Integrations as CoreIntegrations } from '@sentry/core'; +import { getMainCarrier, setHubOnCarrier } from '@sentry/hub'; import { NodeOptions } from './backend'; import { NodeClient } from './client'; import { Console, Http, LinkedErrors, OnUncaughtException, OnUnhandledRejection } from './integrations'; @@ -66,5 +67,10 @@ export function init(options: NodeOptions = {}): void { if (options.defaultIntegrations === undefined) { options.defaultIntegrations = defaultIntegrations; } + + if (process.domain) { + setHubOnCarrier(getMainCarrier(), getCurrentHub()); + } + initAndBind(NodeClient, options); } From 32f7ff0c0113ed1b73e44b5a1e61fad0acffe3f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Og=C3=B3rek?= Date: Tue, 16 Oct 2018 14:21:10 +0200 Subject: [PATCH 10/11] i did it... --- packages/browser/package.json | 1 + packages/browser/rollup.config.js | 4 ++ packages/hub/src/hub.ts | 62 ++++++++++++++++++++++--------- packages/hub/src/interfaces.ts | 4 +- packages/node/src/sdk.ts | 3 +- packages/node/test/domain.test.ts | 36 ++++++++++-------- packages/node/test/index.test.ts | 1 - yarn.lock | 4 ++ 8 files changed, 78 insertions(+), 37 deletions(-) diff --git a/packages/browser/package.json b/packages/browser/package.json index b0f76974c020..99471394525f 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -42,6 +42,7 @@ "rollup-plugin-license": "^0.6.0", "rollup-plugin-node-resolve": "^3.3.0", "rollup-plugin-npm": "^2.0.0", + "rollup-plugin-shim": "^1.0.0", "rollup-plugin-typescript2": "^0.13.0", "rollup-plugin-uglify": "^3.0.0", "sinon": "^5.0.3", diff --git a/packages/browser/rollup.config.js b/packages/browser/rollup.config.js index 3516947446f1..6c51fecc40ea 100644 --- a/packages/browser/rollup.config.js +++ b/packages/browser/rollup.config.js @@ -3,6 +3,7 @@ import uglify from 'rollup-plugin-uglify'; import resolve from 'rollup-plugin-node-resolve'; import typescript from 'rollup-plugin-typescript2'; import license from 'rollup-plugin-license'; +import shim from 'rollup-plugin-shim'; const commitHash = require('child_process') .execSync('git rev-parse --short HEAD', { encoding: 'utf-8' }) @@ -17,6 +18,9 @@ const bundleConfig = { }, context: 'window', plugins: [ + shim({ + domain: `export default {}`, + }), typescript({ tsconfig: 'tsconfig.build.json', tsconfigOverride: { compilerOptions: { declaration: false } }, diff --git a/packages/hub/src/hub.ts b/packages/hub/src/hub.ts index 51ab54c95797..2f07f96e5f15 100644 --- a/packages/hub/src/hub.ts +++ b/packages/hub/src/hub.ts @@ -9,9 +9,20 @@ import { } from '@sentry/types'; import { logger } from '@sentry/utils/logger'; import { getGlobalObject, uuid4 } from '@sentry/utils/misc'; +import * as domain from 'domain'; import { Carrier, Layer } from './interfaces'; import { Scope } from './scope'; +declare module 'domain' { + export let active: Domain; + /** + * Extension for domain interface + */ + export interface Domain { + __SENTRY__?: Carrier; + } +} + /** * API compatibility version of this hub. * @@ -291,7 +302,7 @@ export function getMainCarrier(): Carrier { carrier.__SENTRY__ = carrier.__SENTRY__ || { hub: undefined, }; - return carrier.__SENTRY__; + return carrier; } /** @@ -299,18 +310,13 @@ export function getMainCarrier(): Carrier { * * @returns The old replaced hub */ -export function makeMain(hub?: Hub): Hub | undefined { +export function makeMain(hub: Hub): Hub { const registry = getMainCarrier(); - const oldHub = registry.hub; - registry.hub = hub; + const oldHub = getHubFromCarrier(registry); + setHubOnCarrier(registry, hub); return oldHub; } -/** Domain interface with attached Hub */ -interface SentryDomain extends NodeJS.Domain { - __SENTRY__?: Carrier; -} - /** * Returns the default hub instance. * @@ -319,26 +325,46 @@ interface SentryDomain extends NodeJS.Domain { * Otherwise, the currently registered hub will be returned. */ export function getCurrentHub(): Hub { + // Get main carrier (global for every environment) const registry = getMainCarrier(); - if (!registry.hub || registry.hub.isOlderThan(API_VERSION)) { - registry.hub = new Hub(); + // If there's no hub, or its an old API, assign a new one + if (!hasHubOnCarrier(registry) || getHubFromCarrier(registry).isOlderThan(API_VERSION)) { + setHubOnCarrier(registry, new Hub()); } + // Prefer domains over global if they are there try { - const domain = process.domain as SentryDomain; + const activeDomain = domain.active; - if (!domain.__SENTRY__) { - return registry.hub; + // If there no active domain, just return global hub + if (!activeDomain) { + return getHubFromCarrier(registry); } - if (!domain.__SENTRY__.hub || domain.__SENTRY__.hub.isOlderThan(API_VERSION)) { - domain.__SENTRY__.hub = new Hub(registry.hub.getStackTop().client, Scope.clone(registry.hub.getStackTop().scope)); + // If there's no hub on current domain, or its an old API, assign a new one + if (!hasHubOnCarrier(activeDomain) || getHubFromCarrier(activeDomain).isOlderThan(API_VERSION)) { + const registryHubTopStack = getHubFromCarrier(registry).getStackTop(); + setHubOnCarrier(activeDomain, new Hub(registryHubTopStack.client, Scope.clone(registryHubTopStack.scope))); } - return domain.__SENTRY__.hub; + // Return hub that lives on a domain + return getHubFromCarrier(activeDomain); } catch (_Oo) { - return registry.hub; + // Return hub that lives on a global object + return getHubFromCarrier(registry); + } +} + +/** + * This will tell whether a carrier has a hub on it or not + * @param carrier object + */ +export function hasHubOnCarrier(carrier: any): boolean { + if (carrier && carrier.__SENTRY__ && carrier.__SENTRY__.hub) { + return true; + } else { + return false; } } diff --git a/packages/hub/src/interfaces.ts b/packages/hub/src/interfaces.ts index 99f2066b77b8..e1c25ba4969b 100644 --- a/packages/hub/src/interfaces.ts +++ b/packages/hub/src/interfaces.ts @@ -9,5 +9,7 @@ export interface Layer { /** An object that contains a hub and maintains a scope stack. */ export interface Carrier { - hub?: Hub; + __SENTRY__?: { + hub?: Hub; + }; } diff --git a/packages/node/src/sdk.ts b/packages/node/src/sdk.ts index bc92185e979b..af32e8f38c0c 100644 --- a/packages/node/src/sdk.ts +++ b/packages/node/src/sdk.ts @@ -1,5 +1,6 @@ import { getCurrentHub, initAndBind, Integrations as CoreIntegrations } from '@sentry/core'; import { getMainCarrier, setHubOnCarrier } from '@sentry/hub'; +import * as domain from 'domain'; import { NodeOptions } from './backend'; import { NodeClient } from './client'; import { Console, Http, LinkedErrors, OnUncaughtException, OnUnhandledRejection } from './integrations'; @@ -68,7 +69,7 @@ export function init(options: NodeOptions = {}): void { options.defaultIntegrations = defaultIntegrations; } - if (process.domain) { + if (domain.active) { setHubOnCarrier(getMainCarrier(), getCurrentHub()); } diff --git a/packages/node/test/domain.test.ts b/packages/node/test/domain.test.ts index b4ac33e3aeec..327c8ff80cc2 100644 --- a/packages/node/test/domain.test.ts +++ b/packages/node/test/domain.test.ts @@ -26,35 +26,39 @@ describe('domains', () => { const d = domain.create(); d.run(() => { expect(getCurrentHub()).toBe(getCurrentHub()); - d.exit(); }); }); test('concurrent domain hubs', done => { const d1 = domain.create(); const d2 = domain.create(); + let d1done = false; + let d2done = false; d1.run(() => { - getCurrentHub() - .getStack() - .push({ client: 'process' }); - + const hub = getCurrentHub(); + hub.getStack().push({ client: 'process' }); + expect(hub.getStack()[1]).toEqual({ client: 'process' }); + // Just in case so we don't have to worry which one finishes first + // (although it always should be d2) setTimeout(() => { - expect(getCurrentHub().getStack()[1]).toEqual({ client: 'process' }); - d1.exit(); - }, 50); + d1done = true; + if (d2done) { + done(); + } + }); }); d2.run(() => { - getCurrentHub() - .getStack() - .push({ client: 'local' }); - + const hub = getCurrentHub(); + hub.getStack().push({ client: 'local' }); + expect(hub.getStack()[1]).toEqual({ client: 'local' }); setTimeout(() => { - expect(getCurrentHub().getStack()[1]).toEqual({ client: 'local' }); - d2.exit(); - done(); - }, 100); + d2done = true; + if (d1done) { + done(); + } + }); }); }); }); diff --git a/packages/node/test/index.test.ts b/packages/node/test/index.test.ts index 1305603b5776..5af3ae473a00 100644 --- a/packages/node/test/index.test.ts +++ b/packages/node/test/index.test.ts @@ -217,7 +217,6 @@ describe('SentryNode', () => { expect(event.message).toBe('test domain'); expect(event.exception).toBeUndefined(); done(); - d.exit(); return event; }, dsn, diff --git a/yarn.lock b/yarn.lock index 50047f65fc7f..7642a9612a1d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7859,6 +7859,10 @@ rollup-plugin-npm@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/rollup-plugin-npm/-/rollup-plugin-npm-2.0.0.tgz#8a28ffdb5160bc8e1e371de39ed71faf009d655c" +rollup-plugin-shim@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/rollup-plugin-shim/-/rollup-plugin-shim-1.0.0.tgz#b00f5cb44cdae81358c5342fe82fa71a1602b56b" + rollup-plugin-typescript2@^0.13.0: version "0.13.0" resolved "https://registry.yarnpkg.com/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.13.0.tgz#5fc838657d05af82e04554832cadf06cdb32f657" From bfcac22598ef58b846615fb3a68a50ee2700fe75 Mon Sep 17 00:00:00 2001 From: Daniel Griesser Date: Wed, 17 Oct 2018 10:38:28 +0200 Subject: [PATCH 11/11] Update CHANGELOG.md --- CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8ae0030756d..105423ec1afc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,7 @@ - [browser] fix: Make `addBreadcrumb` sync internally, `beforeBreadcrumb` is now only sync - [browser] fix: Remove internal `console` guard in `beforeBreadcrumb` - [core]: Integrations now live on the `Client`. This means that when binding a new Client to the `Hub` the client - itself can decide which integration should run. It shouldn't break anything, Integrations just internally work - differently. + itself can decide which integration should run. ## 4.1.1