Skip to content

Commit 804a311

Browse files
committed
ref: Make Event instances serializeable
1 parent 68b248a commit 804a311

File tree

8 files changed

+204
-167
lines changed

8 files changed

+204
-167
lines changed

packages/browser/src/backend.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export class BrowserBackend extends BaseBackend<BrowserOptions> {
5858
*/
5959
public eventFromException(exception: any, hint?: EventHint): SyncPromise<Event> {
6060
const syntheticException = (hint && hint.syntheticException) || undefined;
61-
const event = eventFromUnknownInput(exception, syntheticException);
61+
const event = eventFromUnknownInput(exception, syntheticException, 'error');
6262
addExceptionMechanism(event, {
6363
handled: true,
6464
type: 'generic',

packages/browser/src/eventbuilder.ts

+14-5
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,19 @@ import {
66
isDOMException,
77
isError,
88
isErrorEvent,
9+
isEvent,
910
isPlainObject,
1011
} from '@sentry/utils';
1112

1213
import { eventFromPlainObject, eventFromStacktrace, prepareFramesForEvent } from './parsers';
1314
import { computeStackTrace } from './tracekit';
1415

1516
/** JSDoc */
16-
export function eventFromUnknownInput(exception: unknown, syntheticException?: Error): Event {
17+
export function eventFromUnknownInput(
18+
exception: unknown,
19+
syntheticException?: Error,
20+
type?: 'error' | 'promise',
21+
): Event {
1722
let event: Event;
1823

1924
if (isErrorEvent(exception as ErrorEvent) && (exception as ErrorEvent).error) {
@@ -41,13 +46,17 @@ export function eventFromUnknownInput(exception: unknown, syntheticException?: E
4146
event = eventFromStacktrace(computeStackTrace(exception as Error));
4247
return event;
4348
}
44-
if (isPlainObject(exception as {})) {
45-
// If it is plain Object, serialize it manually and extract options
49+
if (isPlainObject(exception) || isEvent(exception)) {
50+
// If it is plain Object or Event, serialize it manually and extract options
4651
// This will allow us to group events based on top-level keys
4752
// which is much better than creating new group when any key/value change
4853
const objectException = exception as {};
49-
event = eventFromPlainObject(objectException, syntheticException);
50-
addExceptionTypeValue(event, 'Custom Object', undefined);
54+
event = eventFromPlainObject(objectException, syntheticException, type);
55+
addExceptionTypeValue(
56+
event,
57+
isEvent(exception) ? exception.constructor.name : 'Custom Object',
58+
type === 'promise' ? 'UnhandledRejection' : 'Error',
59+
);
5160
addExceptionMechanism(event, {
5261
synthetic: true,
5362
});

packages/browser/src/helpers.ts

+2-88
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { captureException, getCurrentHub, withScope } from '@sentry/core';
22
import { Event as SentryEvent, Mechanism, WrappedFunction } from '@sentry/types';
3-
import { addExceptionMechanism, addExceptionTypeValue, isString, normalize } from '@sentry/utils';
3+
import { addExceptionMechanism, addExceptionTypeValue, htmlTreeAsString, normalize } from '@sentry/utils';
44

55
const debounceDuration: number = 1000;
66
let keypressTimeout: number | undefined;
@@ -188,17 +188,7 @@ export function breadcrumbEventHandler(eventName: string, debounce: boolean = fa
188188
lastCapturedEvent = event;
189189

190190
const captureBreadcrumb = () => {
191-
// try/catch both:
192-
// - accessing event.target (see getsentry/raven-js#838, #768)
193-
// - `htmlTreeAsString` because it's complex, and just accessing the DOM incorrectly
194-
// can throw an exception in some circumstances.
195-
let target;
196-
try {
197-
target = event.target ? _htmlTreeAsString(event.target as Node) : _htmlTreeAsString((event as unknown) as Node);
198-
} catch (e) {
199-
target = '<unknown>';
200-
}
201-
191+
const target = htmlTreeAsString(event.target as Node);
202192
if (target.length === 0) {
203193
return;
204194
}
@@ -268,79 +258,3 @@ export function keypressEventHandler(): (event: Event) => void {
268258
}, debounceDuration) as any) as number;
269259
};
270260
}
271-
272-
/**
273-
* Given a child DOM element, returns a query-selector statement describing that
274-
* and its ancestors
275-
* e.g. [HTMLElement] => body > div > input#foo.btn[name=baz]
276-
* @returns generated DOM path
277-
*/
278-
function _htmlTreeAsString(elem: Node): string {
279-
let currentElem: Node | null = elem;
280-
const MAX_TRAVERSE_HEIGHT = 5;
281-
const MAX_OUTPUT_LEN = 80;
282-
const out = [];
283-
let height = 0;
284-
let len = 0;
285-
const separator = ' > ';
286-
const sepLength = separator.length;
287-
let nextStr;
288-
289-
while (currentElem && height++ < MAX_TRAVERSE_HEIGHT) {
290-
nextStr = _htmlElementAsString(currentElem as HTMLElement);
291-
// bail out if
292-
// - nextStr is the 'html' element
293-
// - the length of the string that would be created exceeds MAX_OUTPUT_LEN
294-
// (ignore this limit if we are on the first iteration)
295-
if (nextStr === 'html' || (height > 1 && len + out.length * sepLength + nextStr.length >= MAX_OUTPUT_LEN)) {
296-
break;
297-
}
298-
299-
out.push(nextStr);
300-
301-
len += nextStr.length;
302-
currentElem = currentElem.parentNode;
303-
}
304-
305-
return out.reverse().join(separator);
306-
}
307-
308-
/**
309-
* Returns a simple, query-selector representation of a DOM element
310-
* e.g. [HTMLElement] => input#foo.btn[name=baz]
311-
* @returns generated DOM path
312-
*/
313-
function _htmlElementAsString(elem: HTMLElement): string {
314-
const out = [];
315-
let className;
316-
let classes;
317-
let key;
318-
let attr;
319-
let i;
320-
321-
if (!elem || !elem.tagName) {
322-
return '';
323-
}
324-
325-
out.push(elem.tagName.toLowerCase());
326-
if (elem.id) {
327-
out.push(`#${elem.id}`);
328-
}
329-
330-
className = elem.className;
331-
if (className && isString(className)) {
332-
classes = className.split(/\s+/);
333-
for (i = 0; i < classes.length; i++) {
334-
out.push(`.${classes[i]}`);
335-
}
336-
}
337-
const attrWhitelist = ['type', 'name', 'title', 'alt'];
338-
for (i = 0; i < attrWhitelist.length; i++) {
339-
key = attrWhitelist[i];
340-
attr = elem.getAttribute(key);
341-
if (attr) {
342-
out.push(`[${key}="${attr}"]`);
343-
}
344-
}
345-
return out.join('');
346-
}

packages/browser/src/integrations/globalhandlers.ts

+14-40
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,17 @@ import { Event, Integration, Severity } from '@sentry/types';
33
import {
44
addExceptionMechanism,
55
addExceptionTypeValue,
6-
extractExceptionKeysForMessage,
76
getGlobalObject,
87
getLocationHref,
98
isErrorEvent,
109
isPrimitive,
1110
isString,
1211
logger,
13-
normalizeToSize,
1412
truncate,
1513
} from '@sentry/utils';
1614

1715
import { eventFromUnknownInput } from '../eventbuilder';
1816
import { shouldIgnoreOnError } from '../helpers';
19-
import { computeStackTrace } from '../tracekit';
2017

2118
/** JSDoc */
2219
interface GlobalHandlersIntegrations {
@@ -101,7 +98,7 @@ export class GlobalHandlers implements Integration {
10198

10299
const event = isPrimitive(error)
103100
? self._eventFromIncompleteOnError(msg, url, line, column)
104-
: self._enhanceEventWithInitialFrame(eventFromUnknownInput(error), url, line, column);
101+
: self._enhanceEventWithInitialFrame(eventFromUnknownInput(error, undefined, 'error'), url, line, column);
105102

106103
const client = getCurrentHub().getClient();
107104
const maxValueLength = (client && client.getOptions().maxValueLength) || 250;
@@ -154,8 +151,9 @@ export class GlobalHandlers implements Integration {
154151
return false;
155152
}
156153

157-
const stack = computeStackTrace(error);
158-
const event = stack.failed ? self._eventFromIncompleteRejection(error) : eventFromUnknownInput(error);
154+
const event = isPrimitive(error)
155+
? self._eventFromIncompleteRejection(error)
156+
: eventFromUnknownInput(error, undefined, 'promise');
159157

160158
const client = getCurrentHub().getClient();
161159
const maxValueLength = (client && client.getOptions().maxValueLength) || 250;
@@ -213,6 +211,16 @@ export class GlobalHandlers implements Integration {
213211
return this._enhanceEventWithInitialFrame(event, url, line, column);
214212
}
215213

214+
/**
215+
* This function creates an Event from an TraceKitStackTrace that has part of it missing.
216+
*/
217+
private _eventFromIncompleteRejection(error: any): Event {
218+
return {
219+
level: Severity.Error,
220+
message: `Non-Error promise rejection captured with value: ${error}`,
221+
};
222+
}
223+
216224
/** JSDoc */
217225
private _enhanceEventWithInitialFrame(event: Event, url: any, line: any, column: any): Event {
218226
event.exception = event.exception || {};
@@ -233,38 +241,4 @@ export class GlobalHandlers implements Integration {
233241

234242
return event;
235243
}
236-
237-
/**
238-
* This function creates an Event from an TraceKitStackTrace that has part of it missing.
239-
*/
240-
private _eventFromIncompleteRejection(error: any): Event {
241-
const event: Event = {
242-
level: Severity.Error,
243-
};
244-
245-
if (isPrimitive(error)) {
246-
event.exception = {
247-
values: [
248-
{
249-
type: 'UnhandledRejection',
250-
value: `Non-Error promise rejection captured with value: ${error}`,
251-
},
252-
],
253-
};
254-
} else {
255-
event.exception = {
256-
values: [
257-
{
258-
type: 'UnhandledRejection',
259-
value: `Non-Error promise rejection captured with keys: ${extractExceptionKeysForMessage(error)}`,
260-
},
261-
],
262-
};
263-
event.extra = {
264-
__serialized__: normalizeToSize(error),
265-
};
266-
}
267-
268-
return event;
269-
}
270244
}

packages/browser/src/parsers.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,14 @@ export function exceptionFromStacktrace(stacktrace: TraceKitStackTrace): Excepti
3333
/**
3434
* @hidden
3535
*/
36-
export function eventFromPlainObject(exception: {}, syntheticException?: Error): Event {
36+
export function eventFromPlainObject(exception: {}, syntheticException?: Error, type?: 'error' | 'promise'): Event {
3737
const event: Event = {
3838
extra: {
3939
__serialized__: normalizeToSize(exception),
4040
},
41-
message: `Non-Error exception captured with keys: ${extractExceptionKeysForMessage(exception)}`,
41+
message: `Non-Error ${
42+
type === 'promise' ? 'promise rejection' : 'exception'
43+
} captured with keys: ${extractExceptionKeysForMessage(exception)}`,
4244
};
4345

4446
if (syntheticException) {

packages/utils/src/is.ts

+24
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,30 @@ export function isPlainObject(wat: any): boolean {
8484
return Object.prototype.toString.call(wat) === '[object Object]';
8585
}
8686

87+
/**
88+
* Checks whether given value's type is an Event instance
89+
* {@link isEvent}.
90+
*
91+
* @param wat A value to be checked.
92+
* @returns A boolean representing the result.
93+
*/
94+
export function isEvent(wat: any): wat is Event {
95+
// tslint:disable-next-line:strict-type-predicates
96+
return typeof Event !== 'undefined' && wat instanceof Event;
97+
}
98+
99+
/**
100+
* Checks whether given value's type is an Element instance
101+
* {@link isElement}.
102+
*
103+
* @param wat A value to be checked.
104+
* @returns A boolean representing the result.
105+
*/
106+
export function isElement(wat: any): wat is Element {
107+
// tslint:disable-next-line:strict-type-predicates
108+
return typeof Element !== 'undefined' && wat instanceof Element;
109+
}
110+
87111
/**
88112
* Checks whether given value's type is an regexp
89113
* {@link isRegExp}.

packages/utils/src/misc.ts

+85
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Event, Integration, WrappedFunction } from '@sentry/types';
2+
import { isString } from './is';
23

34
/** Internal */
45
interface SentryGlobal {
@@ -247,3 +248,87 @@ export function getLocationHref(): string {
247248
return '';
248249
}
249250
}
251+
252+
/**
253+
* Given a child DOM element, returns a query-selector statement describing that
254+
* and its ancestors
255+
* e.g. [HTMLElement] => body > div > input#foo.btn[name=baz]
256+
* @returns generated DOM path
257+
*/
258+
export function htmlTreeAsString(elem: Node): string {
259+
// try/catch both:
260+
// - accessing event.target (see getsentry/raven-js#838, #768)
261+
// - `htmlTreeAsString` because it's complex, and just accessing the DOM incorrectly
262+
// can throw an exception in some circumstances.
263+
try {
264+
let currentElem: Node | null = elem;
265+
const MAX_TRAVERSE_HEIGHT = 5;
266+
const MAX_OUTPUT_LEN = 80;
267+
const out = [];
268+
let height = 0;
269+
let len = 0;
270+
const separator = ' > ';
271+
const sepLength = separator.length;
272+
let nextStr;
273+
274+
while (currentElem && height++ < MAX_TRAVERSE_HEIGHT) {
275+
nextStr = _htmlElementAsString(currentElem as HTMLElement);
276+
// bail out if
277+
// - nextStr is the 'html' element
278+
// - the length of the string that would be created exceeds MAX_OUTPUT_LEN
279+
// (ignore this limit if we are on the first iteration)
280+
if (nextStr === 'html' || (height > 1 && len + out.length * sepLength + nextStr.length >= MAX_OUTPUT_LEN)) {
281+
break;
282+
}
283+
284+
out.push(nextStr);
285+
286+
len += nextStr.length;
287+
currentElem = currentElem.parentNode;
288+
}
289+
290+
return out.reverse().join(separator);
291+
} catch (_oO) {
292+
return '<unknown>';
293+
}
294+
}
295+
296+
/**
297+
* Returns a simple, query-selector representation of a DOM element
298+
* e.g. [HTMLElement] => input#foo.btn[name=baz]
299+
* @returns generated DOM path
300+
*/
301+
function _htmlElementAsString(elem: HTMLElement): string {
302+
const out = [];
303+
let className;
304+
let classes;
305+
let key;
306+
let attr;
307+
let i;
308+
309+
if (!elem || !elem.tagName) {
310+
return '';
311+
}
312+
313+
out.push(elem.tagName.toLowerCase());
314+
if (elem.id) {
315+
out.push(`#${elem.id}`);
316+
}
317+
318+
className = elem.className;
319+
if (className && isString(className)) {
320+
classes = className.split(/\s+/);
321+
for (i = 0; i < classes.length; i++) {
322+
out.push(`.${classes[i]}`);
323+
}
324+
}
325+
const attrWhitelist = ['type', 'name', 'title', 'alt'];
326+
for (i = 0; i < attrWhitelist.length; i++) {
327+
key = attrWhitelist[i];
328+
attr = elem.getAttribute(key);
329+
if (attr) {
330+
out.push(`[${key}="${attr}"]`);
331+
}
332+
}
333+
return out.join('');
334+
}

0 commit comments

Comments
 (0)