From 7e081b527e5c8f56d655239461134e2f12753ad8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Og=C3=B3rek?= Date: Tue, 19 Feb 2019 15:24:33 +0100 Subject: [PATCH] [v5] Major utils rewrite (#1902) * ref: Move ExtendedError to a types package * ref: Remove assign util in favor of Object.assign * ref: Pass Event to sendEvent instead of just a body * ref: Remove isArray and isNaN utils * ref: Rewrite normalization and removed unused utils * Utils rewamp changelog --- CHANGELOG.md | 14 + .../browser/src/integrations/breadcrumbs.ts | 22 +- .../src/integrations/globalhandlers.ts | 6 +- packages/browser/src/integrations/helpers.ts | 11 +- .../browser/src/integrations/linkederrors.ts | 9 +- .../browser/src/integrations/pluggable/vue.ts | 4 +- packages/browser/src/parsers.ts | 12 +- packages/browser/src/tracekit.ts | 10 +- packages/browser/src/transports/base.ts | 4 +- packages/browser/src/transports/beacon.ts | 6 +- packages/browser/src/transports/fetch.ts | 6 +- packages/browser/src/transports/xhr.ts | 6 +- packages/browser/test/integration/common.js | 2 +- .../test/integrations/linkederrors.test.ts | 5 +- .../browser/test/mocks/simpletransport.ts | 4 +- packages/browser/test/transports/base.test.ts | 2 +- .../browser/test/transports/beacon.test.ts | 4 +- .../browser/test/transports/fetch.test.ts | 4 +- packages/browser/test/transports/xhr.test.ts | 4 +- packages/core/src/basebackend.ts | 3 +- packages/core/src/dsn.ts | 6 +- .../core/src/integrations/extraerrordata.ts | 20 +- .../core/src/integrations/inboundfilters.ts | 3 +- packages/core/src/transports/noop.ts | 4 +- .../lib/integrations/extraerrordata.test.ts | 9 +- packages/hub/src/scope.ts | 27 +- packages/node/src/backend.ts | 7 +- packages/node/src/handlers.ts | 7 +- .../node/src/integrations/linkederrors.ts | 9 +- packages/node/src/parsers.ts | 9 +- packages/node/src/transports/base.ts | 8 +- packages/node/src/transports/http.ts | 6 +- packages/node/src/transports/https.ts | 6 +- .../test/integrations/linkederrors.test.ts | 5 +- packages/node/test/transports/http.test.ts | 40 +- packages/node/test/transports/https.test.ts | 48 +- packages/types/src/error.ts | 6 + packages/types/src/index.ts | 1 + packages/types/src/transport.ts | 3 +- packages/utils/src/is.ts | 56 +- packages/utils/src/object.ts | 329 +++------ packages/utils/src/string.ts | 37 +- packages/utils/test/object.test.ts | 691 ++++++++---------- packages/utils/test/string.test.ts | 41 +- 44 files changed, 622 insertions(+), 894 deletions(-) create mode 100644 packages/types/src/error.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d2f975d8052..c937fe7db356 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,20 @@ since we removed some methods from the public API and removed some classes from - **breaking** [core] ref: Use `SyncPromise` internally, this reduces memory pressure by a lot. - **breaking** [browser] ref: Removed `BrowserBackend` from default export. - **breaking** [node] ref: Removed `BrowserBackend` from default export. +- ref: Move internal `ExtendedError` to a types package +- **breaking** [core] ref: Pass `Event` to `sendEvent` instead of already stringified data +- [utils] feat: Introduce `isSyntheticEvent` util +- **breaking** [utils] ref: remove `isArray` util in favor of `Array.isArray` +- **breaking** [utils] ref: Remove `isNaN` util in favor of `Number.isNaN` +- **breaking** [utils] ref: Remove `isFunction` util in favor of `typeof === 'function'` +- **breaking** [utils] ref: Remove `isUndefined` util in favor of `=== void 0` +- **breaking** [utils] ref: Remove `assign` util in favor of `Object.assign` +- **breaking** [utils] ref: Remove `includes` util in favor of native `includes` +- **breaking** [utils] ref: Rename `serializeKeysToEventMessage` to `keysToEventMessage` +- **breaking** [utils] ref: Rename `limitObjectDepthToSize` to `normalizeToSize` and rewrite its internals +- **breaking** [utils] ref: Rename `safeNormalize` to `normalize` and rewrite its internals +- **breaking** [utils] ref: Remove `serialize`, `deserialize`, `clone` and `serializeObject` functions +- **breaking** [utils] ref: Rewrite normalization functions by removing most of them and leaving just `normalize` and `normalizeToSize` ## 4.6.2 diff --git a/packages/browser/src/integrations/breadcrumbs.ts b/packages/browser/src/integrations/breadcrumbs.ts index 1a9bfc2613c1..2fcbd7c0f876 100644 --- a/packages/browser/src/integrations/breadcrumbs.ts +++ b/packages/browser/src/integrations/breadcrumbs.ts @@ -1,10 +1,10 @@ import { API, getCurrentHub } from '@sentry/core'; import { Breadcrumb, BreadcrumbHint, Integration, Severity } from '@sentry/types'; -import { isFunction, isString } from '@sentry/utils/is'; +import { isString } from '@sentry/utils/is'; import { logger } from '@sentry/utils/logger'; import { getEventDescription, getGlobalObject, parseUrl } from '@sentry/utils/misc'; -import { deserialize, fill, serializeObject } from '@sentry/utils/object'; -import { includes, safeJoin } from '@sentry/utils/string'; +import { fill, normalize } from '@sentry/utils/object'; +import { safeJoin } from '@sentry/utils/string'; import { supportsBeacon, supportsHistory, supportsNativeFetch } from '@sentry/utils/supports'; import { BrowserClient } from '../client'; import { breadcrumbEventHandler, keypressEventHandler, wrap } from './helpers'; @@ -87,7 +87,7 @@ export class Breadcrumbs implements Integration { 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 (filterUrl && url.includes(filterUrl)) { addSentryBreadcrumb(data); return result; } @@ -132,7 +132,7 @@ export class Breadcrumbs implements Integration { category: 'console', data: { extra: { - arguments: serializeObject(args, 2), + arguments: normalize(args, 2), }, logger: 'console', }, @@ -143,7 +143,7 @@ export class Breadcrumbs implements Integration { if (level === 'assert') { if (args[0] === false) { breadcrumbData.message = `Assertion failed: ${safeJoin(args.slice(1), ' ') || 'console.assert'}`; - breadcrumbData.data.extra.arguments = serializeObject(args.slice(1), 2); + breadcrumbData.data.extra.arguments = normalize(args.slice(1), 2); } } @@ -205,7 +205,7 @@ export class Breadcrumbs implements Integration { 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 (filterUrl && url.includes(filterUrl)) { if (method === 'POST' && args[1] && args[1].body) { addSentryBreadcrumb(args[1].body); } @@ -338,7 +338,7 @@ export class Breadcrumbs implements Integration { /** JSDoc */ function wrapProp(prop: string, xhr: XMLHttpRequest): void { // TODO: Fix XHR types - if (prop in xhr && isFunction((xhr as { [key: string]: any })[prop])) { + if (prop in xhr && typeof (xhr as { [key: string]: any })[prop] === 'function') { fill(xhr, prop, original => wrap(original, { mechanism: { @@ -372,7 +372,7 @@ export class Breadcrumbs implements Integration { 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))) { + if (isString(url) && (filterUrl && url.includes(filterUrl))) { this.__sentry_own_request__ = true; } } @@ -424,7 +424,7 @@ export class Breadcrumbs implements Integration { wrapProp(prop, xhr); }); - if ('onreadystatechange' in xhr && isFunction(xhr.onreadystatechange)) { + if ('onreadystatechange' in xhr && typeof xhr.onreadystatechange === 'function') { fill(xhr, 'onreadystatechange', function(original: () => void): void { return wrap( original, @@ -496,7 +496,7 @@ export class Breadcrumbs implements Integration { function addSentryBreadcrumb(serializedData: string): void { // There's always something that can go wrong with deserialization... try { - const event: { [key: string]: any } = deserialize(serializedData); + const event: { [key: string]: any } = JSON.parse(serializedData); Breadcrumbs.addBreadcrumb( { category: 'sentry', diff --git a/packages/browser/src/integrations/globalhandlers.ts b/packages/browser/src/integrations/globalhandlers.ts index 1b04a2cfdb28..87dad8aabebe 100644 --- a/packages/browser/src/integrations/globalhandlers.ts +++ b/packages/browser/src/integrations/globalhandlers.ts @@ -1,7 +1,6 @@ import { getCurrentHub } from '@sentry/core'; import { Event, Integration } from '@sentry/types'; import { logger } from '@sentry/utils/logger'; -import { safeNormalize, serialize } from '@sentry/utils/object'; import { truncate } from '@sentry/utils/string'; import { addExceptionTypeValue, eventFromStacktrace } from '../parsers'; import { @@ -114,10 +113,7 @@ export class GlobalHandlers implements Integration { }, }; - const fallbackValue = - typeof stacktrace.original !== 'undefined' - ? `${truncate(serialize(safeNormalize(stacktrace.original)), 300)}` - : ''; + const fallbackValue = stacktrace.original ? truncate(JSON.stringify(stacktrace.original), 300) : ''; const fallbackType = stacktrace.mechanism === 'onunhandledrejection' ? 'UnhandledRejection' : 'Error'; // This makes sure we have type/value in every exception diff --git a/packages/browser/src/integrations/helpers.ts b/packages/browser/src/integrations/helpers.ts index bddf5ff0324f..0e8402bc2a46 100644 --- a/packages/browser/src/integrations/helpers.ts +++ b/packages/browser/src/integrations/helpers.ts @@ -1,8 +1,7 @@ import { captureException, getCurrentHub, withScope } from '@sentry/core'; import { Event as SentryEvent, Mechanism, WrappedFunction } from '@sentry/types'; -import { isFunction } from '@sentry/utils/is'; import { htmlTreeAsString } from '@sentry/utils/misc'; -import { serializeObject } from '@sentry/utils/object'; +import { normalize } from '@sentry/utils/object'; const debounceDuration: number = 1000; let keypressTimeout: number | undefined; @@ -42,7 +41,8 @@ export function wrap( } = {}, before?: WrappedFunction, ): any { - if (!isFunction(fn)) { + // tslint:disable-next-line:strict-type-predicates + if (typeof fn !== 'function') { return fn; } @@ -64,7 +64,8 @@ export function wrap( } const sentryWrapped: WrappedFunction = function(this: any): void { - if (before && isFunction(before)) { + // tslint:disable-next-line:strict-type-predicates + if (before && typeof before === 'function') { before.apply(this, arguments); } @@ -96,7 +97,7 @@ export function wrap( processedEvent.extra = { ...processedEvent.extra, - arguments: serializeObject(args, 2), + arguments: normalize(args, 2), }; return processedEvent; diff --git a/packages/browser/src/integrations/linkederrors.ts b/packages/browser/src/integrations/linkederrors.ts index bda28e490a6c..b77c4e2db8bc 100644 --- a/packages/browser/src/integrations/linkederrors.ts +++ b/packages/browser/src/integrations/linkederrors.ts @@ -1,18 +1,11 @@ import { addGlobalEventProcessor, getCurrentHub } from '@sentry/core'; -import { Event, EventHint, Exception, Integration } from '@sentry/types'; +import { Event, EventHint, Exception, ExtendedError, Integration } from '@sentry/types'; import { exceptionFromStacktrace } from '../parsers'; import { computeStackTrace } from '../tracekit'; const DEFAULT_KEY = 'cause'; const DEFAULT_LIMIT = 5; -/** - * Just an Error object with arbitrary attributes attached to it. - */ -interface ExtendedError extends Error { - [key: string]: any; -} - /** Adds SDK info to an event. */ export class LinkedErrors implements Integration { /** diff --git a/packages/browser/src/integrations/pluggable/vue.ts b/packages/browser/src/integrations/pluggable/vue.ts index 461a7844240e..b8bd02dd1d3d 100644 --- a/packages/browser/src/integrations/pluggable/vue.ts +++ b/packages/browser/src/integrations/pluggable/vue.ts @@ -1,6 +1,6 @@ import { captureException, getCurrentHub, withScope } from '@sentry/core'; import { Event, Integration } from '@sentry/types'; -import { isPlainObject, isUndefined } from '@sentry/utils/is'; +import { isPlainObject } from '@sentry/utils/is'; import { logger } from '@sentry/utils/logger'; import { getGlobalObject } from '@sentry/utils/misc'; @@ -82,7 +82,7 @@ export class Vue implements Integration { } } - if (!isUndefined(info)) { + if (info !== void 0) { metadata.lifecycleHook = info; } diff --git a/packages/browser/src/parsers.ts b/packages/browser/src/parsers.ts index 67a03f5218f4..c90d7463369c 100644 --- a/packages/browser/src/parsers.ts +++ b/packages/browser/src/parsers.ts @@ -1,6 +1,6 @@ import { Event, Exception, StackFrame } from '@sentry/types'; -import { limitObjectDepthToSize, serializeKeysToEventMessage } from '@sentry/utils/object'; -import { includes } from '@sentry/utils/string'; +import { normalizeToSize } from '@sentry/utils/object'; +import { keysToEventMessage } from '@sentry/utils/string'; import { md5 } from './md5'; import { computeStackTrace, StackFrame as TraceKitStackFrame, StackTrace as TraceKitStackTrace } from './tracekit'; @@ -38,10 +38,10 @@ export function eventFromPlainObject(exception: {}, syntheticException: Error | const exceptionKeys = Object.keys(exception).sort(); const event: Event = { extra: { - __serialized__: limitObjectDepthToSize(exception), + __serialized__: normalizeToSize(exception), }, fingerprint: [md5(exceptionKeys.join(''))], - message: `Non-Error exception captured with keys: ${serializeKeysToEventMessage(exceptionKeys)}`, + message: `Non-Error exception captured with keys: ${keysToEventMessage(exceptionKeys)}`, }; if (syntheticException) { @@ -82,12 +82,12 @@ export function prepareFramesForEvent(stack: TraceKitStackFrame[]): StackFrame[] const lastFrameFunction = localStack[localStack.length - 1].func || ''; // If stack starts with one of our API calls, remove it (starts, meaning it's the top of the stack - aka last call) - if (includes(firstFrameFunction, 'captureMessage') || includes(firstFrameFunction, 'captureException')) { + if (firstFrameFunction.includes('captureMessage') || firstFrameFunction.includes('captureException')) { localStack = localStack.slice(1); } // If stack ends with one of our internal API calls, remove it (ends, meaning it's the bottom of the stack - aka top-most call) - if (includes(lastFrameFunction, 'sentryWrapped')) { + if (lastFrameFunction.includes('sentryWrapped')) { localStack = localStack.slice(0, -1); } diff --git a/packages/browser/src/tracekit.ts b/packages/browser/src/tracekit.ts index 4176df2c8d9b..dd1039fc8ddc 100644 --- a/packages/browser/src/tracekit.ts +++ b/packages/browser/src/tracekit.ts @@ -1,6 +1,6 @@ // tslint:disable -import { isUndefined, isError, isErrorEvent } from '@sentry/utils/is'; +import { isError, isErrorEvent } from '@sentry/utils/is'; import { getGlobalObject } from '@sentry/utils/misc'; /** @@ -712,7 +712,7 @@ TraceKit.computeStackTrace = (function computeStackTraceWrapper() { for (var i = 0; i < maxLines; ++i) { line = source[lineNo - i] + line; - if (!isUndefined(line)) { + if (line !== void 0) { if ((m = reGuessFunction.exec(line))) { return m[1]; } else if ((m = reFunctionArgNames.exec(line))) { @@ -751,7 +751,7 @@ TraceKit.computeStackTrace = (function computeStackTraceWrapper() { line -= 1; // convert to 0-based index for (var i = start; i < end; ++i) { - if (!isUndefined(source[i])) { + if (source[i] !== void 0) { context.push(source[i]); } } @@ -845,7 +845,7 @@ TraceKit.computeStackTrace = (function computeStackTraceWrapper() { * @memberof TraceKit.computeStackTrace */ function findSourceByFunctionBody(func: any) { - if (isUndefined(window && window.document)) { + if (window && window.document === void 0) { return; } @@ -1005,7 +1005,7 @@ TraceKit.computeStackTrace = (function computeStackTraceWrapper() { // NOTE: It's messing out our integration tests in Karma, let's see if we can live with it – Kamil // parts[4] = submatch[2]; // parts[5] = null; // no column when eval - } else if (i === 0 && !parts[5] && !isUndefined(ex.columnNumber)) { + } else if (i === 0 && !parts[5] && ex.columnNumber !== void 0) { // FireFox uses this awesome columnNumber property for its top frame // Also note, Firefox's column number is 0-based and everything else expects 1-based, // so adding 1 diff --git a/packages/browser/src/transports/base.ts b/packages/browser/src/transports/base.ts index baf9688fe9be..637e77a76f55 100644 --- a/packages/browser/src/transports/base.ts +++ b/packages/browser/src/transports/base.ts @@ -1,5 +1,5 @@ import { API } from '@sentry/core'; -import { Response, Transport, TransportOptions } from '@sentry/types'; +import { Event, Response, Transport, TransportOptions } from '@sentry/types'; import { SentryError } from '@sentry/utils/error'; import { PromiseBuffer } from '@sentry/utils/promisebuffer'; @@ -20,7 +20,7 @@ export abstract class BaseTransport implements Transport { /** * @inheritDoc */ - public async sendEvent(_: string): Promise { + public async sendEvent(_: Event): Promise { throw new SentryError('Transport Class has to implement `sendEvent` method'); } diff --git a/packages/browser/src/transports/beacon.ts b/packages/browser/src/transports/beacon.ts index e2ffd37ac67f..ec4f8382bac3 100644 --- a/packages/browser/src/transports/beacon.ts +++ b/packages/browser/src/transports/beacon.ts @@ -1,4 +1,4 @@ -import { Response, Status } from '@sentry/types'; +import { Event, Response, Status } from '@sentry/types'; import { getGlobalObject } from '@sentry/utils/misc'; import { BaseTransport } from './base'; @@ -9,8 +9,8 @@ export class BeaconTransport extends BaseTransport { /** * @inheritDoc */ - public async sendEvent(body: string): Promise { - const result = global.navigator.sendBeacon(this.url, body); + public async sendEvent(event: Event): Promise { + const result = global.navigator.sendBeacon(this.url, JSON.stringify(event)); return this.buffer.add( Promise.resolve({ diff --git a/packages/browser/src/transports/fetch.ts b/packages/browser/src/transports/fetch.ts index f27356b41bd7..1d94982c35d5 100644 --- a/packages/browser/src/transports/fetch.ts +++ b/packages/browser/src/transports/fetch.ts @@ -1,4 +1,4 @@ -import { Response, Status } from '@sentry/types'; +import { Event, Response, Status } from '@sentry/types'; import { getGlobalObject } from '@sentry/utils/misc'; import { supportsReferrerPolicy } from '@sentry/utils/supports'; import { BaseTransport } from './base'; @@ -10,9 +10,9 @@ export class FetchTransport extends BaseTransport { /** * @inheritDoc */ - public async sendEvent(body: string): Promise { + public async sendEvent(event: Event): Promise { const defaultOptions: RequestInit = { - body, + body: JSON.stringify(event), method: 'POST', // Despite all stars in the sky saying that Edge supports old draft syntax, aka 'never', 'always', 'origin' and 'default // https://caniuse.com/#feat=referrer-policy diff --git a/packages/browser/src/transports/xhr.ts b/packages/browser/src/transports/xhr.ts index 15e6e6b9d670..f70e49ce443f 100644 --- a/packages/browser/src/transports/xhr.ts +++ b/packages/browser/src/transports/xhr.ts @@ -1,4 +1,4 @@ -import { Response, Status } from '@sentry/types'; +import { Event, Response, Status } from '@sentry/types'; import { BaseTransport } from './base'; /** `XHR` based transport */ @@ -6,7 +6,7 @@ export class XHRTransport extends BaseTransport { /** * @inheritDoc */ - public async sendEvent(body: string): Promise { + public async sendEvent(event: Event): Promise { return this.buffer.add( new Promise((resolve, reject) => { const request = new XMLHttpRequest(); @@ -26,7 +26,7 @@ export class XHRTransport extends BaseTransport { }; request.open('POST', this.url); - request.send(body); + request.send(JSON.stringify(event)); }), ); } diff --git a/packages/browser/test/integration/common.js b/packages/browser/test/integration/common.js index 319a71ff43b1..0092647bd3ef 100644 --- a/packages/browser/test/integration/common.js +++ b/packages/browser/test/integration/common.js @@ -107,7 +107,7 @@ function initSDK() { // stub transport so we don't actually transmit any data function DummyTransport() {} DummyTransport.prototype.sendEvent = function(event) { - sentryData.push(JSON.parse(event)); + sentryData.push(event); done(sentryData); return Promise.resolve({ status: 'success', diff --git a/packages/browser/test/integrations/linkederrors.test.ts b/packages/browser/test/integrations/linkederrors.test.ts index 27a6dfdc995b..f4b9c1a56057 100644 --- a/packages/browser/test/integrations/linkederrors.test.ts +++ b/packages/browser/test/integrations/linkederrors.test.ts @@ -1,3 +1,4 @@ +import { ExtendedError } from '@sentry/types'; import { expect } from 'chai'; import { stub } from 'sinon'; import { BrowserBackend } from '../../src/backend'; @@ -5,10 +6,6 @@ import { LinkedErrors } from '../../src/integrations/linkederrors'; let linkedErrors: LinkedErrors; -interface ExtendedError extends Error { - [key: string]: any; -} - describe('LinkedErrors', () => { beforeEach(() => { linkedErrors = new LinkedErrors(); diff --git a/packages/browser/test/mocks/simpletransport.ts b/packages/browser/test/mocks/simpletransport.ts index 23c90efb27fe..27d100d36c1a 100644 --- a/packages/browser/test/mocks/simpletransport.ts +++ b/packages/browser/test/mocks/simpletransport.ts @@ -1,8 +1,8 @@ -import { Response, Status } from '../../src'; +import { Event, Response, Status } from '../../src'; import { BaseTransport } from '../../src/transports'; export class SimpleTransport extends BaseTransport { - public async sendEvent(_: string): Promise { + public async sendEvent(_: Event): Promise { return this.buffer.add( Promise.resolve({ status: Status.fromHttpCode(200), diff --git a/packages/browser/test/transports/base.test.ts b/packages/browser/test/transports/base.test.ts index 907eb6190c88..5da9501730b8 100644 --- a/packages/browser/test/transports/base.test.ts +++ b/packages/browser/test/transports/base.test.ts @@ -10,7 +10,7 @@ describe('BaseTransport', () => { const transport = new SimpleTransport({ dsn: testDsn }); try { - await transport.sendEvent(''); + await transport.sendEvent({}); } catch (e) { expect(e.message).equal('Transport Class has to implement `sendEvent` method'); } diff --git a/packages/browser/test/transports/beacon.test.ts b/packages/browser/test/transports/beacon.test.ts index 9d6bce60be74..50615c068363 100644 --- a/packages/browser/test/transports/beacon.test.ts +++ b/packages/browser/test/transports/beacon.test.ts @@ -33,7 +33,7 @@ describe('BeaconTransport', () => { it('sends a request to Sentry servers', async () => { sendBeacon.returns(true); - return transport.sendEvent(JSON.stringify(payload)).then(res => { + return transport.sendEvent(payload).then(res => { expect(res.status).equal(Status.Success); expect(sendBeacon.calledOnce).equal(true); expect(sendBeacon.calledWith(transportUrl, JSON.stringify(payload))).equal(true); @@ -43,7 +43,7 @@ describe('BeaconTransport', () => { it('rejects with failed status', async () => { sendBeacon.returns(false); - return transport.sendEvent(JSON.stringify(payload)).catch(res => { + return transport.sendEvent(payload).catch(res => { expect(res.status).equal(Status.Failed); expect(sendBeacon.calledOnce).equal(true); expect(sendBeacon.calledWith(transportUrl, JSON.stringify(payload))).equal(true); diff --git a/packages/browser/test/transports/fetch.test.ts b/packages/browser/test/transports/fetch.test.ts index b7be26bb3e46..ba8e9edbc5d4 100644 --- a/packages/browser/test/transports/fetch.test.ts +++ b/packages/browser/test/transports/fetch.test.ts @@ -35,7 +35,7 @@ describe('FetchTransport', () => { fetch.returns(Promise.resolve(response)); - return transport.sendEvent(JSON.stringify(payload)).then(res => { + return transport.sendEvent(payload).then(res => { expect(res.status).equal(Status.Success); expect(fetch.calledOnce).equal(true); expect( @@ -53,7 +53,7 @@ describe('FetchTransport', () => { fetch.returns(Promise.reject(response)); - return transport.sendEvent(JSON.stringify(payload)).catch(res => { + return transport.sendEvent(payload).catch(res => { expect(res.status).equal(403); expect(fetch.calledOnce).equal(true); expect( diff --git a/packages/browser/test/transports/xhr.test.ts b/packages/browser/test/transports/xhr.test.ts index ef586c1778cd..cb126a9e8604 100644 --- a/packages/browser/test/transports/xhr.test.ts +++ b/packages/browser/test/transports/xhr.test.ts @@ -34,7 +34,7 @@ describe('XHRTransport', () => { it('sends a request to Sentry servers', async () => { server.respondWith('POST', transportUrl, [200, {}, '']); - return transport.sendEvent(JSON.stringify(payload)).then(res => { + return transport.sendEvent(payload).then(res => { expect(res.status).equal(Status.Success); const request = server.requests[0]; expect(server.requests.length).equal(1); @@ -46,7 +46,7 @@ describe('XHRTransport', () => { it('rejects with non-200 status code', done => { server.respondWith('POST', transportUrl, [403, {}, '']); - transport.sendEvent(JSON.stringify(payload)).catch(res => { + transport.sendEvent(payload).catch(res => { expect(res.status).equal(403); const request = server.requests[0]; diff --git a/packages/core/src/basebackend.ts b/packages/core/src/basebackend.ts index 5b20a843bfc7..b51a6574dd13 100644 --- a/packages/core/src/basebackend.ts +++ b/packages/core/src/basebackend.ts @@ -1,7 +1,6 @@ import { Event, EventHint, Options, Severity, Transport } from '@sentry/types'; import { SentryError } from '@sentry/utils/error'; import { logger } from '@sentry/utils/logger'; -import { serialize } from '@sentry/utils/object'; import { SyncPromise } from '@sentry/utils/syncpromise'; import { NoopTransport } from './transports/noop'; @@ -95,7 +94,7 @@ export abstract class BaseBackend implements Backend { * @inheritDoc */ public sendEvent(event: Event): void { - this.transport.sendEvent(serialize(event)).catch(reason => { + this.transport.sendEvent(event).catch(reason => { logger.error(`Error while sending event: ${reason}`); }); } diff --git a/packages/core/src/dsn.ts b/packages/core/src/dsn.ts index 364cf2187ca0..643ffa7e662a 100644 --- a/packages/core/src/dsn.ts +++ b/packages/core/src/dsn.ts @@ -1,7 +1,5 @@ import { DsnComponents, DsnLike, DsnProtocol } from '@sentry/types'; import { SentryError } from '@sentry/utils/error'; -import { isNaN } from '@sentry/utils/is'; -import { assign } from '@sentry/utils/object'; /** Regular expression used to parse a Dsn. */ const DSN_REGEX = /^(?:(\w+):)\/\/(?:(\w+)(?::(\w+))?@)([\w\.-]+)(?::(\d+))?\/(.+)/; @@ -67,7 +65,7 @@ export class Dsn implements DsnComponents { path = split.slice(0, -1).join('/'); projectId = split.pop() as string; } - assign(this, { host, pass, path, projectId, port, protocol, user }); + Object.assign(this, { host, pass, path, projectId, port, protocol, user }); } /** Maps Dsn components into this instance. */ @@ -93,7 +91,7 @@ export class Dsn implements DsnComponents { throw new SentryError(`Invalid Dsn: Unsupported protocol "${this.protocol}"`); } - if (this.port && isNaN(parseInt(this.port, 10))) { + if (this.port && Number.isNaN(parseInt(this.port, 10))) { throw new SentryError(`Invalid Dsn: Invalid port number "${this.port}"`); } } diff --git a/packages/core/src/integrations/extraerrordata.ts b/packages/core/src/integrations/extraerrordata.ts index dd57657e20a7..bad2f877cf78 100644 --- a/packages/core/src/integrations/extraerrordata.ts +++ b/packages/core/src/integrations/extraerrordata.ts @@ -1,15 +1,8 @@ import { addGlobalEventProcessor, getCurrentHub } from '@sentry/hub'; -import { Event, EventHint, Integration } from '@sentry/types'; -import { isError, isString } from '@sentry/utils/is'; +import { Event, EventHint, ExtendedError, Integration } from '@sentry/types'; +import { isError, isPlainObject } from '@sentry/utils/is'; import { logger } from '@sentry/utils/logger'; -import { safeNormalize } from '@sentry/utils/object'; - -/** - * Just an Error object with arbitrary attributes attached to it. - */ -interface ExtendedError extends Error { - [key: string]: unknown; -} +import { normalize } from '@sentry/utils/object'; /** Patch toString calls to return proper name for wrapped functions */ export class ExtraErrorData implements Integration { @@ -50,13 +43,15 @@ export class ExtraErrorData implements Integration { let extra = { ...event.extra, }; - const normalizedErrorData = safeNormalize(errorData); - if (!isString(normalizedErrorData)) { + + const normalizedErrorData = normalize(errorData); + if (isPlainObject(normalizedErrorData)) { extra = { ...event.extra, ...normalizedErrorData, }; } + return { ...event, extra, @@ -84,6 +79,7 @@ export class ExtraErrorData implements Integration { if (isError(value)) { value = (value as Error).name || (value as Error).constructor.name; } + // tslint:disable:no-unsafe-any extraErrorInfo[key] = value; } result = { diff --git a/packages/core/src/integrations/inboundfilters.ts b/packages/core/src/integrations/inboundfilters.ts index 2ce7a88f9934..ce55b23b68ae 100644 --- a/packages/core/src/integrations/inboundfilters.ts +++ b/packages/core/src/integrations/inboundfilters.ts @@ -3,7 +3,6 @@ import { Client, Event, Integration } 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'; // "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. @@ -148,7 +147,7 @@ export class InboundFilters implements Integration { if (isRegExp(pattern)) { return (pattern as RegExp).test(value); } else if (typeof pattern === 'string') { - return includes(value, pattern); + return value.includes(pattern); } else { return false; } diff --git a/packages/core/src/transports/noop.ts b/packages/core/src/transports/noop.ts index 4bad9131885e..21e56df43d79 100644 --- a/packages/core/src/transports/noop.ts +++ b/packages/core/src/transports/noop.ts @@ -1,11 +1,11 @@ -import { Response, Status, Transport } from '@sentry/types'; +import { Event, Response, Status, Transport } from '@sentry/types'; /** Noop transport */ export class NoopTransport implements Transport { /** * @inheritDoc */ - public async sendEvent(_: string): Promise { + public async sendEvent(_: Event): Promise { return Promise.resolve({ reason: `NoopTransport: Event has been skipped because no Dsn is configured.`, status: Status.Skipped, diff --git a/packages/core/test/lib/integrations/extraerrordata.test.ts b/packages/core/test/lib/integrations/extraerrordata.test.ts index df409fa94b28..1d1057932404 100644 --- a/packages/core/test/lib/integrations/extraerrordata.test.ts +++ b/packages/core/test/lib/integrations/extraerrordata.test.ts @@ -1,13 +1,6 @@ -import { SentryEvent } from '@sentry/types'; +import { ExtendedError, SentryEvent } from '@sentry/types'; import { ExtraErrorData } from '../../../src/integrations/extraerrordata'; -/** - * Just an Error object with arbitrary attributes attached to it. - */ -interface ExtendedError extends Error { - [key: string]: any; -} - const extraErrorData = new ExtraErrorData(); let event: SentryEvent; diff --git a/packages/hub/src/scope.ts b/packages/hub/src/scope.ts index fbceaa7440dd..9b32e8c10a1c 100644 --- a/packages/hub/src/scope.ts +++ b/packages/hub/src/scope.ts @@ -1,7 +1,7 @@ import { Breadcrumb, Event, EventHint, EventProcessor, Scope as ScopeInterface, Severity, User } from '@sentry/types'; -import { isFunction, isThenable } from '@sentry/utils/is'; +import { isThenable } from '@sentry/utils/is'; import { getGlobalObject } from '@sentry/utils/misc'; -import { assign, safeNormalize } from '@sentry/utils/object'; +import { normalize } from '@sentry/utils/object'; import { SyncPromise } from '@sentry/utils/syncpromise'; /** @@ -60,7 +60,8 @@ export class Scope implements ScopeInterface { ): SyncPromise { return new SyncPromise((resolve, reject) => { const processor = processors[index]; - if (event === null || !isFunction(processor)) { + // tslint:disable-next-line:strict-type-predicates + if (event === null || typeof processor !== 'function') { resolve(event); } else { const result = processor({ ...event }, hint) as Event | null; @@ -81,7 +82,7 @@ export class Scope implements ScopeInterface { * @inheritdoc */ public setUser(user: User): Scope { - this.user = safeNormalize(user); + this.user = normalize(user); return this; } @@ -89,7 +90,7 @@ export class Scope implements ScopeInterface { * @inheritdoc */ public setTag(key: string, value: string): Scope { - this.tags = { ...this.tags, [key]: safeNormalize(value) }; + this.tags = { ...this.tags, [key]: normalize(value) }; return this; } @@ -97,7 +98,7 @@ export class Scope implements ScopeInterface { * @inheritdoc */ public setExtra(key: string, extra: any): Scope { - this.extra = { ...this.extra, [key]: safeNormalize(extra) }; + this.extra = { ...this.extra, [key]: normalize(extra) }; return this; } @@ -105,7 +106,7 @@ export class Scope implements ScopeInterface { * @inheritdoc */ public setFingerprint(fingerprint: string[]): Scope { - this.fingerprint = safeNormalize(fingerprint); + this.fingerprint = normalize(fingerprint); return this; } @@ -113,7 +114,7 @@ export class Scope implements ScopeInterface { * @inheritdoc */ public setLevel(level: Severity): Scope { - this.level = safeNormalize(level); + this.level = normalize(level); return this; } @@ -123,12 +124,12 @@ export class Scope implements ScopeInterface { */ public static clone(scope?: Scope): Scope { const newScope = new Scope(); - assign(newScope, scope, { + Object.assign(newScope, scope, { scopeListeners: [], }); if (scope) { - newScope.extra = assign(scope.extra); - newScope.tags = assign(scope.tags) as any; + newScope.extra = { ...scope.extra }; + newScope.tags = { ...scope.tags }; newScope.breadcrumbs = [...scope.breadcrumbs]; newScope.eventProcessors = [...scope.eventProcessors]; } @@ -153,8 +154,8 @@ export class Scope implements ScopeInterface { public addBreadcrumb(breadcrumb: Breadcrumb, maxBreadcrumbs?: number): void { this.breadcrumbs = maxBreadcrumbs !== undefined && maxBreadcrumbs >= 0 - ? [...this.breadcrumbs, safeNormalize(breadcrumb)].slice(-maxBreadcrumbs) - : [...this.breadcrumbs, safeNormalize(breadcrumb)]; + ? [...this.breadcrumbs, normalize(breadcrumb)].slice(-maxBreadcrumbs) + : [...this.breadcrumbs, normalize(breadcrumb)]; } /** diff --git a/packages/node/src/backend.ts b/packages/node/src/backend.ts index c6e9033f6c20..eafbad64f8fc 100644 --- a/packages/node/src/backend.ts +++ b/packages/node/src/backend.ts @@ -1,7 +1,8 @@ import { BaseBackend, Dsn, getCurrentHub } from '@sentry/core'; import { Event, EventHint, Options, Severity, Transport } from '@sentry/types'; import { isError, isPlainObject } from '@sentry/utils/is'; -import { limitObjectDepthToSize, serializeKeysToEventMessage } from '@sentry/utils/object'; +import { normalizeToSize } from '@sentry/utils/object'; +import { keysToEventMessage } from '@sentry/utils/string'; import { SyncPromise } from '@sentry/utils/syncpromise'; import { createHash } from 'crypto'; import { extractStackFromError, parseError, parseStack, prepareFramesForEvent } from './parsers'; @@ -81,10 +82,10 @@ export class NodeBackend extends BaseBackend { // This will allow us to group events based on top-level keys // which is much better than creating new group when any key/value change const keys = Object.keys(exception as {}).sort(); - const message = `Non-Error exception captured with keys: ${serializeKeysToEventMessage(keys)}`; + const message = `Non-Error exception captured with keys: ${keysToEventMessage(keys)}`; getCurrentHub().configureScope(scope => { - scope.setExtra('__serialized__', limitObjectDepthToSize(exception as {})); + scope.setExtra('__serialized__', normalizeToSize(exception as {})); scope.setFingerprint([ createHash('md5') .update(keys.join('')) diff --git a/packages/node/src/handlers.ts b/packages/node/src/handlers.ts index ecb59603b155..5eda022d6a37 100644 --- a/packages/node/src/handlers.ts +++ b/packages/node/src/handlers.ts @@ -1,8 +1,9 @@ import { captureException, getCurrentHub } from '@sentry/core'; import { Event } from '@sentry/types'; import { forget } from '@sentry/utils/async'; +import { isString } from '@sentry/utils/is'; import { logger } from '@sentry/utils/logger'; -import { serialize } from '@sentry/utils/object'; +import { normalize } from '@sentry/utils/object'; import * as cookie from 'cookie'; import * as domain from 'domain'; import * as http from 'http'; @@ -94,9 +95,9 @@ function extractRequestData(req: { [key: string]: any }): { [key: string]: strin data = ''; } } - if (data && typeof data !== 'string' && {}.toString.call(data) !== '[object String]') { + if (data && !isString(data)) { // Make sure the request body is a string - data = serialize(data); + data = JSON.stringify(normalize(data)); } // request interface diff --git a/packages/node/src/integrations/linkederrors.ts b/packages/node/src/integrations/linkederrors.ts index b43f8609633b..00c6cedda548 100644 --- a/packages/node/src/integrations/linkederrors.ts +++ b/packages/node/src/integrations/linkederrors.ts @@ -1,18 +1,11 @@ import { addGlobalEventProcessor, getCurrentHub } from '@sentry/core'; -import { Event, EventHint, Exception, Integration } from '@sentry/types'; +import { Event, EventHint, Exception, ExtendedError, Integration } from '@sentry/types'; import { SyncPromise } from '@sentry/utils/syncpromise'; import { getExceptionFromError } from '../parsers'; const DEFAULT_KEY = 'cause'; const DEFAULT_LIMIT = 5; -/** - * Just an Error object with arbitrary attributes attached to it. - */ -interface ExtendedError extends Error { - [key: string]: any; -} - /** Adds SDK info to an event. */ export class LinkedErrors implements Integration { /** diff --git a/packages/node/src/parsers.ts b/packages/node/src/parsers.ts index 6e15bd890464..4e9c1f861d01 100644 --- a/packages/node/src/parsers.ts +++ b/packages/node/src/parsers.ts @@ -1,4 +1,4 @@ -import { Event, Exception, StackFrame } from '@sentry/types'; +import { Event, Exception, ExtendedError, StackFrame } from '@sentry/types'; import { basename, dirname } from '@sentry/utils/path'; import { snipLine } from '@sentry/utils/string'; import { SyncPromise } from '@sentry/utils/syncpromise'; @@ -19,13 +19,6 @@ export function resetFileContentCache(): void { FILE_CONTENT_CACHE.clear(); } -/** - * Just an Error object with arbitrary attributes attached to it. - */ -interface ExtendedError extends Error { - [key: string]: any; -} - /** JSDoc */ function getFunction(frame: stacktrace.StackFrame): string { try { diff --git a/packages/node/src/transports/base.ts b/packages/node/src/transports/base.ts index 08c5ce5c41de..becf36ba988d 100644 --- a/packages/node/src/transports/base.ts +++ b/packages/node/src/transports/base.ts @@ -1,5 +1,5 @@ import { API } from '@sentry/core'; -import { Response, Status, Transport, TransportOptions } from '@sentry/types'; +import { Event, Response, Status, Transport, TransportOptions } from '@sentry/types'; import { SentryError } from '@sentry/utils/error'; import { PromiseBuffer } from '@sentry/utils/promisebuffer'; import * as fs from 'fs'; @@ -71,7 +71,7 @@ export abstract class BaseTransport implements Transport { } /** JSDoc */ - protected async sendWithModule(httpModule: HTTPRequest, body: string): Promise { + protected async sendWithModule(httpModule: HTTPRequest, event: Event): Promise { return this.buffer.add( new Promise((resolve, reject) => { const req = httpModule.request(this.getRequestOptions(), (res: http.IncomingMessage) => { @@ -97,7 +97,7 @@ export abstract class BaseTransport implements Transport { }); }); req.on('error', reject); - req.end(body); + req.end(JSON.stringify(event)); }), ); } @@ -105,7 +105,7 @@ export abstract class BaseTransport implements Transport { /** * @inheritDoc */ - public async sendEvent(_: string): Promise { + public async sendEvent(_: Event): Promise { throw new SentryError('Transport Class has to implement `sendEvent` method.'); } diff --git a/packages/node/src/transports/http.ts b/packages/node/src/transports/http.ts index dfd818b67750..f31a9fc09d79 100644 --- a/packages/node/src/transports/http.ts +++ b/packages/node/src/transports/http.ts @@ -1,4 +1,4 @@ -import { Response, TransportOptions } from '@sentry/types'; +import { Event, Response, TransportOptions } from '@sentry/types'; import { SentryError } from '@sentry/utils/error'; import * as http from 'http'; import * as HttpsProxyAgent from 'https-proxy-agent'; @@ -20,10 +20,10 @@ export class HTTPTransport extends BaseTransport { /** * @inheritDoc */ - public async sendEvent(body: string): Promise { + public async sendEvent(event: Event): Promise { if (!this.module) { throw new SentryError('No module available in HTTPTransport'); } - return this.sendWithModule(this.module, body); + return this.sendWithModule(this.module, event); } } diff --git a/packages/node/src/transports/https.ts b/packages/node/src/transports/https.ts index 41da385c0924..2b2726387d11 100644 --- a/packages/node/src/transports/https.ts +++ b/packages/node/src/transports/https.ts @@ -1,4 +1,4 @@ -import { Response, TransportOptions } from '@sentry/types'; +import { Event, Response, TransportOptions } from '@sentry/types'; import { SentryError } from '@sentry/utils/error'; import * as https from 'https'; import * as HttpsProxyAgent from 'https-proxy-agent'; @@ -20,10 +20,10 @@ export class HTTPSTransport extends BaseTransport { /** * @inheritDoc */ - public async sendEvent(body: string): Promise { + public async sendEvent(event: Event): Promise { if (!this.module) { throw new SentryError('No module available in HTTPSTransport'); } - return this.sendWithModule(this.module, body); + return this.sendWithModule(this.module, event); } } diff --git a/packages/node/test/integrations/linkederrors.test.ts b/packages/node/test/integrations/linkederrors.test.ts index 97c1c28db899..9a391fc918b8 100644 --- a/packages/node/test/integrations/linkederrors.test.ts +++ b/packages/node/test/integrations/linkederrors.test.ts @@ -1,13 +1,10 @@ +import { ExtendedError } from '@sentry/types'; import { Event } from '../../src'; import { NodeBackend } from '../../src/backend'; import { LinkedErrors } from '../../src/integrations/linkederrors'; let linkedErrors: LinkedErrors; -interface ExtendedError extends Error { - [key: string]: any; -} - describe('LinkedErrors', () => { beforeEach(() => { linkedErrors = new LinkedErrors(); diff --git a/packages/node/test/transports/http.test.ts b/packages/node/test/transports/http.test.ts index be99f0117d69..fa6810fe28ec 100644 --- a/packages/node/test/transports/http.test.ts +++ b/packages/node/test/transports/http.test.ts @@ -44,11 +44,9 @@ describe('HTTPTransport', () => { test('send 200', async () => { const transport = createTransport({ dsn }); - await transport.sendEvent( - JSON.stringify({ - message: 'test', - }), - ); + await transport.sendEvent({ + message: 'test', + }); const requestOptions = (transport.module!.request as jest.Mock).mock.calls[0][0]; assertBasicOptions(requestOptions); @@ -60,11 +58,9 @@ describe('HTTPTransport', () => { const transport = createTransport({ dsn }); try { - await transport.sendEvent( - JSON.stringify({ - message: 'test', - }), - ); + await transport.sendEvent({ + message: 'test', + }); } catch (e) { const requestOptions = (transport.module!.request as jest.Mock).mock.calls[0][0]; assertBasicOptions(requestOptions); @@ -80,11 +76,9 @@ describe('HTTPTransport', () => { const transport = createTransport({ dsn }); try { - await transport.sendEvent( - JSON.stringify({ - message: 'test', - }), - ); + await transport.sendEvent({ + message: 'test', + }); } catch (e) { const requestOptions = (transport.module!.request as jest.Mock).mock.calls[0][0]; assertBasicOptions(requestOptions); @@ -100,11 +94,9 @@ describe('HTTPTransport', () => { a: 'b', }, }); - await transport.sendEvent( - JSON.stringify({ - message: 'test', - }), - ); + await transport.sendEvent({ + message: 'test', + }); const requestOptions = (transport.module!.request as jest.Mock).mock.calls[0][0]; assertBasicOptions(requestOptions); @@ -117,11 +109,9 @@ describe('HTTPTransport', () => { dsn, httpProxy: 'http://example.com:8080', }); - await transport.sendEvent( - JSON.stringify({ - message: 'test', - }), - ); + await transport.sendEvent({ + message: 'test', + }); const requestOptions = (transport.module!.request as jest.Mock).mock.calls[0][0]; assertBasicOptions(requestOptions); diff --git a/packages/node/test/transports/https.test.ts b/packages/node/test/transports/https.test.ts index 18d759a926ab..61ce02f95be9 100644 --- a/packages/node/test/transports/https.test.ts +++ b/packages/node/test/transports/https.test.ts @@ -50,11 +50,9 @@ describe('HTTPSTransport', () => { test('send 200', async () => { const transport = createTransport({ dsn }); - await transport.sendEvent( - JSON.stringify({ - message: 'test', - }), - ); + await transport.sendEvent({ + message: 'test', + }); const requestOptions = (transport.module!.request as jest.Mock).mock.calls[0][0]; assertBasicOptions(requestOptions); @@ -66,11 +64,9 @@ describe('HTTPSTransport', () => { const transport = createTransport({ dsn }); try { - await transport.sendEvent( - JSON.stringify({ - message: 'test', - }), - ); + await transport.sendEvent({ + message: 'test', + }); } catch (e) { const requestOptions = (transport.module!.request as jest.Mock).mock.calls[0][0]; assertBasicOptions(requestOptions); @@ -86,11 +82,9 @@ describe('HTTPSTransport', () => { const transport = createTransport({ dsn }); try { - await transport.sendEvent( - JSON.stringify({ - message: 'test', - }), - ); + await transport.sendEvent({ + message: 'test', + }); } catch (e) { const requestOptions = (transport.module!.request as jest.Mock).mock.calls[0][0]; assertBasicOptions(requestOptions); @@ -106,11 +100,9 @@ describe('HTTPSTransport', () => { a: 'b', }, }); - await transport.sendEvent( - JSON.stringify({ - message: 'test', - }), - ); + await transport.sendEvent({ + message: 'test', + }); const requestOptions = (transport.module!.request as jest.Mock).mock.calls[0][0]; assertBasicOptions(requestOptions); @@ -123,11 +115,9 @@ describe('HTTPSTransport', () => { dsn, httpsProxy: 'https://example.com:8080', }); - await transport.sendEvent( - JSON.stringify({ - message: 'test', - }), - ); + await transport.sendEvent({ + message: 'test', + }); const requestOptions = (transport.module!.request as jest.Mock).mock.calls[0][0]; assertBasicOptions(requestOptions); @@ -144,11 +134,9 @@ describe('HTTPSTransport', () => { caCerts: './some/path.pem', dsn, }); - await transport.sendEvent( - JSON.stringify({ - message: 'test', - }), - ); + await transport.sendEvent({ + message: 'test', + }); const requestOptions = (transport.module!.request as jest.Mock).mock.calls[0][0]; assertBasicOptions(requestOptions); expect(requestOptions.ca).toEqual('mockedCert'); diff --git a/packages/types/src/error.ts b/packages/types/src/error.ts new file mode 100644 index 000000000000..d94becdbaf1f --- /dev/null +++ b/packages/types/src/error.ts @@ -0,0 +1,6 @@ +/** + * Just an Error object with arbitrary attributes attached to it. + */ +export interface ExtendedError extends Error { + [key: string]: any; +} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 99d49400fa0e..f1273ce3aa61 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -1,6 +1,7 @@ export { Breadcrumb, BreadcrumbHint } from './breadcrumb'; export { Client } from './client'; export { Dsn, DsnComponents, DsnLike, DsnProtocol } from './dsn'; +export { ExtendedError } from './error'; export { Event, EventHint } from './event'; export { EventProcessor } from './eventprocessor'; export { Exception } from './exception'; diff --git a/packages/types/src/transport.ts b/packages/types/src/transport.ts index 2b45da45d419..8595cd11cddf 100644 --- a/packages/types/src/transport.ts +++ b/packages/types/src/transport.ts @@ -1,4 +1,5 @@ import { DsnLike } from './dsn'; +import { Event } from './event'; import { Response } from './response'; /** Transport used sending data to Sentry */ @@ -8,7 +9,7 @@ export interface Transport { * * @param body String body that should be sent to Sentry. */ - sendEvent(body: string): Promise; + sendEvent(event: Event): Promise; /** * Call this function to wait until all pending requests have been sent. diff --git a/packages/utils/src/is.ts b/packages/utils/src/is.ts index 283378d09c71..b0de578e4532 100644 --- a/packages/utils/src/is.ts +++ b/packages/utils/src/is.ts @@ -51,28 +51,6 @@ export function isDOMException(wat: any): boolean { return Object.prototype.toString.call(wat) === '[object DOMException]'; } -/** - * Checks whether given value's type is an undefined - * {@link isUndefined}. - * - * @param wat A value to be checked. - * @returns A boolean representing the result. - */ -export function isUndefined(wat: any): boolean { - return wat === void 0; -} - -/** - * Checks whether given value's type is a function - * {@link isFunction}. - * - * @param wat A value to be checked. - * @returns A boolean representing the result. - */ -export function isFunction(wat: any): boolean { - return typeof wat === 'function'; -} - /** * Checks whether given value's type is a string * {@link isString}. @@ -95,17 +73,6 @@ export function isPrimitive(wat: any): boolean { return wat === null || (typeof wat !== 'object' && typeof wat !== 'function'); } -/** - * Checks whether given value's type is an array - * {@link isArray}. - * - * @param wat A value to be checked. - * @returns A boolean representing the result. - */ -export function isArray(wat: any): boolean { - return Object.prototype.toString.call(wat) === '[object Array]'; -} - /** * Checks whether given value's type is an object literal * {@link isPlainObject}. @@ -129,22 +96,23 @@ export function isRegExp(wat: any): boolean { } /** - * Checks whether given value's type is a NaN - * {@link isNaN}. - * + * Checks whether given value has a then function. * @param wat A value to be checked. - * @returns A boolean representing the result. */ -export function isNaN(wat: any): boolean { - return wat !== wat; +export function isThenable(wat: any): boolean { + // tslint:disable:no-unsafe-any + return Boolean(wat && wat.then && typeof wat.then === 'function'); + // tslint:enable:no-unsafe-any } /** - * Checks whether given value has a then function. + * Checks whether given value's type is a SyntheticEvent + * {@link isSyntheticEvent}. + * * @param wat A value to be checked. + * @returns A boolean representing the result. */ -export function isThenable(wat: any): boolean { - // tslint:disable:no-unsafe-any - return Boolean(wat && wat.then && isFunction(wat.then)); - // tslint:enable:no-unsafe-any +export function isSyntheticEvent(wat: any): boolean { + // tslint:disable-next-line:no-unsafe-any + return isPlainObject(wat) && 'nativeEvent' in wat && 'preventDefault' in wat && 'stopPropagation' in wat; } diff --git a/packages/utils/src/object.ts b/packages/utils/src/object.ts index 1e286b63e941..531b90c6766f 100644 --- a/packages/utils/src/object.ts +++ b/packages/utils/src/object.ts @@ -1,51 +1,6 @@ -import { WrappedFunction } from '@sentry/types'; -import { isArray, isNaN, isPlainObject, isPrimitive, isUndefined } from './is'; +import { ExtendedError, WrappedFunction } from '@sentry/types'; +import { isError, isPlainObject, isPrimitive, isSyntheticEvent } from './is'; import { Memo } from './memo'; -import { truncate } from './string'; - -/** - * Just an Error object with arbitrary attributes attached to it. - */ -interface ExtendedError extends Error { - [key: string]: any; -} - -/** - * Serializes the given object into a string. - * Like JSON.stringify, but doesn't throw on circular references. - * - * @param object A JSON-serializable object. - * @returns A string containing the serialized object. - */ -export function serialize(object: T): string { - return JSON.stringify(object, serializer({ normalize: false })); -} - -/** - * Deserializes an object from a string previously serialized with - * {@link serialize}. - * - * @param str A serialized object. - * @returns The deserialized object. - */ -export function deserialize(str: string): T { - return JSON.parse(str) as T; -} - -/** - * Creates a deep copy of the given object. - * - * The object must be serializable, i.e.: - * - It must not contain any cycles - * - Only primitive types are allowed (object, array, number, string, boolean) - * - Its depth should be considerably low for performance reasons - * - * @param object A JSON-serializable object. - * @returns The object clone. - */ -export function clone(object: T): T { - return deserialize(serialize(object)); -} /** * Wrap a given object method with a higher-order function @@ -102,134 +57,6 @@ export function urlEncode(object: { [key: string]: any }): string { .join('&'); } -// Default Node.js REPL depth -const MAX_SERIALIZE_EXCEPTION_DEPTH = 3; -// 100kB, as 200kB is max payload size, so half sounds reasonable -const MAX_SERIALIZE_EXCEPTION_SIZE = 100 * 1024; -const MAX_SERIALIZE_KEYS_LENGTH = 40; - -/** JSDoc */ -function utf8Length(value: string): number { - // tslint:disable-next-line:no-bitwise - return ~-encodeURI(value).split(/%..|./).length; -} - -/** JSDoc */ -function jsonSize(value: any): number { - return utf8Length(JSON.stringify(value)); -} - -/** JSDoc */ -function serializeValue(value: T): T | string { - const type = Object.prototype.toString.call(value); - - if (typeof value === 'string') { - return truncate(value, 40); - } else if (type === '[object Object]') { - // Node.js REPL notation - return '[Object]'; - } else if (type === '[object Array]') { - // Node.js REPL notation - return '[Array]'; - } else { - return normalizeValue(value) as T; - } -} - -/** JSDoc */ -export function serializeObject(value: T, depth: number): T | string | {} { - if (depth === 0) { - return serializeValue(value); - } - - if (isPlainObject(value)) { - const serialized: { [key: string]: any } = {}; - const val = value as { - [key: string]: any; - }; - - Object.keys(val).forEach((key: string) => { - serialized[key] = serializeObject(val[key], depth - 1); - }); - - return serialized; - } else if (isArray(value)) { - const val = (value as any) as T[]; - return val.map(v => serializeObject(v, depth - 1)); - } - - return serializeValue(value); -} - -/** JSDoc */ -export function limitObjectDepthToSize( - object: { [key: string]: any }, - depth: number = MAX_SERIALIZE_EXCEPTION_DEPTH, - maxSize: number = MAX_SERIALIZE_EXCEPTION_SIZE, -): T { - const serialized = serializeObject(object, depth); - - if (jsonSize(serialize(serialized)) > maxSize) { - return limitObjectDepthToSize(object, depth - 1); - } - - return serialized as T; -} - -/** JSDoc */ -export function serializeKeysToEventMessage(keys: string[], maxLength: number = MAX_SERIALIZE_KEYS_LENGTH): string { - if (!keys.length) { - return '[object has no keys]'; - } - - if (keys[0].length >= maxLength) { - return truncate(keys[0], maxLength); - } - - for (let includedKeys = keys.length; includedKeys > 0; includedKeys--) { - const serialized = keys.slice(0, includedKeys).join(', '); - if (serialized.length > maxLength) { - continue; - } - if (includedKeys === keys.length) { - return serialized; - } - return truncate(serialized, maxLength); - } - - return ''; -} - -// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Polyfill -/** JSDoc */ -export function assign(target: any, ...args: any[]): object { - if (target === null || target === undefined) { - throw new TypeError('Cannot convert undefined or null to object'); - } - - const to = Object(target) as { - [key: string]: any; - }; - - // tslint:disable-next-line:prefer-for-of - for (let i = 0; i < args.length; i++) { - const source = args[i]; - if (source !== null) { - for (const nextKey in source as { - [key: string]: any; - }) { - if (Object.prototype.hasOwnProperty.call(source, nextKey)) { - to[nextKey] = (source as { - [key: string]: any; - })[nextKey]; - } - } - } - } - - return to; -} - /** * Transforms Error object into an object literal with all it's attributes * attached to it. @@ -261,6 +88,50 @@ function objectifyError(error: ExtendedError): object { return err; } +/** Calculates bytes size of input string */ +function utf8Length(value: string): number { + // tslint:disable-next-line:no-bitwise + return ~-encodeURI(value).split(/%..|./).length; +} + +/** Calculates bytes size of input object */ +function jsonSize(value: any): number { + return utf8Length(JSON.stringify(value)); +} + +/** JSDoc */ +export function normalizeToSize( + object: { [key: string]: any }, + // Default Node.js REPL depth + depth: number = 3, + // 100kB, as 200kB is max payload size, so half sounds reasonable + maxSize: number = 100 * 1024, +): T { + const serialized = normalize(object, depth); + + if (jsonSize(serialized) > maxSize) { + return normalizeToSize(object, depth - 1, maxSize); + } + + return serialized as T; +} + +/** Transforms any input value into a string form, either primitive value or a type of the input */ +function stringifyValue(value: T): T | string { + // Node.js REPL notation + const type = Object.prototype.toString.call(value) as string; + + if (isPrimitive(value)) { + return value; + } else if (type === '[object Object]') { + return '[Object]'; + } else if (type === '[object Array]') { + return '[Array]'; + } else { + return type; + } +} + /** * normalizeValue() * @@ -271,12 +142,9 @@ function objectifyError(error: ExtendedError): object { * - filter global objects */ function normalizeValue(value: any, key?: any): any { - if (key === 'domain' && typeof value === 'object' && (value as { _events: any })._events) { - return '[Domain]'; - } - - if (key === 'domainEmitter') { - return '[DomainEmitter]'; + if (isError(value)) { + // tslint:disable-next-line:no-unsafe-any + return objectifyError(value); } if (typeof (global as any) !== 'undefined' && value === global) { @@ -291,12 +159,17 @@ function normalizeValue(value: any, key?: any): any { return '[Document]'; } - if (value instanceof Date) { - return `[Date] ${value}`; + if (key === 'domain' && typeof value === 'object' && (value as { _events: any })._events) { + return '[Domain]'; } - if (value instanceof Error) { - return objectifyError(value); + if (key === 'domainEmitter') { + return '[DomainEmitter]'; + } + + // React's SyntheticEvent thingy + if (isSyntheticEvent(value)) { + return '[SyntheticEvent]'; } // tslint:disable-next-line:strict-type-predicates @@ -304,11 +177,11 @@ function normalizeValue(value: any, key?: any): any { return Object.getPrototypeOf(value) ? value.constructor.name : 'Event'; } - if (isNaN(value)) { + if (Number.isNaN(value as number)) { return '[NaN]'; } - if (isUndefined(value)) { + if (value === void 0) { return '[undefined]'; } @@ -320,54 +193,68 @@ function normalizeValue(value: any, key?: any): any { } /** - * Decycles an object to make it safe for json serialization. + * Walks an object to perform a normalization on it * - * @param obj Object to be decycled + * @param key of object that's walked in current iteration + * @param value object to be walked + * @param depth Optional number indicating how deep should walking be performed * @param memo Optional Memo class handling decycling */ -export function decycle(obj: any, memo: Memo = new Memo()): any { - // tslint:disable-next-line:no-unsafe-any - const copy = isArray(obj) ? obj.slice() : isPlainObject(obj) ? assign({}, obj) : obj; +export function walk(key: string, value: any, depth: number, memo: Memo = new Memo()): any { + // tslint:disable:no-unsafe-any - if (!isPrimitive(obj)) { - if (memo.memoize(obj)) { - return '[Circular ~]'; - } - // tslint:disable-next-line - for (const key in obj) { - // Avoid iterating over fields in the prototype if they've somehow been exposed to enumeration. - if (!Object.prototype.hasOwnProperty.call(obj, key)) { - continue; - } - // tslint:disable-next-line - copy[key] = decycle(obj[key], memo); + // If we reach the maximum depth, stringify whatever left out of it + if (depth === 0) { + return stringifyValue(value); + } + + // If value implements `toJSON` method, call it and return early + if (value !== null && value !== undefined && typeof value.toJSON === 'function') { + return value.toJSON(); + } + + const normalized = normalizeValue(value, key); + // If its a primitive, there are no branches left to walk, so we can just bail out as theres no point in going down that branch any further + if (isPrimitive(normalized)) { + return normalized; + } + + // Make a copy of the value to prevent any mutations + const copy = Array.isArray(normalized) ? [...normalized] : isPlainObject(normalized) ? { ...normalized } : normalized; + + // If we already walked that branch, bail out + if (memo.memoize(value)) { + return '[Circular ~]'; + } + + for (const innerKey in normalized) { + // Avoid iterating over fields in the prototype if they've somehow been exposed to enumeration. + if (!Object.prototype.hasOwnProperty.call(normalized, innerKey)) { + continue; } - memo.unmemoize(obj); + copy[innerKey] = walk(innerKey, normalized[innerKey], depth - 1, memo); } - return copy; -} + memo.unmemoize(value); -/** - * serializer() - * - * Remove circular references, - * translates undefined/NaN values to "[undefined]"/"[NaN]" respectively, - * and takes care of Error objects serialization - */ -function serializer(options: { normalize: boolean } = { normalize: true }): (key: string, value: any) => any { - // tslint:disable-next-line - return (key: string, value: object) => (options.normalize ? normalizeValue(decycle(value), key) : decycle(value)); + return copy; } /** - * safeNormalize() + * normalize() * - * Creates a copy of the input by applying serializer function on it and parsing it back to unify the data + * - Creates a copy to prevent original input mutation + * - Skip non-enumerablers + * - Calls `toJSON` if implemented + * - Removes circular references + * - Translates non-serializeable values (undefined/NaN/Functions) to serializable format + * - Translates known global objects/Classes to a string representations + * - Takes care of Error objects serialization + * - Optionally limit depth of final output */ -export function safeNormalize(input: any): any { +export function normalize(input: any, depth: number = +Infinity): any { try { - return JSON.parse(JSON.stringify(input, serializer({ normalize: true }))); + return JSON.parse(JSON.stringify(input, (key: string, value: any) => walk(key, value, depth))); } catch (_oO) { return '**non-serializable**'; } diff --git a/packages/utils/src/string.ts b/packages/utils/src/string.ts index 03a9e68ee8a8..68ece1afb80b 100644 --- a/packages/utils/src/string.ts +++ b/packages/utils/src/string.ts @@ -1,5 +1,3 @@ -import { isString } from './is'; - /** * Truncates given string to the maximum characters count * @@ -8,7 +6,7 @@ import { isString } from './is'; * @returns string Encoded */ export function truncate(str: string, max: number = 0): string { - if (max === 0 || !isString(str)) { + if (max === 0) { return str; } return str.length <= max ? str : `${str.substr(0, max)}...`; @@ -82,17 +80,26 @@ export function safeJoin(input: any[], delimiter?: string): string { return output.join(delimiter); } -/** - * Checks if given value is included in the target - * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/includes#Polyfill - * @param target source string - * @param search string to be looked for - * @returns An answer - */ -export function includes(target: string, search: string): boolean { - if (search.length > target.length) { - return false; - } else { - return target.indexOf(search) !== -1; +/** Merges provided array of keys into */ +export function keysToEventMessage(keys: string[], maxLength: number = 40): string { + if (!keys.length) { + return '[object has no keys]'; + } + + if (keys[0].length >= maxLength) { + return truncate(keys[0], maxLength); } + + for (let includedKeys = keys.length; includedKeys > 0; includedKeys--) { + const serialized = keys.slice(0, includedKeys).join(', '); + if (serialized.length > maxLength) { + continue; + } + if (includedKeys === keys.length) { + return serialized; + } + return truncate(serialized, maxLength); + } + + return ''; } diff --git a/packages/utils/test/object.test.ts b/packages/utils/test/object.test.ts index f7a4c86c9479..9a50bdb92209 100644 --- a/packages/utils/test/object.test.ts +++ b/packages/utils/test/object.test.ts @@ -1,293 +1,4 @@ -import { - clone, - decycle, - deserialize, - fill, - safeNormalize, - serialize, - serializeKeysToEventMessage, - urlEncode, -} from '../src/object'; - -const MATRIX = [ - { name: 'boolean', object: true, serialized: 'true' }, - { name: 'number', object: 42, serialized: '42' }, - { name: 'string', object: 'test', serialized: '"test"' }, - { name: 'array', object: [1, 'test'], serialized: '[1,"test"]' }, - { name: 'object', object: { a: 'test' }, serialized: '{"a":"test"}' }, -]; - -describe('clone()', () => { - for (const entry of MATRIX) { - test(`clones a ${entry.name}`, () => { - expect(clone(entry.object)).toEqual(entry.object); - }); - } -}); - -describe('decycle()', () => { - test('decycles circular objects', () => { - const circular = { - foo: 1, - }; - circular.bar = circular; - - const decycled = decycle(circular); - - expect(decycled).toEqual({ - foo: 1, - bar: '[Circular ~]', - }); - }); - - test('decycles complex circular objects', () => { - const circular = { - foo: 1, - }; - circular.bar = [ - { - baz: circular, - }, - circular, - ]; - circular.qux = circular.bar[0].baz; - - const decycled = decycle(circular); - - expect(decycled).toEqual({ - bar: [ - { - baz: '[Circular ~]', - }, - '[Circular ~]', - ], - foo: 1, - qux: '[Circular ~]', - }); - }); - - test('dont mutate original object', () => { - const circular = { - foo: 1, - }; - circular.bar = circular; - - const decycled = decycle(circular); - - expect(decycled).toEqual({ - foo: 1, - bar: '[Circular ~]', - }); - - expect(circular.bar).toBe(circular); - expect(decycled).not.toBe(circular); - }); - - test('dont mutate original complex object', () => { - const circular = { - foo: 1, - }; - circular.bar = [ - { - baz: circular, - }, - circular, - ]; - circular.qux = circular.bar[0].baz; - - const decycled = decycle(circular); - - expect(decycled).toEqual({ - bar: [ - { - baz: '[Circular ~]', - }, - '[Circular ~]', - ], - foo: 1, - qux: '[Circular ~]', - }); - - expect(circular.bar[0].baz).toBe(circular); - expect(circular.bar[1]).toBe(circular); - expect(circular.qux).toBe(circular.bar[0].baz); - expect(decycled).not.toBe(circular); - }); - - test('skip non-enumerable properties', () => { - const circular = { - foo: 1, - }; - circular.bar = circular; - Object.defineProperty(circular, 'baz', { - enumerable: true, - value: circular, - }); - Object.defineProperty(circular, 'qux', { - enumerable: false, - value: circular, - }); - - const decycled = decycle(circular); - - expect(decycled).toEqual({ - bar: '[Circular ~]', - baz: '[Circular ~]', - foo: 1, - }); - }); -}); - -describe('serialize()', () => { - for (const entry of MATRIX) { - test(`serializes a ${entry.name}`, () => { - expect(serialize(entry.object)).toEqual(entry.serialized); - }); - } - - describe('cyclical structures', () => { - test('circular objects', () => { - const obj = { name: 'Alice' }; - // @ts-ignore - obj.self = obj; - expect(serialize(obj)).toEqual(JSON.stringify({ name: 'Alice', self: '[Circular ~]' })); - }); - - test('circular objects with intermediaries', () => { - const obj = { name: 'Alice' }; - // @ts-ignore - obj.identity = { self: obj }; - expect(serialize(obj)).toEqual(JSON.stringify({ name: 'Alice', identity: { self: '[Circular ~]' } })); - }); - - test('circular objects deeper', () => { - const obj = { name: 'Alice', child: { name: 'Bob' } }; - // @ts-ignore - obj.child.self = obj.child; - expect(serialize(obj)).toEqual( - JSON.stringify({ - name: 'Alice', - child: { name: 'Bob', self: '[Circular ~]' }, - }), - ); - }); - - test('circular objects deeper with intermediaries', () => { - const obj = { name: 'Alice', child: { name: 'Bob' } }; - // @ts-ignore - obj.child.identity = { self: obj.child }; - expect(serialize(obj)).toEqual( - JSON.stringify({ - name: 'Alice', - child: { name: 'Bob', identity: { self: '[Circular ~]' } }, - }), - ); - }); - - test('circular objects in an array', () => { - const obj = { name: 'Alice' }; - // @ts-ignore - obj.self = [obj, obj]; - expect(serialize(obj)).toEqual( - JSON.stringify({ - name: 'Alice', - self: ['[Circular ~]', '[Circular ~]'], - }), - ); - }); - - test('circular objects deeper in an array', () => { - const obj = { - name: 'Alice', - children: [{ name: 'Bob' }, { name: 'Eve' }], - }; - // @ts-ignore - obj.children[0].self = obj.children[0]; - // @ts-ignore - obj.children[1].self = obj.children[1]; - expect(serialize(obj)).toEqual( - JSON.stringify({ - name: 'Alice', - children: [{ name: 'Bob', self: '[Circular ~]' }, { name: 'Eve', self: '[Circular ~]' }], - }), - ); - }); - - test('circular arrays', () => { - const obj: object[] = []; - obj.push(obj); - obj.push(obj); - expect(serialize(obj)).toEqual(JSON.stringify(['[Circular ~]', '[Circular ~]'])); - }); - - test('circular arrays with intermediaries', () => { - const obj: object[] = []; - obj.push({ name: 'Alice', self: obj }); - obj.push({ name: 'Bob', self: obj }); - expect(serialize(obj)).toEqual( - JSON.stringify([{ name: 'Alice', self: '[Circular ~]' }, { name: 'Bob', self: '[Circular ~]' }]), - ); - }); - - test('repeated objects in objects', () => { - const obj = {}; - const alice = { name: 'Alice' }; - // @ts-ignore - obj.alice1 = alice; - // @ts-ignore - obj.alice2 = alice; - expect(serialize(obj)).toEqual( - JSON.stringify({ - alice1: { name: 'Alice' }, - alice2: { name: 'Alice' }, - }), - ); - }); - - test('repeated objects in arrays', () => { - const alice = { name: 'Alice' }; - const obj = [alice, alice]; - expect(serialize(obj)).toEqual(JSON.stringify([{ name: 'Alice' }, { name: 'Alice' }])); - }); - }); -}); - -describe('deserialize()', () => { - for (const entry of MATRIX) { - test(`deserializes a ${entry.name}`, () => { - // tslint:disable:no-inferred-empty-object-type - expect(deserialize(entry.serialized)).toEqual(entry.object); - }); - } -}); - -describe('serializeKeysToEventMessage()', () => { - test('no keys', () => { - expect(serializeKeysToEventMessage([], 10)).toEqual('[object has no keys]'); - }); - - test('one key should be returned as a whole if not over the length limit', () => { - expect(serializeKeysToEventMessage(['foo'], 10)).toEqual('foo'); - expect(serializeKeysToEventMessage(['foobarbazx'], 10)).toEqual('foobarbazx'); - }); - - test('one key should be appended with ... and truncated when over the limit', () => { - expect(serializeKeysToEventMessage(['foobarbazqux'], 10)).toEqual('foobarbazq...'); - }); - - test('multiple keys should be joined as a whole if not over the length limit', () => { - expect(serializeKeysToEventMessage(['foo', 'bar'], 10)).toEqual('foo, bar'); - }); - - test('multiple keys should include only as much keys as can fit into the limit', () => { - expect(serializeKeysToEventMessage(['foo', 'bar', 'baz'], 10)).toEqual('foo, bar'); - expect(serializeKeysToEventMessage(['foo', 'verylongkey', 'baz'], 10)).toEqual('foo'); - }); - - test('multiple keys should truncate first key if its too long', () => { - expect(serializeKeysToEventMessage(['foobarbazqux', 'bar', 'baz'], 10)).toEqual('foobarbazq...'); - }); -}); +import { fill, normalize, urlEncode } from '../src/object'; describe('fill()', () => { test('wraps a method by calling a replacement function on it', () => { @@ -371,115 +82,98 @@ describe('urlEncode()', () => { }); }); -describe('safeNormalize()', () => { - test('return same value for simple input', () => { - expect(safeNormalize('foo')).toEqual('foo'); - expect(safeNormalize(42)).toEqual(42); - expect(safeNormalize(true)).toEqual(true); - expect(safeNormalize(null)).toEqual(null); - }); +describe('normalize()', () => { + describe('acts as a pass-through for simple-cases', () => { + test('return same value for simple input', () => { + expect(normalize('foo')).toEqual('foo'); + expect(normalize(42)).toEqual(42); + expect(normalize(true)).toEqual(true); + expect(normalize(null)).toEqual(null); + }); - test('return same object or arrays for referenced inputs', () => { - expect(safeNormalize({ foo: 'bar' })).toEqual({ foo: 'bar' }); - expect(safeNormalize([42])).toEqual([42]); + test('return same object or arrays for referenced inputs', () => { + expect(normalize({ foo: 'bar' })).toEqual({ foo: 'bar' }); + expect(normalize([42])).toEqual([42]); + }); }); - test('return [undefined] string for undefined values', () => { - expect(safeNormalize(undefined)).toEqual('[undefined]'); - }); + test('extracts extra properties from error objects', () => { + const obj = new Error('Wubba Lubba Dub Dub'); + // @ts-ignore + obj.reason = new TypeError("I'm pickle Riiick!"); + // @ts-ignore + obj.extra = 'some extra prop'; - test('return [NaN] string for NaN values', () => { - expect(safeNormalize(NaN)).toEqual('[NaN]'); - }); + // Stack is inconsistent across browsers, so override it and just make sure its stringified + obj.stack = 'x'; + // @ts-ignore + obj.reason.stack = 'x'; - test('iterates through array and object values to replace undefined/NaN values', () => { - expect(safeNormalize(['foo', 42, undefined, NaN])).toEqual(['foo', 42, '[undefined]', '[NaN]']); - expect( - safeNormalize({ - foo: 42, - bar: undefined, - baz: NaN, - }), - ).toEqual({ - foo: 42, - bar: '[undefined]', - baz: '[NaN]', - }); - }); + // IE 10/11 + // @ts-ignore + delete obj.description; + // @ts-ignore + delete obj.reason.description; - test('iterates through array and object values, but recursively', () => { - expect(safeNormalize(['foo', 42, [[undefined]], [NaN]])).toEqual(['foo', 42, [['[undefined]']], ['[NaN]']]); - expect( - safeNormalize({ - foo: 42, - bar: { - baz: { - quz: undefined, - }, - }, - wat: { - no: NaN, - }, - }), - ).toEqual({ - foo: 42, - bar: { - baz: { - quz: '[undefined]', - }, - }, - wat: { - no: '[NaN]', + expect(normalize(obj)).toEqual({ + message: 'Wubba Lubba Dub Dub', + name: 'Error', + stack: 'x', + reason: { + message: "I'm pickle Riiick!", + name: 'TypeError', + stack: 'x', }, + extra: 'some extra prop', }); }); - describe('cyclical structures', () => { - test('must normalize circular objects', () => { + describe('decycles cyclical structures', () => { + test('circular objects', () => { const obj = { name: 'Alice' }; // @ts-ignore obj.self = obj; - expect(safeNormalize(obj)).toEqual({ name: 'Alice', self: '[Circular ~]' }); + expect(normalize(obj)).toEqual({ name: 'Alice', self: '[Circular ~]' }); }); - test('must normalize circular objects with intermediaries', () => { + test('circular objects with intermediaries', () => { const obj = { name: 'Alice' }; // @ts-ignore obj.identity = { self: obj }; - expect(safeNormalize(obj)).toEqual({ name: 'Alice', identity: { self: '[Circular ~]' } }); + expect(normalize(obj)).toEqual({ name: 'Alice', identity: { self: '[Circular ~]' } }); }); - test('must normalize circular objects deeper', () => { + test('deep circular objects', () => { const obj = { name: 'Alice', child: { name: 'Bob' } }; // @ts-ignore obj.child.self = obj.child; - expect(safeNormalize(obj)).toEqual({ + expect(normalize(obj)).toEqual({ name: 'Alice', child: { name: 'Bob', self: '[Circular ~]' }, }); }); - test('must normalize circular objects deeper with intermediaries', () => { + test('deep circular objects with intermediaries', () => { const obj = { name: 'Alice', child: { name: 'Bob' } }; // @ts-ignore obj.child.identity = { self: obj.child }; - expect(safeNormalize(obj)).toEqual({ + expect(normalize(obj)).toEqual({ name: 'Alice', child: { name: 'Bob', identity: { self: '[Circular ~]' } }, }); }); - test('must normalize circular objects in an array', () => { + test('circular objects in an array', () => { const obj = { name: 'Alice' }; // @ts-ignore obj.self = [obj, obj]; - expect(safeNormalize(obj)).toEqual({ + expect(normalize(obj)).toEqual({ name: 'Alice', self: ['[Circular ~]', '[Circular ~]'], }); }); - test('must normalize circular objects deeper in an array', () => { + test('deep circular objects in an array', () => { const obj = { name: 'Alice', children: [{ name: 'Bob' }, { name: 'Eve' }], @@ -488,54 +182,49 @@ describe('safeNormalize()', () => { obj.children[0].self = obj.children[0]; // @ts-ignore obj.children[1].self = obj.children[1]; - expect(safeNormalize(obj)).toEqual({ + expect(normalize(obj)).toEqual({ name: 'Alice', children: [{ name: 'Bob', self: '[Circular ~]' }, { name: 'Eve', self: '[Circular ~]' }], }); }); - test('must normalize circular arrays', () => { + test('circular arrays', () => { const obj: object[] = []; obj.push(obj); obj.push(obj); - expect(safeNormalize(obj)).toEqual(['[Circular ~]', '[Circular ~]']); + expect(normalize(obj)).toEqual(['[Circular ~]', '[Circular ~]']); }); - test('must normalize circular arrays with intermediaries', () => { + test('circular arrays with intermediaries', () => { const obj: object[] = []; obj.push({ name: 'Alice', self: obj }); obj.push({ name: 'Bob', self: obj }); - expect(safeNormalize(obj)).toEqual([ - { name: 'Alice', self: '[Circular ~]' }, - { name: 'Bob', self: '[Circular ~]' }, - ]); + expect(normalize(obj)).toEqual([{ name: 'Alice', self: '[Circular ~]' }, { name: 'Bob', self: '[Circular ~]' }]); }); - test('must normalize repeated objects in objects', () => { + test('repeated objects in objects', () => { const obj = {}; const alice = { name: 'Alice' }; // @ts-ignore obj.alice1 = alice; // @ts-ignore obj.alice2 = alice; - expect(safeNormalize(obj)).toEqual({ + expect(normalize(obj)).toEqual({ alice1: { name: 'Alice' }, alice2: { name: 'Alice' }, }); }); - test('must normalize repeated objects in arrays', () => { + test('repeated objects in arrays', () => { const alice = { name: 'Alice' }; const obj = [alice, alice]; - expect(safeNormalize(obj)).toEqual([{ name: 'Alice' }, { name: 'Alice' }]); + expect(normalize(obj)).toEqual([{ name: 'Alice' }, { name: 'Alice' }]); }); - test('must normalize error objects, including extra properties', () => { + test('error objects with circular references', () => { const obj = new Error('Wubba Lubba Dub Dub'); // @ts-ignore - obj.reason = new TypeError("I'm pickle Riiick!"); - // @ts-ignore - obj.extra = 'some extra prop'; + obj.reason = obj; // Stack is inconsistent across browsers, so override it and just make sure its stringified obj.stack = 'x'; @@ -545,46 +234,252 @@ describe('safeNormalize()', () => { // IE 10/11 // @ts-ignore delete obj.description; - // @ts-ignore - delete obj.reason.description; - const result = safeNormalize(obj); - - expect(result).toEqual({ + expect(normalize(obj)).toEqual({ message: 'Wubba Lubba Dub Dub', name: 'Error', stack: 'x', - reason: { - message: "I'm pickle Riiick!", - name: 'TypeError', - stack: 'x', + reason: '[Circular ~]', + }); + }); + }); + + describe('dont mutate and skip non-enumerables', () => { + test('simple object', () => { + const circular = { + foo: 1, + }; + circular.bar = circular; + + const normalized = normalize(circular); + expect(normalized).toEqual({ + foo: 1, + bar: '[Circular ~]', + }); + + expect(circular.bar).toBe(circular); + expect(normalized).not.toBe(circular); + }); + + test('complex object', () => { + const circular = { + foo: 1, + }; + circular.bar = [ + { + baz: circular, + }, + circular, + ]; + circular.qux = circular.bar[0].baz; + + const normalized = normalize(circular); + expect(normalized).toEqual({ + bar: [ + { + baz: '[Circular ~]', + }, + '[Circular ~]', + ], + foo: 1, + qux: '[Circular ~]', + }); + + expect(circular.bar[0].baz).toBe(circular); + expect(circular.bar[1]).toBe(circular); + expect(circular.qux).toBe(circular.bar[0].baz); + expect(normalized).not.toBe(circular); + }); + + test('object with non-enumerable properties', () => { + const circular = { + foo: 1, + }; + circular.bar = circular; + circular.baz = { + one: 1337, + }; + Object.defineProperty(circular, 'qux', { + enumerable: true, + value: circular, + }); + Object.defineProperty(circular, 'quaz', { + enumerable: false, + value: circular, + }); + Object.defineProperty(circular.baz, 'two', { + enumerable: false, + value: circular, + }); + + expect(normalize(circular)).toEqual({ + bar: '[Circular ~]', + baz: { + one: 1337, }, - extra: 'some extra prop', + foo: 1, + qux: '[Circular ~]', }); }); }); - test('must normalize error objects with circular references', () => { - const obj = new Error('Wubba Lubba Dub Dub'); - // @ts-ignore - obj.reason = obj; + describe('calls toJSON if implemented', () => { + test('primitive values', () => { + // tslint:disable:no-construct + const a = new Number(1); + a.toJSON = () => 10; + const b = new String('2'); + b.toJSON = () => '20'; + expect(normalize(a)).toEqual(10); + expect(normalize(b)).toEqual('20'); + }); + + test('objects, arrays and classes', () => { + const a = Object.create({}); + a.toJSON = () => 1; + function B(): void { + /*no-empty*/ + } + B.prototype.toJSON = () => 2; + const c = []; + c.toJSON = () => 3; + expect(normalize([{ a }, { b: new B() }, c])).toEqual([{ a: 1 }, { b: 2 }, 3]); + }); + }); - // Stack is inconsistent across browsers, so override it and just make sure its stringified - obj.stack = 'x'; - // @ts-ignore - obj.reason.stack = 'x'; + describe('changes unserializeable/global values/classes to its string representation', () => { + test('primitive values', () => { + expect(normalize(undefined)).toEqual('[undefined]'); + expect(normalize(NaN)).toEqual('[NaN]'); + }); - // IE 10/11 - // @ts-ignore - delete obj.description; + test('functions', () => { + expect( + normalize(() => { + /* no-empty */ + }), + ).toEqual('[Function: ]'); + const foo = () => { + /* no-empty */ + }; + expect(normalize(foo)).toEqual('[Function: foo]'); + }); - const result = safeNormalize(obj); + test('primitive values in objects/arrays', () => { + expect(normalize(['foo', 42, undefined, NaN])).toEqual(['foo', 42, '[undefined]', '[NaN]']); + expect( + normalize({ + foo: 42, + bar: undefined, + baz: NaN, + }), + ).toEqual({ + foo: 42, + bar: '[undefined]', + baz: '[NaN]', + }); + }); - expect(result).toEqual({ - message: 'Wubba Lubba Dub Dub', - name: 'Error', - stack: 'x', - reason: '[Circular ~]', + test('primitive values in deep objects/arrays', () => { + expect(normalize(['foo', 42, [[undefined]], [NaN]])).toEqual(['foo', 42, [['[undefined]']], ['[NaN]']]); + expect( + normalize({ + foo: 42, + bar: { + baz: { + quz: undefined, + }, + }, + wat: { + no: NaN, + }, + }), + ).toEqual({ + foo: 42, + bar: { + baz: { + quz: '[undefined]', + }, + }, + wat: { + no: '[NaN]', + }, + }); + }); + + test('known Classes like Reacts SyntheticEvents', () => { + const obj = { + foo: { + nativeEvent: 'wat', + preventDefault: 'wat', + stopPropagation: 'wat', + }, + }; + expect(normalize(obj)).toEqual({ + foo: '[SyntheticEvent]', + }); + }); + }); + + describe('can limit object to depth', () => { + test('single level', () => { + const obj = { + foo: [], + }; + + expect(normalize(obj, 1)).toEqual({ + foo: '[Array]', + }); + }); + + test('two levels', () => { + const obj = { + foo: [1, 2, []], + }; + + expect(normalize(obj, 2)).toEqual({ + foo: [1, 2, '[Array]'], + }); + }); + + test('multiple levels with various inputs', () => { + const obj = { + foo: { + bar: { + baz: 1, + qux: [ + { + rick: 'morty', + }, + ], + }, + }, + bar: 1, + baz: [ + { + something: 'else', + fn: () => { + /*no-empty*/ + }, + }, + ], + }; + + expect(normalize(obj, 3)).toEqual({ + bar: 1, + baz: [ + { + something: 'else', + fn: '[object Function]', + }, + ], + foo: { + bar: { + baz: 1, + qux: '[Array]', + }, + }, + }); }); }); }); diff --git a/packages/utils/test/string.test.ts b/packages/utils/test/string.test.ts index c8d531149088..e1166ca2dd3c 100644 --- a/packages/utils/test/string.test.ts +++ b/packages/utils/test/string.test.ts @@ -1,4 +1,4 @@ -import { truncate } from '../src/string'; +import { keysToEventMessage, truncate } from '../src/string'; describe('truncate()', () => { test('it works as expected', () => { @@ -8,19 +8,32 @@ describe('truncate()', () => { expect(truncate(new Array(1000).join('f'), 0)).toEqual(new Array(1000).join('f')); expect(truncate(new Array(1000).join('f'), 0)).toEqual(new Array(1000).join('f')); }); +}); + +describe('keysToEventMessage()', () => { + test('no keys', () => { + expect(keysToEventMessage([], 10)).toEqual('[object has no keys]'); + }); + + test('one key should be returned as a whole if not over the length limit', () => { + expect(keysToEventMessage(['foo'], 10)).toEqual('foo'); + expect(keysToEventMessage(['foobarbazx'], 10)).toEqual('foobarbazx'); + }); + + test('one key should be appended with ... and truncated when over the limit', () => { + expect(keysToEventMessage(['foobarbazqux'], 10)).toEqual('foobarbazq...'); + }); + + test('multiple keys should be joined as a whole if not over the length limit', () => { + expect(keysToEventMessage(['foo', 'bar'], 10)).toEqual('foo, bar'); + }); + + test('multiple keys should include only as much keys as can fit into the limit', () => { + expect(keysToEventMessage(['foo', 'bar', 'baz'], 10)).toEqual('foo, bar'); + expect(keysToEventMessage(['foo', 'verylongkey', 'baz'], 10)).toEqual('foo'); + }); - test('it instantly returns input when non-string is passed', () => { - // @ts-ignore - expect(truncate(2)).toEqual(2); - // @ts-ignore - expect(truncate(undefined, 123)).toEqual(undefined); - // @ts-ignore - expect(truncate(null)).toEqual(null); - const obj = {}; - // @ts-ignore - expect(truncate(obj, '42')).toEqual(obj); - const arr: any[] = []; - // @ts-ignore - expect(truncate(arr)).toEqual(arr); + test('multiple keys should truncate first key if its too long', () => { + expect(keysToEventMessage(['foobarbazqux', 'bar', 'baz'], 10)).toEqual('foobarbazq...'); }); });