Skip to content

Commit 2a40a3d

Browse files
committed
Crashlytics Add Internal Keys feature for Unity Metadata
1 parent 976ea2f commit 2a40a3d

File tree

5 files changed

+165
-8
lines changed

5 files changed

+165
-8
lines changed

firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/MetaDataStoreTest.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,32 @@ public void testWriteKeys_readDifferentSession() {
222222
assertEquals(0, readKeys.size());
223223
}
224224

225+
public void testWriteKeys_readSeparateFromUser() {
226+
final Map<String, String> keys =
227+
new HashMap<String, String>() {
228+
{
229+
put(KEY_1, VALUE_1);
230+
}
231+
};
232+
233+
final Map<String, String> internalKeys =
234+
new HashMap<String, String>() {
235+
{
236+
put(KEY_2, VALUE_2);
237+
put(KEY_3, VALUE_3);
238+
}
239+
};
240+
241+
storeUnderTest.writeKeyData(SESSION_ID_1, keys);
242+
storeUnderTest.writeKeyData(SESSION_ID_1, internalKeys, /*isInternal=*/ true);
243+
244+
final Map<String, String> readKeys = storeUnderTest.readKeyData(SESSION_ID_1);
245+
final Map<String, String> readInternalKeys = storeUnderTest.readKeyData(SESSION_ID_1, true);
246+
247+
assertEqualMaps(keys, readKeys);
248+
assertEqualMaps(internalKeys, readInternalKeys);
249+
}
250+
225251
public void testReadKeys_noStoredData() {
226252
final Map<String, String> readKeys = storeUnderTest.readKeyData(SESSION_ID_1);
227253
assertEquals(0, readKeys.size());

firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CrashlyticsController.java

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ class CrashlyticsController {
6767
private final DataCollectionArbiter dataCollectionArbiter;
6868
private final CrashlyticsFileMarker crashMarker;
6969
private final UserMetadata userMetadata;
70+
private final InternalKeys internalKeys;
7071

7172
private final CrashlyticsBackgroundWorker backgroundWorker;
7273

@@ -109,6 +110,7 @@ class CrashlyticsController {
109110
CrashlyticsFileMarker crashMarker,
110111
AppData appData,
111112
UserMetadata userMetadata,
113+
InternalKeys internalKeys,
112114
LogFileManager logFileManager,
113115
LogFileManager.DirectoryProvider logFileDirectoryProvider,
114116
SessionReportingCoordinator sessionReportingCoordinator,
@@ -122,6 +124,7 @@ class CrashlyticsController {
122124
this.crashMarker = crashMarker;
123125
this.appData = appData;
124126
this.userMetadata = userMetadata;
127+
this.internalKeys = internalKeys;
125128
this.logFileManager = logFileManager;
126129
this.logFileDirectoryProvider = logFileDirectoryProvider;
127130
this.nativeComponent = nativeComponent;
@@ -434,14 +437,28 @@ void setCustomKey(String key, String value) {
434437
return;
435438
}
436439
}
437-
cacheKeyData(userMetadata.getCustomKeys());
440+
cacheKeyData(userMetadata.getCustomKeys(), false);
438441
}
439442

440443
void setCustomKeys(Map<String, String> keysAndValues) {
441444
// Write all the key/value pairs before doing anything computationally expensive.
442445
userMetadata.setCustomKeys(keysAndValues);
443446
// Once all the key/value pairs are added, update the cache.
444-
cacheKeyData(userMetadata.getCustomKeys());
447+
cacheKeyData(userMetadata.getCustomKeys(), false);
448+
}
449+
450+
void setInternalKey(String key, String value) {
451+
try {
452+
internalKeys.setInternalKey(key, value);
453+
} catch (IllegalArgumentException ex) {
454+
if (context != null && CommonUtils.isAppDebuggable(context)) {
455+
throw ex;
456+
} else {
457+
Logger.getLogger().e("Attempting to set custom attribute with null key, ignoring.");
458+
return;
459+
}
460+
}
461+
cacheKeyData(internalKeys.getInternalKeys(), true);
445462
}
446463

447464
/**
@@ -475,13 +492,13 @@ public Void call() throws Exception {
475492
* crash happens immediately after setting a value. If this becomes a problem, we can investigate
476493
* writing synchronously, or potentially add an explicit user-facing API for synchronous writes.
477494
*/
478-
private void cacheKeyData(final Map<String, String> keyData) {
495+
private void cacheKeyData(final Map<String, String> keyData, boolean isInternal) {
479496
backgroundWorker.submit(
480497
new Callable<Void>() {
481498
@Override
482499
public Void call() throws Exception {
483500
final String currentSessionId = getCurrentSessionId();
484-
new MetaDataStore(getFilesDir()).writeKeyData(currentSessionId, keyData);
501+
new MetaDataStore(getFilesDir()).writeKeyData(currentSessionId, keyData, isInternal);
485502
return null;
486503
}
487504
});

firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CrashlyticsCore.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ public boolean onPreExecute(AppData appData, SettingsDataProvider settingsProvid
131131
initializationMarker = new CrashlyticsFileMarker(INITIALIZATION_MARKER_FILE_NAME, fileStore);
132132

133133
final UserMetadata userMetadata = new UserMetadata();
134-
134+
final InternalKeys internalKeys = new InternalKeys();
135135
final LogFileDirectoryProvider logFileDirectoryProvider =
136136
new LogFileDirectoryProvider(fileStore);
137137
final LogFileManager logFileManager = new LogFileManager(context, logFileDirectoryProvider);
@@ -160,6 +160,7 @@ public boolean onPreExecute(AppData appData, SettingsDataProvider settingsProvid
160160
crashMarker,
161161
appData,
162162
userMetadata,
163+
internalKeys,
163164
logFileManager,
164165
logFileDirectoryProvider,
165166
sessionReportingCoordinator,
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// Copyright 2021 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.crashlytics.internal.common;
16+
17+
import androidx.annotation.NonNull;
18+
19+
import com.google.firebase.crashlytics.internal.Logger;
20+
import java.util.ArrayList;
21+
import java.util.Collections;
22+
import java.util.HashMap;
23+
import java.util.List;
24+
import java.util.Map;
25+
26+
/** Handles custom attributes internal to the SDK, eg. for custom Unity metadata. */
27+
public class InternalKeys {
28+
static final int MAX_INTERNAL_KEYS = 64;
29+
static final int MAX_INTERNAL_KEY_SIZE = 8192;
30+
31+
private final Map<String, String> internal_keys = new HashMap<>();
32+
33+
public InternalKeys() {}
34+
35+
@NonNull
36+
public Map<String, String> getInternalKeys() {
37+
return Collections.unmodifiableMap(internal_keys);
38+
}
39+
40+
public void setInternalKey(String key, String value) {
41+
setSyncInternalKeys(
42+
new HashMap<String, String>() {
43+
{
44+
put(sanitizeKey(key), sanitizeAttribute(value));
45+
}
46+
});
47+
}
48+
49+
/** Gatekeeper function for access to attributes */
50+
private synchronized void setSyncInternalKeys(Map<String, String> keysAndValues) {
51+
// We want all access to the attributes hashmap to be locked so that there is no way to create
52+
// a race condition and add more than MAX_ATTRIBUTES keys.
53+
54+
// Update any existing keys first, then add any additional keys
55+
Map<String, String> currentKeys = new HashMap<String, String>();
56+
Map<String, String> newKeys = new HashMap<String, String>();
57+
58+
// Split into current and new keys
59+
for (Map.Entry<String, String> entry : keysAndValues.entrySet()) {
60+
String key = sanitizeKey(entry.getKey());
61+
String value = (entry.getValue() == null) ? "" : sanitizeAttribute(entry.getValue());
62+
if (internal_keys.containsKey(key)) {
63+
currentKeys.put(key, value);
64+
} else {
65+
newKeys.put(key, value);
66+
}
67+
}
68+
69+
internal_keys.putAll(currentKeys);
70+
71+
// Add new keys if there is space
72+
if (internal_keys.size() + newKeys.size() > MAX_INTERNAL_KEYS) {
73+
int keySlotsLeft = MAX_INTERNAL_KEYS - internal_keys.size();
74+
Logger.getLogger()
75+
.v("Exceeded maximum number of internal keys (" + MAX_INTERNAL_KEYS + ").");
76+
List<String> newKeyList = new ArrayList<>(newKeys.keySet());
77+
newKeys.keySet().retainAll(newKeyList.subList(0, keySlotsLeft));
78+
}
79+
internal_keys.putAll(newKeys);
80+
}
81+
82+
/** Checks that the key is not null then sanitizes it. */
83+
private static String sanitizeKey(String key) {
84+
if (key == null) {
85+
throw new IllegalArgumentException("Custom attribute key must not be null.");
86+
}
87+
return sanitizeAttribute(key);
88+
}
89+
90+
/** Trims the string and truncates it to MAX_ATTRIBUTE_SIZE. */
91+
private static String sanitizeAttribute(String input) {
92+
if (input != null) {
93+
input = input.trim();
94+
if (input.length() > MAX_INTERNAL_KEY_SIZE) {
95+
input = input.substring(0, MAX_INTERNAL_KEY_SIZE);
96+
}
97+
}
98+
return input;
99+
}
100+
}

firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/MetaDataStore.java

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ class MetaDataStore {
4141

4242
private static final String USERDATA_SUFFIX = "user";
4343
private static final String KEYDATA_SUFFIX = "keys";
44+
private static final String INTERNAL_KEYDATA_SUFFIX = "internal-keys";
4445
private static final String METADATA_EXT = ".meta";
4546

4647
private static final String KEY_USER_ID = "userId";
@@ -83,9 +84,12 @@ public UserMetadata readUserData(String sessionId) {
8384
}
8485
return new UserMetadata();
8586
}
86-
8787
public void writeKeyData(String sessionId, Map<String, String> keyData) {
88-
final File f = getKeysFileForSession(sessionId);
88+
writeKeyData(sessionId, keyData, false);
89+
}
90+
91+
void writeKeyData(String sessionId, Map<String, String> keyData, boolean isInternal) {
92+
final File f = isInternal ? getInternalKeysFileForSession(sessionId) : getKeysFileForSession(sessionId);
8993
Writer writer = null;
9094
try {
9195
final String keyDataString = keysDataToJson(keyData);
@@ -100,7 +104,11 @@ public void writeKeyData(String sessionId, Map<String, String> keyData) {
100104
}
101105

102106
public Map<String, String> readKeyData(String sessionId) {
103-
final File f = getKeysFileForSession(sessionId);
107+
return readKeyData(sessionId, false);
108+
}
109+
110+
Map<String, String> readKeyData(String sessionId, boolean isInternal) {
111+
final File f = isInternal ? getInternalKeysFileForSession(sessionId) : getKeysFileForSession(sessionId);
104112
if (!f.exists()) {
105113
return Collections.emptyMap();
106114
}
@@ -127,6 +135,11 @@ public File getKeysFileForSession(String sessionId) {
127135
return new File(filesDir, sessionId + KEYDATA_SUFFIX + METADATA_EXT);
128136
}
129137

138+
@NonNull
139+
public File getInternalKeysFileForSession(String sessionId) {
140+
return new File(filesDir, sessionId + INTERNAL_KEYDATA_SUFFIX + METADATA_EXT);
141+
}
142+
130143
private static UserMetadata jsonToUserData(String json) throws JSONException {
131144
final JSONObject dataObj = new JSONObject(json);
132145
UserMetadata metadata = new UserMetadata();

0 commit comments

Comments
 (0)