Skip to content

Add fragment trace sampling rate config flag #3546

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Mar 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import com.google.firebase.perf.BuildConfig;
import com.google.firebase.perf.config.ConfigurationConstants.CollectionDeactivated;
import com.google.firebase.perf.config.ConfigurationConstants.CollectionEnabled;
import com.google.firebase.perf.config.ConfigurationConstants.FragmentSamplingRate;
import com.google.firebase.perf.config.ConfigurationConstants.LogSourceName;
import com.google.firebase.perf.config.ConfigurationConstants.NetworkEventCountBackground;
import com.google.firebase.perf.config.ConfigurationConstants.NetworkEventCountForeground;
Expand Down Expand Up @@ -729,6 +730,43 @@ public String getAndCacheLogSourceName() {
return configFlag.getDefault();
}

/** Returns what percentage of fragment traces should be collected, range is [0.00f, 1.00f]. */
public float getFragmentSamplingRate() {
// Order of precedence is:
// 1. If the value exists in Android Manifest, convert from [0.00f, 100.00f] to [0.00f, 1.00f]
// and return this value.
// 2. If the value exists through Firebase Remote Config, cache and return this value.
// 3. If the value exists in device cache, return this value.
// 4. Otherwise, return default value.
FragmentSamplingRate config = FragmentSamplingRate.getInstance();

// 1. Reads value in Android Manifest (it is set by developers during build time).
Optional<Float> metadataValue = getMetadataFloat(config);
if (metadataValue.isAvailable()) {
// Sampling percentage from metadata needs to convert from [0.00f, 100.00f] to [0.00f, 1.00f].
float samplingRate = metadataValue.get() / 100.0f;
if (isSamplingRateValid(samplingRate)) {
return samplingRate;
}
}

// 2. Reads value from Firebase Remote Config, saves this value in cache layer if valid.
Optional<Float> rcValue = getRemoteConfigFloat(config);
if (rcValue.isAvailable() && isSamplingRateValid(rcValue.get())) {
deviceCacheManager.setValue(config.getDeviceCacheFlag(), rcValue.get());
return rcValue.get();
}

// 3. Reads value from cache layer.
Optional<Float> deviceCacheValue = getDeviceCacheFloat(config);
if (deviceCacheValue.isAvailable() && isSamplingRateValid(deviceCacheValue.get())) {
return deviceCacheValue.get();
}

// 4. Returns default value if there is no valid value from above approaches.
return config.getDefault();
}

// endregion

// Helper functions for interaction with Metadata layer.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -623,4 +623,42 @@ protected String getDeviceCacheFlag() {
return "com.google.firebase.perf.LogSourceName";
}
}

