Skip to content

Commit 21ef253

Browse files
authored
Merge pull request #2 from AdevintaSpain/traits_cache
[IntegrationOptions] Trait diffing
2 parents e18376a + 39ef25b commit 21ef253

File tree

9 files changed

+306
-17
lines changed

9 files changed

+306
-17
lines changed

build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ dependencies {
7373

7474
androidTestImplementation 'com.android.support.test:runner:1.0.2'
7575
androidTestImplementation 'com.android.support.test:rules:1.0.2'
76+
androidTestImplementation 'com.linkedin.dexmaker:dexmaker-mockito:2.25.0'
77+
7678
androidTestImplementation 'junit:junit:4.12'
7779
}
7880

src/androidTest/java/com/segment/analytics/android/integrations/appboy/AppboyAndroidTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ public void testIdentifyCallsChangeUser() {
3333
Traits traits = createTraits(testUserId);
3434
IdentifyPayload identifyPayload = new IdentifyPayloadBuilder().traits(traits).build();
3535
Logger logger = Logger.with(Analytics.LogLevel.DEBUG);
36-
AppboyIntegration integration = new AppboyIntegration(Appboy.getInstance(getContext()), "foo", logger, true, new DefaultUserIdMapper());
36+
AppboyIntegration integration = new AppboyIntegration(Appboy.getInstance(getContext()), "foo", logger, true,
37+
new PreferencesTraitsCache(getContext()), new DefaultUserIdMapper());
3738
integration.identify(identifyPayload);
3839

3940
assertEquals(testUserId, Appboy.getInstance(getContext()).getCurrentUser().getUserId());

src/androidTest/java/com/segment/analytics/android/integrations/appboy/AppboyIntegrationOptionsAndroidTest.java

Lines changed: 131 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,50 +2,172 @@
22

33
import android.support.test.runner.AndroidJUnit4;
44
import com.appboy.Appboy;
5+
import com.appboy.AppboyUser;
56
import com.appboy.configuration.AppboyConfig;
67
import com.segment.analytics.Analytics;
78
import com.segment.analytics.Traits;
89
import com.segment.analytics.integrations.IdentifyPayload;
910
import com.segment.analytics.integrations.Logger;
1011
import com.segment.analytics.test.IdentifyPayloadBuilder;
12+
import org.junit.After;
13+
import org.junit.Before;
1114
import org.junit.BeforeClass;
1215
import org.junit.Test;
1316
import org.junit.runner.RunWith;
17+
import org.mockito.InOrder;
18+
import org.mockito.Mock;
19+
import org.mockito.Mockito;
20+
import org.mockito.MockitoAnnotations;
1421

1522
import static android.support.test.InstrumentationRegistry.getContext;
1623
import static com.segment.analytics.Utils.createTraits;
17-
import static org.junit.Assert.assertEquals;
24+
import static org.mockito.Mockito.times;
25+
import static org.mockito.Mockito.verify;
26+
import static org.mockito.Mockito.when;
1827

1928
@RunWith(AndroidJUnit4.class)
2029
public class AppboyIntegrationOptionsAndroidTest {
2130

22-
private static final String ORIGINAL_USER_ID = "testUser";
31+
private static final String USER_ID = "testUser";
32+
private static final String OTHER_USER_ID = "testUser2";
2333
private static final String TRANSFORMED_USER_ID = "transformedUser";
34+
private static final String TRAIT_EMAIL = "test@segment.com";
35+
private static final String TRAIT_EMAIL_UPDATED = "updated@segment.com";
36+
private static final String TRAIT_CITY = "city";
37+
private static final String TRAIT_COUNTRY = "country";
38+
private static final String CUSTOM_TRAIT_KEY = "custom_trait_key";
39+
private static final String CUSTOM_KEY_VALUE = "custom_key_value";
40+
41+
@Mock Appboy appboy;
42+
@Mock AppboyUser appboyUser;
43+
44+
private AppboyIntegration appboyIntegration;
45+
private PreferencesTraitsCache traitsCache;
2446

2547
@BeforeClass
2648
public static void beforeClass() {
2749
AppboyConfig appboyConfig = new AppboyConfig.Builder().setApiKey("testkey").build();
2850
Appboy.configure(getContext(), appboyConfig);
2951
}
3052

53+
@Before
54+
public void setUp() throws Exception {
55+
MockitoAnnotations.initMocks(this);
56+
57+
when(appboy.getCurrentUser()).thenReturn(appboyUser);
58+
59+
Logger logger = Logger.with(Analytics.LogLevel.DEBUG);
60+
61+
traitsCache = new PreferencesTraitsCache(getContext());
62+
63+
appboyIntegration = new AppboyIntegration(
64+
appboy, "foo", logger, true,
65+
traitsCache, new ReplaceUserIdMapper());
66+
}
67+
68+
@After
69+
public void tearDown() throws Exception {
70+
traitsCache.clear();
71+
}
72+
3173
@Test
3274
public void testUserIdMapperTransformsAppboyUserId() {
33-
Traits traits = createTraits(ORIGINAL_USER_ID);
34-
IdentifyPayload identifyPayload = new IdentifyPayloadBuilder().traits(traits).build();
75+
Traits traits = createTraits(USER_ID);
3576

36-
Logger logger = Logger.with(Analytics.LogLevel.DEBUG);
77+
callIdentifyWithTraits(traits);
78+
79+
verify(appboy).changeUser(TRANSFORMED_USER_ID);
80+
}
81+
82+
@Test
83+
public void testShouldNotTriggerAppboyUpdateIfTraitDoesntChange() {
84+
Traits traits = createTraits(USER_ID);
85+
86+
Traits.Address address = new Traits.Address();
87+
address.putCity(TRAIT_CITY);
88+
address.putCountry(TRAIT_COUNTRY);
3789

38-
AppboyIntegration integration = new AppboyIntegration(Appboy.getInstance(getContext()), "foo", logger, true, new ReplaceUserIdMapper());
90+
traits.putAddress(address);
91+
traits.putEmail(TRAIT_EMAIL);
92+
traits.put(CUSTOM_TRAIT_KEY, CUSTOM_KEY_VALUE);
93+
94+
callIdentifyWithTraits(traits);
95+
callIdentifyWithTraits(traits);
96+
callIdentifyWithTraits(traits);
97+
98+
verify(appboyUser, times(1)).setEmail(TRAIT_EMAIL);
99+
verify(appboyUser, times(1)).setHomeCity(TRAIT_CITY);
100+
verify(appboyUser, times(1)).setCountry(TRAIT_COUNTRY);
101+
verify(appboyUser, times(1)).setCustomUserAttribute(CUSTOM_TRAIT_KEY, CUSTOM_KEY_VALUE);
102+
}
103+
104+
@Test
105+
public void testShouldTriggerUpdateIfTraitChanges() {
106+
Traits traits = createTraits(USER_ID);
107+
traits.putEmail(TRAIT_EMAIL);
108+
callIdentifyWithTraits(traits);
109+
callIdentifyWithTraits(traits);
39110

40-
integration.identify(identifyPayload);
111+
Traits traitsUpdate = createTraits(USER_ID);
112+
traitsUpdate.putEmail(TRAIT_EMAIL_UPDATED);
113+
callIdentifyWithTraits(traitsUpdate);
114+
callIdentifyWithTraits(traitsUpdate);
115+
116+
InOrder inOrder = Mockito.inOrder(appboyUser);
117+
inOrder.verify(appboyUser, times(1)).setEmail(TRAIT_EMAIL);
118+
inOrder.verify(appboyUser, times(1)).setEmail(TRAIT_EMAIL_UPDATED);
119+
}
120+
121+
@Test
122+
public void testAvoidTriggeringRepeatedUserIdUpdates() {
123+
Traits traits = createTraits(USER_ID);
124+
traits.putEmail(TRAIT_EMAIL);
125+
126+
callIdentifyWithTraits(traits);
127+
callIdentifyWithTraits(traits);
128+
129+
verify(appboyUser, times(1)).setEmail(TRAIT_EMAIL);
130+
verify(appboy, times(1)).changeUser(TRANSFORMED_USER_ID);
131+
}
132+
133+
@Test
134+
public void clearCacheIfUserIdChanges() {
135+
Traits traits = createTraits(USER_ID);
136+
traits.putEmail(TRAIT_EMAIL);
137+
138+
Traits traitsUpdate = createTraits(OTHER_USER_ID);
139+
traitsUpdate.putEmail(TRAIT_EMAIL);
140+
141+
callIdentifyWithTraits(traits);
142+
callIdentifyWithTraits(traitsUpdate);
143+
144+
verify(appboyUser, times(2)).setEmail(TRAIT_EMAIL);
145+
}
146+
147+
@Test
148+
public void clearCacheOnReset() {
149+
Traits traits = createTraits(USER_ID);
150+
traits.putEmail(TRAIT_EMAIL);
151+
152+
callIdentifyWithTraits(traits);
153+
154+
appboyIntegration.reset();
155+
156+
callIdentifyWithTraits(traits);
157+
158+
verify(appboyUser, times(2)).setEmail(TRAIT_EMAIL);
159+
}
160+
161+
public void callIdentifyWithTraits(Traits traits) {
162+
IdentifyPayload identifyPayload = new IdentifyPayloadBuilder().traits(traits).build();
41163

42-
assertEquals(TRANSFORMED_USER_ID, Appboy.getInstance(getContext()).getCurrentUser().getUserId());
164+
appboyIntegration.identify(identifyPayload);
43165
}
44166

45167
class ReplaceUserIdMapper implements UserIdMapper {
46168
@Override
47169
public String transformUserId(String segmentUserId) {
48-
return segmentUserId.replaceFirst(ORIGINAL_USER_ID, TRANSFORMED_USER_ID);
170+
return segmentUserId.replaceFirst(USER_ID, TRANSFORMED_USER_ID);
49171
}
50172
}
51173
}

src/main/java/com/segment/analytics/android/integrations/appboy/AppboyIntegration.java

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import com.segment.analytics.integrations.Logger;
2121
import com.segment.analytics.integrations.TrackPayload;
2222

23+
import java.util.Map;
2324
import org.json.JSONObject;
2425

2526
import java.math.BigDecimal;
@@ -77,10 +78,15 @@ public Integration<?> create(ValueMap settings, Analytics analytics) {
7778
userIdMapper = new DefaultUserIdMapper();
7879
}
7980

81+
TraitsCache traitsCache = null;
82+
if (options.isTraitDiffingEnabled()) {
83+
traitsCache = new PreferencesTraitsCache(analytics.getApplication());
84+
}
85+
8086
Appboy.configure(analytics.getApplication().getApplicationContext(), builder.build());
8187
Appboy appboy = Appboy.getInstance(analytics.getApplication());
8288
logger.verbose("Configured Appboy+Segment integration and initialized Appboy.");
83-
return new AppboyIntegration(appboy, apiKey, logger, inAppMessageRegistrationEnabled, userIdMapper);
89+
return new AppboyIntegration(appboy, apiKey, logger, inAppMessageRegistrationEnabled, traitsCache, userIdMapper);
8490
}
8591

