Skip to content

Commit e6b01db

Browse files
authored
Add first-open time to Remote Config fetch request. (#3653)
* Add first-open time to Remote Config fetch request. * Removed dependency on jodaTime and replaced it with SimpleDateFormat, as per review comments. * fixed error in FetchHttpClientTest * fixed a comment nit
1 parent 09b7445 commit e6b01db

File tree

5 files changed

+124
-25
lines changed

5 files changed

+124
-25
lines changed

firebase-config/src/main/java/com/google/firebase/remoteconfig/RemoteConfigConstants.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ public final class RemoteConfigConstants {
4848
RequestFieldKey.APP_VERSION,
4949
RequestFieldKey.PACKAGE_NAME,
5050
RequestFieldKey.SDK_VERSION,
51-
RequestFieldKey.ANALYTICS_USER_PROPERTIES
51+
RequestFieldKey.ANALYTICS_USER_PROPERTIES,
52+
RequestFieldKey.FIRST_OPEN_TIME
5253
})
5354
@Retention(RetentionPolicy.SOURCE)
5455
public @interface RequestFieldKey {
@@ -64,6 +65,7 @@ public final class RemoteConfigConstants {
6465
String PACKAGE_NAME = "packageName";
6566
String SDK_VERSION = "sdkVersion";
6667
String ANALYTICS_USER_PROPERTIES = "analyticsUserProperties";
68+
String FIRST_OPEN_TIME = "firstOpenTime";
6769
}
6870

6971
/** Keys of fields in the Fetch response body from the Firebase Remote Config server. */

firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigFetchHandler.java

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,12 @@ public class ConfigFetchHandler {
7979
*/
8080
@VisibleForTesting static final int HTTP_TOO_MANY_REQUESTS = 429;
8181

82+
/**
83+
* First-open time key name in GA user-properties. First-open time is a predefined user-dimension
84+
* automatically collected by GA.
85+
*/
86+
@VisibleForTesting static final String FIRST_OPEN_TIME_KEY = "_fot";
87+
8288
private final FirebaseInstallationsApi firebaseInstallations;
8389
private final Provider<AnalyticsConnector> analyticsConnector;
8490

@@ -110,7 +116,6 @@ public ConfigFetchHandler(
110116
this.fetchedConfigsCache = fetchedConfigsCache;
111117
this.frcBackendApiClient = frcBackendApiClient;
112118
this.frcMetadata = frcMetadata;
113-
114119
this.customHttpHeaders = customHttpHeaders;
115120
}
116121

@@ -311,6 +316,7 @@ private FetchResponse fetchFromBackend(
311316
getUserProperties(),
312317
frcMetadata.getLastFetchETag(),
313318
customHttpHeaders,
319+
getFirstOpenTime(),
314320
currentTime);
315321

316322
if (response.getLastFetchETag() != null) {
@@ -492,8 +498,8 @@ private void updateLastFetchStatusAndTime(
492498
}
493499

494500
/**
495-
* Returns the list of user properties in Analytics. If the Analytics SDK is not available,
496-
* returns an empty list.
501+
* Returns the list of custom user properties in Analytics. If the Analytics SDK is not available,
502+
* this method returns an empty list.
497503
*/
498504
@WorkerThread
499505
private Map<String, String> getUserProperties() {
@@ -510,6 +516,20 @@ private Map<String, String> getUserProperties() {
510516
return userPropertiesMap;
511517
}
512518

519+
/**
520+
* Returns first-open time from Analytics. If the Analytics SDK is not available, or if Analytics
521+
* does not have first-open time for the app, this method returns null.
522+
*/
523+
@WorkerThread
524+
private Long getFirstOpenTime() {
525+
AnalyticsConnector connector = this.analyticsConnector.get();
526+
if (connector == null) {
527+
return null;
528+
}
529+
530+
return (Long) connector.getUserProperties(/*includeInternal=*/ true).get(FIRST_OPEN_TIME_KEY);
531+
}
532+
513533
/** Used to verify that the fetch handler is getting Analytics as expected. */
514534
@VisibleForTesting
515535
public Provider<AnalyticsConnector> getAnalyticsConnector() {

firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigFetchHttpClient.java

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import static com.google.firebase.remoteconfig.RemoteConfigConstants.RequestFieldKey.APP_ID;
2222
import static com.google.firebase.remoteconfig.RemoteConfigConstants.RequestFieldKey.APP_VERSION;
2323
import static com.google.firebase.remoteconfig.RemoteConfigConstants.RequestFieldKey.COUNTRY_CODE;
24+
import static com.google.firebase.remoteconfig.RemoteConfigConstants.RequestFieldKey.FIRST_OPEN_TIME;
2425
import static com.google.firebase.remoteconfig.RemoteConfigConstants.RequestFieldKey.INSTANCE_ID;
2526
import static com.google.firebase.remoteconfig.RemoteConfigConstants.RequestFieldKey.INSTANCE_ID_TOKEN;
2627
import static com.google.firebase.remoteconfig.RemoteConfigConstants.RequestFieldKey.LANGUAGE_CODE;
@@ -58,6 +59,7 @@
5859
import java.net.HttpURLConnection;
5960
import java.net.URL;
6061
import java.net.URLConnection;
62+
import java.text.SimpleDateFormat;
6163
import java.util.Date;
6264
import java.util.HashMap;
6365
import java.util.Locale;
@@ -92,6 +94,9 @@ public class ConfigFetchHttpClient {
9294
private final long connectTimeoutInSeconds;
9395
private final long readTimeoutInSeconds;
9496

97+
/** ISO-8601 UTC timestamp format. */
98+
private static final String ISO_DATE_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
99+
95100
/** Creates a client for {@link #fetch}ing data from the Firebase Remote Config server. */
96101
public ConfigFetchHttpClient(
97102
Context context,
@@ -163,6 +168,7 @@ HttpURLConnection createHttpURLConnection() throws FirebaseRemoteConfigException
163168
* server uses this ETag to determine if there has been a change in the response body since
164169
* the last fetch.
165170
* @param customHeaders custom HTTP headers that will be sent to the FRC server.
171+
* @param firstOpenTime first time the app was opened. This value comes from Google Analytics.
166172
* @param currentTime the current time on the device that is performing the fetch.
167173
*/
168174
// TODO(issues/263): Set custom headers in ConfigFetchHttpClient's constructor.
@@ -174,6 +180,7 @@ FetchResponse fetch(
174180
Map<String, String> analyticsUserProperties,
175181
String lastFetchETag,
176182
Map<String, String> customHeaders,
183+
Long firstOpenTime,
177184
Date currentTime)
178185
throws FirebaseRemoteConfigException {
179186
setUpUrlConnection(urlConnection, lastFetchETag, installationAuthToken, customHeaders);
@@ -182,7 +189,8 @@ FetchResponse fetch(
182189
JSONObject fetchResponse;
183190
try {
184191
byte[] requestBody =
185-
createFetchRequestBody(installationId, installationAuthToken, analyticsUserProperties)
192+
createFetchRequestBody(
193+
installationId, installationAuthToken, analyticsUserProperties, firstOpenTime)
186194
.toString()
187195
.getBytes("utf-8");
188196
setFetchRequestBody(urlConnection, requestBody);
@@ -292,7 +300,8 @@ private String getFingerprintHashForPackage() {
292300
private JSONObject createFetchRequestBody(
293301
String installationId,
294302
String installationAuthToken,
295-
Map<String, String> analyticsUserProperties)
303+
Map<String, String> analyticsUserProperties,
304+
Long firstOpenTime)
296305
throws FirebaseRemoteConfigClientException {
297306
Map<String, Object> requestBodyMap = new HashMap<>();
298307

@@ -315,7 +324,7 @@ private JSONObject createFetchRequestBody(
315324
: locale.toString();
316325
requestBodyMap.put(LANGUAGE_CODE, languageCode);
317326

318-
requestBodyMap.put(PLATFORM_VERSION, Integer.toString(android.os.Build.VERSION.SDK_INT));
327+
requestBodyMap.put(PLATFORM_VERSION, Integer.toString(Build.VERSION.SDK_INT));
319328

320329
requestBodyMap.put(TIME_ZONE, TimeZone.getDefault().getID());
321330

@@ -336,9 +345,19 @@ private JSONObject createFetchRequestBody(
336345

337346
requestBodyMap.put(ANALYTICS_USER_PROPERTIES, new JSONObject(analyticsUserProperties));
338347

348+
if (firstOpenTime != null) {
349+
requestBodyMap.put(FIRST_OPEN_TIME, convertToISOString(firstOpenTime));
350+
}
351+
339352
return new JSONObject(requestBodyMap);
340353
}
341354

355+
private String convertToISOString(long millisFromEpoch) {
356+
SimpleDateFormat isoDateFormat = new SimpleDateFormat(ISO_DATE_PATTERN);
357+
isoDateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
358+
return isoDateFormat.format(millisFromEpoch);
359+
}
360+
342361
private void setFetchRequestBody(HttpURLConnection urlConnection, byte[] requestBody)
343362
throws IOException {
344363
urlConnection.setFixedLengthStreamingMode(requestBody.length);

firebase-config/src/test/java/com/google/firebase/remoteconfig/internal/ConfigFetchHandlerTest.java

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import static com.google.firebase.remoteconfig.RemoteConfigConstants.ResponseFieldKey.STATE;
2727
import static com.google.firebase.remoteconfig.internal.ConfigFetchHandler.BACKOFF_TIME_DURATIONS_IN_MINUTES;
2828
import static com.google.firebase.remoteconfig.internal.ConfigFetchHandler.DEFAULT_MINIMUM_FETCH_INTERVAL_IN_SECONDS;
29+
import static com.google.firebase.remoteconfig.internal.ConfigFetchHandler.FIRST_OPEN_TIME_KEY;
2930
import static com.google.firebase.remoteconfig.internal.ConfigFetchHandler.HTTP_TOO_MANY_REQUESTS;
3031
import static com.google.firebase.remoteconfig.internal.ConfigMetadataClient.LAST_FETCH_TIME_NO_FETCH_YET;
3132
import static com.google.firebase.remoteconfig.internal.ConfigMetadataClient.NO_BACKOFF_TIME;
@@ -42,6 +43,7 @@
4243
import static java.util.concurrent.TimeUnit.MINUTES;
4344
import static java.util.concurrent.TimeUnit.SECONDS;
4445
import static org.mockito.ArgumentMatchers.any;
46+
import static org.mockito.ArgumentMatchers.anyBoolean;
4547
import static org.mockito.ArgumentMatchers.eq;
4648
import static org.mockito.Mockito.doReturn;
4749
import static org.mockito.Mockito.doThrow;
@@ -195,6 +197,7 @@ public void fetch_firstFetch_includesInstallationAuthToken() throws Exception {
195197
/* analyticsUserProperties= */ any(),
196198
/* lastFetchETag= */ any(),
197199
/* customHeaders= */ any(),
200+
/* firstOpenTime= */ any(),
198201
/* currentTime= */ any());
199202
}
200203

@@ -394,7 +397,7 @@ public void fetch_gettingFetchCacheFails_doesNotThrowException() throws Exceptio
394397

395398
@Test
396399
public void fetch_fetchBackendCallFails_taskThrowsException() throws Exception {
397-
when(mockBackendFetchApiClient.fetch(any(), any(), any(), any(), any(), any(), any()))
400+
when(mockBackendFetchApiClient.fetch(any(), any(), any(), any(), any(), any(), any(), any()))
398401
.thenThrow(
399402
new FirebaseRemoteConfigClientException("Fetch failed due to an unexpected error."));
400403

@@ -628,21 +631,45 @@ public void fetch_serverReturnsUnexpectedCode_throwsServerException() throws Exc
628631
}
629632

630633
@Test
631-
public void fetch_hasAnalyticsSdk_sendsUserProperties() throws Exception {
634+
public void fetch_hasAnalyticsSdk_sendsUserPropertiesAndFirstOpenTime() throws Exception {
632635
// Provide the mock Analytics SDK.
633636
AnalyticsConnector mockAnalyticsConnector = mock(AnalyticsConnector.class);
634637
fetchHandler = getNewFetchHandler(mockAnalyticsConnector);
638+
long firstOpenTime = 1636146000000L;
635639

636-
Map<String, String> userProperties =
640+
Map<String, String> customUserProperties =
637641
ImmutableMap.of("up_key1", "up_val1", "up_key2", "up_val2");
642+
Map<String, Object> allUserProperties =
643+
ImmutableMap.of(
644+
"up_key1", "up_val1", "up_key2", "up_val2", FIRST_OPEN_TIME_KEY, firstOpenTime);
638645
when(mockAnalyticsConnector.getUserProperties(/*includeInternal=*/ false))
646+
.thenReturn(ImmutableMap.copyOf(customUserProperties));
647+
when(mockAnalyticsConnector.getUserProperties(/*includeInternal=*/ true))
648+
.thenReturn(ImmutableMap.copyOf(allUserProperties));
649+
650+
fetchCallToHttpClientUpdatesClockAndReturnsConfig(firstFetchedContainer);
651+
652+
assertWithMessage("Fetch() failed!").that(fetchHandler.fetch().isSuccessful()).isTrue();
653+
654+
verifyBackendIsCalled(customUserProperties, firstOpenTime);
655+
}
656+
657+
@Test
658+
public void fetch_hasAnalyticsSdk_sendsUserPropertiesNoFirstOpenTime() throws Exception {
659+
// Provide the mock Analytics SDK.
660+
AnalyticsConnector mockAnalyticsConnector = mock(AnalyticsConnector.class);
661+
fetchHandler = getNewFetchHandler(mockAnalyticsConnector);
662+
663+
Map<String, String> userProperties =
664+
ImmutableMap.of("up_key1", "up_val1", "up_key2", "up_val2");
665+
when(mockAnalyticsConnector.getUserProperties(anyBoolean()))
639666
.thenReturn(ImmutableMap.copyOf(userProperties));
640667

641668
fetchCallToHttpClientUpdatesClockAndReturnsConfig(firstFetchedContainer);
642669

643670
assertWithMessage("Fetch() failed!").that(fetchHandler.fetch().isSuccessful()).isTrue();
644671

645-
verifyBackendIsCalled(userProperties);
672+
verifyBackendIsCalled(userProperties, null);
646673
}
647674

648675
@Test
@@ -751,6 +778,7 @@ private void setBackendResponseConfigsTo(ConfigContainer container) throws Excep
751778
/* analyticsUserProperties= */ any(),
752779
/* lastFetchETag= */ any(),
753780
/* customHeaders= */ any(),
781+
/* firstOpenTime= */ any(),
754782
/* currentTime= */ any());
755783
}
756784

@@ -762,6 +790,7 @@ private void setBackendResponseToNoChange(Date date) throws Exception {
762790
/* analyticsUserProperties= */ any(),
763791
/* lastFetchETag= */ any(),
764792
/* customHeaders= */ any(),
793+
/* firstOpenTime= */ any(),
765794
/* currentTime= */ any()))
766795
.thenReturn(FetchResponse.forBackendHasNoUpdates(date));
767796
}
@@ -776,6 +805,7 @@ private void fetchCallToBackendThrowsException(int httpErrorCode) throws Excepti
776805
/* analyticsUserProperties= */ any(),
777806
/* lastFetchETag= */ any(),
778807
/* customHeaders= */ any(),
808+
/* firstOpenTime= */ any(),
779809
/* currentTime= */ any());
780810
}
781811

@@ -855,10 +885,12 @@ private void verifyBackendIsCalled() throws Exception {
855885
/* analyticsUserProperties= */ any(),
856886
/* lastFetchETag= */ any(),
857887
/* customHeaders= */ any(),
888+
/* firstOpenTime= */ any(),
858889
/* currentTime= */ any());
859890
}
860891

861-
private void verifyBackendIsCalled(Map<String, String> userProperties) throws Exception {
892+
private void verifyBackendIsCalled(Map<String, String> userProperties, Long firstOpenTime)
893+
throws Exception {
862894
verify(mockBackendFetchApiClient)
863895
.fetch(
864896
any(HttpURLConnection.class),
@@ -867,6 +899,7 @@ private void verifyBackendIsCalled(Map<String, String> userProperties) throws Ex
867899
/* analyticsUserProperties= */ eq(userProperties),
868900
/* lastFetchETag= */ any(),
869901
/* customHeaders= */ any(),
902+
/* firstOpenTime= */ eq(firstOpenTime),
870903
/* currentTime= */ any());
871904
}
872905

@@ -879,6 +912,7 @@ private void verifyBackendIsNeverCalled() throws Exception {
879912
/* analyticsUserProperties= */ any(),
880913
/* lastFetchETag= */ any(),
881914
/* customHeaders= */ any(),
915+
/* firstOpenTime= */ any(),
882916
/* currentTime= */ any());
883917
}
884918

@@ -891,6 +925,7 @@ private void verifyETags(@Nullable String requestETag, String responseETag) thro
891925
/* analyticsUserProperties= */ any(),
892926
/* lastFetchETag= */ eq(requestETag),
893927
/* customHeaders= */ any(),
928+
/* firstOpenTime= */ any(),
894929
/* currentTime= */ any());
895930
assertThat(metadataClient.getLastFetchETag()).isEqualTo(responseETag);
896931
}

0 commit comments

Comments
 (0)