protected static final class FragmentSamplingRate extends ConfigurationFlag<Float> {
private static FragmentSamplingRate instance;

private FragmentSamplingRate() {
super();
}

protected static synchronized FragmentSamplingRate getInstance() {
if (instance == null) {
instance = new FragmentSamplingRate();
}
return instance;
}

@Override
protected Float getDefault() {
// Sampling rate range is [0.00f, 1.00f]. By default, sampling rate is 1.00f, which is 100%.
// 0.00f means 0%, Fireperf will not capture any event for fragment trace from the device,
// 1.00f means 100%, Fireperf will capture all events for fragment trace from the device.
return 1.00f;
}

@Override
protected String getRemoteConfigFlag() {
return "fpr_vc_fragment_sampling_rate";
}

@Override
protected String getDeviceCacheFlag() {
return "com.google.firebase.perf.FragmentSamplingRate";
}

@Override
protected String getMetadataFlag() {
return "fragment_sampling_percentage";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@ public class ConfigResolverTest extends FirebasePerformanceTestBase {
private static final String SESSIONS_MAX_DURATION_MIN_CACHE_KEY =
"com.google.firebase.perf.SessionsMaxDurationMinutes";

// Fragment trace sampling rate flags
private static final String FRAGMENT_SAMPLING_RATE_FRC_KEY = "fpr_vc_fragment_sampling_rate";
private static final String FRAGMENT_SAMPLING_RATE_CACHE_KEY =
"com.google.firebase.perf.FragmentSamplingRate";

private ConfigResolver testConfigResolver;

@Mock private RemoteConfigManager mockRemoteConfigManager;
Expand Down Expand Up @@ -2482,4 +2487,237 @@ public void getAndCacheLogSourceName_invalidRemoteConfigData_returnsCache() {
verify(mockDeviceCacheManager, times(1))
.setValue(eq("com.google.firebase.perf.LogSourceName"), eq("FIREPERF_INTERNAL_LOW"));
}

@Test
public void getFragmentSamplingRate_validMetadata_returnsMetadata() {
// #1 pass: Validate that method returns Remote Config Value when there is no metadata value.
when(mockRemoteConfigManager.getFloat(FRAGMENT_SAMPLING_RATE_FRC_KEY))
.thenReturn(Optional.of(0.01f));

assertThat(testConfigResolver.getFragmentSamplingRate()).isEqualTo(0.01f);

// #2 pass: Validate that method returns Metadata value which takes higher precedence.
Bundle bundle = new Bundle();
bundle.putFloat("fragment_sampling_percentage", 20.0f);
testConfigResolver.setMetadataBundle(new ImmutableBundle(bundle));

assertThat(testConfigResolver.getFragmentSamplingRate()).isEqualTo(0.2f);
}

@Test
public void getFragmentSamplingRate_validMetadata_notSaveMetadataInCache() {
Bundle bundle = new Bundle();
bundle.putFloat("fragment_sampling_percentage", 20.0f);
testConfigResolver.setMetadataBundle(new ImmutableBundle(bundle));

assertThat(testConfigResolver.getFragmentSamplingRate()).isEqualTo(0.2f);

verify(mockDeviceCacheManager, never()).setValue(any(), any());
}

@Test
public void getFragmentSamplingRate_invalidAndroidMetadataBundle_returnDefaultValue() {
when(mockRemoteConfigManager.getFloat(FRAGMENT_SAMPLING_RATE_FRC_KEY))
.thenReturn(Optional.absent());
when(mockDeviceCacheManager.getFloat(FRAGMENT_SAMPLING_RATE_CACHE_KEY))
.thenReturn(Optional.absent());

assertThat(testConfigResolver.getFragmentSamplingRate()).isEqualTo(1.0f);

// Case #1: Android Metadata bundle value is too high.
Bundle bundle = new Bundle();
bundle.putFloat("fragment_sampling_percentage", 200.00f);
testConfigResolver.setMetadataBundle(new ImmutableBundle(bundle));

assertThat(testConfigResolver.getFragmentSamplingRate()).isEqualTo(1.0f);

// Case #2: Android Metadata bundle value is too low.
bundle = new Bundle();
bundle.putFloat("fragment_sampling_percentage", -1.00f);
testConfigResolver.setMetadataBundle(new ImmutableBundle(bundle));

assertThat(testConfigResolver.getFragmentSamplingRate()).isEqualTo(1.0f);
}

@Test
public void getFragmentSamplingRate_invalidAndroidMetadataBundle_returnRemoteConfigValue() {
when(mockRemoteConfigManager.getFloat(FRAGMENT_SAMPLING_RATE_FRC_KEY))
.thenReturn(Optional.of(0.25f));

assertThat(testConfigResolver.getFragmentSamplingRate()).isEqualTo(0.25f);

// Case #1: Android Metadata bundle value is too high.
Bundle bundle = new Bundle();
bundle.putFloat("fragment_sampling_percentage", 200.00f);
testConfigResolver.setMetadataBundle(new ImmutableBundle(bundle));

assertThat(testConfigResolver.getFragmentSamplingRate()).isEqualTo(0.25f);

// Case #2: Android Metadata bundle value is too low.
bundle = new Bundle();
bundle.putFloat("fragment_sampling_percentage", -1.00f);
testConfigResolver.setMetadataBundle(new ImmutableBundle(bundle));

assertThat(testConfigResolver.getFragmentSamplingRate()).isEqualTo(0.25f);
}

@Test
public void getFragmentSamplingRate_invalidMetadataBundle_returnCacheValue() {
when(mockRemoteConfigManager.getFloat(FRAGMENT_SAMPLING_RATE_FRC_KEY))
.thenReturn(Optional.absent());
when(mockDeviceCacheManager.getFloat(FRAGMENT_SAMPLING_RATE_CACHE_KEY))
.thenReturn(Optional.of(1.0f));

assertThat(testConfigResolver.getFragmentSamplingRate()).isEqualTo(1.0f);

// Case #1: Android Metadata bundle value is too high.
Bundle bundle = new Bundle();
bundle.putFloat("fragment_sampling_percentage", 200.00f);
testConfigResolver.setMetadataBundle(new ImmutableBundle(bundle));

assertThat(testConfigResolver.getFragmentSamplingRate()).isEqualTo(1.0f);

// Case #2: Android Metadata bundle value is too low.
bundle = new Bundle();
bundle.putFloat("fragment_sampling_percentage", -1.00f);
testConfigResolver.setMetadataBundle(new ImmutableBundle(bundle));

assertThat(testConfigResolver.getFragmentSamplingRate()).isEqualTo(1.0f);
}

@Test
public void getFragmentSamplingRate_validRemoteConfig_returnRemoteConfigValue() {
when(mockRemoteConfigManager.getFloat(FRAGMENT_SAMPLING_RATE_FRC_KEY))
.thenReturn(Optional.of(0.25f));

assertThat(testConfigResolver.getFragmentSamplingRate()).isEqualTo(0.25f);
verify(mockDeviceCacheManager, times(1))
.setValue(eq(FRAGMENT_SAMPLING_RATE_CACHE_KEY), eq(0.25f));

when(mockRemoteConfigManager.getFloat(FRAGMENT_SAMPLING_RATE_FRC_KEY))
.thenReturn(Optional.of(0.0f));

assertThat(testConfigResolver.getFragmentSamplingRate()).isEqualTo(0.0f);
verify(mockDeviceCacheManager, times(1))
.setValue(eq(FRAGMENT_SAMPLING_RATE_CACHE_KEY), eq(0.0f));

when(mockRemoteConfigManager.getFloat(FRAGMENT_SAMPLING_RATE_FRC_KEY))
.thenReturn(Optional.of(0.00005f));

assertThat(testConfigResolver.getFragmentSamplingRate()).isEqualTo(0.00005f);
verify(mockDeviceCacheManager, times(1))
.setValue(eq(FRAGMENT_SAMPLING_RATE_CACHE_KEY), eq(0.00005f));

when(mockRemoteConfigManager.getFloat(FRAGMENT_SAMPLING_RATE_FRC_KEY))
.thenReturn(Optional.of(0.0000000001f));

assertThat(testConfigResolver.getFragmentSamplingRate()).isEqualTo(0.0000000001f);
verify(mockDeviceCacheManager, times(1))
.setValue(eq(FRAGMENT_SAMPLING_RATE_CACHE_KEY), eq(0.0000000001f));
}

@Test
public void getFragmentSamplingRate_invalidRemoteConfig_returnDefaultValue() {
// Mock behavior that device cache doesn't have session sampling rate value.
when(mockDeviceCacheManager.getFloat(FRAGMENT_SAMPLING_RATE_CACHE_KEY))
.thenReturn(Optional.absent());

// Case #1: Firebase Remote Config value is too high.
when(mockRemoteConfigManager.getFloat(FRAGMENT_SAMPLING_RATE_FRC_KEY))
.thenReturn(Optional.of(1.01f));

assertThat(testConfigResolver.getFragmentSamplingRate()).isEqualTo(1.0f);
verify(mockDeviceCacheManager, never()).setValue(any(), any());

// Case #2: Firebase Remote Config value is too low.
when(mockRemoteConfigManager.getFloat(FRAGMENT_SAMPLING_RATE_FRC_KEY))
.thenReturn(Optional.of(-0.1f));

assertThat(testConfigResolver.getFragmentSamplingRate()).isEqualTo(1.0f);
verify(mockDeviceCacheManager, never()).setValue(any(), any());
}

@Test
public void getFragmentSamplingRate_invalidRemoteConfig_returnCacheValue() {
// Mock behavior that device cache doesn't have session sampling rate value.
when(mockDeviceCacheManager.getFloat(FRAGMENT_SAMPLING_RATE_CACHE_KEY))
.thenReturn(Optional.of(0.25f));

// Case #1: Firebase Remote Config value is too high.
when(mockRemoteConfigManager.getFloat(FRAGMENT_SAMPLING_RATE_FRC_KEY))
.thenReturn(Optional.of(1.01f));

assertThat(testConfigResolver.getFragmentSamplingRate()).isEqualTo(0.25f);
verify(mockDeviceCacheManager, never()).setValue(any(), any());

// Case #2: Firebase Remote Config value is too low.
when(mockRemoteConfigManager.getFloat(FRAGMENT_SAMPLING_RATE_FRC_KEY))
.thenReturn(Optional.of(-0.1f));

assertThat(testConfigResolver.getFragmentSamplingRate()).isEqualTo(0.25f);
verify(mockDeviceCacheManager, never()).setValue(any(), any());
}

@Test
public void getFragmentSamplingRate_validCache_returnCacheValue() {
when(mockDeviceCacheManager.getFloat(FRAGMENT_SAMPLING_RATE_CACHE_KEY))
.thenReturn(Optional.of(1.0f));

when(mockRemoteConfigManager.getFloat(FRAGMENT_SAMPLING_RATE_FRC_KEY))
.thenReturn(Optional.absent());

assertThat(testConfigResolver.getFragmentSamplingRate()).isEqualTo(1.0f);
}

@Test
public void getFragmentSamplingRate_invalidCache_returnDefaultValue() {
// Mock behavior that remote config doesn't have session sampling rate value.
when(mockRemoteConfigManager.getFloat(FRAGMENT_SAMPLING_RATE_FRC_KEY))
.thenReturn(Optional.absent());

// Case #1: Device Cache value is too high.
when(mockDeviceCacheManager.getFloat(FRAGMENT_SAMPLING_RATE_CACHE_KEY))
.thenReturn(Optional.of(10.0f));

assertThat(testConfigResolver.getFragmentSamplingRate()).isEqualTo(1.0f);

// Case #2: Device Cache value is too low.
when(mockDeviceCacheManager.getFloat(FRAGMENT_SAMPLING_RATE_CACHE_KEY))
.thenReturn(Optional.of(-1.0f));

assertThat(testConfigResolver.getFragmentSamplingRate()).isEqualTo(1.0f);
}

@Test
public void
getFragmentSamplingRate_metadataAndRemoteConfigAndCacheAreSet_metadataHasHighestConfigPrecedence() {
// Set cache value.
when(mockDeviceCacheManager.getFloat(FRAGMENT_SAMPLING_RATE_CACHE_KEY))
.thenReturn(Optional.of(0.2f));

// Set remote config value.
when(mockRemoteConfigManager.getFloat(FRAGMENT_SAMPLING_RATE_FRC_KEY))
.thenReturn(Optional.of(0.3f));

// Set Android Manifest value.
Bundle bundle = new Bundle();
bundle.putFloat("fragment_sampling_percentage", 4.0f);
testConfigResolver.setMetadataBundle(new ImmutableBundle(bundle));

assertThat(testConfigResolver.getFragmentSamplingRate()).isEqualTo(0.04f);
}

@Test
public void
getFragmentSamplingRate_remoteConfigAndCacheAreSet_remoteConfigHasHighestConfigPrecedence() {
// Set cache value.
when(mockDeviceCacheManager.getFloat(FRAGMENT_SAMPLING_RATE_CACHE_KEY))
.thenReturn(Optional.of(0.2f));

// Set remote config value.
when(mockRemoteConfigManager.getFloat(FRAGMENT_SAMPLING_RATE_FRC_KEY))
.thenReturn(Optional.of(0.3f));

assertThat(testConfigResolver.getFragmentSamplingRate()).isEqualTo(0.3f);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import com.google.firebase.perf.config.ConfigurationConstants.CollectionDeactivated;
import com.google.firebase.perf.config.ConfigurationConstants.CollectionEnabled;
import com.google.firebase.perf.config.ConfigurationConstants.FragmentSamplingRate;
import com.google.firebase.perf.config.ConfigurationConstants.LogSourceName;
import com.google.firebase.perf.config.ConfigurationConstants.NetworkEventCountBackground;
import com.google.firebase.perf.config.ConfigurationConstants.NetworkEventCountForeground;
Expand Down Expand Up @@ -253,4 +254,15 @@ public void getInstance_LogSourceName_validateConstants() {
assertThat(LogSourceName.getLogSourceName(675L)).isEqualTo("FIREPERF_INTERNAL_LOW");
assertThat(LogSourceName.getLogSourceName(676L)).isEqualTo("FIREPERF_INTERNAL_HIGH");
}

@Test
public void getInstance_FragmentSamplingRate_validateConstants() {
FragmentSamplingRate configFlag = FragmentSamplingRate.getInstance();

assertThat(configFlag.getDefault()).isEqualTo(1.0f);
assertThat(configFlag.getDeviceCacheFlag())
.isEqualTo("com.google.firebase.perf.FragmentSamplingRate");
assertThat(configFlag.getRemoteConfigFlag()).isEqualTo("fpr_vc_fragment_sampling_rate");
assertThat(configFlag.getMetadataFlag()).isEqualTo("fragment_sampling_percentage");
}
}