Skip to content

Commit 83094cd

Browse files
committed
fix: fixed callstack parsing when there is java + javascript
1 parent f13caff commit 83094cd

File tree

4 files changed

+152
-62
lines changed

4 files changed

+152
-62
lines changed

src/client.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
1-
import { BrowserClient, defaultStackParser, makeFetchTransport } from '@sentry/browser';
1+
import { BrowserClient, makeFetchTransport } from '@sentry/browser';
22
import { BrowserTransportOptions } from '@sentry/browser/types/transports/types';
33
import { FetchImpl } from '@sentry/browser/types/transports/utils';
44
import { BaseClient } from '@sentry/core';
55

66
import { ClientReportEnvelope, ClientReportItem, Envelope, Event, EventHint, Outcome, SeverityLevel, Transport, UserFeedback } from '@sentry/types';
77
import { SentryError, dateTimestampInSeconds, logger } from '@sentry/utils';
88

9+
import { alert } from '@nativescript/core';
910
import { defaultSdkInfo } from './integrations/sdkinfo';
1011
import { NativescriptClientOptions } from './options';
1112
import { NativeTransport } from './transports/native';
12-
import { NATIVE } from './wrapper';
13-
import { alert } from '@nativescript/core';
14-
import { mergeOutcomes } from './utils/outcome';
1513
import { createUserFeedbackEnvelope, items } from './utils/envelope';
14+
import { mergeOutcomes } from './utils/outcome';
15+
import { NATIVE } from './wrapper';
16+
1617

1718
/**
1819
* The Sentry React Native SDK Client.
@@ -47,7 +48,7 @@ export class NativescriptClient extends BaseClient<NativescriptClientOptions> {
4748
dsn: options.dsn,
4849
transport: options.transport,
4950
transportOptions: options.transportOptions,
50-
stackParser: options.stackParser || defaultStackParser,
51+
stackParser: options.stackParser,
5152
integrations: [],
5253
_metadata: options._metadata,
5354
});
@@ -59,6 +60,10 @@ export class NativescriptClient extends BaseClient<NativescriptClientOptions> {
5960
* @inheritDoc
6061
*/
6162
public eventFromException(_exception: unknown, _hint?: EventHint): PromiseLike<Event> {
63+
// N put stackTrace in "stackTrace" instead of "stacktrace"
64+
if (_exception['stackTrace']) {
65+
_exception['stacktrace'] = _exception['stackTrace'];
66+
}
6267
return this._browserClient.eventFromException(_exception, _hint);
6368
}
6469

src/integrations/debugsymbolicator.ts

Lines changed: 78 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { addGlobalEventProcessor, getCurrentHub } from '@sentry/core';
2-
import { Event, EventHint, Integration, StackFrame } from '@sentry/types';
3-
import { logger } from '@sentry/utils';
2+
import { Event, EventHint, Integration, StackFrame, StackLineParser } from '@sentry/types';
3+
import { logger, stackParserFromStackParserOptions } from '@sentry/utils';
44

55
// const INTERNAL_CALLSITES_REGEX = new RegExp(['/Libraries/Renderer/oss/NativescriptRenderer-dev\\.js$', '/Libraries/BatchedBridge/MessageQueue\\.js$'].join('|'));
66

