Skip to content

Commit

Permalink
Jpmunz/embr 4790 pass js stacktrace to ios (#79)
Browse files Browse the repository at this point in the history
  • Loading branch information
jpmunz authored Aug 26, 2024
1 parent a4390d6 commit 763c2d5
Show file tree
Hide file tree
Showing 8 changed files with 101 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ public void clearAllUserPersonas(Promise promise) {

@ReactMethod
public void logMessageWithSeverityAndProperties(String message, String severity, ReadableMap properties,
Promise promise) {
String stacktrace, Promise promise) {
try {
final Map<String, Object> props = properties != null ? properties.toHashMap() : null;
if (severity.equals("info")) {
Expand Down
1 change: 1 addition & 0 deletions packages/core/ios/RNEmbrace/EmbraceManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ @interface RCT_EXTERN_MODULE(EmbraceManager, NSObject)
RCT_EXTERN_METHOD(logMessageWithSeverityAndProperties:(NSString *)message
severity:(NSString *)severity
properties:(NSDictionary)properties
stacktrace:(NSString *)stacktrace
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)

Expand Down
10 changes: 8 additions & 2 deletions packages/core/ios/RNEmbrace/EmbraceManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -301,20 +301,26 @@ class EmbraceManager: NSObject {
}
}

@objc(logMessageWithSeverityAndProperties:severity:properties:resolver:rejecter:)
@objc(logMessageWithSeverityAndProperties:severity:properties:stacktrace:resolver:rejecter:)
func logMessageWithSeverityAndProperties(
_ message: String,
severity: String,
properties: NSDictionary,
stacktrace: String,
resolver resolve: @escaping RCTPromiseResolveBlock,
rejecter reject: @escaping RCTPromiseRejectBlock
) {

let severityValue = self.severityFromString(from: severity)
guard let attributes = properties as? [String: String] else {
guard var attributes = properties as? [String: String] else {
reject("LOG_MESSAGE_INVALID_PROPERTIES", "Properties should be [String: String]", nil)
return
}

if (!stacktrace.isEmpty) {
attributes.updateValue(stacktrace, forKey: "exception.stacktrace")
}

Embrace.client?.log(
message,
severity: severityValue,
Expand Down
74 changes: 55 additions & 19 deletions packages/core/src/__tests__/embrace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,14 @@ jest.mock("react-native", () => ({
message: string,
severity: string,
properties: Record<string, any>,
stacktrace: string,
) =>
mockLogMessageWithSeverityAndProperties(message, severity, properties),
mockLogMessageWithSeverityAndProperties(
message,
severity,
properties,
stacktrace,
),
logHandledError: (message: string, properties?: Record<string, any>) =>
mockLogHandledError(message, properties),
addUserPersona: (persona: string) => mockAddUserPersona(persona),
Expand Down Expand Up @@ -133,8 +139,15 @@ jest.mock("react-native", () => ({
},
}));

const mockSt = "this is a fake stack trace";
const testView = "View";

const mockGenerateStackTrace = jest.fn();
jest.mock("../utils/ErrorUtil", () => ({
...jest.requireActual("../utils/ErrorUtil"),
generateStackTrace: () => mockGenerateStackTrace(),
}));

describe("User Identifier Tests", () => {
const testUserId = "testUser";
beforeEach(() => {
Expand Down Expand Up @@ -182,6 +195,10 @@ describe("Logs Test", () => {
const INFO = "info";
const ERROR = "error";

beforeEach(() => {
mockGenerateStackTrace.mockReturnValue(mockSt);
});

test("addBreadcrumb", async () => {
await addBreadcrumb(testView);
expect(mockAddBreadcrumb).toHaveBeenCalledWith(testView);
Expand All @@ -199,26 +216,34 @@ describe("Logs Test", () => {
const testProps = {foo: "bar"};

test.each`
message | severity | properties
${testMessage} | ${INFO} | ${testProps}
${testMessage} | ${WARNING} | ${testProps}
${testMessage} | ${ERROR} | ${testProps}
`("should run $severity log", async ({message, severity, properties}) => {
await logMessage(message, severity, properties);
expect(mockLogMessageWithSeverityAndProperties).toHaveBeenCalledWith(
message,
severity,
properties,
);
});
message | severity | properties | stackTrace
${testMessage} | ${INFO} | ${testProps} | ${""}
${testMessage} | ${INFO} | ${testProps} | ${""}
${testMessage} | ${WARNING} | ${testProps} | ${mockSt}
${testMessage} | ${WARNING} | ${testProps} | ${mockSt}
${testMessage} | ${ERROR} | ${testProps} | ${mockSt}
${testMessage} | ${ERROR} | ${testProps} | ${mockSt}
`(
"should run $severity log",
async ({message, severity, properties, stackTrace}) => {
await logMessage(message, severity, properties);
expect(mockLogMessageWithSeverityAndProperties).toHaveBeenCalledWith(
message,
severity,
properties,
stackTrace,
);
},
);
});

test("logInfo", async () => {
await logInfo("test message");
expect(mockLogMessageWithSeverityAndProperties).toHaveBeenCalledWith(
`test message`,
INFO,
{},
undefined,
"",
);
});

Expand All @@ -227,7 +252,8 @@ describe("Logs Test", () => {
expect(mockLogMessageWithSeverityAndProperties).toHaveBeenCalledWith(
`test message`,
WARNING,
{},
undefined,
mockSt,
);
});

Expand All @@ -236,7 +262,8 @@ describe("Logs Test", () => {
expect(mockLogMessageWithSeverityAndProperties).toHaveBeenCalledWith(
`test message`,
ERROR,
{},
undefined,
mockSt,
);
});
});
Expand Down Expand Up @@ -289,7 +316,9 @@ describe("Custom Views Tests", () => {
});

test("endView", async () => {
await endView(testView);
const promiseToResolve = endView(testView);
jest.runAllTimers();
await promiseToResolve;
expect(mockEndView).toHaveBeenCalledWith(testView);
});
});
Expand All @@ -308,12 +337,19 @@ describe("Session Properties Tests", () => {

describe("Payers Test", () => {
test("setUserAsPayer", async () => {
const result = await setUserAsPayer();
const promiseToResolve = setUserAsPayer();
jest.runAllTimers();
const result = await promiseToResolve;

expect(mockSetUserAsPayer).toHaveBeenCalled();
expect(result).toBe(false);
});
test("clearUserAsPayer", async () => {
const result = await clearUserAsPayer();
const promiseToResolve = clearUserAsPayer();

jest.runAllTimers();
const result = await promiseToResolve;

expect(mockClearUserAsPayer).toHaveBeenCalled();
expect(result).toBe(false);
});
Expand Down
10 changes: 8 additions & 2 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {NativeModules, Platform} from "react-native";

import * as embracePackage from "../package.json";

import {handleGlobalError} from "./utils/ErrorUtil";
import {generateStackTrace, handleGlobalError} from "./utils/ErrorUtil";
import {ApplyInterceptorStrategy} from "./networkInterceptors/ApplyInterceptor";
import {SessionStatus} from "./interfaces/Types";
import {
Expand Down Expand Up @@ -121,15 +121,18 @@ export const initialize = async ({
allRejections: true,
onUnhandled: (_: unknown, error: Error) => {
let message = `${UNHANDLED_PROMISE_REJECTION_PREFIX}: ${error}`;
let stackTrace = "";

if (error instanceof Error) {
message = `${UNHANDLED_PROMISE_REJECTION_PREFIX}: ${error.message}`;
stackTrace = error.stack || "";
}

return NativeModules.EmbraceManager.logMessageWithSeverityAndProperties(
message,
ERROR,
{},
stackTrace,
);
},
onHandled: noOp,
Expand Down Expand Up @@ -205,10 +208,13 @@ export const logMessage = (
severity: "info" | "warning" | "error" = "error",
properties?: Properties,
): Promise<boolean> => {
const stacktrace = severity === INFO ? "" : generateStackTrace();

return NativeModules.EmbraceManager.logMessageWithSeverityAndProperties(
message,
severity,
properties || {},
properties,
stacktrace,
);
};

Expand Down
7 changes: 6 additions & 1 deletion packages/core/src/utils/ErrorUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,9 @@ const handleGlobalError: GlobalErrorHandler =
handleError(error, callback);
};

export {handleGlobalError};
const generateStackTrace = (): string => {
const err = new Error();

Check warning on line 19 in packages/core/src/utils/ErrorUtil.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/utils/ErrorUtil.ts#L19

Added line #L19 was not covered by tests
return err.stack || "";
};

export {handleGlobalError, generateStackTrace};
2 changes: 1 addition & 1 deletion packages/core/test-project/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1452,7 +1452,7 @@ SPEC CHECKSUMS:
React-utils: 4476b7fcbbd95cfd002f3e778616155241d86e31
ReactCommon: ecad995f26e0d1e24061f60f4e5d74782f003f12
SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d
Yoga: ae3c32c514802d30f687a04a6a35b348506d411f
Yoga: 2f71ecf38d934aecb366e686278102a51679c308

PODFILE CHECKSUM: 8a247e59201368a342e5530a540a10f61aa7e0f3

Expand Down
23 changes: 21 additions & 2 deletions packages/core/test-project/ios/RNEmbraceTests/RNEmbraceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ class EmbraceLogsTests: XCTestCase {

func testLogMessageWithSeverity() async throws {
module.logMessageWithSeverityAndProperties("my log message", severity:"warning", properties: NSDictionary(),
stacktrace: "",
resolver: promise.resolve, rejecter: promise.reject)

let exportedLogs = try await getExportedLogs()
Expand All @@ -200,14 +201,16 @@ class EmbraceLogsTests: XCTestCase {
XCTAssertEqual(exportedLogs[0].severity?.description, "WARN")
XCTAssertEqual(exportedLogs[0].body?.description, "my log message")
XCTAssertEqual(exportedLogs[0].attributes["emb.type"]!.description, "sys.log")
XCTAssertNil(exportedLogs[0].attributes["exception.stacktrace"])
}


func testLogMessageWithSeverityAndProperties() async throws {
module.logMessageWithSeverityAndProperties("my log message", severity:"error", properties: NSDictionary(dictionary: [
"prop1": "foo",
"prop2": "bar"
]), resolver: promise.resolve, rejecter: promise.reject)
]),
stacktrace: "",
resolver: promise.resolve, rejecter: promise.reject)

let exportedLogs = try await getExportedLogs()

Expand All @@ -218,9 +221,25 @@ class EmbraceLogsTests: XCTestCase {
XCTAssertEqual(exportedLogs[0].attributes["emb.type"]!.description, "sys.log")
XCTAssertEqual(exportedLogs[0].attributes["prop1"]!.description, "foo")
XCTAssertEqual(exportedLogs[0].attributes["prop2"]!.description, "bar")
XCTAssertNil(exportedLogs[0].attributes["exception.stacktrace"])
}


func testLogMessageWithStackTrace() async throws {
module.logMessageWithSeverityAndProperties("my log message", severity:"warning", properties: NSDictionary(),
stacktrace: "my stack trace",
resolver: promise.resolve, rejecter: promise.reject)

let exportedLogs = try await getExportedLogs()

XCTAssertEqual(promise.resolveCalls.count, 1)
XCTAssertEqual(exportedLogs.count, 1)
XCTAssertEqual(exportedLogs[0].severity?.description, "WARN")
XCTAssertEqual(exportedLogs[0].body?.description, "my log message")
XCTAssertEqual(exportedLogs[0].attributes["emb.type"]!.description, "sys.log")
XCTAssertEqual(exportedLogs[0].attributes["exception.stacktrace"]!.description, "my stack trace")
}

}

class EmbraceSpansTests: XCTestCase {
Expand Down

0 comments on commit 763c2d5

Please sign in to comment.