Skip to content

Commit

Permalink
Merge pull request #91 from kthai-nr/js_error_hex
Browse files Browse the repository at this point in the history
JavaScript errors as handled exceptions
  • Loading branch information
kennyt276 committed Apr 21, 2023
2 parents 79dbf3f + df940ad commit a93282e
Show file tree
Hide file tree
Showing 14 changed files with 279 additions and 55 deletions.
15 changes: 0 additions & 15 deletions .babelrc

This file was deleted.

12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,16 @@ See the examples below, and for more detail, see [New Relic IOS SDK doc](https:/

## How to see JSErrors(Fatal/Non Fatal) in NewRelic One?

### React Native Agent v1.2.0 and above:
JavaScript errors and promise rejections can be seen in the `Handled Exceptions` tab in New Relic One. You will be able to see the event trail, attributes, and stack trace for every JavaScript error recorded.

You can also build a dashboard for these errors using this query:

```sql
SELECT * FROM MobileHandledException SINCE 24 hours ago
```

### React Native Agent v1.1.0 and below:
There is no section for JavaScript errors, but you can see JavaScript errors in custom events and also query them in NRQL explorer.

<img width="1753" alt="Screen Shot 2022-02-10 at 12 41 11 PM" src="https://user-images.githubusercontent.com/89222514/153474861-87213e70-c3fb-4e14-aee7-a6a3fb482f73.png">
Expand All @@ -414,7 +424,7 @@ You can also build dashboard for errors using this query:

## Symbolicating a stack trace

Currently there is no symbolication of Javascript errors. Please follow the steps described [here for Symbolication](https://reactnative.dev/docs/0.64/symbolication).
The agent supports symbolication of JavaScript errors in debug mode only. Symbolicated errors are shown as Handled Exceptions in New Relic One. If you want to manually symboliate, please follow the steps described [here for Symbolication](https://reactnative.dev/docs/0.64/symbolication).

### Symbolication for Javascript errors are coming in future releases.

Expand Down
1 change: 1 addition & 0 deletions __mocks__/nrm-modular-agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export default {
onNavigationStateChange:jest.fn(),
componentDidAppearListener:jest.fn(),
onStateChange:jest.fn(),
recordHandledException:jest.fn(),

isAgentStarted: (name, callback) => {
callback(true);
Expand Down
43 changes: 43 additions & 0 deletions android/src/main/java/com/NewRelic/NRMModularAgentModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
import com.newrelic.agent.android.util.NetworkFailure;


import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class NRMModularAgentModule extends ReactContextBaseJavaModule {
Expand Down Expand Up @@ -393,4 +395,45 @@ public void recordStack(String errorName, String errorMessage, String errorStack
Log.w("NRMA", e.getMessage());
}
}

@ReactMethod
public void recordHandledException(ReadableMap exceptionDictionary) {
if(exceptionDictionary == null) {
Log.w("NRMA", "Null dictionary given to recordHandledException");
}

Map<String, Object> exceptionMap = exceptionDictionary.toHashMap();
// Remove these attributes to avoid conflict with existing attributes
exceptionMap.remove("app");
exceptionMap.remove("platform");

if(!exceptionMap.containsKey("stackFrames")) {
Log.w("NRMA", "No stack frames in recordHandledException");
return;
}
Map<String, Object> stackFramesMap = (Map<String, Object>) exceptionMap.get("stackFrames");
NewRelicReactNativeException exception = new NewRelicReactNativeException(
(String) exceptionMap.get("message"),
generateStackTraceElements(stackFramesMap));
exceptionMap.remove("stackFrames");
NewRelic.recordHandledException(exception, exceptionMap);
}

private StackTraceElement[] generateStackTraceElements(Map<String, Object> stackFrameMap) {
try {
List<StackTraceElement> stackTraceList = new ArrayList<>();
for(int i = 0; i < stackFrameMap.size(); ++i) {
Map<String, Object> element = (Map<String, Object>) stackFrameMap.get(Integer.toString(i));
String methodName = (String) element.getOrDefault("methodName", "");
String fileName = (String) element.getOrDefault("file", "");
int lineNumber = element.get("lineNumber") != null ? ((Double) element.get("lineNumber")).intValue() : 1;
StackTraceElement stackTraceElement = new StackTraceElement(" ", methodName, fileName, lineNumber);
stackTraceList.add(stackTraceElement);
}
return stackTraceList.toArray(new StackTraceElement[0]);
} catch(Exception e) {
NewRelic.recordHandledException(e);
return null;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.NewRelic;

public class NewRelicReactNativeException extends Exception {
public NewRelicReactNativeException(String message, StackTraceElement[] stackTraceElements) {
super(message);
setStackTrace(stackTraceElements);
}
}
10 changes: 10 additions & 0 deletions babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module.exports = {
"presets": [
[
"module:metro-react-native-babel-preset"
]
],
"plugins": [
"@babel/plugin-proposal-class-properties"
]
}
32 changes: 10 additions & 22 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -305,8 +305,12 @@ class NewRelic {

/**
* Records javascript errors for react-native.
* @param e {Error} A JavaScript error.
*/
recordError(e) {
async recordError(e) {
await this.recordError(e, false);
}
async recordError(e, isFatal) {
if(e) {
if(!this.JSAppVersion) {
this.LOG.error('unable to capture JS error. Make sure to call NewRelic.setJSAppVersion() at the start of your application.');
Expand All @@ -323,13 +327,7 @@ class NewRelic {
}

if(error !== undefined) {
this.NRMAModularAgentWrapper.execute(
"recordStack",
error.name,
error.message,
error.stack,
false,
this.JSAppVersion)
this.NRMAModularAgentWrapper.execute("recordHandledException", error, this.JSAppVersion, isFatal)
} else {
this.LOG.warn('undefined error name or message');
}
Expand Down Expand Up @@ -465,16 +463,11 @@ class NewRelic {
addNewRelicErrorHandler() {
if (global && global.ErrorUtils && !this.state.didAddErrorHandler) {
const previousHandler = global.ErrorUtils.getGlobalHandler();
global.ErrorUtils.setGlobalHandler((error, isFatal) => {
global.ErrorUtils.setGlobalHandler(async (error, isFatal) => {
if (!this.JSAppVersion) {
this.LOG.error('unable to capture JS error. Make sure to call NewRelic.setJSAppVersion() at the start of your application.');
}
this.NRMAModularAgentWrapper.execute('recordStack',
error.name,
error.message,
error.stack,
isFatal,
this.JSAppVersion);
await this.recordError(error, isFatal);
previousHandler(error, isFatal);
});
// prevent us from adding the error handler multiple times.
Expand All @@ -489,14 +482,9 @@ class NewRelic {
const prevTracker = getUnhandledPromiseRejectionTracker();

if (!this.state.didAddPromiseRejection) {
setUnhandledPromiseRejectionTracker((id, error) => {
setUnhandledPromiseRejectionTracker(async (id, error) => {
if(error != undefined) {
this.NRMAModularAgentWrapper.execute('recordStack',
error.name,
error.message,
error.stack,
false,
this.JSAppVersion);
await this.recordError(error);
} else {
this.recordBreadcrumb("Possible Unhandled Promise Rejection", {id: id})
}
Expand Down
37 changes: 37 additions & 0 deletions ios/bridge/NRMModularAgent.m
Original file line number Diff line number Diff line change
Expand Up @@ -353,5 +353,42 @@ - (dispatch_queue_t)methodQueue{
[NewRelic recordCustomEvent:@"JS Errors" attributes:dict];
}

RCT_EXPORT_METHOD(recordHandledException:(NSDictionary* _Nullable)exceptionDictionary) {
if (exceptionDictionary == nil) {
NSLog(@"[NRMA] Null dictionary given to recordHandledException");
return;
}
NSMutableDictionary* attributes = [NSMutableDictionary new];
attributes[@"name"] = exceptionDictionary[@"name"];
attributes[@"reason"] = exceptionDictionary[@"message"];
attributes[@"fatal"] = exceptionDictionary[@"isFatal"];
attributes[@"cause"] = exceptionDictionary[@"message"];
attributes[@"JSAppVersion"] = exceptionDictionary[@"JSAppVersion"];
attributes[@"minify"] = exceptionDictionary[@"minify"];
attributes[@"dev"] = exceptionDictionary[@"dev"];
attributes[@"runModule"] = exceptionDictionary[@"runModule"];
attributes[@"modulesOnly"] = exceptionDictionary[@"modulesOnly"];

NSMutableDictionary* stackFramesDict = exceptionDictionary[@"stackFrames"];
if(stackFramesDict == nil) {
NSLog(@"[NRMA] No stack frames given to recordHandledException");
return;
}
NSMutableArray* stackFramesArr = [NSMutableArray new];
for (int i = 0; i < [stackFramesDict count]; ++i) {
NSMutableDictionary* currStackFrame = stackFramesDict[@(i).stringValue];
NSMutableDictionary* stackTraceElement = [NSMutableDictionary new];
stackTraceElement[@"file"] = (currStackFrame[@"file"] && currStackFrame[@"file"] != [NSNull null]) ? currStackFrame[@"file"] : @" ";
stackTraceElement[@"line"] = (currStackFrame[@"lineNumber"] && currStackFrame[@"lineNumber"] != [NSNull null]) ? currStackFrame[@"lineNumber"] : 0;
stackTraceElement[@"method"] = (currStackFrame[@"methodName"] && currStackFrame[@"methodName"] != [NSNull null]) ? currStackFrame[@"methodName"] : @" ";
stackTraceElement[@"class"] = @" ";
[stackFramesArr addObject:stackTraceElement];
}

attributes[@"stackTraceElements"] = stackFramesArr;
[NewRelic recordHandledExceptionWithStackTrace:attributes];

}

@end

1 change: 0 additions & 1 deletion jestSetup.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,5 @@ jest.mock('./index.js', () => ({
incrementAttribute: jest.fn(),
setJSAppVersion: jest.fn(),
setUserId: jest.fn(),
recordError: jest.fn(),
sendConsole: jest.fn()
}));
30 changes: 15 additions & 15 deletions new-relic/__tests__/new-relic.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -236,29 +236,29 @@ describe('New Relic', () => {
expect(MockNRM.removeAllAttributes.mock.calls.length).toBe(1);
});

it('should record JS error with a given valid error', () => {
it('should record JS error with a given valid error', async () => {
NewRelic.setJSAppVersion('new version 123');

NewRelic.recordError(new TypeError);
NewRelic.recordError(new Error);
NewRelic.recordError(new EvalError);
NewRelic.recordError(new RangeError);
NewRelic.recordError(new ReferenceError);
NewRelic.recordError('fakeErrorName');
await NewRelic.recordError(new TypeError);
await NewRelic.recordError(new Error);
await NewRelic.recordError(new EvalError);
await NewRelic.recordError(new RangeError);
await NewRelic.recordError(new ReferenceError);
await NewRelic.recordError('fakeErrorName');

expect(MockNRM.recordStack.mock.calls.length).toBe(6);
expect(MockNRM.recordHandledException.mock.calls.length).toBe(6);
});

it('should not record JS error with a bad error', () => {
it('should not record JS error with a bad error', async () => {
NewRelic.setJSAppVersion('123');

NewRelic.recordError(undefined);
NewRelic.recordError(null);
NewRelic.recordError(123);
NewRelic.recordError(true);
NewRelic.recordError('');
await NewRelic.recordError(undefined);
await NewRelic.recordError(null);
await NewRelic.recordError(123);
await NewRelic.recordError(true);
await NewRelic.recordError('');

expect(MockNRM.recordStack.mock.calls.length).toBe(0);
expect(MockNRM.recordHandledException.mock.calls.length).toBe(0);
});

it('should set max event buffer time', () => {
Expand Down
67 changes: 67 additions & 0 deletions new-relic/__tests__/stack-frame-editor.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright (c) 2022-present New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

import StackFrameEditor from '../stack-frame-editor';

describe('Stack Frame Editor', () => {
it('should parse file names and return properties', () => {
let stackFrameArr = [
{ file: "http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false&modulesOnly=false&runModule=true&app=org.reactjs.native.example.test_app" },
{ file: "(native)" },
{ file: "path/to/file.js" },
{ file: "https://cdn.somewherefast.com/utils.min.js" },
{ file: "/Users/test/Library/Developer/Xcode/DerivedData/test_app-randomletters/Build/Products/Release-iphonesimulator/main.jsbundle"}
];
let properties = StackFrameEditor.parseFileNames(stackFrameArr);
expect(properties).toBeDefined();
expect(Object.keys(properties).length).toEqual(6);
expect(stackFrameArr[0].file).toEqual("http://localhost:8081/index.bundle");
});

it('should not affect regular file names', () => {
let stackFrameArr = [
{ file: "(native)" },
{ file: "path/to/file.js" },
{ file: "https://cdn.somewherefast.com/utils.min.js" },
{ file: "/Users/test/Library/Developer/Xcode/DerivedData/test_app-randomletters/Build/Products/Release-iphonesimulator/main.jsbundle"}
];

let properties = StackFrameEditor.parseFileNames(stackFrameArr);
expect(properties).toBeDefined();
expect(Object.keys(properties).length).toEqual(0);
expect(stackFrameArr[0].file).toEqual("(native)");
expect(stackFrameArr[1].file).toEqual("path/to/file.js");
expect(stackFrameArr[2].file).toEqual("https://cdn.somewherefast.com/utils.min.js");

});

it('should read properties from bundle file name', () => {
let properties = StackFrameEditor.readPropertiesFromFileName("http://localhost:8081/index.bundle?platform=ios&dev=true&minify=false&modulesOnly=false&runModule=true&app=org.reactjs.native.example.test_app");
expect(properties).toBeDefined();
expect(Object.keys(properties).length).toEqual(6);
expect(properties.platform).toEqual("ios");
expect(properties.dev).toEqual("true");
expect(properties.minify).toEqual("false");
expect(properties.modulesOnly).toEqual("false");
expect(properties.runModule).toEqual("true");
expect(properties.app).toEqual("org.reactjs.native.example.test_app");
});

it('should read no properties from regular file names', () => {
let stackFrameArr = [
{ file: "(native)" },
{ file: "path/to/file.js" },
{ file: "https://cdn.somewherefast.com/utils.min.js" },
{ file: "/Users/test/Library/Developer/Xcode/DerivedData/test_app-randomletters/Build/Products/Release-iphonesimulator/main.jsbundle"}
];

for (const obj in stackFrameArr) {
let properties = StackFrameEditor.readPropertiesFromFileName(obj.file);
expect(properties).toBeDefined();
expect(Object.keys(properties).length).toEqual(0);
}
});

});
Loading

0 comments on commit a93282e

Please sign in to comment.