8000 browser: captureException and captureMessage methods · phthhieu/sentry-javascript@2f3ba6e · GitHub
[go: up one dir, main page]

Skip to content

Commit 2f3ba6e

Browse files
committed
browser: captureException and captureMessage methods
1 parent 858693d commit 2f3ba6e

File tree

9 files changed

+272
-122
lines changed

9 files changed

+272
-122
lines changed

packages/browser/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
"@sentry/hub": "4.0.0-beta.12",
2020
"@sentry/minimal": "4.0.0-beta.12",
2121
"@sentry/types": "4.0.0-beta.12",
22-
"@sentry/utils": "4.0.0-beta.12"
22+
"@sentry/utils": "4.0.0-beta.12",
23+
"@types/md5": "2.1.32",
24+
"md5": "2.2.1"
2325
},
2426
"devDependencies": {
2527
"chai": "^4.1.2",

packages/browser/src/backend.ts

Lines changed: 108 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
1-
import { Backend, DSN, Options, SentryError } from '../../core/dist';
2-
import { addBreadcrumb, captureEvent } from '../../minimal/dist';
3-
import { SentryEvent, SentryResponse, StackFrame } from '../../types/dist';
4-
import { supportsFetch } from '../../utils/supports';
5-
import { Raven } from './raven';
1+
import { Backend, DSN, Options, SentryError } from '@sentry/core';
2+
import { SentryEvent, SentryResponse } from '@sentry/types';
63
import {
7-
StackFrame as TraceKitStackFrame,
8-
StackTrace as TraceKitStackTrace,
9-
} from './tracekit';
4+
isDOMError,
5+
isDOMException,
6+
isError,
7+
isErrorEvent,
8+
isPlainObject,
9+
} from '@sentry/utils/is';
10+
import { supportsFetch } from '@sentry/utils/supports';
11+
import {
12+
eventFromStacktrace,
13+
getEventOptionsFromPlainObject,
14+
prepareFramesForEvent,
15+
} from './parsers';
16+
import { computeStackTrace } from './tracekit';
1017
import { FetchTransport, XHRTransport } from './transports';
1118

