From 3fa2f02e05ec90bcb29eb349e0b1cb26504cad06 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Fri, 12 Jul 2024 11:49:03 +0200 Subject: [PATCH] fix(replay): Add missing properties to android nav breadcrumbs --- .../RNSentryReplayBreadcrumbConverterTest.kt | 75 ++++++++++++++- ...SentryReplayBreadcrumbConverterTests.swift | 96 ++++++++++++++++++- .../RNSentryReplayBreadcrumbConverter.java | 27 ++++-- 3 files changed, 185 insertions(+), 13 deletions(-) diff --git a/RNSentryAndroidTester/app/src/test/java/io/sentry/rnsentryandroidtester/RNSentryReplayBreadcrumbConverterTest.kt b/RNSentryAndroidTester/app/src/test/java/io/sentry/rnsentryandroidtester/RNSentryReplayBreadcrumbConverterTest.kt index 2226254dd6..257aea123f 100644 --- a/RNSentryAndroidTester/app/src/test/java/io/sentry/rnsentryandroidtester/RNSentryReplayBreadcrumbConverterTest.kt +++ b/RNSentryAndroidTester/app/src/test/java/io/sentry/rnsentryandroidtester/RNSentryReplayBreadcrumbConverterTest.kt @@ -1,8 +1,10 @@ package io.sentry.rnsentryandroidtester import io.sentry.Breadcrumb +import io.sentry.SentryLevel import io.sentry.react.RNSentryReplayBreadcrumbConverter import io.sentry.rrweb.RRWebBreadcrumbEvent +import io.sentry.rrweb.RRWebEventType import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith @@ -12,7 +14,42 @@ import org.junit.runners.JUnit4 class RNSentryReplayBreadcrumbConverterTest { @Test - fun testConvertForegroundBreadcrumb() { + fun convertNavigationBreadcrumb() { + val converter = RNSentryReplayBreadcrumbConverter() + val testBreadcrumb = Breadcrumb() + testBreadcrumb.level = SentryLevel.INFO + testBreadcrumb.type = "navigation" + testBreadcrumb.category = "navigation" + testBreadcrumb.setData("from", "HomeScreen") + testBreadcrumb.setData("to", "ProfileScreen") + val actual = converter.convert(testBreadcrumb) as RRWebBreadcrumbEvent + + assertRRWebBreadcrumbDefaults(actual) + assertEquals(SentryLevel.INFO, actual.level) + assertEquals("navigation", actual.category) + assertEquals("HomeScreen", actual.data?.get("from")) + assertEquals("ProfileScreen", actual.data?.get("to")) + } + + @Test + fun convertNavigationBreadcrumbWithOnlyTo() { + val converter = RNSentryReplayBreadcrumbConverter() + val testBreadcrumb = Breadcrumb() + testBreadcrumb.level = SentryLevel.INFO + testBreadcrumb.type = "navigation" + testBreadcrumb.category = "navigation" + testBreadcrumb.setData("to", "ProfileScreen") + val actual = converter.convert(testBreadcrumb) as RRWebBreadcrumbEvent + + assertRRWebBreadcrumbDefaults(actual) + assertEquals(SentryLevel.INFO, actual.level) + assertEquals("navigation", actual.category) + assertEquals(null, actual.data?.get("from")) + assertEquals("ProfileScreen", actual.data?.get("to")) + } + + @Test + fun convertForegroundBreadcrumb() { val converter = RNSentryReplayBreadcrumbConverter() val testBreadcrumb = Breadcrumb() testBreadcrumb.type = "navigation" @@ -20,11 +57,12 @@ class RNSentryReplayBreadcrumbConverterTest { testBreadcrumb.setData("state", "foreground"); val actual = converter.convert(testBreadcrumb) as RRWebBreadcrumbEvent + assertRRWebBreadcrumbDefaults(actual) assertEquals("app.foreground", actual.category) } @Test - fun testConvertBackgroundBreadcrumb() { + fun convertBackgroundBreadcrumb() { val converter = RNSentryReplayBreadcrumbConverter() val testBreadcrumb = Breadcrumb() testBreadcrumb.type = "navigation" @@ -32,6 +70,7 @@ class RNSentryReplayBreadcrumbConverterTest { testBreadcrumb.setData("state", "background"); val actual = converter.convert(testBreadcrumb) as RRWebBreadcrumbEvent + assertRRWebBreadcrumbDefaults(actual) assertEquals("app.background", actual.category) } @@ -53,6 +92,32 @@ class RNSentryReplayBreadcrumbConverterTest { assertEquals(null, actual) } + @Test + fun convertTouchBreadcrumb() { + val converter = RNSentryReplayBreadcrumbConverter() + val testBreadcrumb = Breadcrumb() + testBreadcrumb.level = SentryLevel.INFO + testBreadcrumb.type = "user" + testBreadcrumb.category = "touch" + testBreadcrumb.message = "this won't be used for replay" + testBreadcrumb.setData( + "path", + arrayListOf(mapOf( + "element" to "element4", + "file" to "file4"))) + val actual = converter.convert(testBreadcrumb) as RRWebBreadcrumbEvent + + assertRRWebBreadcrumbDefaults(actual) + assertEquals(SentryLevel.INFO, actual.level) + assertEquals("ui.tap", actual.category) + assertEquals(1, actual.data?.keys?.size) + assertEquals( + arrayListOf(mapOf( + "element" to "element4", + "file" to "file4")), + actual.data?.get("path")) + } + @Test fun doesNotConvertNullPath() { val actual = RNSentryReplayBreadcrumbConverter.getTouchPathMessage(null) @@ -96,4 +161,10 @@ class RNSentryReplayBreadcrumbConverterTest { mapOf("name" to "name7"))) assertEquals("label5(element5, file5) > label4(file4) > label3(element3) > label2", actual) } + + private fun assertRRWebBreadcrumbDefaults(actual: RRWebBreadcrumbEvent) { + assertEquals("default", actual.breadcrumbType) + assertEquals(actual.breadcrumbTimestamp * 1000, actual.timestamp.toDouble(), 0.05) + assert(actual.breadcrumbTimestamp > 0) + } } diff --git a/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryReplayBreadcrumbConverterTests.swift b/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryReplayBreadcrumbConverterTests.swift index 95dddaa3af..85f4376e6f 100644 --- a/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryReplayBreadcrumbConverterTests.swift +++ b/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryReplayBreadcrumbConverterTests.swift @@ -1,7 +1,57 @@ import XCTest +import Sentry final class RNSentryReplayBreadcrumbConverterTests: XCTestCase { + func testConvertNavigationBreadcrumb() { + let converter = RNSentryReplayBreadcrumbConverter() + let testBreadcrumb = Breadcrumb() + testBreadcrumb.timestamp = Date() + testBreadcrumb.level = .info + testBreadcrumb.type = "navigation" + testBreadcrumb.category = "navigation" + testBreadcrumb.data = [ + "from": "HomeScreen", + "to": "ProfileScreen", + ] + let actual = converter.convert(from: testBreadcrumb) + + XCTAssertNotNil(actual) + let event = actual!.serialize() + let data = event["data"] as! [String: Any?] + let payload = data["payload"] as! [String: Any?] + let payloadData = payload["data"] as! [String: Any?] + assertRRWebBreadcrumbDefaults(actual: event) + XCTAssertEqual("info", payload["level"] as! String) + XCTAssertEqual("navigation", payload["category"] as! String) + XCTAssertEqual("HomeScreen", payloadData["from"] as! String) + XCTAssertEqual("ProfileScreen", payloadData["to"] as! String) + } + + func testConvertNavigationBreadcrumbWithOnlyTo() { + let converter = RNSentryReplayBreadcrumbConverter() + let testBreadcrumb = Breadcrumb() + testBreadcrumb.timestamp = Date() + testBreadcrumb.level = .info + testBreadcrumb.type = "navigation" + testBreadcrumb.category = "navigation" + testBreadcrumb.data = [ + "to": "ProfileScreen", + ] + let actual = converter.convert(from: testBreadcrumb) + + XCTAssertNotNil(actual) + let event = actual!.serialize() + let data = event["data"] as! [String: Any?] + let payload = data["payload"] as! [String: Any?] + let payloadData = payload["data"] as! [String: Any?] + assertRRWebBreadcrumbDefaults(actual: event) + XCTAssertEqual("info", payload["level"] as! String) + XCTAssertEqual("navigation", payload["category"] as! String) + XCTAssertNil(payloadData["from"] ?? nil) + XCTAssertEqual("ProfileScreen", payloadData["to"] as! String) + } + func testConvertForegroundBreadcrumb() { let converter = RNSentryReplayBreadcrumbConverter() let testBreadcrumb = Breadcrumb() @@ -11,8 +61,10 @@ final class RNSentryReplayBreadcrumbConverterTests: XCTestCase { let actual = converter.convert(from: testBreadcrumb) XCTAssertNotNil(actual) - let data = actual!.serialize()["data"] as! [String: Any?]; + let event = actual!.serialize() + let data = event["data"] as! [String: Any?] let payload = data["payload"] as! [String: Any?]; + assertRRWebBreadcrumbDefaults(actual: event) XCTAssertEqual(payload["category"] as! String, "app.foreground") } @@ -25,8 +77,10 @@ final class RNSentryReplayBreadcrumbConverterTests: XCTestCase { let actual = converter.convert(from: testBreadcrumb) XCTAssertNotNil(actual) - let data = actual!.serialize()["data"] as! [String: Any?]; + let event = actual!.serialize() + let data = event["data"] as! [String: Any?] let payload = data["payload"] as! [String: Any?]; + assertRRWebBreadcrumbDefaults(actual: event) XCTAssertEqual(payload["category"] as! String, "app.background") } @@ -46,6 +100,36 @@ final class RNSentryReplayBreadcrumbConverterTests: XCTestCase { XCTAssertNil(actual) } + func testConvertTouchBreadcrumb() { + let converter = RNSentryReplayBreadcrumbConverter() + let testBreadcrumb = Breadcrumb() + testBreadcrumb.timestamp = Date() + testBreadcrumb.level = .info + testBreadcrumb.type = "user" + testBreadcrumb.category = "touch" + testBreadcrumb.message = "this won't be used for replay" + testBreadcrumb.data = [ + "path": [ + ["element": "element4", "file": "file4"] + ] + ] + let actual = converter.convert(from: testBreadcrumb) + + XCTAssertNotNil(actual) + let event = actual!.serialize() + let data = event["data"] as! [String: Any?] + let payload = data["payload"] as! [String: Any?] + let payloadData = payload["data"] as! [String: Any?] + assertRRWebBreadcrumbDefaults(actual: event) + XCTAssertEqual("info", payload["level"] as! String) + XCTAssertEqual("ui.tap", payload["category"] as! String) + XCTAssertEqual(1, payloadData.keys.count) + XCTAssertEqual([[ + "element": "element4", + "file": "file4" + ]], payloadData["path"] as! [[String: String]]) + } + func testTouchMessageReturnsNilOnEmptyArray() throws { let actual = RNSentryReplayBreadcrumbConverter.getTouchPathMessage(from: []) XCTAssertEqual(actual, nil); @@ -88,4 +172,12 @@ final class RNSentryReplayBreadcrumbConverterTests: XCTestCase { XCTAssertEqual(actual, "label5(element5, file5) > label4(file4) > label3(element3) > label2"); } + private func assertRRWebBreadcrumbDefaults(actual: [String: Any?]) { + let data = actual["data"] as! [String: Any?] + let payload = data["payload"] as! [String: Any?] + XCTAssertEqual("default", payload["type"] as! String) + XCTAssertEqual((payload["timestamp"] as! Double) * 1000.0, Double(actual["timestamp"] as! Int), accuracy: 1.0) + XCTAssertTrue(payload["timestamp"] as! Double > 0.0) + } + } diff --git a/android/src/main/java/io/sentry/react/RNSentryReplayBreadcrumbConverter.java b/android/src/main/java/io/sentry/react/RNSentryReplayBreadcrumbConverter.java index ede86e4f08..4d6457a155 100644 --- a/android/src/main/java/io/sentry/react/RNSentryReplayBreadcrumbConverter.java +++ b/android/src/main/java/io/sentry/react/RNSentryReplayBreadcrumbConverter.java @@ -38,10 +38,7 @@ public RNSentryReplayBreadcrumbConverter() { return convertTouchBreadcrumb(breadcrumb); } if (breadcrumb.getCategory().equals("navigation")) { - final RRWebBreadcrumbEvent rrWebBreadcrumb = new RRWebBreadcrumbEvent(); - rrWebBreadcrumb.setCategory(breadcrumb.getCategory()); - rrWebBreadcrumb.setData(breadcrumb.getData()); - return rrWebBreadcrumb; + return convertNavigationBreadcrumb(breadcrumb); } if (breadcrumb.getCategory().equals("xhr")) { return convertNetworkBreadcrumb(breadcrumb); @@ -60,6 +57,14 @@ public RNSentryReplayBreadcrumbConverter() { return nativeBreadcrumb; } + @TestOnly + public @NotNull RRWebEvent convertNavigationBreadcrumb(final @NotNull Breadcrumb breadcrumb) { + final RRWebBreadcrumbEvent rrWebBreadcrumb = new RRWebBreadcrumbEvent(); + rrWebBreadcrumb.setCategory(breadcrumb.getCategory()); + setRRWebEventDefaultsFrom(rrWebBreadcrumb, breadcrumb); + return rrWebBreadcrumb; + } + @TestOnly public @NotNull RRWebEvent convertTouchBreadcrumb(final @NotNull Breadcrumb breadcrumb) { final RRWebBreadcrumbEvent rrWebBreadcrumb = new RRWebBreadcrumbEvent(); @@ -69,11 +74,7 @@ public RNSentryReplayBreadcrumbConverter() { rrWebBreadcrumb.setMessage(RNSentryReplayBreadcrumbConverter .getTouchPathMessage(breadcrumb.getData("path"))); - rrWebBreadcrumb.setLevel(breadcrumb.getLevel()); - rrWebBreadcrumb.setData(breadcrumb.getData()); - rrWebBreadcrumb.setTimestamp(breadcrumb.getTimestamp().getTime()); - rrWebBreadcrumb.setBreadcrumbTimestamp(breadcrumb.getTimestamp().getTime() / 1000.0); - rrWebBreadcrumb.setBreadcrumbType("default"); + setRRWebEventDefaultsFrom(rrWebBreadcrumb, breadcrumb); return rrWebBreadcrumb; } @@ -175,4 +176,12 @@ public RNSentryReplayBreadcrumbConverter() { rrWebSpanEvent.setData(data); return rrWebSpanEvent; } + + private void setRRWebEventDefaultsFrom(final @NotNull RRWebBreadcrumbEvent rrWebBreadcrumb, final @NotNull Breadcrumb breadcrumb) { + rrWebBreadcrumb.setLevel(breadcrumb.getLevel()); + rrWebBreadcrumb.setData(breadcrumb.getData()); + rrWebBreadcrumb.setTimestamp(breadcrumb.getTimestamp().getTime()); + rrWebBreadcrumb.setBreadcrumbTimestamp(breadcrumb.getTimestamp().getTime() / 1000.0); + rrWebBreadcrumb.setBreadcrumbType("default"); + } }