Skip to content

Commit

Permalink
Add fid to Crashlytics report (#5052)
Browse files Browse the repository at this point in the history
* Add fid to Crashlytics report

* Format
  • Loading branch information
mrober authored Jun 20, 2023
1 parent bb4a41a commit 5d56114
Show file tree
Hide file tree
Showing 13 changed files with 149 additions and 65 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,15 @@ private IdManager createIdManager(String instanceId, DataCollectionArbiter arbit
public void testCreateUUID() {
final String fid = "test_fid";
final IdManager idManager = createIdManager(fid, MOCK_ARBITER_ENABLED);
final String installId = idManager.getCrashlyticsInstallId();
final String installId = idManager.getInstallIds().getCrashlyticsInstallId();
assertNotNull(installId);

assertEquals(installId, prefs.getString(IdManager.PREFKEY_INSTALLATION_UUID, null));
assertNull(prefs.getString(IdManager.PREFKEY_ADVERTISING_ID, null));
assertEquals(fid, prefs.getString(IdManager.PREFKEY_FIREBASE_IID, null));

// subsequent calls should return the same id
assertEquals(installId, idManager.getCrashlyticsInstallId());
assertEquals(installId, idManager.getInstallIds().getCrashlyticsInstallId());
}

public void testGetIdExceptionalCase_doesNotRotateInstallId() {
Expand All @@ -93,7 +93,7 @@ public void testGetIdExceptionalCase_doesNotRotateInstallId() {

final IdManager idManager =
new IdManager(getContext(), getContext().getPackageName(), fis, MOCK_ARBITER_ENABLED);
final String actualInstallId = idManager.getCrashlyticsInstallId();
final String actualInstallId = idManager.getInstallIds().getCrashlyticsInstallId();
assertNotNull(actualInstallId);
assertEquals(expectedInstallId, actualInstallId);
}
Expand All @@ -111,15 +111,15 @@ public void testInstanceIdChanges_dataCollectionEnabled() {
// Initialize the manager with a different FID.
IdManager idManager = createIdManager(newFid, MOCK_ARBITER_ENABLED);

String installId = idManager.getCrashlyticsInstallId();
String installId = idManager.getInstallIds().getCrashlyticsInstallId();
assertNotNull(installId);
assertFalse(installId.equals(oldUuid));

assertEquals(installId, prefs.getString(IdManager.PREFKEY_INSTALLATION_UUID, null));
assertEquals(newFid, prefs.getString(IdManager.PREFKEY_FIREBASE_IID, null));

// subsequent calls should return the same id
assertEquals(installId, idManager.getCrashlyticsInstallId());
assertEquals(installId, idManager.getInstallIds().getCrashlyticsInstallId());
}

void validateInstanceIdDoesntChange(boolean dataCollectionEnabled) {
Expand All @@ -136,7 +136,7 @@ void validateInstanceIdDoesntChange(boolean dataCollectionEnabled) {
IdManager idManager =
createIdManager(fid, dataCollectionEnabled ? MOCK_ARBITER_ENABLED : MOCK_ARBITER_DISABLED);

String installId = idManager.getCrashlyticsInstallId();
String installId = idManager.getInstallIds().getCrashlyticsInstallId();
assertNotNull(installId);

// Test that the UUID didn't change.
Expand All @@ -146,15 +146,15 @@ void validateInstanceIdDoesntChange(boolean dataCollectionEnabled) {
assertEquals(fid, prefs.getString(IdManager.PREFKEY_FIREBASE_IID, null));

// subsequent calls should return the same id
assertEquals(oldUuid, idManager.getCrashlyticsInstallId());
assertEquals(oldUuid, idManager.getInstallIds().getCrashlyticsInstallId());
}

public void testInstanceIdDoesntChange_dataCollectionEnabled() {
validateInstanceIdDoesntChange(/*dataCollectionEnabled=*/ true);
validateInstanceIdDoesntChange(/* dataCollectionEnabled= */ true);
}

public void testInstanceIdDoesntChange_dataCollectionDisabled() {
validateInstanceIdDoesntChange(/*dataCollectionEnabled=*/ false);
validateInstanceIdDoesntChange(/* dataCollectionEnabled= */ false);
}

public void testInstanceIdRotatesWithDataCollectionFlag() {
Expand All @@ -169,35 +169,42 @@ public void testInstanceIdRotatesWithDataCollectionFlag() {

// Initialize the manager with the same FID.
IdManager idManager = createIdManager(originalFid, MOCK_ARBITER_ENABLED);
String firstUuid = idManager.getCrashlyticsInstallId();
String firstUuid = idManager.getInstallIds().getCrashlyticsInstallId();
assertNotNull(firstUuid);
assertEquals(originalUuid, firstUuid);

// subsequent calls should return the same id
assertEquals(firstUuid, idManager.getCrashlyticsInstallId());
assertEquals(firstUuid, idManager.getInstallIds().getCrashlyticsInstallId());
assertEquals(
firstUuid, createIdManager(originalFid, MOCK_ARBITER_ENABLED).getCrashlyticsInstallId());
firstUuid,
createIdManager(originalFid, MOCK_ARBITER_ENABLED)
.getInstallIds()
.getCrashlyticsInstallId());

// Disable data collection manager and confirm we get a different id
idManager = createIdManager(originalFid, MOCK_ARBITER_DISABLED);
String secondUuid = idManager.getCrashlyticsInstallId();
String secondUuid = idManager.getInstallIds().getCrashlyticsInstallId();
assertNotSame(secondUuid, firstUuid);
assertEquals(secondUuid, idManager.getCrashlyticsInstallId());
assertEquals(secondUuid, idManager.getInstallIds().getCrashlyticsInstallId());
assertEquals(
secondUuid, createIdManager(null, MOCK_ARBITER_DISABLED).getCrashlyticsInstallId());
secondUuid,
createIdManager(null, MOCK_ARBITER_DISABLED).getInstallIds().getCrashlyticsInstallId());
// Check that we cached an synthetic FID
final SharedPreferences prefs = CommonUtils.getSharedPrefs(getContext());
String cachedFid = prefs.getString(IdManager.PREFKEY_FIREBASE_IID, null);
assertTrue(IdManager.isSyntheticFid(cachedFid));

// re-enable data collection
idManager = createIdManager(originalFid, MOCK_ARBITER_ENABLED);
String thirdUuid = idManager.getCrashlyticsInstallId();
String thirdUuid = idManager.getInstallIds().getCrashlyticsInstallId();
assertNotSame(thirdUuid, firstUuid);
assertNotSame(thirdUuid, secondUuid);
assertEquals(thirdUuid, idManager.getCrashlyticsInstallId());
assertEquals(thirdUuid, idManager.getInstallIds().getCrashlyticsInstallId());
assertEquals(
thirdUuid, createIdManager(originalFid, MOCK_ARBITER_ENABLED).getCrashlyticsInstallId());
thirdUuid,
createIdManager(originalFid, MOCK_ARBITER_ENABLED)
.getInstallIds()
.getCrashlyticsInstallId());
// The cached ID should be back to the original
cachedFid = prefs.getString(IdManager.PREFKEY_FIREBASE_IID, null);
assertEquals(cachedFid, originalFid);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ public class SessionReportingCoordinatorTest {
@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;
@Mock private CrashlyticsReport.Session.Event.Builder mockEventBuilder;
Expand All @@ -74,7 +75,12 @@ public void setUp() {

reportingCoordinator =
new SessionReportingCoordinator(
dataCapture, reportPersistence, reportSender, logFileManager, reportMetadata);
dataCapture,
reportPersistence,
reportSender,
logFileManager,
reportMetadata,
idManager);
}

@Test
Expand Down Expand Up @@ -442,6 +448,7 @@ public void onReportSend_successfulReportsAreDeleted() {
when(reportSender.enqueueReport(mockReport1, false)).thenReturn(successfulTask);
when(reportSender.enqueueReport(mockReport2, false)).thenReturn(failedTask);

when(idManager.fetchTrueFid()).thenReturn("fid");
reportingCoordinator.sendReports(Runnable::run);

verify(reportSender).enqueueReport(mockReport1, false);
Expand Down Expand Up @@ -491,6 +498,7 @@ private static CrashlyticsReport mockReport(String sessionId) {
final CrashlyticsReport.Session mockSession = mock(CrashlyticsReport.Session.class);
when(mockSession.getIdentifier()).thenReturn(sessionId);
when(mockReport.getSession()).thenReturn(mockSession);
when(mockReport.withFirebaseInstallationId(anyString())).thenReturn(mockReport);
return mockReport;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import com.google.firebase.crashlytics.internal.common.DeliveryMechanism;
import com.google.firebase.crashlytics.internal.common.ExecutorUtils;
import com.google.firebase.crashlytics.internal.common.InstallIdProvider;
import com.google.firebase.crashlytics.internal.common.InstallIdProvider.InstallIds;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import org.json.JSONObject;
Expand Down Expand Up @@ -380,13 +381,7 @@ public void testNoAvailableSettingsLoad() throws Exception {
}

private SettingsRequest buildSettingsRequest() {
InstallIdProvider installIdProvider =
new InstallIdProvider() {
@Override
public String getCrashlyticsInstallId() {
return installationId;
}
};
InstallIdProvider installIdProvider = () -> InstallIds.createWithoutFid(installationId);

return new SettingsRequest(
googleAppId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import com.google.firebase.crashlytics.internal.Logger;
import com.google.firebase.crashlytics.internal.common.CommonUtils;
import com.google.firebase.crashlytics.internal.common.InstallIdProvider;
import com.google.firebase.crashlytics.internal.common.InstallIdProvider.InstallIds;
import com.google.firebase.crashlytics.internal.network.HttpGetRequest;
import com.google.firebase.crashlytics.internal.network.HttpRequestFactory;
import com.google.firebase.crashlytics.internal.network.HttpResponse;
Expand Down Expand Up @@ -230,13 +231,7 @@ public void testRequestWasSuccessful_unsuccessfulStatusCodes() {
}

private SettingsRequest buildSettingsRequest(String instanceId) {
final InstallIdProvider installIdProvider =
new InstallIdProvider() {
@Override
public String getCrashlyticsInstallId() {
return INSTALLATION_ID;
}
};
final InstallIdProvider installIdProvider = () -> InstallIds.createWithoutFid(INSTALLATION_ID);
return new SettingsRequest(
GOOGLE_APP_ID,
DEVICE_MODEL,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,9 @@ class CLSUUID {
this.populateSequenceNumber(bytes);
this.populatePID(bytes);

// appInstallationUUID may be empty but is guaranteed not to be null.
// sha1 it to ensure the string is long enough, has only valid hex characters, and to
// increase entropy.
final String idSha = CommonUtils.sha1(idManager.getCrashlyticsInstallId());
final String idSha = CommonUtils.sha1(idManager.getInstallIds().getCrashlyticsInstallId());
final String timeSeqPid = CommonUtils.hexify(bytes);

_clsId =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -749,7 +749,7 @@ private static StaticSessionData.AppData createAppData(IdManager idManager, AppD
idManager.getAppIdentifier(),
appData.versionCode,
appData.versionName,
idManager.getCrashlyticsInstallId(),
idManager.getInstallIds().getCrashlyticsInstallId(),
DeliveryMechanism.determineFrom(appData.installerPackageName).getId(),
appData.developmentPlatformProvider);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,8 @@ private CrashlyticsReport.Builder buildReportData() {
return CrashlyticsReport.builder()
.setSdkVersion(BuildConfig.VERSION_NAME)
.setGmpAppId(appData.googleAppId)
.setInstallationUuid(idManager.getCrashlyticsInstallId())
.setInstallationUuid(idManager.getInstallIds().getCrashlyticsInstallId())
.setFirebaseInstallationId(idManager.getInstallIds().getFirebaseInstallationId())
.setBuildVersion(appData.versionCode)
.setDisplayVersion(appData.versionName)
.setPlatform(REPORT_ANDROID_PLATFORM);
Expand All @@ -188,7 +189,7 @@ private CrashlyticsReport.Session.Application populateSessionApplicationData() {
.setIdentifier(idManager.getAppIdentifier())
.setVersion(appData.versionCode)
.setDisplayVersion(appData.versionName)
.setInstallationUuid(idManager.getCrashlyticsInstallId())
.setInstallationUuid(idManager.getInstallIds().getCrashlyticsInstallId())
.setDevelopmentPlatform(appData.developmentPlatformProvider.getDevelopmentPlatform())
.setDevelopmentPlatformVersion(
appData.developmentPlatformProvider.getDevelopmentPlatformVersion());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.google.android.gms.tasks.Task;
import com.google.firebase.crashlytics.internal.Logger;
import com.google.firebase.installations.FirebaseInstallationsApi;
Expand All @@ -35,7 +36,7 @@ public class IdManager implements InstallIdProvider {
static final String PREFKEY_FIREBASE_IID = "firebase.installation.id";
static final String PREFKEY_LEGACY_INSTALLATION_UUID = "crashlytics.installation.id";

/** Regex for stripping all non-alphnumeric characters from ALL the identifier fields. */
/** Regex for stripping all non-alphanumeric characters from ALL the identifier fields. */
private static final Pattern ID_PATTERN = Pattern.compile("[^\\p{Alnum}]");

private static final String SYNTHETIC_FID_PREFIX = "SYN_";
Expand All @@ -51,8 +52,8 @@ public class IdManager implements InstallIdProvider {

private final DataCollectionArbiter dataCollectionArbiter;

// Crashlytics maintains a Crashlytics-specific install id, used in the crash processing backend
private String crashlyticsInstallId;
// Stores a Crashlytics-specific install id, and Firebase installation id.
private InstallIds installIds;

/**
* @param appContext Application {@link Context}
Expand Down Expand Up @@ -101,9 +102,9 @@ private static String formatId(String id) {
*/
@Override
@NonNull
public synchronized String getCrashlyticsInstallId() {
if (crashlyticsInstallId != null) {
return crashlyticsInstallId;
public synchronized InstallIds getInstallIds() {
if (!shouldRefresh()) {
return installIds;
}

Logger.getLogger().v("Determining Crashlytics installation ID...");
Expand All @@ -125,28 +126,37 @@ public synchronized String getCrashlyticsInstallId() {

if (trueFid.equals(cachedFid)) {
// the current FID is the same as the cached FID, so we keep the cached Crashlytics ID
crashlyticsInstallId = readCachedCrashlyticsInstallId(prefs);
installIds = InstallIds.create(readCachedCrashlyticsInstallId(prefs), trueFid);
} else {
// the current FID has changed, so we generate a new Crashlytics ID
crashlyticsInstallId = createAndCacheCrashlyticsInstallId(trueFid, prefs);
installIds = InstallIds.create(createAndCacheCrashlyticsInstallId(trueFid, prefs), trueFid);
}
} else { // data collection is NOT enabled; we can't use the FID
if (isSyntheticFid(cachedFid)) {
// We already have a cached synthetic FID, so we don't need to change the Crashlytics ID
crashlyticsInstallId = readCachedCrashlyticsInstallId(prefs);
installIds = InstallIds.createWithoutFid(readCachedCrashlyticsInstallId(prefs));
} else {
// we don't have a synthetic FID, so we need to replace the cached FID with a synthetic
// one and create a new Crashlytics install id.
crashlyticsInstallId = createAndCacheCrashlyticsInstallId(createSyntheticFid(), prefs);
installIds =
InstallIds.createWithoutFid(
createAndCacheCrashlyticsInstallId(createSyntheticFid(), prefs));
}
}
if (crashlyticsInstallId == null) {
// Should not happen but we don't want to throw any exceptions
Logger.getLogger().w("Unable to determine Crashlytics Install Id, creating a new one.");
crashlyticsInstallId = createAndCacheCrashlyticsInstallId(createSyntheticFid(), prefs);
}
Logger.getLogger().v("Crashlytics installation ID: " + crashlyticsInstallId);
return crashlyticsInstallId;
Logger.getLogger().v("Install IDs: " + installIds);
return installIds;
}

/**
* Returns true if we have not cached an InstallIds, or should force refresh the fid.
*
* <p>We should force refresh the fid if data collection is enabled but we don't have a cached
* fid. This can happen if data collection was disabled at crash time, but enabled at upload time.
*/
private boolean shouldRefresh() {
return installIds == null
|| (installIds.getFirebaseInstallationId() == null
&& dataCollectionArbiter.isAutomaticDataCollectionEnabled());
}

static String createSyntheticFid() {
Expand All @@ -163,14 +173,15 @@ private String readCachedCrashlyticsInstallId(SharedPreferences prefs) {

/** Makes a blocking call to query FID. If the call fails, logs a warning and returns null. */
@Nullable
private String fetchTrueFid() {
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public String fetchTrueFid() {
Task<String> currentFidTask = firebaseInstallationsApi.getId();
String currentFid = null;

try {
currentFid = Utils.awaitEvenIfOnMainThread(currentFidTask);
} catch (Exception e) {
Logger.getLogger().w("Failed to retrieve Firebase Installations ID.", e);
Logger.getLogger().w("Failed to retrieve Firebase Installation ID.", e);
}
return currentFid;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,33 @@

package com.google.firebase.crashlytics.internal.common;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.google.auto.value.AutoValue;

public interface InstallIdProvider {

/** @return an ID that uniquely identifies the app installation on the current device. */
String getCrashlyticsInstallId();
/** Returns an InstallIds that uniquely identifies the app installation on the current device. */
InstallIds getInstallIds();

@AutoValue
abstract class InstallIds {
@NonNull
public abstract String getCrashlyticsInstallId();

@Nullable
public abstract String getFirebaseInstallationId();

/** Creates an InstallIds with just a crashlyticsInstallId, no firebaseInstallationId. */
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public static InstallIds createWithoutFid(String crashlyticsInstallId) {
return create(crashlyticsInstallId, /* firebaseInstallationId= */ null);
}

static InstallIds create(String crashlyticsInstallId, @Nullable String firebaseInstallationId) {
return new AutoValue_InstallIdProvider_InstallIds(
crashlyticsInstallId, firebaseInstallationId);
}
}
}
Loading

0 comments on commit 5d56114

Please sign in to comment.