12-
const STACKTRACE_LIMIT = 50;
13-
1419
/**
1520
* Configuration options for the Sentry Browser SDK.
1621
* @see BrowserClient for more information.
@@ -86,34 +91,108 @@ export class BrowserBackend implements Backend {
8691
* @inheritDoc
8792
*/
8893
public async eventFromException(exception: any): Promise<SentryEvent> {
89-
const originalSend = Raven._sendProcessedPayload;
90-
try {
91-
let event!: SentryEvent;
92-
Raven._sendProcessedPayload = evt => {
93-
event = evt;
94-
};
95-
Raven.captureException(exception);
96-
return event;
97-
} finally {
98-
Raven._sendProcessedPayload = originalSend;
94+
if (isErrorEvent(exception) && exception.error) {
95+
// If it is an ErrorEvent with `error` property, extract it to get actual Error
96+
exception = exception.error; // tslint:disable-line:no-parameter-reassignment
97+
} else if (isDOMError(exception) || isDOMException(exception)) {
98+
// If it is a DOMError or DOMException (which are legacy APIs, but still supported in some browsers)
99+
// then we just extract the name and message, as they don't provide anything else
100+
// https://developer.mozilla.org/en-US/docs/Web/API/DOMError
101+
// https://developer.mozilla.org/en-US/docs/Web/API/DOMException
102+
const name =
103+
exception.name || (isDOMError(exception) ? 'DOMError' : 'DOMException');
104+
const message = exception.message
105+
? `${name}: ${exception.message}`
106+
: name;
107+
108+
return this.eventFromMessage(message);
109+
} else if (isError(exception)) {
110+
// we have a real Error object, do nothing
111+
} else if (isPlainObject(exception)) {
112+
// If it is plain Object, serialize it manually and extract options
113+
// This will allow us to group events based on top-level keys
114+
// which is much better than creating new group when any key/value change
115+
const options = getEventOptionsFromPlainObject(exception);
116+
exception = new Error(options.message); // tslint:disable-line:no-parameter-reassignment
117+
} else {
118+
// If none of previous checks were valid, then it means that
119+
// it's not a DOMError/DOMException
120+
// it's not a plain Object
121+
// it's not a valid ErrorEvent (one with an error property)
122+
// it's not an Error
123+
// So bail out and capture it as a simple message:
124+
return this.eventFromMessage(exception);
99125
}
126+
127+
// TODO: Create `shouldDropEvent` method to gather all user-options
128+
129+
const event = eventFromStacktrace(computeStackTrace(exception));
130+
131+
return {
132+
...event,
133+
exception: {
134+
...event.exception,
135+
mechanism: {
136+
handled: true,
137+
type: 'generic',
138+
},
139+
},
140+
};
100141
}
101142

102143
/**
103144
* @inheritDoc
104145
*/
105146
public async eventFromMessage(message: string): Promise<SentryEvent> {
106-
const originalSend = Raven._sendProcessedPayload;
147+
message = String(message); // tslint:disable-line:no-parameter-reassignment
148+
149+
// Generate a "synthetic" stack trace from this point.
150+
// NOTE: If you are a Sentry user, and you are seeing this stack frame, it is NOT indicative
151+
// of a bug with Raven.js. Sentry generates synthetic traces either by configuration,
152+
// or if it catches a thrown object without a "stack" property.
153+
// Neither DOMError or DOMException provide stacktrace and we most likely wont get it this way as well
154+
// but it's barely any overhead so we may at least try
155+
let syntheticException: Error;
107156
try {
108-
let event!: SentryEvent;
109-
Raven._sendProcessedPayload = evt => {
110-
event = evt;
111-
};
112-
Raven.captureMessage(message);
113-
return event;
114-
} finally {
115-
Raven._sendProcessedPayload = originalSend;
157+
throw new Error(message);
158+
} catch (exception) {
159+
syntheticException = exception;
160+
// null exception name so `Error` isn't prefixed to msg
161+
(syntheticException as any).name = null; // tslint:disable-line:no-null-keyword
116162
}
163+
164+
const stacktrace = computeStackTrace(syntheticException);
165+
const frames = prepareFramesForEvent(stacktrace.stack);
166+
167+
return {
168+
fingerprint: [message],
169+
message,
170+
stacktrace: {
171+
frames,
172+
},
173+
};
174+
175+
// TODO: Revisit ignoreUrl behavior
176+
177+
// Since we know this is a synthetic trace, the top frame (this function call)
178+
// MUST be from Raven.js, so mark it for trimming
179+
// We add to the trim counter so that callers can choose to trim extra frames, such
180+
// as utility functions.
181+
182+
// stack[0] is `throw new Error(msg)` call itself, we are interested in the frame that was just before that, stack[1]
183+
// let initialCall = Array.isArray(stack.stack) && stack.stack[1];
184+
185+
// if stack[1] is `eventFromException`, it means that someone passed a string to it and we redirected that call
186+
// to be handled by `eventFromMessage`, thus `initialCall` is the 3rd one, not 2nd
187+
// initialCall => captureException(string) => captureMessage(string)
188+
// TODO: Verify if this is actually a correct name
189+
// if (initialCall && initialCall.func === 'eventFromException') {
190+
// initialCall = stack.stack[2];
191+
// }
192+
193+
// const fileurl = (initialCall && initialCall.url) || '';
194+
195+
// TODO: Create `shouldDropEvent` method to gather all user-options
117196
}
118197

119198
/**
@@ -152,66 +231,4 @@ export class BrowserBackend implements Backend {
152231
public storeScope(): void {
153232
// Noop
154233
}
155-
156-
private prepareFrames(
157-
stackInfo: TraceKitStackTrace,
158-
options: {
159-
trimHeadFrames: number;
160-
} = {
161-
trimHeadFrames: 0,
162-
},
163-
): StackFrame[] {
164-
if (stackInfo.stack && stackInfo.stack.length) {
165-
const frames = stackInfo.stack.map((frame: TraceKitStackFrame) =>
166-
this.normalizeFrame(frame, stackInfo.url),
167-
);
168-
169-
// e.g. frames captured via captureMessage throw
170-
for (let j = 0; j < options.trimHeadFrames && j < frames.length; j++) {
171-
frames[j].in_app = false;
172-
}
173-
174-
return frames.slice(0, STACKTRACE_LIMIT);
175-
} else {
176-
return [];
177-
}
178-
}
179-
180-
/**
181-
* @inheritDoc
182-
*/
183-
private normalizeFrame(
184-
frame: TraceKitStackFrame,
185-
stackInfoUrl: string,
186-
): StackFrame {
187-
// normalize the frames data
188-
const normalized = {
189-
colno: frame.column,
190-
filename: frame.url,
191-
function: frame.func || '?',
192-
in_app: true,
193-
lineno: frame.line,
194-
};
195-
196-
// Case when we don't have any information about the error
197-
// E.g. throwing a string or raw object, instead of an `Error` in Firefox
198-
// Generating synthetic error doesn't add any value here
199-
//
200-
// We should probably somehow let a user know that they should fix their code
201-
if (!frame.url) {
202-
normalized.filename = stackInfoUrl; // fallback to whole stacks url from onerror handler
203-
}
204-
205-
// TODO: This has to be fixed
206-
// determine if an exception came from outside of our app
207-
// first we check the global includePaths list.
208-
// Now we check for fun, if the function name is Raven or TraceKit
209-
// finally, we do a last ditch effort and check for raven.min.js
210-
normalized.in_app = !(
211-
/(Sentry|TraceKit)\./.test(normalized.function) ||
212-
/raven\.(min\.)?js$/.test(normalized.filename)
213-
);
214-
215-
return normalized;
216-
}
217234
}

