Skip to content

Commit a2c275c

Browse files
authored
Standardize support for Firebase products that integrate with Remote Config. (#2222)
1 parent 2c088d7 commit a2c275c

File tree

4 files changed

+123
-47
lines changed

4 files changed

+123
-47
lines changed

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

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,36 @@
1717
import android.os.Bundle;
1818
import androidx.annotation.NonNull;
1919
import com.google.firebase.analytics.connector.AnalyticsConnector;
20+
import java.util.Collections;
21+
import java.util.HashMap;
22+
import java.util.Map;
2023
import org.json.JSONObject;
2124

2225
public class Personalization {
2326
public static final String ANALYTICS_ORIGIN_PERSONALIZATION = "fp";
24-
public static final String ANALYTICS_PULL_EVENT = "_fpc";
25-
public static final String ARM_KEY = "_fpid";
26-
public static final String ARM_VALUE = "_fpct";
27-
static final String PERSONALIZATION_ID = "personalizationId";
27+
28+
// The PARAM suffix identifies log keys sent to Google Analytics.
29+
public static final String EXTERNAL_EVENT = "personalization_assignment";
30+
public static final String EXTERNAL_RC_PARAMETER_PARAM = "arm_key";
31+
public static final String EXTERNAL_ARM_VALUE_PARAM = "arm_value";
32+
public static final String PERSONALIZATION_ID = "personalizationId";
33+
public static final String EXTERNAL_PERSONALIZATION_ID_PARAM = "personalization_id";
34+
public static final String ARM_INDEX = "armIndex";
35+
public static final String EXTERNAL_ARM_INDEX_PARAM = "arm_index";
36+
public static final String GROUP = "group";
37+
public static final String EXTERNAL_GROUP_PARAM = "group";
38+
39+
public static final String INTERNAL_EVENT = "_fpc";
40+
public static final String CHOICE_ID = "choiceId";
41+
public static final String INTERNAL_CHOICE_ID_PARAM = "_fpid";
2842

2943
/** The app's Firebase Analytics client. */
3044
private final AnalyticsConnector analyticsConnector;
3145

46+
/** Remote Config parameter key and choice ID pairs that have already been logged to Analytics. */
47+
private final Map<String, String> loggedChoiceIds =
48+
Collections.synchronizedMap(new HashMap<String, String>());
49+
3250
/** Creates an instance of {@code Personalization}. */
3351
public Personalization(@NonNull AnalyticsConnector analyticsConnector) {
3452
this.analyticsConnector = analyticsConnector;
@@ -38,11 +56,11 @@ public Personalization(@NonNull AnalyticsConnector analyticsConnector) {
3856
* Called when a Personalization parameter value (an arm) is retrieved, and uses Google Analytics
3957
* for Firebase to log metadata if it's a Personalization parameter.
4058
*
41-
* @param key Remote Config parameter
59+
* @param rcParameter Remote Config parameter
4260
* @param configContainer {@link ConfigContainer} containing Personalization metadata for {@code
4361
* key}
4462
*/
45-
public void logArmActive(@NonNull String key, @NonNull ConfigContainer configContainer) {
63+
public void logArmActive(@NonNull String rcParameter, @NonNull ConfigContainer configContainer) {
4664
JSONObject ids = configContainer.getPersonalizationMetadata();
4765
if (ids.length() < 1) {
4866
return;
@@ -53,14 +71,34 @@ public void logArmActive(@NonNull String key, @NonNull ConfigContainer configCon
5371
return;
5472
}
5573

56-
JSONObject metadata = ids.optJSONObject(key);
74+
JSONObject metadata = ids.optJSONObject(rcParameter);
5775
if (metadata == null) {
5876
return;
5977
}
6078

61-
Bundle params = new Bundle();
62-
params.putString(ARM_KEY, metadata.optString(PERSONALIZATION_ID));
63-
params.putString(ARM_VALUE, values.optString(key));
64-
analyticsConnector.logEvent(ANALYTICS_ORIGIN_PERSONALIZATION, ANALYTICS_PULL_EVENT, params);
79+
String choiceId = metadata.optString(CHOICE_ID);
80+
if (choiceId.isEmpty()) {
81+
return;
82+
}
83+
84+
synchronized (loggedChoiceIds) {
85+
if (choiceId.equals(loggedChoiceIds.get(rcParameter))) {
86+
return;
87+
}
88+
loggedChoiceIds.put(rcParameter, choiceId);
89+
}
90+
91+
Bundle logParams = new Bundle();
92+
logParams.putString(EXTERNAL_RC_PARAMETER_PARAM, rcParameter);
93+
logParams.putString(EXTERNAL_ARM_VALUE_PARAM, values.optString(rcParameter));
94+
logParams.putString(EXTERNAL_PERSONALIZATION_ID_PARAM, metadata.optString(PERSONALIZATION_ID));
95+
logParams.putInt(EXTERNAL_ARM_INDEX_PARAM, metadata.optInt(ARM_INDEX, -1));
96+
logParams.putString(EXTERNAL_GROUP_PARAM, metadata.optString(GROUP));
97+
analyticsConnector.logEvent(ANALYTICS_ORIGIN_PERSONALIZATION, EXTERNAL_EVENT, logParams);
98+
99+
Bundle internalLogParams = new Bundle();
100+
internalLogParams.putString(INTERNAL_CHOICE_ID_PARAM, choiceId);
101+
analyticsConnector.logEvent(
102+
ANALYTICS_ORIGIN_PERSONALIZATION, INTERNAL_EVENT, internalLogParams);
65103
}
66104
}

firebase-config/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -958,7 +958,7 @@ public void personalization_hasMetadata_successful() throws Exception {
958958
.when(mockAnalyticsConnector)
959959
.logEvent(
960960
eq(Personalization.ANALYTICS_ORIGIN_PERSONALIZATION),
961-
eq(Personalization.ANALYTICS_PULL_EVENT),
961+
eq(Personalization.EXTERNAL_EVENT),
962962
any(Bundle.class));
963963

964964
ConfigContainer configContainer =
@@ -986,18 +986,18 @@ public void personalization_hasMetadata_successful() throws Exception {
986986
verify(mockAnalyticsConnector, times(2))
987987
.logEvent(
988988
eq(Personalization.ANALYTICS_ORIGIN_PERSONALIZATION),
989-
eq(Personalization.ANALYTICS_PULL_EVENT),
989+
eq(Personalization.EXTERNAL_EVENT),
990990
any(Bundle.class));
991991
assertThat(fakeLogs).hasSize(2);
992992

993993
Bundle params1 = new Bundle();
994-
params1.putString(Personalization.ARM_KEY, "id1");
995-
params1.putString(Personalization.ARM_VALUE, "value1");
994+
params1.putString(Personalization.EXTERNAL_RC_PARAMETER_PARAM, "id1");
995+
params1.putString(Personalization.EXTERNAL_ARM_VALUE_PARAM, "value1");
996996
assertThat(fakeLogs.get(0).toString()).isEqualTo(params1.toString());
997997

998998
Bundle params2 = new Bundle();
999-
params2.putString(Personalization.ARM_KEY, "id2");
1000-
params2.putString(Personalization.ARM_VALUE, "value2");
999+
params2.putString(Personalization.EXTERNAL_RC_PARAMETER_PARAM, "id2");
1000+
params2.putString(Personalization.EXTERNAL_ARM_VALUE_PARAM, "value2");
10011001
assertThat(fakeLogs.get(1).toString()).isEqualTo(params2.toString());
10021002
});
10031003
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,9 @@ public void read_validContainerWithPersonalization_returnsContainer() throws Exc
9090
ConfigContainer configWithPersonalization =
9191
ConfigContainer.newBuilder(configContainer)
9292
.withPersonalizationMetadata(
93-
new JSONObject(ImmutableMap.of(Personalization.ARM_KEY, "arm_value")))
93+
new JSONObject(
94+
"{long_param: {personalizationId: 'id1'}, "
95+
+ "string_param: {personalizationId: 'id2'}}"))
9496
.build();
9597
storageClient.write(configWithPersonalization);
9698
Preconditions.checkArgument(getFileAsString().equals(configWithPersonalization.toString()));

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

Lines changed: 65 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,25 @@
1616

1717
import static com.google.common.truth.Truth.assertThat;
1818
import static com.google.firebase.remoteconfig.internal.Personalization.ANALYTICS_ORIGIN_PERSONALIZATION;
19-
import static com.google.firebase.remoteconfig.internal.Personalization.ANALYTICS_PULL_EVENT;
20-
import static com.google.firebase.remoteconfig.internal.Personalization.ARM_KEY;
21-
import static com.google.firebase.remoteconfig.internal.Personalization.ARM_VALUE;
19+
import static com.google.firebase.remoteconfig.internal.Personalization.EXTERNAL_ARM_INDEX_PARAM;
20+
import static com.google.firebase.remoteconfig.internal.Personalization.EXTERNAL_ARM_VALUE_PARAM;
21+
import static com.google.firebase.remoteconfig.internal.Personalization.EXTERNAL_EVENT;
22+
import static com.google.firebase.remoteconfig.internal.Personalization.EXTERNAL_GROUP_PARAM;
23+
import static com.google.firebase.remoteconfig.internal.Personalization.EXTERNAL_PERSONALIZATION_ID_PARAM;
24+
import static com.google.firebase.remoteconfig.internal.Personalization.EXTERNAL_RC_PARAMETER_PARAM;
25+
import static com.google.firebase.remoteconfig.internal.Personalization.INTERNAL_CHOICE_ID_PARAM;
26+
import static com.google.firebase.remoteconfig.internal.Personalization.INTERNAL_EVENT;
27+
import static org.mockito.AdditionalMatchers.or;
2228
import static org.mockito.ArgumentMatchers.any;
29+
import static org.mockito.ArgumentMatchers.anyString;
2330
import static org.mockito.ArgumentMatchers.eq;
2431
import static org.mockito.Mockito.doAnswer;
2532
import static org.mockito.Mockito.times;
2633
import static org.mockito.Mockito.verify;
2734
import static org.mockito.MockitoAnnotations.initMocks;
2835

2936
import android.os.Bundle;
37+
import com.google.common.truth.Correspondence;
3038
import com.google.firebase.analytics.connector.AnalyticsConnector;
3139
import java.util.ArrayList;
3240
import java.util.Date;
@@ -45,6 +53,10 @@
4553
@Config(manifest = Config.NONE)
4654
public class PersonalizationTest {
4755
private static final ConfigContainer CONFIG_CONTAINER;
56+
private static final Bundle LOG_PARAMS_1 = new Bundle();
57+
private static final Bundle LOG_PARAMS_2 = new Bundle();
58+
private static final Bundle INTERNAL_LOG_PARAMS_1 = new Bundle();
59+
private static final Bundle INTERNAL_LOG_PARAMS_2 = new Bundle();
4860

4961
static {
5062
try {
@@ -55,15 +67,37 @@ public class PersonalizationTest {
5567
.withFetchTime(new Date(1))
5668
.withPersonalizationMetadata(
5769
new JSONObject(
58-
"{key1: {personalizationId: 'id1'}, key2: {personalizationId: 'id2'}}"))
70+
"{key1: {personalizationId: 'p13n1', armIndex: 0,"
71+
+ " choiceId: 'id1', group: 'BASELINE'},"
72+
+ " key2: {personalizationId: 'p13n2', armIndex: 1,"
73+
+ " choiceId: 'id2', group: 'P13N'}}"))
5974
.build();
6075
} catch (JSONException e) {
6176
throw new RuntimeException(e);
6277
}
78+
79+
LOG_PARAMS_1.putString(EXTERNAL_RC_PARAMETER_PARAM, "key1");
80+
LOG_PARAMS_1.putString(EXTERNAL_ARM_VALUE_PARAM, "value1");
81+
LOG_PARAMS_1.putString(EXTERNAL_PERSONALIZATION_ID_PARAM, "p13n1");
82+
LOG_PARAMS_1.putInt(EXTERNAL_ARM_INDEX_PARAM, 0);
83+
LOG_PARAMS_1.putString(EXTERNAL_GROUP_PARAM, "BASELINE");
84+
85+
LOG_PARAMS_2.putString(EXTERNAL_RC_PARAMETER_PARAM, "key2");
86+
LOG_PARAMS_2.putString(EXTERNAL_ARM_VALUE_PARAM, "value2");
87+
LOG_PARAMS_2.putString(EXTERNAL_PERSONALIZATION_ID_PARAM, "p13n2");
88+
LOG_PARAMS_2.putInt(EXTERNAL_ARM_INDEX_PARAM, 1);
89+
LOG_PARAMS_2.putString(EXTERNAL_GROUP_PARAM, "P13N");
90+
91+
INTERNAL_LOG_PARAMS_1.putString(INTERNAL_CHOICE_ID_PARAM, "id1");
92+
93+
INTERNAL_LOG_PARAMS_2.putString(INTERNAL_CHOICE_ID_PARAM, "id2");
6394
}
6495

6596
private static final List<Bundle> FAKE_LOGS = new ArrayList<>();
6697

98+
private static final Correspondence<Bundle, String> TO_STRING =
99+
Correspondence.transforming(Bundle::toString, "as String is");
100+
67101
private Personalization personalization;
68102

69103
@Mock private AnalyticsConnector mockAnalyticsConnector;
@@ -74,8 +108,7 @@ public void setUp() {
74108

75109
doAnswer(invocation -> FAKE_LOGS.add(invocation.getArgument(2)))
76110
.when(mockAnalyticsConnector)
77-
.logEvent(
78-
eq(ANALYTICS_ORIGIN_PERSONALIZATION), eq(ANALYTICS_PULL_EVENT), any(Bundle.class));
111+
.logEvent(eq(ANALYTICS_ORIGIN_PERSONALIZATION), anyString(), any(Bundle.class));
79112

80113
personalization = new Personalization(mockAnalyticsConnector);
81114

@@ -88,43 +121,46 @@ public void logArmActive_nonPersonalizationKey_notLogged() {
88121

89122
verify(mockAnalyticsConnector, times(0))
90123
.logEvent(
91-
eq(ANALYTICS_ORIGIN_PERSONALIZATION), eq(ANALYTICS_PULL_EVENT), any(Bundle.class));
124+
eq(ANALYTICS_ORIGIN_PERSONALIZATION),
125+
or(eq(EXTERNAL_EVENT), eq(INTERNAL_EVENT)),
126+
any(Bundle.class));
92127
assertThat(FAKE_LOGS).isEmpty();
93128
}
94129

95130
@Test
96-
public void logArmActive_singlePersonalizationKey_loggedOnce() {
131+
public void logArmActive_singlePersonalizationKey_loggedInternallyAndExternally() {
97132
personalization.logArmActive("key1", CONFIG_CONTAINER);
98133

99134
verify(mockAnalyticsConnector, times(1))
100-
.logEvent(
101-
eq(ANALYTICS_ORIGIN_PERSONALIZATION), eq(ANALYTICS_PULL_EVENT), any(Bundle.class));
102-
assertThat(FAKE_LOGS).hasSize(1);
135+
.logEvent(eq(ANALYTICS_ORIGIN_PERSONALIZATION), eq(EXTERNAL_EVENT), any(Bundle.class));
136+
verify(mockAnalyticsConnector, times(1))
137+
.logEvent(eq(ANALYTICS_ORIGIN_PERSONALIZATION), eq(INTERNAL_EVENT), any(Bundle.class));
138+
assertThat(FAKE_LOGS).hasSize(2);
103139

104-
Bundle params = new Bundle();
105-
params.putString(ARM_KEY, "id1");
106-
params.putString(ARM_VALUE, "value1");
107-
assertThat(FAKE_LOGS.get(0).toString()).isEqualTo(params.toString());
140+
assertThat(FAKE_LOGS)
141+
.comparingElementsUsing(TO_STRING)
142+
.containsExactly(LOG_PARAMS_1.toString(), INTERNAL_LOG_PARAMS_1.toString());
108143
}
109144

110145
@Test
111-
public void logArmActive_multiplePersonalizationKeys_loggedMultiple() {
146+
public void logArmActive_multiplePersonalizationKeys_loggedInternallyAndExternally() {
112147
personalization.logArmActive("key1", CONFIG_CONTAINER);
113148
personalization.logArmActive("key2", CONFIG_CONTAINER);
149+
personalization.logArmActive("key1", CONFIG_CONTAINER);
114150

115151
verify(mockAnalyticsConnector, times(2))
116-
.logEvent(
117-
eq(ANALYTICS_ORIGIN_PERSONALIZATION), eq(ANALYTICS_PULL_EVENT), any(Bundle.class));
118-
assertThat(FAKE_LOGS).hasSize(2);
119-
120-
Bundle params1 = new Bundle();
121-
params1.putString(ARM_KEY, "id1");
122-
params1.putString(ARM_VALUE, "value1");
123-
assertThat(FAKE_LOGS.get(0).toString()).isEqualTo(params1.toString());
124-
125-
Bundle params2 = new Bundle();
126-
params2.putString(ARM_KEY, "id2");
127-
params2.putString(ARM_VALUE, "value2");
128-
assertThat(FAKE_LOGS.get(1).toString()).isEqualTo(params2.toString());
152+
.logEvent(eq(ANALYTICS_ORIGIN_PERSONALIZATION), eq(EXTERNAL_EVENT), any(Bundle.class));
153+
verify(mockAnalyticsConnector, times(2))
154+
.logEvent(eq(ANALYTICS_ORIGIN_PERSONALIZATION), eq(INTERNAL_EVENT), any(Bundle.class));
155+
assertThat(FAKE_LOGS).hasSize(4);
156+
157+
assertThat(FAKE_LOGS)
158+
.comparingElementsUsing(TO_STRING)
159+
.containsAtLeast(LOG_PARAMS_1.toString(), LOG_PARAMS_2.toString())
160+
.inOrder();
161+
assertThat(FAKE_LOGS)
162+
.comparingElementsUsing(TO_STRING)
163+
.containsAtLeast(INTERNAL_LOG_PARAMS_1.toString(), INTERNAL_LOG_PARAMS_2.toString())
164+
.inOrder();
129165
}
130166
}

0 commit comments

Comments
 (0)