From 37f8134354bca1534a0468f3525f1df29d7ffa32 Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Thu, 5 Dec 2024 09:22:45 -0800 Subject: [PATCH 1/2] m157 mergeback (#6562) Auto-generated PR for cleaning up release m157 NO_RELEASE_CHANGE --------- Co-authored-by: davidmotson Co-authored-by: David Motsonashvili --- firebase-crashlytics-ndk/CHANGELOG.md | 3 +++ firebase-crashlytics-ndk/gradle.properties | 4 ++-- firebase-crashlytics/CHANGELOG.md | 9 +++++++++ firebase-crashlytics/gradle.properties | 4 ++-- firebase-perf/CHANGELOG.md | 9 +++++++++ firebase-perf/firebase-perf.gradle | 4 ++-- firebase-perf/gradle.properties | 4 ++-- firebase-sessions/CHANGELOG.md | 10 +++------- firebase-sessions/gradle.properties | 4 ++-- 9 files changed, 34 insertions(+), 17 deletions(-) diff --git a/firebase-crashlytics-ndk/CHANGELOG.md b/firebase-crashlytics-ndk/CHANGELOG.md index d2407922159..b5b8f7868d4 100644 --- a/firebase-crashlytics-ndk/CHANGELOG.md +++ b/firebase-crashlytics-ndk/CHANGELOG.md @@ -1,6 +1,9 @@ # Unreleased +# 19.3.0 +* [changed] Updated `firebase-crashlytics` dependency to v19.3.0 + # 19.2.1 * [changed] Updated `firebase-crashlytics` dependency to v19.2.1 diff --git a/firebase-crashlytics-ndk/gradle.properties b/firebase-crashlytics-ndk/gradle.properties index 7bce8216702..345224dc4c7 100644 --- a/firebase-crashlytics-ndk/gradle.properties +++ b/firebase-crashlytics-ndk/gradle.properties @@ -1,2 +1,2 @@ -version=19.2.2 -latestReleasedVersion=19.2.1 +version=19.3.1 +latestReleasedVersion=19.3.0 diff --git a/firebase-crashlytics/CHANGELOG.md b/firebase-crashlytics/CHANGELOG.md index cef0472e205..5b57a9581e7 100644 --- a/firebase-crashlytics/CHANGELOG.md +++ b/firebase-crashlytics/CHANGELOG.md @@ -1,7 +1,16 @@ # Unreleased + + +# 19.3.0 * [fixed] Fixed inefficiency in the Kotlin `FirebaseCrashlytics.setCustomKeys` extension. * [fixed] Execute failure listener outside the main thread [#6535] + +## Kotlin +The Kotlin extensions library transitively includes the updated +`firebase-crashlytics` library. The Kotlin extensions library has no additional +updates. + # 19.2.1 * [changed] Updated protobuf dependency to `3.25.5` to fix [CVE-2024-7254](https://nvd.nist.gov/vuln/detail/CVE-2024-7254). diff --git a/firebase-crashlytics/gradle.properties b/firebase-crashlytics/gradle.properties index 7bce8216702..345224dc4c7 100644 --- a/firebase-crashlytics/gradle.properties +++ b/firebase-crashlytics/gradle.properties @@ -1,2 +1,2 @@ -version=19.2.2 -latestReleasedVersion=19.2.1 +version=19.3.1 +latestReleasedVersion=19.3.0 diff --git a/firebase-perf/CHANGELOG.md b/firebase-perf/CHANGELOG.md index 10e74519624..3486a9000f6 100644 --- a/firebase-perf/CHANGELOG.md +++ b/firebase-perf/CHANGELOG.md @@ -1,6 +1,15 @@ # Unreleased +# 21.0.3 +* [changed] Bump internal dependencies. + + +## Kotlin +The Kotlin extensions library transitively includes the updated +`firebase-performance` library. The Kotlin extensions library has no additional +updates. + # 21.0.2 * [fixed] Fixed `IllegalStateException` that happened when starting a trace before Firebase initializes. diff --git a/firebase-perf/firebase-perf.gradle b/firebase-perf/firebase-perf.gradle index 703c1f22d55..0e72a774788 100644 --- a/firebase-perf/firebase-perf.gradle +++ b/firebase-perf/firebase-perf.gradle @@ -118,7 +118,7 @@ dependencies { api("com.google.firebase:firebase-components:18.0.0") api("com.google.firebase:firebase-config:21.5.0") api("com.google.firebase:firebase-installations:17.2.0") - api("com.google.firebase:firebase-sessions:2.0.0") { + api("com.google.firebase:firebase-sessions:2.0.7") { exclude group: 'com.google.firebase', module: 'firebase-common' exclude group: 'com.google.firebase', module: 'firebase-common-ktx' exclude group: 'com.google.firebase', module: 'firebase-components' @@ -138,4 +138,4 @@ dependencies { testImplementation libs.mockito.core testImplementation 'org.mockito:mockito-inline:5.2.0' testImplementation group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.6' -} \ No newline at end of file +} diff --git a/firebase-perf/gradle.properties b/firebase-perf/gradle.properties index 78431c23fa5..2f30a84d58e 100644 --- a/firebase-perf/gradle.properties +++ b/firebase-perf/gradle.properties @@ -15,7 +15,7 @@ # # -version=21.0.3 -latestReleasedVersion=21.0.2 +version=21.0.4 +latestReleasedVersion=21.0.3 android.enableUnitTestBinaryResources=true diff --git a/firebase-sessions/CHANGELOG.md b/firebase-sessions/CHANGELOG.md index 17d0d0f2cca..7147a6bf504 100644 --- a/firebase-sessions/CHANGELOG.md +++ b/firebase-sessions/CHANGELOG.md @@ -1,17 +1,13 @@ # Unreleased -* [fixed] Removed extraneous logs that risk leaking internal identifiers. +# 2.0.7 +* [fixed] Removed extraneous logs that risk leaking internal identifiers. + # 2.0.6 * [changed] Updated protobuf dependency to `3.25.5` to fix [CVE-2024-7254](https://github.com/advisories/GHSA-735f-pc8j-v9w8). - -## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-sessions` library. The Kotlin extensions library has no additional -updates. - # 2.0.5 * [unchanged] Updated to keep SDK versions aligned. diff --git a/firebase-sessions/gradle.properties b/firebase-sessions/gradle.properties index aa47fa7b133..84c76caede3 100644 --- a/firebase-sessions/gradle.properties +++ b/firebase-sessions/gradle.properties @@ -12,5 +12,5 @@ # See the License for the specific language governing permissions and # limitations under the License. -version=2.0.7 -latestReleasedVersion=2.0.6 +version=2.0.8 +latestReleasedVersion=2.0.7 From 71ceb1b2fb2dd40b4373116621ff9caa8fa8b898 Mon Sep 17 00:00:00 2001 From: Tejas Deshpande Date: Thu, 5 Dec 2024 15:53:30 -0500 Subject: [PATCH 2/2] Add an overload to FirebaseCrashlytics.recordException to attach additional custom key value pairs (#6528) This PR adds a method `public void recordException(@NonNull Throwable throwable, CustomKeysAndValues keysAndValues)` as an overload to the existing `recordException` method in Crashlytics to allow attaching additional custom keys to the specific event. Details: - The total number of custom key/value pairs are still restricted to 64 - App level custom keys take precedence over event specific custom keys for the 64 key/value pair limit - The values of event keys override the value of global custom keys if they're identical Additionally: - Creates a new EventMetadata class to represent `sessionId` and `timestamp` attached to non fatal events. --- firebase-crashlytics/CHANGELOG.md | 3 +- firebase-crashlytics/api.txt | 3 +- .../common/CrashlyticsControllerTest.java | 8 +- .../internal/common/CrashlyticsCoreTest.java | 137 +++++++++++++ .../SessionReportingCoordinatorTest.java | 184 +++++++++++++----- .../crashlytics/FirebaseCrashlytics.java | 27 ++- .../common/CrashlyticsController.java | 10 +- .../internal/common/CrashlyticsCore.java | 4 +- .../common/SessionReportingCoordinator.java | 48 +++-- .../internal/metadata/EventMetadata.kt | 31 +++ .../internal/metadata/KeysMap.java | 6 +- .../internal/metadata/UserMetadata.java | 44 +++++ 12 files changed, 428 insertions(+), 77 deletions(-) create mode 100644 firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/metadata/EventMetadata.kt diff --git a/firebase-crashlytics/CHANGELOG.md b/firebase-crashlytics/CHANGELOG.md index 5b57a9581e7..4d526585b44 100644 --- a/firebase-crashlytics/CHANGELOG.md +++ b/firebase-crashlytics/CHANGELOG.md @@ -1,5 +1,6 @@ # Unreleased - +* [feature] Added an overload for `recordException` that allows logging additional custom + keys to the non fatal event [#3551] # 19.3.0 * [fixed] Fixed inefficiency in the Kotlin `FirebaseCrashlytics.setCustomKeys` extension. diff --git a/firebase-crashlytics/api.txt b/firebase-crashlytics/api.txt index 3fb22e31b6e..c8fa0079300 100644 --- a/firebase-crashlytics/api.txt +++ b/firebase-crashlytics/api.txt @@ -20,10 +20,11 @@ package com.google.firebase.crashlytics { method public void deleteUnsentReports(); method public boolean didCrashOnPreviousExecution(); method @NonNull public static com.google.firebase.crashlytics.FirebaseCrashlytics getInstance(); + method public boolean isCrashlyticsCollectionEnabled(); method public void log(@NonNull String); method public void recordException(@NonNull Throwable); + method public void recordException(@NonNull Throwable, @NonNull com.google.firebase.crashlytics.CustomKeysAndValues); method public void sendUnsentReports(); - method public boolean isCrashlyticsCollectionEnabled(); method public void setCrashlyticsCollectionEnabled(boolean); method public void setCrashlyticsCollectionEnabled(@Nullable Boolean); method public void setCustomKey(@NonNull String, boolean); diff --git a/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/CrashlyticsControllerTest.java b/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/CrashlyticsControllerTest.java index 82cd14bcf74..9260a7f7950 100644 --- a/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/CrashlyticsControllerTest.java +++ b/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/CrashlyticsControllerTest.java @@ -44,6 +44,7 @@ import com.google.firebase.crashlytics.internal.NativeSessionFileProvider; import com.google.firebase.crashlytics.internal.analytics.AnalyticsEventLogger; import com.google.firebase.crashlytics.internal.concurrency.CrashlyticsWorkers; +import com.google.firebase.crashlytics.internal.metadata.EventMetadata; import com.google.firebase.crashlytics.internal.metadata.LogFileManager; import com.google.firebase.crashlytics.internal.metadata.UserMetadata; import com.google.firebase.crashlytics.internal.model.CrashlyticsReport; @@ -57,6 +58,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.TreeSet; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; @@ -216,14 +218,14 @@ public void testWriteNonFatal_callsSessionReportingCoordinatorPersistNonFatal() when(mockSessionReportingCoordinator.listSortedOpenSessionIds()) .thenReturn(new TreeSet<>(Collections.singleton(sessionId))); - controller.writeNonFatalException(thread, nonFatal); + controller.writeNonFatalException(thread, nonFatal, Map.of()); crashlyticsWorkers.common.submit(() -> controller.doCloseSessions(testSettingsProvider)); crashlyticsWorkers.common.await(); crashlyticsWorkers.diskWrite.await(); verify(mockSessionReportingCoordinator) - .persistNonFatalEvent(eq(nonFatal), eq(thread), eq(sessionId), anyLong()); + .persistNonFatalEvent(eq(nonFatal), eq(thread), any(EventMetadata.class)); } @SdkSuppress(minSdkVersion = 30) // ApplicationExitInfo @@ -377,7 +379,7 @@ public void testLoggedExceptionsAfterCrashOk() { testSettingsProvider, Thread.currentThread(), new RuntimeException()); // This should not throw. - controller.writeNonFatalException(Thread.currentThread(), new RuntimeException()); + controller.writeNonFatalException(Thread.currentThread(), new RuntimeException(), Map.of()); } /** diff --git a/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/CrashlyticsCoreTest.java b/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/CrashlyticsCoreTest.java index 6bd0924f3d0..56ac7f825c5 100644 --- a/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/CrashlyticsCoreTest.java +++ b/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/CrashlyticsCoreTest.java @@ -159,6 +159,143 @@ public void testCustomAttributes() throws Exception { assertEquals(longValue, metadata.getCustomKeys().get(key1)); } + @Test + public void testCustomAttributes_retrievedWithEmptyEventKeys() throws Exception { + UserMetadata metadata = crashlyticsCore.getController().getUserMetadata(); + + assertTrue(metadata.getCustomKeys(Map.of()).isEmpty()); + + final String id = "id012345"; + crashlyticsCore.setUserId(id); + crashlyticsWorkers.common.await(); + assertEquals(id, metadata.getUserId()); + + final StringBuffer idBuffer = new StringBuffer(id); + while (idBuffer.length() < UserMetadata.MAX_ATTRIBUTE_SIZE) { + idBuffer.append("0"); + } + final String longId = idBuffer.toString(); + final String superLongId = longId + "more chars"; + + crashlyticsCore.setUserId(superLongId); + crashlyticsWorkers.common.await(); + assertEquals(longId, metadata.getUserId()); + + final String key1 = "key1"; + final String value1 = "value1"; + crashlyticsCore.setCustomKey(key1, value1); + crashlyticsWorkers.common.await(); + assertEquals(value1, metadata.getCustomKeys(Map.of()).get(key1)); + + // Adding an existing key with the same value should return false + assertFalse(metadata.setCustomKey(key1, value1)); + assertTrue(metadata.setCustomKey(key1, "someOtherValue")); + assertTrue(metadata.setCustomKey(key1, value1)); + assertFalse(metadata.setCustomKey(key1, value1)); + + final String longValue = longId.replaceAll("0", "x"); + final String superLongValue = longValue + "some more chars"; + + // test truncation of custom keys and attributes + crashlyticsCore.setCustomKey(superLongId, superLongValue); + crashlyticsWorkers.common.await(); + assertNull(metadata.getCustomKeys(Map.of()).get(superLongId)); + assertEquals(longValue, metadata.getCustomKeys().get(longId)); + + // test the max number of attributes. We've already set 2. + for (int i = 2; i < UserMetadata.MAX_ATTRIBUTES; ++i) { + final String key = "key" + i; + final String value = "value" + i; + crashlyticsCore.setCustomKey(key, value); + crashlyticsWorkers.common.await(); + assertEquals(value, metadata.getCustomKeys(Map.of()).get(key)); + } + // should be full now, extra key, value pairs will be dropped. + final String key = "new key"; + crashlyticsCore.setCustomKey(key, "some value"); + crashlyticsWorkers.common.await(); + assertFalse(metadata.getCustomKeys(Map.of()).containsKey(key)); + + // should be able to update existing keys + crashlyticsCore.setCustomKey(key1, longValue); + crashlyticsWorkers.common.await(); + assertEquals(longValue, metadata.getCustomKeys(Map.of()).get(key1)); + + // when we set a key to null, it should still exist with an empty value + crashlyticsCore.setCustomKey(key1, null); + crashlyticsWorkers.common.await(); + assertEquals("", metadata.getCustomKeys(Map.of()).get(key1)); + + // keys and values are trimmed. + crashlyticsCore.setCustomKey(" " + key1 + " ", " " + longValue + " "); + crashlyticsWorkers.common.await(); + assertTrue(metadata.getCustomKeys(Map.of()).containsKey(key1)); + assertEquals(longValue, metadata.getCustomKeys(Map.of()).get(key1)); + } + + @Test + public void testCustomKeysMergedWithEventKeys() throws Exception { + UserMetadata metadata = crashlyticsCore.getController().getUserMetadata(); + + Map keysAndValues = new HashMap<>(); + keysAndValues.put("customKey1", "value"); + keysAndValues.put("customKey2", "value"); + keysAndValues.put("customKey3", "value"); + + crashlyticsCore.setCustomKeys(keysAndValues); + crashlyticsWorkers.common.await(); + + Map eventKeysAndValues = new HashMap<>(); + eventKeysAndValues.put("eventKey1", "eventValue"); + eventKeysAndValues.put("eventKey2", "eventValue"); + + // Tests reading custom keys with event keys. + assertEquals(keysAndValues.size(), metadata.getCustomKeys().size()); + assertEquals(keysAndValues.size(), metadata.getCustomKeys(Map.of()).size()); + assertEquals( + keysAndValues.size() + eventKeysAndValues.size(), + metadata.getCustomKeys(eventKeysAndValues).size()); + + // Tests event keys don't add to custom keys in future reads. + assertEquals(keysAndValues.size(), metadata.getCustomKeys().size()); + assertEquals(keysAndValues.size(), metadata.getCustomKeys(Map.of()).size()); + + // Tests additional event keys. + eventKeysAndValues.put("eventKey3", "eventValue"); + eventKeysAndValues.put("eventKey4", "eventValue"); + assertEquals( + keysAndValues.size() + eventKeysAndValues.size(), + metadata.getCustomKeys(eventKeysAndValues).size()); + + // Tests overriding custom key with event keys. + keysAndValues.put("eventKey1", "value"); + crashlyticsCore.setCustomKeys(keysAndValues); + crashlyticsWorkers.common.await(); + + assertEquals("value", metadata.getCustomKeys().get("eventKey1")); + assertEquals("value", metadata.getCustomKeys(Map.of()).get("eventKey1")); + assertEquals("eventValue", metadata.getCustomKeys(eventKeysAndValues).get("eventKey1")); + + // Test the event key behavior when the count of custom keys is max. + for (int i = keysAndValues.size(); i < UserMetadata.MAX_ATTRIBUTES; ++i) { + final String key = "key" + i; + final String value = "value" + i; + crashlyticsCore.setCustomKey(key, value); + crashlyticsWorkers.common.await(); + assertEquals(value, metadata.getCustomKeys().get(key)); + } + + assertEquals(UserMetadata.MAX_ATTRIBUTES, metadata.getCustomKeys().size()); + + // Tests event keys override global custom keys when the key exists. + assertEquals("value", metadata.getCustomKeys().get("eventKey1")); + assertEquals("value", metadata.getCustomKeys(Map.of()).get("eventKey1")); + assertEquals("eventValue", metadata.getCustomKeys(eventKeysAndValues).get("eventKey1")); + + // Test when event keys *don't* override global custom keys. + assertNull(metadata.getCustomKeys(eventKeysAndValues).get("eventKey2")); + } + @Test public void testBulkCustomKeys() throws Exception { final double DELTA = 1e-15; diff --git a/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/SessionReportingCoordinatorTest.java b/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/SessionReportingCoordinatorTest.java index f1015447441..0f5cf09233b 100644 --- a/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/SessionReportingCoordinatorTest.java +++ b/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/SessionReportingCoordinatorTest.java @@ -34,12 +34,16 @@ import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.Tasks; import com.google.firebase.concurrent.TestOnlyExecutors; +import com.google.firebase.crashlytics.internal.CrashlyticsTestCase; import com.google.firebase.crashlytics.internal.concurrency.CrashlyticsWorkers; +import com.google.firebase.crashlytics.internal.metadata.EventMetadata; import com.google.firebase.crashlytics.internal.metadata.LogFileManager; +import com.google.firebase.crashlytics.internal.metadata.RolloutAssignment; import com.google.firebase.crashlytics.internal.metadata.UserMetadata; import com.google.firebase.crashlytics.internal.model.CrashlyticsReport; import com.google.firebase.crashlytics.internal.model.CrashlyticsReport.CustomAttribute; import com.google.firebase.crashlytics.internal.persistence.CrashlyticsReportPersistence; +import com.google.firebase.crashlytics.internal.persistence.FileStore; import com.google.firebase.crashlytics.internal.send.DataTransportCrashlyticsReportSender; import java.io.File; import java.util.ArrayList; @@ -54,13 +58,14 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; -public class SessionReportingCoordinatorTest { +public class SessionReportingCoordinatorTest extends CrashlyticsTestCase { + + private static final String TEST_SESSION_ID = "testSessionId"; @Mock private CrashlyticsReportDataCapture dataCapture; @Mock private CrashlyticsReportPersistence reportPersistence; @Mock private DataTransportCrashlyticsReportSender reportSender; @Mock private LogFileManager logFileManager; - @Mock private UserMetadata reportMetadata; @Mock private IdManager idManager; @Mock private CrashlyticsReport mockReport; @Mock private CrashlyticsReport.Session.Event mockEvent; @@ -70,6 +75,7 @@ public class SessionReportingCoordinatorTest { @Mock private Exception mockException; @Mock private Thread mockThread; + private UserMetadata reportMetadata; private SessionReportingCoordinator reportingCoordinator; private AutoCloseable mocks; @@ -80,6 +86,9 @@ public class SessionReportingCoordinatorTest { public void setUp() { mocks = MockitoAnnotations.openMocks(this); + FileStore testFileStore = new FileStore(getContext()); + reportMetadata = new UserMetadata(TEST_SESSION_ID, testFileStore, crashlyticsWorkers); + reportingCoordinator = new SessionReportingCoordinator( dataCapture, @@ -138,7 +147,8 @@ public void testNonFatalEvent_persistsNormalPriorityEventWithoutAllThreadsForSes mockEventInteractions(); reportingCoordinator.onBeginSession(sessionId, timestamp); - reportingCoordinator.persistNonFatalEvent(mockException, mockThread, sessionId, timestamp); + reportingCoordinator.persistNonFatalEvent( + mockException, mockThread, new EventMetadata(sessionId, timestamp)); crashlyticsWorkers.diskWrite.await(); @@ -163,7 +173,8 @@ public void testNonFatalEvent_addsLogsToEvent() throws Exception { when(logFileManager.getLogString()).thenReturn(testLog); reportingCoordinator.onBeginSession(sessionId, timestamp); - reportingCoordinator.persistNonFatalEvent(mockException, mockThread, sessionId, timestamp); + reportingCoordinator.persistNonFatalEvent( + mockException, mockThread, new EventMetadata(sessionId, timestamp)); crashlyticsWorkers.diskWrite.await(); @@ -184,7 +195,8 @@ public void testNonFatalEvent_addsNoLogsToEventWhenNoneAvailable() throws Except when(logFileManager.getLogString()).thenReturn(null); reportingCoordinator.onBeginSession(sessionId, timestamp); - reportingCoordinator.persistNonFatalEvent(mockException, mockThread, sessionId, timestamp); + reportingCoordinator.persistNonFatalEvent( + mockException, mockThread, new EventMetadata(sessionId, timestamp)); crashlyticsWorkers.diskWrite.await(); @@ -257,11 +269,12 @@ public void testNonFatalEvent_addsSortedKeysToEvent() throws Exception { expectedCustomAttributes.add(customAttribute1); expectedCustomAttributes.add(customAttribute2); - when(reportMetadata.getCustomKeys()).thenReturn(attributes); - when(reportMetadata.getInternalKeys()).thenReturn(attributes); + addCustomKeysToUserMetadata(attributes); + addInternalKeysToUserMetadata(attributes); reportingCoordinator.onBeginSession(sessionId, timestamp); - reportingCoordinator.persistNonFatalEvent(mockException, mockThread, sessionId, timestamp); + reportingCoordinator.persistNonFatalEvent( + mockException, mockThread, new EventMetadata(sessionId, timestamp)); crashlyticsWorkers.diskWrite.await(); @@ -281,12 +294,9 @@ public void testNonFatalEvent_addsNoKeysToEventWhenNoneAvailable() throws Except final String sessionId = "testSessionId"; - final Map attributes = Collections.emptyMap(); - - when(reportMetadata.getCustomKeys()).thenReturn(attributes); - reportingCoordinator.onBeginSession(sessionId, timestamp); - reportingCoordinator.persistNonFatalEvent(mockException, mockThread, sessionId, timestamp); + reportingCoordinator.persistNonFatalEvent( + mockException, mockThread, new EventMetadata(sessionId, timestamp)); crashlyticsWorkers.diskWrite.await(); @@ -297,33 +307,117 @@ public void testNonFatalEvent_addsNoKeysToEventWhenNoneAvailable() throws Except verify(logFileManager, never()).clearLog(); } + @Test + public void testNonFatalEvent_addsUserInfoKeysToEventWhenAvailable() throws Exception { + final long timestamp = System.currentTimeMillis(); + + mockEventInteractions(); + + final String sessionId = "testSessionId"; + + final String testKey1 = "testKey1"; + final String testValue1 = "testValue1"; + + final Map userInfo = new HashMap<>(); + userInfo.put(testKey1, testValue1); + + final CustomAttribute customAttribute1 = + CustomAttribute.builder().setKey(testKey1).setValue(testValue1).build(); + + final List expectedCustomAttributes = new ArrayList<>(); + expectedCustomAttributes.add(customAttribute1); + + reportingCoordinator.onBeginSession(sessionId, timestamp); + reportingCoordinator.persistNonFatalEvent( + mockException, mockThread, new EventMetadata(sessionId, timestamp, userInfo)); + + crashlyticsWorkers.diskWrite.await(); + + verify(mockEventAppBuilder).setCustomAttributes(expectedCustomAttributes); + verify(mockEventAppBuilder).build(); + verify(mockEventBuilder).setApp(mockEventApp); + verify(mockEventBuilder).build(); + verify(logFileManager, never()).clearLog(); + } + + @Test + public void testNonFatalEvent_mergesUserInfoKeysWithCustomKeys() throws Exception { + final long timestamp = System.currentTimeMillis(); + + mockEventInteractions(); + + final String sessionId = "testSessionId"; + + final String testKey1 = "testKey1"; + final String testValue1 = "testValue1"; + + final String testKey2 = "testKey2"; + final String testValue2 = "testValue2"; + + final Map attributes = new HashMap<>(); + attributes.put(testKey1, testValue1); + attributes.put(testKey2, testValue2); + + addCustomKeysToUserMetadata(attributes); + + final String testValue1UserInfo = "testValue1"; + final String testKey3 = "testKey3"; + final String testValue3 = "testValue3"; + + final Map userInfo = new HashMap<>(); + userInfo.put(testKey1, testValue1UserInfo); + userInfo.put(testKey3, testValue3); + + final CustomAttribute customAttribute1 = + CustomAttribute.builder().setKey(testKey1).setValue(testValue1UserInfo).build(); + final CustomAttribute customAttribute2 = + CustomAttribute.builder().setKey(testKey2).setValue(testValue2).build(); + final CustomAttribute customAttribute3 = + CustomAttribute.builder().setKey(testKey3).setValue(testValue3).build(); + + final List expectedCustomAttributes = + List.of(customAttribute1, customAttribute2, customAttribute3); + + reportingCoordinator.onBeginSession(sessionId, timestamp); + reportingCoordinator.persistNonFatalEvent( + mockException, mockThread, new EventMetadata(sessionId, timestamp, userInfo)); + + crashlyticsWorkers.diskWrite.await(); + + verify(mockEventAppBuilder).setCustomAttributes(expectedCustomAttributes); + verify(mockEventAppBuilder).build(); + verify(mockEventBuilder).setApp(mockEventApp); + verify(mockEventBuilder).build(); + verify(logFileManager, never()).clearLog(); + } + @Test public void testNonFatalEvent_addRolloutsEvent() throws Exception { long timestamp = System.currentTimeMillis(); String sessionId = "testSessionId"; mockEventInteractions(); - List rolloutsState = - new ArrayList(); - rolloutsState.add(mockRolloutAssignment()); - when(reportMetadata.getRolloutsState()).thenReturn(rolloutsState); + List rolloutsState = new ArrayList<>(); + rolloutsState.add(fakeRolloutAssignment()); + reportMetadata.updateRolloutsState(rolloutsState); + crashlyticsWorkers.diskWrite.await(); reportingCoordinator.onBeginSession(sessionId, timestamp); - reportingCoordinator.persistNonFatalEvent(mockException, mockThread, sessionId, timestamp); + reportingCoordinator.persistNonFatalEvent( + mockException, mockThread, new EventMetadata(sessionId, timestamp)); crashlyticsWorkers.diskWrite.await(); verify(mockEventAppBuilder, never()).setCustomAttributes(anyList()); verify(mockEventAppBuilder, never()).build(); verify(mockEventBuilder, never()).setApp(mockEventApp); - verify(reportMetadata).getRolloutsState(); // first build for custom keys // second build for rollouts verify(mockEventBuilder, times(2)).build(); } @Test - public void testFatalEvent_addsSortedCustomKeysToEvent() { + public void testFatalEvent_addsSortedCustomKeysToEvent() throws Exception { final long timestamp = System.currentTimeMillis(); mockEventInteractions(); @@ -348,7 +442,7 @@ public void testFatalEvent_addsSortedCustomKeysToEvent() { expectedCustomAttributes.add(customAttribute1); expectedCustomAttributes.add(customAttribute2); - when(reportMetadata.getCustomKeys()).thenReturn(attributes); + addCustomKeysToUserMetadata(attributes); reportingCoordinator.onBeginSession(sessionId, timestamp); reportingCoordinator.persistFatalEvent(mockException, mockThread, sessionId, timestamp); @@ -361,7 +455,7 @@ public void testFatalEvent_addsSortedCustomKeysToEvent() { } @Test - public void testFatalEvent_addsSortedInternalKeysToEvent() { + public void testFatalEvent_addsSortedInternalKeysToEvent() throws Exception { final long timestamp = System.currentTimeMillis(); mockEventInteractions(); @@ -386,7 +480,7 @@ public void testFatalEvent_addsSortedInternalKeysToEvent() { expectedCustomAttributes.add(customAttribute1); expectedCustomAttributes.add(customAttribute2); - when(reportMetadata.getInternalKeys()).thenReturn(attributes); + addInternalKeysToUserMetadata(attributes); reportingCoordinator.onBeginSession(sessionId, timestamp); reportingCoordinator.persistFatalEvent(mockException, mockThread, sessionId, timestamp); @@ -406,10 +500,6 @@ public void testFatalEvent_addsNoKeysToEventWhenNoneAvailable() { final String sessionId = "testSessionId"; - final Map attributes = Collections.emptyMap(); - - when(reportMetadata.getCustomKeys()).thenReturn(attributes); - reportingCoordinator.onBeginSession(sessionId, timestamp); reportingCoordinator.persistFatalEvent(mockException, mockThread, sessionId, timestamp); @@ -421,15 +511,16 @@ public void testFatalEvent_addsNoKeysToEventWhenNoneAvailable() { } @Test - public void testFatalEvent_addRolloutsToEvent() { + public void testFatalEvent_addRolloutsToEvent() throws Exception { long timestamp = System.currentTimeMillis(); String sessionId = "testSessionId"; mockEventInteractions(); - List rolloutsState = - new ArrayList(); - rolloutsState.add(mockRolloutAssignment()); - when(reportMetadata.getRolloutsState()).thenReturn(rolloutsState); + List rolloutsState = new ArrayList<>(); + rolloutsState.add(fakeRolloutAssignment()); + + reportMetadata.updateRolloutsState(rolloutsState); + crashlyticsWorkers.diskWrite.await(); reportingCoordinator.onBeginSession(sessionId, timestamp); reportingCoordinator.persistFatalEvent(mockException, mockThread, sessionId, timestamp); @@ -437,7 +528,6 @@ public void testFatalEvent_addRolloutsToEvent() { verify(mockEventAppBuilder, never()).setCustomAttributes(anyList()); verify(mockEventAppBuilder, never()).build(); verify(mockEventBuilder, never()).setApp(mockEventApp); - verify(reportMetadata).getRolloutsState(); // first build for custom keys // second build for rollouts verify(mockEventBuilder, times(2)).build(); @@ -522,6 +612,21 @@ public void testRemoveAllReports_deletesPersistedReports() { verify(reportPersistence).deleteAllReports(); } + private void addCustomKeysToUserMetadata(Map customKeys) throws Exception { + reportMetadata.setCustomKeys(customKeys); + for (Map.Entry entry : customKeys.entrySet()) { + reportMetadata.setInternalKey(entry.getKey(), entry.getValue()); + } + crashlyticsWorkers.diskWrite.await(); + } + + private void addInternalKeysToUserMetadata(Map internalKeys) throws Exception { + for (Map.Entry entry : internalKeys.entrySet()) { + reportMetadata.setInternalKey(entry.getKey(), entry.getValue()); + } + crashlyticsWorkers.diskWrite.await(); + } + private void mockEventInteractions() { when(mockEvent.toBuilder()).thenReturn(mockEventBuilder); when(mockEventBuilder.build()).thenReturn(mockEvent); @@ -556,16 +661,7 @@ private static CrashlyticsReportWithSessionId mockReportWithSessionId(String ses mockReport(sessionId), sessionId, new File("fake")); } - private static CrashlyticsReport.Session.Event.RolloutAssignment mockRolloutAssignment() { - return CrashlyticsReport.Session.Event.RolloutAssignment.builder() - .setTemplateVersion(2) - .setParameterKey("my_feature") - .setParameterValue("false") - .setRolloutVariant( - CrashlyticsReport.Session.Event.RolloutAssignment.RolloutVariant.builder() - .setRolloutId("rollout_1") - .setVariantId("enabled") - .build()) - .build(); + private static RolloutAssignment fakeRolloutAssignment() { + return RolloutAssignment.create("rollout_1", "my_feature", "false", "enabled", 2); } } diff --git a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/FirebaseCrashlytics.java b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/FirebaseCrashlytics.java index 1eb2e1a367b..46f992aaf12 100644 --- a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/FirebaseCrashlytics.java +++ b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/FirebaseCrashlytics.java @@ -43,6 +43,7 @@ import com.google.firebase.remoteconfig.interop.FirebaseRemoteConfigInterop; import com.google.firebase.sessions.api.FirebaseSessionsDependencies; import java.util.List; +import java.util.Map; import java.util.concurrent.ExecutorService; /** @@ -208,7 +209,31 @@ public void recordException(@NonNull Throwable throwable) { Logger.getLogger().w("A null value was passed to recordException. Ignoring."); return; } - core.logException(throwable); + + core.logException(throwable, Map.of()); + } + + /** + * Records a non-fatal report to send to Crashlytics. + * + *

Combined with app level custom keys, the event is restricted to a maximum of 64 key/value + * pairs. New keys beyond that limit are ignored. Keys or values that exceed 1024 characters are + * truncated. + * + *

The values of event keys override the values of app level custom keys if they're identical. + * + * @param throwable a {@link Throwable} to be recorded as a non-fatal event. + * @param keysAndValues A dictionary of keys and the values to associate with the non fatal + * exception, in addition to the app level custom keys. + */ + public void recordException( + @NonNull Throwable throwable, @NonNull CustomKeysAndValues keysAndValues) { + if (throwable == null) { // Users could call this with null despite the annotation. + Logger.getLogger().w("A null value was passed to recordException. Ignoring."); + return; + } + + core.logException(throwable, keysAndValues.keysAndValues); } /** diff --git a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CrashlyticsController.java b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CrashlyticsController.java index 7726d9e6924..b55a26678d4 100644 --- a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CrashlyticsController.java +++ b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CrashlyticsController.java @@ -35,6 +35,7 @@ import com.google.firebase.crashlytics.internal.analytics.AnalyticsEventLogger; import com.google.firebase.crashlytics.internal.concurrency.CrashlyticsTasks; import com.google.firebase.crashlytics.internal.concurrency.CrashlyticsWorkers; +import com.google.firebase.crashlytics.internal.metadata.EventMetadata; import com.google.firebase.crashlytics.internal.metadata.LogFileManager; import com.google.firebase.crashlytics.internal.metadata.UserMetadata; import com.google.firebase.crashlytics.internal.model.CrashlyticsReport; @@ -405,7 +406,10 @@ void writeToLog(final long timestamp, final String msg) { } /** Log a caught exception - write out Throwable as event section of protobuf */ - void writeNonFatalException(@NonNull final Thread thread, @NonNull final Throwable ex) { + void writeNonFatalException( + @NonNull final Thread thread, + @NonNull final Throwable ex, + @NonNull Map eventKeys) { // Capture and close over the current time, so that we get the exact call time, // rather than the time at which the task executes. final long timestampMillis = System.currentTimeMillis(); @@ -417,7 +421,9 @@ void writeNonFatalException(@NonNull final Thread thread, @NonNull final Throwab Logger.getLogger().w("Tried to write a non-fatal exception while no session was open."); return; } - reportingCoordinator.persistNonFatalEvent(ex, thread, currentSessionId, timestampSeconds); + EventMetadata eventMetadata = + new EventMetadata(currentSessionId, timestampSeconds, eventKeys); + reportingCoordinator.persistNonFatalEvent(ex, thread, eventMetadata); } } diff --git a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CrashlyticsCore.java b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CrashlyticsCore.java index d967447590e..f1f250e52b9 100644 --- a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CrashlyticsCore.java +++ b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CrashlyticsCore.java @@ -313,9 +313,9 @@ public static String getVersion() { * Throwable was thrown. The Throwable will always be processed on a background thread, so it is * safe to invoke this method from the main thread. */ - public void logException(@NonNull Throwable throwable) { + public void logException(@NonNull Throwable throwable, @NonNull Map eventKeys) { crashlyticsWorkers.common.submit( - () -> controller.writeNonFatalException(Thread.currentThread(), throwable)); + () -> controller.writeNonFatalException(Thread.currentThread(), throwable, eventKeys)); } /** diff --git a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/SessionReportingCoordinator.java b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/SessionReportingCoordinator.java index 29e2301591a..5abc282d556 100644 --- a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/SessionReportingCoordinator.java +++ b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/SessionReportingCoordinator.java @@ -25,6 +25,7 @@ import com.google.android.gms.tasks.Tasks; import com.google.firebase.crashlytics.internal.Logger; import com.google.firebase.crashlytics.internal.concurrency.CrashlyticsWorkers; +import com.google.firebase.crashlytics.internal.metadata.EventMetadata; import com.google.firebase.crashlytics.internal.metadata.LogFileManager; import com.google.firebase.crashlytics.internal.metadata.UserMetadata; import com.google.firebase.crashlytics.internal.model.CrashlyticsReport; @@ -124,13 +125,14 @@ public void onBeginSession(@NonNull String sessionId, long timestampSeconds) { public void persistFatalEvent( @NonNull Throwable event, @NonNull Thread thread, @NonNull String sessionId, long timestamp) { Logger.getLogger().v("Persisting fatal event for session " + sessionId); - persistEvent(event, thread, sessionId, EVENT_TYPE_CRASH, timestamp, true); + EventMetadata eventMetadata = new EventMetadata(sessionId, timestamp); + persistEvent(event, thread, EVENT_TYPE_CRASH, eventMetadata, true); } public void persistNonFatalEvent( - @NonNull Throwable event, @NonNull Thread thread, @NonNull String sessionId, long timestamp) { - Logger.getLogger().v("Persisting non-fatal event for session " + sessionId); - persistEvent(event, thread, sessionId, EVENT_TYPE_LOGGED, timestamp, false); + @NonNull Throwable event, @NonNull Thread thread, @NonNull EventMetadata eventMetadata) { + Logger.getLogger().v("Persisting non-fatal event for session " + eventMetadata.getSessionId()); + persistEvent(event, thread, EVENT_TYPE_LOGGED, eventMetadata, false); } @RequiresApi(api = Build.VERSION_CODES.R) @@ -253,23 +255,20 @@ private CrashlyticsReportWithSessionId ensureHasFid(CrashlyticsReportWithSession } private CrashlyticsReport.Session.Event addMetaDataToEvent( - CrashlyticsReport.Session.Event capturedEvent) { + CrashlyticsReport.Session.Event capturedEvent, Map eventCustomKeys) { CrashlyticsReport.Session.Event eventWithLogsAndCustomKeys = - addLogsAndCustomKeysToEvent(capturedEvent, logFileManager, reportMetadata); + addLogsCustomKeysAndEventKeysToEvent( + capturedEvent, logFileManager, reportMetadata, eventCustomKeys); CrashlyticsReport.Session.Event eventWithRollouts = addRolloutsStateToEvent(eventWithLogsAndCustomKeys, reportMetadata); return eventWithRollouts; } - private CrashlyticsReport.Session.Event addLogsAndCustomKeysToEvent( - CrashlyticsReport.Session.Event capturedEvent) { - return addLogsAndCustomKeysToEvent(capturedEvent, logFileManager, reportMetadata); - } - - private CrashlyticsReport.Session.Event addLogsAndCustomKeysToEvent( + private CrashlyticsReport.Session.Event addLogsCustomKeysAndEventKeysToEvent( CrashlyticsReport.Session.Event capturedEvent, LogFileManager logFileManager, - UserMetadata reportMetadata) { + UserMetadata reportMetadata, + Map eventKeys) { final CrashlyticsReport.Session.Event.Builder eventBuilder = capturedEvent.toBuilder(); final String content = logFileManager.getLogString(); @@ -284,7 +283,7 @@ private CrashlyticsReport.Session.Event addLogsAndCustomKeysToEvent( // logFileManager.clearLog(); // Clear log to prepare for next event. final List sortedCustomAttributes = - getSortedCustomAttributes(reportMetadata.getCustomKeys()); + getSortedCustomAttributes(reportMetadata.getCustomKeys(eventKeys)); final List sortedInternalKeys = getSortedCustomAttributes(reportMetadata.getInternalKeys()); @@ -299,6 +298,14 @@ private CrashlyticsReport.Session.Event addLogsAndCustomKeysToEvent( return eventBuilder.build(); } + private CrashlyticsReport.Session.Event addLogsAndCustomKeysToEvent( + CrashlyticsReport.Session.Event capturedEvent, + LogFileManager logFileManager, + UserMetadata reportMetadata) { + return addLogsCustomKeysAndEventKeysToEvent( + capturedEvent, logFileManager, reportMetadata, Map.of()); + } + private CrashlyticsReport.Session.Event addRolloutsStateToEvent( CrashlyticsReport.Session.Event capturedEvent, UserMetadata reportMetadata) { List reportRolloutAssignments = @@ -319,9 +326,8 @@ private CrashlyticsReport.Session.Event addRolloutsStateToEvent( private void persistEvent( @NonNull Throwable event, @NonNull Thread thread, - @NonNull String sessionId, @NonNull String eventType, - long timestamp, + @NonNull EventMetadata eventMetadata, boolean isFatal) { final boolean isHighPriority = eventType.equals(EVENT_TYPE_CRASH); @@ -331,23 +337,25 @@ private void persistEvent( event, thread, eventType, - timestamp, + eventMetadata.getTimestamp(), EVENT_THREAD_IMPORTANCE, MAX_CHAINED_EXCEPTION_DEPTH, isFatal); - CrashlyticsReport.Session.Event finallizedEvent = addMetaDataToEvent(capturedEvent); + CrashlyticsReport.Session.Event finallizedEvent = + addMetaDataToEvent(capturedEvent, eventMetadata.getAdditionalCustomKeys()); // Non-fatal, persistence write task we move to diskWriteWorker if (!isFatal) { crashlyticsWorkers.diskWrite.submit( () -> { Logger.getLogger().d("disk worker: log non-fatal event to persistence"); - reportPersistence.persistEvent(finallizedEvent, sessionId, isHighPriority); + reportPersistence.persistEvent( + finallizedEvent, eventMetadata.getSessionId(), isHighPriority); }); return; } - reportPersistence.persistEvent(finallizedEvent, sessionId, isHighPriority); + reportPersistence.persistEvent(finallizedEvent, eventMetadata.getSessionId(), isHighPriority); } private boolean onReportSendComplete(@NonNull Task task) { diff --git a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/metadata/EventMetadata.kt b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/metadata/EventMetadata.kt new file mode 100644 index 00000000000..fef5899cef5 --- /dev/null +++ b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/metadata/EventMetadata.kt @@ -0,0 +1,31 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.firebase.crashlytics.internal.metadata + +/** + * A class that represents information to attach to a specific event. + * + * @property sessionId the sessionId to attach to the event. + * @property timestamp the timestamp to attach to the event. + * @property additionalCustomKeys a [Map] of key value pairs to attach to the event, + * in addition to the global custom keys. + */ +internal data class EventMetadata +@JvmOverloads +constructor( + val sessionId: String, + val timestamp: Long, + val additionalCustomKeys: Map = mapOf() +) diff --git a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/metadata/KeysMap.java b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/metadata/KeysMap.java index 620c7dca9d9..5d688b5e134 100644 --- a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/metadata/KeysMap.java +++ b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/metadata/KeysMap.java @@ -47,11 +47,11 @@ public synchronized boolean setKey(String key, String value) { String sanitizedKey = sanitizeKey(key); // The entry can be added if we're under the size limit or we're updating an existing entry if (keys.size() < maxEntries || keys.containsKey(sanitizedKey)) { - String santitizedAttribute = sanitizeString(value, maxEntryLength); - if (CommonUtils.nullSafeEquals(keys.get(sanitizedKey), santitizedAttribute)) { + String sanitizedAttribute = sanitizeString(value, maxEntryLength); + if (CommonUtils.nullSafeEquals(keys.get(sanitizedKey), sanitizedAttribute)) { return false; } - keys.put(sanitizedKey, value == null ? "" : santitizedAttribute); + keys.put(sanitizedKey, value == null ? "" : sanitizedAttribute); return true; } Logger.getLogger() diff --git a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/metadata/UserMetadata.java b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/metadata/UserMetadata.java index 36ca7fda8a5..ec7403078d7 100644 --- a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/metadata/UserMetadata.java +++ b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/metadata/UserMetadata.java @@ -17,10 +17,13 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.google.errorprone.annotations.CanIgnoreReturnValue; +import com.google.firebase.crashlytics.internal.Logger; import com.google.firebase.crashlytics.internal.common.CommonUtils; import com.google.firebase.crashlytics.internal.concurrency.CrashlyticsWorkers; import com.google.firebase.crashlytics.internal.model.CrashlyticsReport; import com.google.firebase.crashlytics.internal.persistence.FileStore; +import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicMarkableReference; @@ -135,6 +138,47 @@ public void setUserId(String identifier) { crashlyticsWorkers.diskWrite.submit(this::serializeUserDataIfNeeded); } + /** + * Returns a {@link Map} containing all the custom keys to attach to the event. + * It overrides the values of app level custom keys with the values of event level custom keys if + * they're identical, and event keys or values that exceed 1024 characters are truncated. + * Combined with app level custom keys, the map is restricted to 64 key value pairs. + * + * @param eventKeys a {@link Map} representing event specific keys. + * @return a {@link Map} containing all the custom keys to attach to the event. + */ + public Map getCustomKeys(Map eventKeys) { + // In case of empty event keys, preserve existing behavior. + if (eventKeys.isEmpty()) { + return customKeys.getKeys(); + } + + // Otherwise merge the event keys with custom keys as appropriate. + Map globalKeys = customKeys.getKeys(); + HashMap result = new HashMap<>(globalKeys); + int eventKeysOverLimit = 0; + for (Map.Entry entry : eventKeys.entrySet()) { + String sanitizedKey = KeysMap.sanitizeString(entry.getKey(), MAX_ATTRIBUTE_SIZE); + if (result.size() < MAX_ATTRIBUTES || result.containsKey(sanitizedKey)) { + String sanitizedValue = KeysMap.sanitizeString(entry.getValue(), MAX_ATTRIBUTE_SIZE); + result.put(sanitizedKey, sanitizedValue); + } else { + eventKeysOverLimit++; + } + } + + if (eventKeysOverLimit > 0) { + Logger.getLogger() + .w( + "Ignored " + + eventKeysOverLimit + + " keys when adding event specific keys. Maximum allowable: " + + MAX_ATTRIBUTE_SIZE); + } + + return Collections.unmodifiableMap(result); + } + /** * @return defensive copy of the custom keys. */