packages/browser/src/integrations/index.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
export { OnUnhandledRejection } from './onunhandledrejection';
2-
export { OnError } from './onerror';
1+
export { GlobalHandlers } from './globalhandlers';
32
export { FunctionToString } from './functiontostring';
43
export { TryCatch } from './trycatch';
54
export { Breadcrumbs } from './breadcrumbs';

packages/browser/src/integrations/onerror.ts

Lines changed: 0 additions & 24 deletions
This file was deleted.

packages/browser/src/parsers.ts

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { SentryEvent, StackFrame } from '@sentry/types';
2+
import {
3+
limitObjectDepthToSize,
4+
serializeKeysToEventMessage,
5+
} from '@sentry/utils/object';
6+
import * as md5proxy from 'md5';
7+
import {
8+
StackFrame as TraceKitStackFrame,
9+
StackTrace as TraceKitStackTrace,
10+
} from './tracekit';
11+
12+
// Workaround for Rollup issue with overloading namespaces
13+
// https://github.com/rollup/rollup/issues/1267#issuecomment-296395734
14+
const md5 = (md5proxy as any).default || md5proxy;
15+
16+
const STACKTRACE_LIMIT = 50;
17+
18+
/** TODO */
19+
export function getEventOptionsFromPlainObject(
20+
exception: Error,
21+
): {
22+
extra: {
23+
__serialized__: object;
24+
};
25+
fingerprint: [string];
26+
message: string;
27+
} {
28+
const exceptionKeys = Object.keys(exception).sort();
29+
return {
30+
extra: {
31+
__serialized__: limitObjectDepthToSize(exception),
32+
},
33+
fingerprint: [md5(exceptionKeys.join(''))],
34+
message: `Non-Error exception captured with keys: ${serializeKeysToEventMessage(
35+
exceptionKeys,
36+
)}`,
37+
};
38+
}
39+
40+
export function eventFromStacktrace(
41+
stacktrace: TraceKitStackTrace,
42+
): SentryEvent {
43+
const frames = prepareFramesForEvent(stacktrace.stack);
44+
// const prefixedMessage =
45+
// (stack.name ? stack.name + ': ' : '') + (stack.message || '');
46+
const transaction =
47+
stacktrace.url ||
48+
(stacktrace.stack && stacktrace.stack[0].url) ||
49+
'<unknown>';
50+
51+
const ex = {
52+
stacktrace: { frames },
53+
type: stacktrace.name,
54+
value: stacktrace.message,
55+
};
56+
57+
if (ex.type === undefined && ex.value === '') {
58+
ex.value = 'Unrecoverable error caught';
59+
}
60+
61+
return {
62+
exception: {
63+
values: [ex],
64+
},
65+
transaction,
66+
};
67+
}
68+
69+
/** TODO */
70+
export function prepareFramesForEvent(
71+
stack: TraceKitStackFrame[],
72+
): StackFrame[] {
73+
if (!stack) {
74+
return [];
75+
}
76+
77+
const topFrameUrl = stack[0].url;
78+
79+
return (
80+
stack
81+
// TODO: REMOVE ME, TESTING ONLY
82+
// Remove frames that don't have filename, colno and lineno.
83+
// Things like `new Promise` called by generated code
84+
// eg. async/await from regenerator
85+
.filter(frame => {
86+
if (frame.url.includes('packages/browser/build/bundle.min.js')) {
87+
return false;
88+
}
89+
if (frame.url === '<anonymous>' && !frame.column && !frame.line) {
90+
return false;
91+
}
92+
return true;
93+
})
94+
.map(
95+
(frame: TraceKitStackFrame): StackFrame => ({
96+
// normalize the frames data
97+
// Case when we don't have any information about the error
98+
// E.g. throwing a string or raw object, instead of an `Error` in Firefox
99+
// Generating synthetic error doesn't add any value here
100+
//
101+
// We should probably somehow let a user know that they should fix their code
102+
103+
// e.g. frames captured via captureMessage throw
104+
// for (let j = 0; j < options.trimHeadFrames && j < frames.length; j++) {
105+
// frames[j].in_app = false;
106+
// }
107+
108+
// TODO: This has to be fixed
109+
// determine if an exception came from outside of our app
110+
// first we check the global includePaths list.
111+
// Now we check for fun, if the function name is Raven or TraceKit
112+
// finally, we do a last ditch effort and check for raven.min.js
113+
// normalized.in_app = !(
114+
// /(Sentry|TraceKit)\./.test(normalized.function) ||
115+
// /raven\.(min\.)?js$/.test(normalized.filename)
116+
// );
117+
colno: frame.column,
118+
filename: frame.url || topFrameUrl,
119+
function: frame.func || '?',
120+
in_app: true,
121+
lineno: frame.line,
122+
}),
123+
)
124+
.slice(0, STACKTRACE_LIMIT)
125+
);
126+
}

0 commit comments

Comments
 (0)
0