8692
@Override
@@ -95,15 +101,18 @@ public String key() {
95101
private final Logger mLogger;
96102
private final boolean mAutomaticInAppMessageRegistrationEnabled;
97103
private final UserIdMapper mUserIdMapper;
104+
private final TraitsCache mTraitsCache;
98105

99106
public AppboyIntegration(Appboy appboy, String token, Logger logger,
100107
boolean automaticInAppMessageRegistrationEnabled,
108+
TraitsCache traitsCache,
101109
UserIdMapper userIdMapper) {
102110
mAppboy = appboy;
103111
mToken = token;
104112
mLogger = logger;
105113
mAutomaticInAppMessageRegistrationEnabled = automaticInAppMessageRegistrationEnabled;
106114
mUserIdMapper = userIdMapper;
115+
mTraitsCache = traitsCache;
107116
}
108117

109118
public String getToken() {
@@ -121,14 +130,32 @@ public void identify(IdentifyPayload identify) {
121130

122131
String userId = identify.userId();
123132
if (!StringUtils.isNullOrBlank(userId)) {
124-
mAppboy.changeUser(mUserIdMapper.transformUserId(userId));
133+
134+
String cachedUserId = mTraitsCache.load().userId();
135+
136+
if (!userId.equals(cachedUserId)) {
137+
mAppboy.changeUser(mUserIdMapper.transformUserId(userId));
138+
139+
if (mTraitsCache != null) {
140+
mTraitsCache.clear();
141+
}
142+
}
125143
}
126144

127145
Traits traits = identify.traits();
146+
128147
if (traits == null) {
129148
return;
130149
}
131150

151+
if (mTraitsCache != null) {
152+
Traits lastEmittedTraits = mTraitsCache.load();
153+
154+
mTraitsCache.save(traits);
155+
156+
traits = diffTraits(traits, lastEmittedTraits);
157+
}
158+
132159
Date birthday = traits.birthday();
133160
if (birthday != null) {
134161
Calendar birthdayCal = Calendar.getInstance(Locale.US);
@@ -200,11 +227,27 @@ public void identify(IdentifyPayload identify) {
200227
mAppboy.getCurrentUser().setCustomUserAttribute(key, (String) value);
201228
} else {
202229
mLogger.info("Appboy can't map segment value for custom Appboy user "
203-
+ "attribute with key %s and value %s", key, value);
230+
+ "attribute with key %s and value %s", key, value);
204231
}
205232
}
206233
}
207234

235+
private Traits diffTraits(Traits traits, Traits lastEmittedTraits) {
236+
if (lastEmittedTraits == null) return traits;
237+
238+
Traits diffed = new Traits();
239+
240+
for (Map.Entry<String, Object> trait : traits.entrySet()) {
241+
Object storedValue = lastEmittedTraits.get(trait.getKey());
242+
243+
if (storedValue == null || !trait.getValue().equals(storedValue)) {
244+
diffed.put(trait.getKey(), trait.getValue());
245+
}
246+
}
247+
248+
return diffed;
249+
}
250+
208251
@Override
209252
public void flush() {
210253
super.flush();
@@ -268,6 +311,15 @@ public void track(TrackPayload track) {
268311
}
269312
}
270313

314+
@Override
315+
public void reset() {
316+
super.reset();
317+
318+
if (mTraitsCache != null) {
319+
mTraitsCache.clear();
320+
}
321+
}
322+
271323
@Override
272324
public void onActivityStarted(Activity activity) {
273325
super.onActivityStarted(activity);

src/main/java/com/segment/analytics/android/integrations/appboy/AppboyIntegrationOptions.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
public class AppboyIntegrationOptions {
44

55
private UserIdMapper userIdMapper;
6+
private boolean enableTraitDiffing;
67

78
public static Builder builder() {
89
return new Builder();
@@ -12,20 +13,32 @@ UserIdMapper getUserIdMapper() {
1213
return userIdMapper;
1314
}
1415

15-
private AppboyIntegrationOptions(UserIdMapper userIdMapper) {
16+
public boolean isTraitDiffingEnabled() {
17+
return enableTraitDiffing;
18+
}
19+
20+
private AppboyIntegrationOptions(UserIdMapper userIdMapper, boolean enableTraitDiffing) {
1621
this.userIdMapper = userIdMapper;
22+
23+
this.enableTraitDiffing = enableTraitDiffing;
1724
}
1825

1926
public static class Builder {
2027
private UserIdMapper userIdMapper;
28+
private boolean traitDiffingEnabled;
2129

2230
public Builder userIdMapper(UserIdMapper userIdMapper) {
2331
this.userIdMapper = userIdMapper;
2432
return this;
2533
}
2634

35+
public Builder enableTraitDiffing(boolean enable) {
36+
this.traitDiffingEnabled = enable;
37+
return this;
38+
}
39+
2740
public AppboyIntegrationOptions build() {
28-
return new AppboyIntegrationOptions(userIdMapper);
41+
return new AppboyIntegrationOptions(userIdMapper, traitDiffingEnabled);
2942
}
3043
}
3144
}

0 commit comments

Comments
 (0)