@@ -19,6 +19,7 @@ interface NativescriptFrame {
1919
* React Native Error
2020
*/
2121
type NativescriptError = Error & {
22+
stackTrace?: string;
2223
framesToPop?: number;
2324
jsEngine?: string;
2425
preventSymbolication?: boolean;
@@ -32,44 +33,81 @@ type NativescriptError = Error & {
3233
// isComponentError?: boolean,
3334
// ...
3435
// };
36+
const UNKNOWN_FUNCTION = undefined;
3537

36-
export function parseErrorStack(e: NativescriptError): NativescriptFrame[] {
37-
if (!e || !e.stack) {
38-
return [];
39-
}
40-
const stacktraceParser = require('stacktrace-parser');
41-
if (Array.isArray(e.stack)) {
42-
return e.stack;
43-
} else {
44-
return stacktraceParser.parse('at ' + e.stack).map(frame => ({
45-
...frame,
46-
column: frame.column != null ? frame.column - 1 : null
47-
}));
48-
}
38+
export interface NativescriptStackFrame extends StackFrame {
39+
native?: boolean;
4940
}
5041

51-
/**
52-
* Converts NativescriptFrames to frames in the Sentry format
53-
* @param frames NativescriptFrame[]
54-
*/
55-
export function convertNativescriptFramesToSentryFrames(frames: NativescriptFrame[]): StackFrame[] {
56-
// Below you will find lines marked with :HACK to prevent showing errors in the sentry ui
57-
// But since this is a debug only feature: This is Fine (TM)
58-
return frames.map(
59-
(frame: NativescriptFrame): StackFrame => {
60-
const inApp = (frame.file && !frame.file.includes('node_modules')) || (!!frame.column && !!frame.lineNumber);
61-
// const inApp =true;
62-
return {
63-
colno: frame.column,
64-
filename: frame.file,
65-
function: frame.methodName,
66-
in_app: inApp,
67-
lineno: inApp ? frame.lineNumber : undefined, // :HACK
68-
platform: inApp ? 'javascript' : 'node' // :HACK
69-
};
42+
// function createFrame(filename, func, lineno, colno) {
43+
44+
function createFrame(frame: Partial<NativescriptStackFrame>) {
45+
frame.in_app = (frame.filename && !frame.filename.includes('node_modules')) || (!!frame.colno && !!frame.lineno);
46+
frame.platform = frame.filename.endsWith('.js') ? 'javascript' : 'android';
47+
48+
49+
return frame;
50+
}
51+
52+
const nativescriptRegex =
53+
/^\s*at (?:(.*\).*?|.*?) ?\()?((?:file|native|webpack|<anonymous>|[-a-z]+:|.*bundle|\/)?.*?)(?::(\d+))?(?::(\d+))?\)?\s*$/i;
54+
55+
const nativescriptFunc = line => {
56+
const parts = nativescriptRegex.exec(line);
57+
if (parts) {
58+
return createFrame({
59+
filename:parts[2],
60+
platform:'javascript',
61+
function:parts[1] || UNKNOWN_FUNCTION,
62+
lineno:parts[3] ? +parts[3] : undefined,
63+
colno:parts[4] ? +parts[4] : undefined
64+
});
65+
}
66+
return null;
67+
};
68+
69+
const nativescriptLineParser: StackLineParser = [30, nativescriptFunc];
70+
71+
const androidRegex =
72+
/^\s*(?:(.*\).*?|.*?) ?\()?((?:Native Method|[-a-z]+:)?.*?)(?::(\d+))?(?::(\d+))?\)?\s*$/i;
73+
74+
const androidFunc = line => {
75+
const parts = androidRegex.exec(line);
76+
if (parts) {
77+
let func = UNKNOWN_FUNCTION, mod;
78+
if (parts[1]) {
79+
const splitted = parts[1].split('.');
80+
func = splitted[splitted.length-1];
81+
mod = splitted.slice(0, -1).join('.');
82+
}
83+
if (!parts[2].endsWith('.java')) {
84+
return null;
7085
}
71-
);
86+
return createFrame({
87+
filename:parts[2],
88+
function:func,
89+
module:mod,
90+
native: func && (func.indexOf('Native Method') !== -1),
91+
lineno:parts[3] ? +parts[3] : undefined,
92+
colno:parts[4] ? +parts[4] : undefined
93+
});
94+
}
95+
return null;
96+
};
97+
98+
const androidLineParser: StackLineParser = [50, androidFunc];
99+
100+
const stackParser = stackParserFromStackParserOptions([nativescriptLineParser, androidLineParser]);
101+
102+
export function parseErrorStack(e: NativescriptError): StackFrame[] {
103+
const stack = e?.['stackTrace'] || e?.stack;
104+
if (!stack) {
105+
return [];
106+
}
107+
console.log('parseErrorStack', stack);
108+
return stackParser(stack);
72109
}
110+
73111
/** Tries to symbolicate the JS stack trace on the device. */
74112
export class DebugSymbolicator implements Integration {
75113
/**
@@ -92,9 +130,11 @@ export class DebugSymbolicator implements Integration {
92130
}
93131
// @ts-ignore
94132
const error: NativescriptError = hint.originalException;
95-
96133
// const parseErrorStack = require('react-native/Libraries/Core/Devtools/parseErrorStack');
97134
const stack = parseErrorStack(error);
135+
console.log('addGlobalEventProcessor', error);
136+
console.log('stack', stack);
137+
98138

99139
// Ideally this should go into contexts but android sdk doesn't support it
100140
event.extra = {
@@ -117,12 +157,11 @@ export class DebugSymbolicator implements Integration {
117157
*/
118158
private async _symbolicate(
119159
event: Event,
120-
stack: NativescriptFrame[]
160+
stack: StackFrame[]
121161
): Promise<void> {
122162
try {
123163
// eslint-disable-next-line @typescript-eslint/no-var-requires
124-
const convertedFrames = convertNativescriptFramesToSentryFrames(stack);
125-
this._replaceFramesInEvent(event, convertedFrames);
164+
this._replaceFramesInEvent(event, stack);
126165
} catch (error) {
127166
if (error instanceof Error) {
128167
logger.warn(`Unable to symbolicate stack trace: ${error.message}`);

src/sdk.ts

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Integrations, defaultStackParser, getCurrentHub, defaultIntegrations as sentryDefaultIntegrations } from '@sentry/browser';
1+
import { Integrations, getCurrentHub, defaultIntegrations as sentryDefaultIntegrations } from '@sentry/browser';
22
import { Hub, Scope, getIntegrationsToSetup, initAndBind, makeMain, setExtra } from '@sentry/core';
33
import { RewriteFrames } from '@sentry/integrations';
44
import { Integration, StackFrame, UserFeedback } from '@sentry/types';
@@ -17,6 +17,47 @@ import { makeNativescriptTransport } from './transports/native';
1717
import { makeUtf8TextEncoder } from './transports/TextEncoder';
1818
import { safeFactory, safeTracesSampler } from './utils/safe';
1919
import { NATIVE } from './wrapper';
20+
import { parseErrorStack } from './integrations/debugsymbolicator';
21+
22+
23+
const STACKTRACE_LIMIT = 50;
24+
function stripSentryFramesAndReverse(stack) {
25+
if (!stack.length) {
26+
return [];
27+
}
28+
29+
let localStack = stack;
30+
31+
const firstFrameFunction = localStack[0].function || '';
32+
const lastFrameFunction = localStack[localStack.length - 1].function || '';
33+
34+
// If stack starts with one of our API calls, remove it (starts, meaning it's the top of the stack - aka last call)
35+
if (firstFrameFunction.indexOf('captureMessage') !== -1 || firstFrameFunction.indexOf('captureException') !== -1) {
36+
localStack = localStack.slice(1);
37+
}
38+
39+
// 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)
40+
if (lastFrameFunction.indexOf('sentryWrapped') !== -1) {
41+
localStack = localStack.slice(0, -1);
42+
}
43+
44+
// The frame where the crash happened, should be the last entry in the array
45+
return localStack
46+
.slice(0, STACKTRACE_LIMIT)
47+
.map(frame => ({
48+
...frame,
49+
filename: frame.filename || localStack[0].filename,
50+
function: frame.function || undefined,
51+
}));
52+
}
53+
const defaultStackParser = (stack, skipFirst = 0) => {
54+
let frames = parseErrorStack({ stack } as any);
55+
56+
if (skipFirst) {
57+
frames = frames.slice(skipFirst);
58+
}
59+
return frames;
60+
};
2061

2162
const IGNORED_DEFAULT_INTEGRATIONS = [
2263
'GlobalHandlers', // We will use the react-native internal handlers
@@ -74,7 +115,7 @@ export function init(passedOptions: NativescriptOptions): void {
74115
if (passedOptions.defaultIntegrations === undefined) {
75116
rewriteFrameIntegration = new RewriteFrames({
76117
iteratee: (frame: StackFrame) => {
77-
if (frame.filename) {
118+
if (frame.platform === 'javascript' && frame.filename) {
78119
let filename = (frame.filename = frame.filename
79120
.replace(/^file\:\/\//, '')
80121
.replace(/^address at /, '')

src/wrapper.android.ts

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { createArrayBuffer, pointsFromBuffer } from '@nativescript-community/arr
22
import { Application, Utils } from '@nativescript/core';
33
import { BaseEnvelopeItemHeaders, Breadcrumb, Envelope, EnvelopeItem, Event, SeverityLevel } from '@sentry/types';
44
import { SentryError } from '@sentry/utils';
5-
import { convertNativescriptFramesToSentryFrames, parseErrorStack } from './integrations/debugsymbolicator';
5+
import { parseErrorStack } from './integrations/debugsymbolicator';
66
import { isHardCrash } from './misc';
77
import { NativescriptOptions } from './options';
88
import { rewriteFrameIntegration } from './sdk';
@@ -46,7 +46,7 @@ export namespace NATIVE {
4646
}
4747
return '';
4848
}
49-
function convertToNativeStacktrace(
49+
function convertToNativeJavascriptStacktrace(
5050
stack: {
5151
file?: string;
5252
filename?: string;
@@ -73,32 +73,35 @@ export namespace NATIVE {
7373
const lineNumber = frame.lineNumber || frame.lineno || 0;
7474
const column = frame.column || frame.colno || 0;
7575
const stackFrame = new io.sentry.protocol.SentryStackFrame();
76-
stackFrame.setModule('');
77-
stackFrame.setFunction(methodName.length > 0 ? methodName.split('.').pop() : methodName);
78-
stackFrame.setFilename(stackFrameToModuleId(frame));
76+
stackFrame.setFunction(methodName);
77+
stackFrame.setFilename(fileName);
7978
stackFrame.setLineno(new java.lang.Integer(lineNumber));
8079
stackFrame.setColno(new java.lang.Integer(column));
81-
stackFrame.setAbsPath(fileName);
8280
stackFrame.setPlatform('javascript');
8381
stackFrame.setInApp(new java.lang.Boolean(frame.in_app || false));
8482
frames.add(stackFrame);
8583
}
8684
nStackTrace.setFrames(frames);
8785
return nStackTrace;
8886
}
89-
function addExceptionInterface(nEvent: io.sentry.SentryEvent, type: string, value: string, stack) {
90-
const exceptions = nEvent.getExceptions() || new java.util.ArrayList<io.sentry.protocol.SentryException>();
87+
function addJavascriptExceptionInterface(nEvent: io.sentry.SentryEvent, type: string, value: string, stack) {
88+
const exceptions = nEvent.getExceptions();
9189

90+
const actualExceptions = new java.util.ArrayList<io.sentry.protocol.SentryException>();
9291
const nException = new io.sentry.protocol.SentryException();
9392
nException.setType(type);
9493
nException.setValue(value);
95-
nException.setModule('');
94+
console.log('test');
95+
// nException.setModule('');
9696
// nException.setModule('com.tns');
9797
nException.setThreadId(new java.lang.Long(java.lang.Thread.currentThread().getId()));
9898

99-
nException.setStacktrace(convertToNativeStacktrace(stack));
100-
exceptions.add(nException);
101-
nEvent.setExceptions(exceptions);
99+
nException.setStacktrace(convertToNativeJavascriptStacktrace(stack));
100+
actualExceptions.add(nException);
101+
if (exceptions) {
102+
actualExceptions.addAll(1, exceptions);
103+
}
104+
nEvent.setExceptions(actualExceptions);
102105
}
103106

104107
/**
@@ -642,6 +645,7 @@ export namespace NATIVE {
642645
// we use this callback to actually try and get the JS stack when a native error is catched
643646
try {
644647
const ex: io.sentry.protocol.SentryException = event.getExceptions().get(0);
648+
console.log('beforeSend', ex, ex.getStacktrace(), ex.getMechanism());
645649
if (ex && ex.getType() === 'NativeScriptException') {
646650
let mechanism = event.getThrowable && event.getThrowable();
647651
if (!mechanism) {
@@ -658,11 +662,12 @@ export namespace NATIVE {
658662
if (throwable ) {
659663
const jsStackTrace: string = (throwable ).getIncomingStackTrace();
660664
if (jsStackTrace) {
661-
const stack = parseErrorStack({ stack: jsStackTrace } as any);
662665

663-
const convertedFrames = convertNativescriptFramesToSentryFrames(stack as any);
664-
convertedFrames.forEach((frame) => rewriteFrameIntegration._iteratee(frame));
665-
addExceptionInterface(event, 'Error', throwable.getMessage(), convertedFrames.reverse());
666+
console.log('jsStackTrace', jsStackTrace);
667+
const stack = parseErrorStack({ stack: 'at ' + jsStackTrace } as any).reverse();
668+
console.log('stack', stack);
669+
stack.forEach((frame) => rewriteFrameIntegration._iteratee(frame));
670+
addJavascriptExceptionInterface(event, 'Error', throwable.getMessage(), stack.reverse());
666671
}
667672
}
668673
}

0 commit comments

Comments
 (0)