-
-
Notifications
You must be signed in to change notification settings - Fork 344
/
Copy pathdebugsymbolicator.ts
163 lines (142 loc) · 5.62 KB
/
debugsymbolicator.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
import type { Event, EventHint, Exception, Integration, StackFrame as SentryStackFrame } from '@sentry/core';
import { logger } from '@sentry/core';
import type { ExtendedError } from '../utils/error';
import { getFramesToPop, isErrorLike } from '../utils/error';
import type * as ReactNative from '../vendor/react-native';
import { fetchSourceContext, parseErrorStack, symbolicateStackTrace } from './debugsymbolicatorutils';
const INTEGRATION_NAME = 'DebugSymbolicator';
// eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor
const INTERNAL_CALLSITES_REGEX = new RegExp(['ReactNativeRenderer-dev\\.js$', 'MessageQueue\\.js$'].join('|'));
/**
* React Native Error
*/
export type ReactNativeError = Error & {
framesToPop?: number;
jsEngine?: string;
preventSymbolication?: boolean;
componentStack?: string;
};
/** Tries to symbolicate the JS stack trace on the device. */
export const debugSymbolicatorIntegration = (): Integration => {
return {
name: INTEGRATION_NAME,
setupOnce: () => {
/* noop */
},
processEvent,
};
};
async function processEvent(event: Event, hint: EventHint): Promise<Event> {
if (event.exception?.values && isErrorLike(hint.originalException)) {
// originalException is ErrorLike object
const errorGroup = getExceptionGroup(hint.originalException);
for (const [index, error] of errorGroup.entries()) {
const symbolicatedFrames = await symbolicate(error.stack, getFramesToPop(error));
symbolicatedFrames && replaceExceptionFramesInException(event.exception.values[index], symbolicatedFrames);
}
} else if (hint.syntheticException && isErrorLike(hint.syntheticException)) {
// syntheticException is Error object
const symbolicatedFrames = await symbolicate(
hint.syntheticException.stack,
getFramesToPop(hint.syntheticException),
);
if (event.exception) {
symbolicatedFrames &&
event.exception.values &&
replaceExceptionFramesInException(event.exception.values[0], symbolicatedFrames);
} else if (event.threads) {
// RN JS doesn't have threads
symbolicatedFrames && replaceThreadFramesInEvent(event, symbolicatedFrames);
}
}
return event;
}
/**
* Symbolicates the stack on the device talking to local dev server.
* Mutates the passed event.
*/
async function symbolicate(rawStack: string, skipFirstFrames: number = 0): Promise<SentryStackFrame[] | null> {
try {
const parsedStack = parseErrorStack(rawStack);
const prettyStack = await symbolicateStackTrace(parsedStack);
if (!prettyStack) {
logger.error('React Native DevServer could not symbolicate the stack trace.');
return null;
}
// This has been changed in an react-native version so stack is contained in here
const newStack = 'stack' in prettyStack ? prettyStack.stack : prettyStack;
// https://github.com/getsentry/sentry-javascript/blob/739d904342aaf9327312f409952f14ceff4ae1ab/packages/utils/src/stacktrace.ts#L23
// Match SentryParser which counts lines of stack (-1 for first line with the Error message)
const skipFirstAdjustedToSentryStackParser = Math.max(skipFirstFrames - 1, 0);
const stackWithoutPoppedFrames = skipFirstAdjustedToSentryStackParser
? newStack.slice(skipFirstAdjustedToSentryStackParser)
: newStack;
const stackWithoutInternalCallsites = stackWithoutPoppedFrames.filter(
(frame: { file?: string }) => frame.file && frame.file.match(INTERNAL_CALLSITES_REGEX) === null,
);
const sentryFrames = await convertReactNativeFramesToSentryFrames(stackWithoutInternalCallsites);
return await fetchSourceContext(sentryFrames);
} catch (error) {
if (error instanceof Error) {
logger.warn(`Unable to symbolicate stack trace: ${error.message}`);
}
return null;
}
}
/**
* Converts ReactNativeFrames to frames in the Sentry format
* @param frames ReactNativeFrame[]
*/
async function convertReactNativeFramesToSentryFrames(frames: ReactNative.StackFrame[]): Promise<SentryStackFrame[]> {
return Promise.all(
frames.map(async (frame: ReactNative.StackFrame): Promise<SentryStackFrame> => {
let inApp = !!frame.column && !!frame.lineNumber;
inApp =
inApp &&
frame.file !== undefined &&
!frame.file.includes('node_modules') &&
!frame.file.includes('native code');
const newFrame: SentryStackFrame = {
lineno: frame.lineNumber,
colno: frame.column,
filename: frame.file,
function: frame.methodName,
in_app: inApp,
};
return newFrame;
}),
);
}
/**
* Replaces the frames in the exception of a error.
* @param event Event
* @param frames StackFrame[]
*/
function replaceExceptionFramesInException(exception: Exception | undefined, frames: SentryStackFrame[]): void {
if (exception?.stacktrace) {
exception.stacktrace.frames = frames.reverse();
}
}
/**
* Replaces the frames in the thread of a message.
* @param event Event
* @param frames StackFrame[]
*/
function replaceThreadFramesInEvent(event: Event, frames: SentryStackFrame[]): void {
if (event.threads?.values?.[0]?.stacktrace) {
event.threads.values[0].stacktrace.frames = frames.reverse();
}
}
/**
* Return a list containing the original exception and also the cause if found.
*
* @param originalException The original exception.
*/
function getExceptionGroup(originalException: unknown): (Error & { stack: string })[] {
const err = originalException as ExtendedError;
const errorGroup: (Error & { stack: string })[] = [];
for (let cause: ExtendedError | undefined = err; isErrorLike(cause); cause = cause.cause) {
errorGroup.push(cause);
}
return errorGroup;
}