From 37226c116c5491e3fec2d27800e1ccc297da9de4 Mon Sep 17 00:00:00 2001 From: Prayansh Srivastava Date: Mon, 8 Jun 2020 11:23:45 -0700 Subject: [PATCH] Use AndroidX lifecycle functions for App Fore/Backgrounded (#667) * init changes * fix tests * run formatter * update build.gradle * add test for unregister lifecycle methods --- analytics/build.gradle | 3 + .../java/com/segment/analytics/Analytics.java | 16 +- .../AnalyticsActivityLifecycleCallbacks.java | 95 +++---- .../com/segment/analytics/AnalyticsTest.java | 239 +++++++++++++----- 4 files changed, 236 insertions(+), 117 deletions(-) diff --git a/analytics/build.gradle b/analytics/build.gradle index a5e8dd79e..92ae80728 100644 --- a/analytics/build.gradle +++ b/analytics/build.gradle @@ -6,6 +6,9 @@ apply from: rootProject.file('gradle/android.gradle') dependencies { api rootProject.ext.deps.supportAnnotations + implementation 'androidx.lifecycle:lifecycle-process:2.2.0' + implementation 'androidx.lifecycle:lifecycle-common-java8:2.2.0' + testImplementation 'junit:junit:4.12' testImplementation('org.robolectric:robolectric:3.5') { exclude group: 'commons-logging', module: 'commons-logging' diff --git a/analytics/src/main/java/com/segment/analytics/Analytics.java b/analytics/src/main/java/com/segment/analytics/Analytics.java index 4f1350cfe..ff81fae7b 100644 --- a/analytics/src/main/java/com/segment/analytics/Analytics.java +++ b/analytics/src/main/java/com/segment/analytics/Analytics.java @@ -48,6 +48,8 @@ import android.os.Message; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.ProcessLifecycleOwner; import com.segment.analytics.integrations.AliasPayload; import com.segment.analytics.integrations.BasePayload; import com.segment.analytics.integrations.GroupPayload; @@ -132,7 +134,8 @@ public void handleMessage(Message msg) { final Cartographer cartographer; private final ProjectSettings.Cache projectSettingsCache; final Crypto crypto; - @Private final Application.ActivityLifecycleCallbacks activityLifecycleCallback; + @Private final AnalyticsActivityLifecycleCallbacks activityLifecycleCallback; + @Private final Lifecycle lifecycle; ProjectSettings projectSettings; // todo: make final (non-final for testing). @Private final String writeKey; final int flushQueueSize; @@ -229,7 +232,8 @@ public static void setSingletonInstance(Analytics analytics) { Crypto crypto, @NonNull List sourceMiddleware, @NonNull Map> destinationMiddleware, - @NonNull final ValueMap defaultProjectSettings) { + @NonNull final ValueMap defaultProjectSettings, + @NonNull Lifecycle lifecycle) { this.application = application; this.networkExecutor = networkExecutor; this.stats = stats; @@ -251,6 +255,7 @@ public static void setSingletonInstance(Analytics analytics) { this.crypto = crypto; this.sourceMiddleware = sourceMiddleware; this.destinationMiddleware = destinationMiddleware; + this.lifecycle = lifecycle; namespaceSharedPreferences(); @@ -315,6 +320,7 @@ public void run() { .build(); application.registerActivityLifecycleCallbacks(activityLifecycleCallback); + lifecycle.addObserver(activityLifecycleCallback); } @Private @@ -965,6 +971,7 @@ public void shutdown() { return; } application.unregisterActivityLifecycleCallbacks(activityLifecycleCallback); + lifecycle.removeObserver(activityLifecycleCallback); // Only supplied by us for testing, so it's ok to shut it down. If we were to make this public, // we'll have to add a check similar to that of AnalyticsNetworkExecutorService below. analyticsExecutor.shutdown(); @@ -1375,7 +1382,7 @@ public Analytics build() { if (executor == null) { executor = Executors.newSingleThreadExecutor(); } - + Lifecycle lifecycle = ProcessLifecycleOwner.get().getLifecycle(); return new Analytics( application, networkExecutor, @@ -1402,7 +1409,8 @@ public Analytics build() { crypto, srcMiddleware, destMiddleware, - defaultProjectSettings); + defaultProjectSettings, + lifecycle); } } diff --git a/analytics/src/main/java/com/segment/analytics/AnalyticsActivityLifecycleCallbacks.java b/analytics/src/main/java/com/segment/analytics/AnalyticsActivityLifecycleCallbacks.java index c34087eaf..cc40a7796 100644 --- a/analytics/src/main/java/com/segment/analytics/AnalyticsActivityLifecycleCallbacks.java +++ b/analytics/src/main/java/com/segment/analytics/AnalyticsActivityLifecycleCallbacks.java @@ -29,11 +29,15 @@ import android.content.pm.PackageInfo; import android.net.Uri; import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.lifecycle.DefaultLifecycleObserver; +import androidx.lifecycle.LifecycleOwner; import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; -class AnalyticsActivityLifecycleCallbacks implements Application.ActivityLifecycleCallbacks { +class AnalyticsActivityLifecycleCallbacks + implements Application.ActivityLifecycleCallbacks, DefaultLifecycleObserver { private Analytics analytics; private ExecutorService analyticsExecutor; private Boolean shouldTrackApplicationLifecycleEvents; @@ -44,7 +48,6 @@ class AnalyticsActivityLifecycleCallbacks implements Application.ActivityLifecyc private AtomicBoolean trackedApplicationLifecycleEvents; private AtomicInteger numberOfActivities; - private AtomicBoolean isChangingActivityConfigurations; private AtomicBoolean firstLaunch; private AnalyticsActivityLifecycleCallbacks( @@ -57,7 +60,6 @@ private AnalyticsActivityLifecycleCallbacks( PackageInfo packageInfo) { this.trackedApplicationLifecycleEvents = new AtomicBoolean(false); this.numberOfActivities = new AtomicInteger(1); - this.isChangingActivityConfigurations = new AtomicBoolean(false); this.firstLaunch = new AtomicBoolean(false); this.analytics = analytics; this.analyticsExecutor = analyticsExecutor; @@ -68,10 +70,29 @@ private AnalyticsActivityLifecycleCallbacks( this.packageInfo = packageInfo; } - @Override - public void onActivityCreated(Activity activity, Bundle bundle) { - analytics.runOnMainThread(IntegrationOperation.onActivityCreated(activity, bundle)); + public void onStop(@NonNull LifecycleOwner owner) { + // App in background + if (shouldTrackApplicationLifecycleEvents) { + analytics.track("Application Backgrounded"); + } + } + public void onStart(@NonNull LifecycleOwner owner) { + // App in foreground + if (shouldTrackApplicationLifecycleEvents) { + Properties properties = new Properties(); + if (firstLaunch.get()) { + properties + .putValue("version", packageInfo.versionName) + .putValue("build", String.valueOf(packageInfo.versionCode)); + } + properties.putValue("from_background", !firstLaunch.getAndSet(false)); + analytics.track("Application Opened", properties); + } + } + + public void onCreate(@NonNull LifecycleOwner owner) { + // App created if (!trackedApplicationLifecycleEvents.getAndSet(true) && shouldTrackApplicationLifecycleEvents) { numberOfActivities.set(0); @@ -87,28 +108,35 @@ public void run() { } }); } + } + } - if (!trackDeepLinks) { - return; - } + @Override + public void onActivityCreated(Activity activity, Bundle bundle) { + analytics.runOnMainThread(IntegrationOperation.onActivityCreated(activity, bundle)); - Intent intent = activity.getIntent(); - if (intent == null || intent.getData() == null) { - return; - } + if (trackDeepLinks) { + trackDeepLink(activity); + } + } - Properties properties = new Properties(); - Uri uri = intent.getData(); - for (String parameter : uri.getQueryParameterNames()) { - String value = uri.getQueryParameter(parameter); - if (value != null && !value.trim().isEmpty()) { - properties.put(parameter, value); - } - } + private void trackDeepLink(Activity activity) { + Intent intent = activity.getIntent(); + if (intent == null || intent.getData() == null) { + return; + } - properties.put("url", uri.toString()); - analytics.track("Deep Link Opened", properties); + Properties properties = new Properties(); + Uri uri = intent.getData(); + for (String parameter : uri.getQueryParameterNames()) { + String value = uri.getQueryParameter(parameter); + if (value != null && !value.trim().isEmpty()) { + properties.put(parameter, value); + } } + + properties.put("url", uri.toString()); + analytics.track("Deep Link Opened", properties); } @Override @@ -122,20 +150,6 @@ public void onActivityStarted(Activity activity) { @Override public void onActivityResumed(Activity activity) { analytics.runOnMainThread(IntegrationOperation.onActivityResumed(activity)); - - if (shouldTrackApplicationLifecycleEvents - && numberOfActivities.incrementAndGet() == 1 - && !isChangingActivityConfigurations.get()) { - - Properties properties = new Properties(); - if (firstLaunch.get()) { - properties - .putValue("version", packageInfo.versionName) - .putValue("build", String.valueOf(packageInfo.versionCode)); - } - properties.putValue("from_background", !firstLaunch.getAndSet(false)); - analytics.track("Application Opened", properties); - } } @Override @@ -146,13 +160,6 @@ public void onActivityPaused(Activity activity) { @Override public void onActivityStopped(Activity activity) { analytics.runOnMainThread(IntegrationOperation.onActivityStopped(activity)); - - isChangingActivityConfigurations.set(activity.isChangingConfigurations()); - if (shouldTrackApplicationLifecycleEvents - && numberOfActivities.decrementAndGet() == 0 - && !isChangingActivityConfigurations.get()) { - analytics.track("Application Backgrounded"); - } } @Override diff --git a/analytics/src/test/java/com/segment/analytics/AnalyticsTest.java b/analytics/src/test/java/com/segment/analytics/AnalyticsTest.java index bf60d7a44..8a4cb4363 100644 --- a/analytics/src/test/java/com/segment/analytics/AnalyticsTest.java +++ b/analytics/src/test/java/com/segment/analytics/AnalyticsTest.java @@ -39,7 +39,7 @@ import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; @@ -57,6 +57,10 @@ import android.content.pm.PackageManager.NameNotFoundException; import android.net.Uri; import android.os.Bundle; +import androidx.lifecycle.DefaultLifecycleObserver; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleObserver; +import androidx.lifecycle.LifecycleOwner; import com.segment.analytics.TestUtils.NoDescriptionMatcher; import com.segment.analytics.integrations.AliasPayload; import com.segment.analytics.integrations.GroupPayload; @@ -109,6 +113,7 @@ public class AnalyticsTest { @Mock Stats stats; @Mock ProjectSettings.Cache projectSettingsCache; @Mock Integration integration; + @Mock Lifecycle lifecycle; private Options defaultOptions; private Integration.Factory factory; private BooleanPreference optOut; @@ -183,7 +188,8 @@ public String key() { Crypto.none(), Collections.emptyList(), Collections.>emptyMap(), - new ValueMap()); + new ValueMap(), + lifecycle); // Used by singleton tests. grantPermission(RuntimeEnvironment.application, Manifest.permission.INTERNET); @@ -774,19 +780,19 @@ public void invalidURlsThrowAndNotCrash() throws Exception { public void trackApplicationLifecycleEventsInstalled() throws NameNotFoundException { Analytics.INSTANCES.clear(); - final AtomicReference callback = - new AtomicReference<>(); + final AtomicReference callback = new AtomicReference<>(); doNothing() - .when(application) - .registerActivityLifecycleCallbacks( + .when(lifecycle) + .addObserver( argThat( - new NoDescriptionMatcher() { + new NoDescriptionMatcher() { @Override - protected boolean matchesSafely(Application.ActivityLifecycleCallbacks item) { - callback.set(item); + protected boolean matchesSafely(LifecycleObserver item) { + callback.set((DefaultLifecycleObserver) item); return true; } })); + LifecycleOwner mockLifecycleOwner = mock(LifecycleOwner.class); analytics = new Analytics( @@ -815,9 +821,10 @@ protected boolean matchesSafely(Application.ActivityLifecycleCallbacks item) { Crypto.none(), Collections.emptyList(), Collections.>emptyMap(), - new ValueMap()); + new ValueMap(), + lifecycle); - callback.get().onActivityCreated(null, null); + callback.get().onCreate(mockLifecycleOwner); verify(integration) .track( @@ -833,9 +840,8 @@ protected boolean matchesSafely(TrackPayload payload) { } })); - callback.get().onActivityCreated(null, null); - verify(integration, times(2)).onActivityCreated(null, null); - verifyNoMoreInteractions(integration); + callback.get().onCreate(mockLifecycleOwner); + verifyNoMoreInteractions(integration); // Application Installed is not duplicated } @Test @@ -860,19 +866,19 @@ public void trackApplicationLifecycleEventsUpdated() throws NameNotFoundExceptio when(application.getPackageName()).thenReturn("com.foo"); when(application.getPackageManager()).thenReturn(packageManager); - final AtomicReference callback = - new AtomicReference<>(); + final AtomicReference callback = new AtomicReference<>(); doNothing() - .when(application) - .registerActivityLifecycleCallbacks( + .when(lifecycle) + .addObserver( argThat( - new NoDescriptionMatcher() { + new NoDescriptionMatcher() { @Override - protected boolean matchesSafely(Application.ActivityLifecycleCallbacks item) { - callback.set(item); + protected boolean matchesSafely(LifecycleObserver item) { + callback.set((DefaultLifecycleObserver) item); return true; } })); + LifecycleOwner mockLifecycleOwner = mock(LifecycleOwner.class); analytics = new Analytics( @@ -901,9 +907,10 @@ protected boolean matchesSafely(Application.ActivityLifecycleCallbacks item) { Crypto.none(), Collections.emptyList(), Collections.>emptyMap(), - new ValueMap()); + new ValueMap(), + lifecycle); - callback.get().onActivityCreated(null, null); + callback.get().onCreate(mockLifecycleOwner); verify(integration) .track( @@ -969,7 +976,8 @@ protected boolean matchesSafely(Application.ActivityLifecycleCallbacks item) { Crypto.none(), Collections.emptyList(), Collections.>emptyMap(), - new ValueMap()); + new ValueMap(), + lifecycle); Activity activity = mock(Activity.class); PackageManager packageManager = mock(PackageManager.class); @@ -1039,7 +1047,8 @@ protected boolean matchesSafely(Application.ActivityLifecycleCallbacks item) { Crypto.none(), Collections.emptyList(), Collections.>emptyMap(), - new ValueMap()); + new ValueMap(), + lifecycle); final String expectedUrl = "app://track.com/open?utm_id=12345&gclid=abcd&nope="; @@ -1111,7 +1120,8 @@ protected boolean matchesSafely(Application.ActivityLifecycleCallbacks item) { Crypto.none(), Collections.emptyList(), Collections.>emptyMap(), - new ValueMap()); + new ValueMap(), + lifecycle); final String expectedUrl = "app://track.com/open?utm_id=12345&gclid=abcd&nope="; @@ -1183,7 +1193,8 @@ protected boolean matchesSafely(Application.ActivityLifecycleCallbacks item) { Crypto.none(), Collections.emptyList(), Collections.>emptyMap(), - new ValueMap()); + new ValueMap(), + lifecycle); Activity activity = mock(Activity.class); @@ -1246,7 +1257,9 @@ protected boolean matchesSafely(Application.ActivityLifecycleCallbacks item) { optOut, Crypto.none(), Collections.emptyList(), - new ValueMap()); + Collections.>emptyMap(), + new ValueMap(), + lifecycle); Activity activity = mock(Activity.class); @@ -1313,7 +1326,8 @@ protected boolean matchesSafely(Application.ActivityLifecycleCallbacks item) { Crypto.none(), Collections.emptyList(), Collections.>emptyMap(), - new ValueMap()); + new ValueMap(), + lifecycle); Activity activity = mock(Activity.class); Bundle bundle = new Bundle(); @@ -1346,19 +1360,19 @@ protected boolean matchesSafely(Application.ActivityLifecycleCallbacks item) { public void trackApplicationLifecycleEventsApplicationOpened() throws NameNotFoundException { Analytics.INSTANCES.clear(); - final AtomicReference callback = - new AtomicReference<>(); + final AtomicReference callback = new AtomicReference<>(); doNothing() - .when(application) - .registerActivityLifecycleCallbacks( + .when(lifecycle) + .addObserver( argThat( - new NoDescriptionMatcher() { + new NoDescriptionMatcher() { @Override - protected boolean matchesSafely(Application.ActivityLifecycleCallbacks item) { - callback.set(item); + protected boolean matchesSafely(LifecycleObserver item) { + callback.set((DefaultLifecycleObserver) item); return true; } })); + LifecycleOwner mockLifecycleOwner = mock(LifecycleOwner.class); analytics = new Analytics( @@ -1387,10 +1401,11 @@ protected boolean matchesSafely(Application.ActivityLifecycleCallbacks item) { Crypto.none(), Collections.emptyList(), Collections.>emptyMap(), - new ValueMap()); + new ValueMap(), + lifecycle); - callback.get().onActivityCreated(null, null); - callback.get().onActivityResumed(null); + callback.get().onCreate(mockLifecycleOwner); + callback.get().onStart(mockLifecycleOwner); verify(integration) .track( @@ -1411,20 +1426,21 @@ public void trackApplicationLifecycleEventsApplicationBackgrounded() throws NameNotFoundException { Analytics.INSTANCES.clear(); - final AtomicReference callback = - new AtomicReference<>(); + final AtomicReference callback = new AtomicReference<>(); doNothing() - .when(application) - .registerActivityLifecycleCallbacks( + .when(lifecycle) + .addObserver( argThat( - new NoDescriptionMatcher() { + new NoDescriptionMatcher() { @Override - protected boolean matchesSafely(Application.ActivityLifecycleCallbacks item) { - callback.set(item); + protected boolean matchesSafely(LifecycleObserver item) { + callback.set((DefaultLifecycleObserver) item); return true; } })); + LifecycleOwner mockLifecycleOwner = mock(LifecycleOwner.class); + analytics = new Analytics( application, @@ -1452,14 +1468,15 @@ protected boolean matchesSafely(Application.ActivityLifecycleCallbacks item) { Crypto.none(), Collections.emptyList(), Collections.>emptyMap(), - new ValueMap()); + new ValueMap(), + lifecycle); Activity backgroundedActivity = mock(Activity.class); when(backgroundedActivity.isChangingConfigurations()).thenReturn(false); - callback.get().onActivityCreated(null, null); - callback.get().onActivityResumed(null); - callback.get().onActivityStopped(backgroundedActivity); + callback.get().onCreate(mockLifecycleOwner); + callback.get().onResume(mockLifecycleOwner); + callback.get().onStop(mockLifecycleOwner); verify(integration) .track( @@ -1477,19 +1494,19 @@ public void trackApplicationLifecycleEventsApplicationForegrounded() throws NameNotFoundException { Analytics.INSTANCES.clear(); - final AtomicReference callback = - new AtomicReference<>(); + final AtomicReference callback = new AtomicReference<>(); doNothing() - .when(application) - .registerActivityLifecycleCallbacks( + .when(lifecycle) + .addObserver( argThat( - new NoDescriptionMatcher() { + new NoDescriptionMatcher() { @Override - protected boolean matchesSafely(Application.ActivityLifecycleCallbacks item) { - callback.set(item); + protected boolean matchesSafely(LifecycleObserver item) { + callback.set((DefaultLifecycleObserver) item); return true; } })); + LifecycleOwner mockLifecycleOwner = mock(LifecycleOwner.class); analytics = new Analytics( @@ -1518,15 +1535,13 @@ protected boolean matchesSafely(Application.ActivityLifecycleCallbacks item) { Crypto.none(), Collections.emptyList(), Collections.>emptyMap(), - new ValueMap()); - - Activity backgroundedActivity = mock(Activity.class); - when(backgroundedActivity.isChangingConfigurations()).thenReturn(false); + new ValueMap(), + lifecycle); - callback.get().onActivityCreated(null, null); - callback.get().onActivityResumed(null); - callback.get().onActivityStopped(backgroundedActivity); - callback.get().onActivityResumed(null); + callback.get().onCreate(mockLifecycleOwner); + callback.get().onStart(mockLifecycleOwner); + callback.get().onStop(mockLifecycleOwner); + callback.get().onStart(mockLifecycleOwner); verify(integration) .track( @@ -1608,7 +1623,8 @@ protected boolean matchesSafely(Application.ActivityLifecycleCallbacks item) { Crypto.none(), Collections.emptyList(), Collections.>emptyMap(), - new ValueMap()); + new ValueMap(), + lifecycle); assertThat(analytics.shutdown).isFalse(); analytics.shutdown(); @@ -1645,6 +1661,88 @@ protected boolean matchesSafely(Application.ActivityLifecycleCallbacks item) { verifyNoMoreInteractions(integration); } + @Test + public void removeLifecycleObserver() throws NameNotFoundException { + Analytics.INSTANCES.clear(); + + final AtomicReference registeredCallback = new AtomicReference<>(); + final AtomicReference unregisteredCallback = new AtomicReference<>(); + doNothing() + .when(lifecycle) + .addObserver( + argThat( + new NoDescriptionMatcher() { + @Override + protected boolean matchesSafely(LifecycleObserver item) { + registeredCallback.set((DefaultLifecycleObserver) item); + return true; + } + })); + doNothing() + .when(lifecycle) + .removeObserver( + argThat( + new NoDescriptionMatcher() { + @Override + protected boolean matchesSafely(LifecycleObserver item) { + unregisteredCallback.set((DefaultLifecycleObserver) item); + return true; + } + })); + LifecycleOwner mockLifecycleOwner = mock(LifecycleOwner.class); + + analytics = + new Analytics( + application, + networkExecutor, + stats, + traitsCache, + analyticsContext, + defaultOptions, + Logger.with(NONE), + "qaz", + Collections.singletonList(factory), + client, + Cartographer.INSTANCE, + projectSettingsCache, + "foo", + DEFAULT_FLUSH_QUEUE_SIZE, + DEFAULT_FLUSH_INTERVAL, + analyticsExecutor, + false, + new CountDownLatch(0), + false, + false, + false, + optOut, + Crypto.none(), + Collections.emptyList(), + Collections.>emptyMap(), + new ValueMap(), + lifecycle); + + assertThat(analytics.shutdown).isFalse(); + analytics.shutdown(); + AnalyticsActivityLifecycleCallbacks lifecycleObserverSpy = + spy(analytics.activityLifecycleCallback); + + // Same callback was registered and unregistered + assertThat(analytics.activityLifecycleCallback).isSameAs(registeredCallback.get()); + assertThat(analytics.activityLifecycleCallback).isSameAs(unregisteredCallback.get()); + + // Verify callbacks do not call through after shutdown + registeredCallback.get().onCreate(mockLifecycleOwner); + verify(lifecycleObserverSpy, never()).onCreate(mockLifecycleOwner); + + registeredCallback.get().onStop(mockLifecycleOwner); + verify(lifecycleObserverSpy, never()).onStop(mockLifecycleOwner); + + registeredCallback.get().onStart(mockLifecycleOwner); + verify(lifecycleObserverSpy, never()).onStart(mockLifecycleOwner); + + verifyNoMoreInteractions(lifecycleObserverSpy); + } + @Test public void loadNonEmptyDefaultProjectSettingsOnNetworkError() throws IOException { Analytics.INSTANCES.clear(); @@ -1690,7 +1788,8 @@ public void loadNonEmptyDefaultProjectSettingsOnNetworkError() throws IOExceptio Crypto.none(), Collections.emptyList(), Collections.>emptyMap(), - defaultProjectSettings); + defaultProjectSettings, + lifecycle); assertThat(analytics.projectSettings).hasSize(2).containsKey("integrations"); assertThat(analytics.projectSettings.integrations()) @@ -1735,7 +1834,8 @@ public void loadEmptyDefaultProjectSettingsOnNetworkError() throws IOException { Crypto.none(), Collections.emptyList(), Collections.>emptyMap(), - defaultProjectSettings); + defaultProjectSettings, + lifecycle); assertThat(analytics.projectSettings).hasSize(2).containsKey("integrations"); assertThat(analytics.projectSettings.integrations()).hasSize(1).containsKey("Segment.io"); @@ -1786,7 +1886,8 @@ public void overwriteSegmentIoIntegration() throws IOException { Crypto.none(), Collections.emptyList(), Collections.>emptyMap(), - defaultProjectSettings); + defaultProjectSettings, + lifecycle); assertThat(analytics.projectSettings).hasSize(2).containsKey("integrations"); assertThat(analytics.projectSettings.integrations()).hasSize(1).containsKey("Segment.io");