Enables UKM recorder in iOS

Bug: 738938
Cq-Include-Trybots: master.tryserver.chromium.mac:ios-simulator-cronet;master.tryserver.chromium.mac:ios-simulator-full-configs
Change-Id: I75950dc5af48af4dcb8b26db9c093cc8837034fd
Reviewed-on: https://chromium-review.googlesource.com/776035
Commit-Queue: Moe Ahmadi (OOO until Nov. 27) <mahmadi@chromium.org>
Reviewed-by: Alexei Svitkine <asvitkine@chromium.org>
Reviewed-by: Sylvain Defresne <sdefresne@chromium.org>
Cr-Commit-Position: refs/heads/master@{#517724}
diff --git a/components/ukm/ukm_recorder_impl.h b/components/ukm/ukm_recorder_impl.h
index 8441c5a..39e88365 100644
--- a/components/ukm/ukm_recorder_impl.h
+++ b/components/ukm/ukm_recorder_impl.h
@@ -15,6 +15,7 @@
 
 namespace metrics {
 class UkmBrowserTest;
+class UkmEGTestHelper;
 }
 
 namespace ukm {
@@ -53,6 +54,7 @@
 
  private:
   friend ::metrics::UkmBrowserTest;
+  friend ::metrics::UkmEGTestHelper;
   friend ::ukm::debug::DebugPage;
 
   // UkmRecorder:
diff --git a/components/ukm/ukm_service.h b/components/ukm/ukm_service.h
index 0048ef1..5c70a842 100644
--- a/components/ukm/ukm_service.h
+++ b/components/ukm/ukm_service.h
@@ -25,6 +25,7 @@
 namespace metrics {
 class MetricsServiceClient;
 class UkmBrowserTest;
+class UkmEGTestHelper;
 }
 
 namespace ukm {
@@ -76,8 +77,9 @@
   static void RegisterPrefs(PrefRegistrySimple* registry);
 
  private:
-  friend ::ukm::debug::DebugPage;
   friend ::metrics::UkmBrowserTest;
+  friend ::metrics::UkmEGTestHelper;
+  friend ::ukm::debug::DebugPage;
 
   FRIEND_TEST_ALL_PREFIXES(UkmServiceTest, AddEntryWithEmptyMetrics);
   FRIEND_TEST_ALL_PREFIXES(UkmServiceTest, EntryBuilderAndSerialization);
diff --git a/ios/chrome/browser/ios_chrome_main_parts.mm b/ios/chrome/browser/ios_chrome_main_parts.mm
index 796b8ce4..2194894 100644
--- a/ios/chrome/browser/ios_chrome_main_parts.mm
+++ b/ios/chrome/browser/ios_chrome_main_parts.mm
@@ -136,13 +136,6 @@
 }
 
 void IOSChromeMainParts::PreMainMessageLoopRun() {
-  // This must occur at PreMainMessageLoopRun because |SetupMetrics()| uses the
-  // blocking pool, which is disabled until the CreateThreads phase of startup.
-  SetupMetrics();
-
-  // Now that the file thread has been started, start recording.
-  StartMetricsRecording();
-
   application_context_->PreMainMessageLoopRun();
 
   // ContentSettingsPattern need to be initialized before creating the
@@ -159,6 +152,15 @@
   ios::ChromeBrowserState* last_used_browser_state =
       browser_state_manager->GetLastUsedBrowserState();
 
+  // This must occur at PreMainMessageLoopRun because |SetupMetrics()| uses the
+  // blocking pool, which is disabled until the CreateThreads phase of startup.
+  // TODO(crbug.com/786494): Investigate whether metrics recording can be
+  // initialized consistently across iOS and non-iOS platforms
+  SetupMetrics();
+
+  // Now that the file thread has been started, start recording.
+  StartMetricsRecording();
+
 #if BUILDFLAG(ENABLE_RLZ)
   // Init the RLZ library. This just schedules a task on the file thread to be
   // run sometime later. If this is the first run we record the installation
diff --git a/ios/chrome/browser/metrics/BUILD.gn b/ios/chrome/browser/metrics/BUILD.gn
index 08d4f88..5a1bbe7 100644
--- a/ios/chrome/browser/metrics/BUILD.gn
+++ b/ios/chrome/browser/metrics/BUILD.gn
@@ -7,6 +7,8 @@
   sources = [
     "field_trial_synchronizer.cc",
     "field_trial_synchronizer.h",
+    "incognito_web_state_observer.h",
+    "incognito_web_state_observer.mm",
     "ios_chrome_metrics_service_accessor.cc",
     "ios_chrome_metrics_service_accessor.h",
     "ios_chrome_metrics_service_client.h",
@@ -53,6 +55,7 @@
     "//ios/chrome/browser/translate",
     "//ios/chrome/browser/variations",
     "//ios/chrome/browser/variations:ios_chrome_ui_string_overrider_factory",
+    "//ios/chrome/browser/web_state_list",
     "//ios/chrome/common",
     "//ios/web",
   ]
@@ -143,19 +146,34 @@
   testonly = true
   sources = [
     "tab_usage_recorder_egtest.mm",
+    "ukm_egtest.mm",
   ]
   deps = [
     ":metrics_internal",
     ":test_support",
     "//base",
     "//base/test:test_support",
+    "//components/browser_sync",
+    "//components/metrics",
+    "//components/metrics_services_manager",
     "//components/strings",
+    "//components/ukm",
+    "//ios/chrome/app/strings:ios_strings_grit",
+    "//ios/chrome/browser",
+    "//ios/chrome/browser/metrics",
+    "//ios/chrome/browser/signin",
+    "//ios/chrome/browser/sync",
     "//ios/chrome/browser/ui",
+    "//ios/chrome/browser/ui/authentication",
+    "//ios/chrome/browser/ui/authentication:authentication_ui",
+    "//ios/chrome/browser/ui/authentication:eg_test_support",
     "//ios/chrome/browser/ui/settings",
+    "//ios/chrome/browser/ui/tab_switcher:egtest_support",
     "//ios/chrome/browser/ui/toolbar/public",
     "//ios/chrome/browser/ui/tools_menu",
     "//ios/chrome/test/app:test_support",
     "//ios/chrome/test/earl_grey:test_support",
+    "//ios/public/provider/chrome/browser/signin:test_support",
     "//ios/testing:ios_test_support",
     "//ios/web:earl_grey_test_support",
     "//ios/web/public/test",
diff --git a/ios/chrome/browser/metrics/incognito_web_state_observer.h b/ios/chrome/browser/metrics/incognito_web_state_observer.h
new file mode 100644
index 0000000..f1d88e4
--- /dev/null
+++ b/ios/chrome/browser/metrics/incognito_web_state_observer.h
@@ -0,0 +1,57 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef IOS_CHROME_BROWSER_METRICS_INCOGNITO_WEB_STATE_OBSERVER_H_
+#define IOS_CHROME_BROWSER_METRICS_INCOGNITO_WEB_STATE_OBSERVER_H_
+
+#include "base/macros.h"
+
+#include "base/scoped_observer.h"
+#import "ios/chrome/browser/tabs/tab_model_list_observer.h"
+#import "ios/chrome/browser/web_state_list/web_state_list.h"
+#import "ios/chrome/browser/web_state_list/web_state_list_observer.h"
+
+// Interface for getting notified when WebStates get added/removed to/from an
+// incognito browser state. For example, implementations can invoke
+// TabModelList::IsOffTheRecordSessionActive() in the body of the observer
+// methods to learn if incognito session is currently active (i.e., at least one
+// incognito tab is open).
+class IncognitoWebStateObserver : public TabModelListObserver,
+                                  public WebStateListObserver {
+ public:
+  IncognitoWebStateObserver();
+  ~IncognitoWebStateObserver() override;
+
+  // TabModelListObserver:
+  void TabModelRegisteredWithBrowserState(
+      TabModel* tab_model,
+      ios::ChromeBrowserState* browser_state) override;
+  void TabModelUnregisteredFromBrowserState(
+      TabModel* tab_model,
+      ios::ChromeBrowserState* browser_state) override;
+
+  // WebStateListObserver:
+  void WebStateInsertedAt(WebStateList* web_state_list,
+                          web::WebState* web_state,
+                          int index,
+                          bool activating) override;
+  void WebStateDetachedAt(WebStateList* web_state_list,
+                          web::WebState* web_state,
+                          int index) override;
+  void WebStateReplacedAt(WebStateList* web_state_list,
+                          web::WebState* old_web_state,
+                          web::WebState* new_web_state,
+                          int index) override;
+
+ protected:
+  virtual void OnIncognitoWebStateAdded() = 0;
+  virtual void OnIncognitoWebStateRemoved() = 0;
+
+ private:
+  ScopedObserver<WebStateList, IncognitoWebStateObserver> scoped_observer_;
+
+  DISALLOW_COPY_AND_ASSIGN(IncognitoWebStateObserver);
+};
+
+#endif  // IOS_CHROME_BROWSER_METRICS_INCOGNITO_WEB_STATE_OBSERVER_H_
diff --git a/ios/chrome/browser/metrics/incognito_web_state_observer.mm b/ios/chrome/browser/metrics/incognito_web_state_observer.mm
new file mode 100644
index 0000000..2cb9295
--- /dev/null
+++ b/ios/chrome/browser/metrics/incognito_web_state_observer.mm
@@ -0,0 +1,85 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "ios/chrome/browser/metrics/incognito_web_state_observer.h"
+
+#include <vector>
+
+#include "ios/chrome/browser/application_context.h"
+#include "ios/chrome/browser/browser_state/chrome_browser_state.h"
+#include "ios/chrome/browser/browser_state/chrome_browser_state_manager.h"
+#import "ios/chrome/browser/tabs/tab_model.h"
+#import "ios/chrome/browser/tabs/tab_model_list.h"
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+IncognitoWebStateObserver::IncognitoWebStateObserver()
+    : scoped_observer_(this) {
+  TabModelList::AddObserver(this);
+
+  // Observe all existing off-the-record TabModels' WebStateLists.
+  std::vector<ios::ChromeBrowserState*> browser_states =
+      GetApplicationContext()
+          ->GetChromeBrowserStateManager()
+          ->GetLoadedBrowserStates();
+
+  for (ios::ChromeBrowserState* browser_state : browser_states) {
+    DCHECK(!browser_state->IsOffTheRecord());
+
+    if (!browser_state->HasOffTheRecordChromeBrowserState())
+      continue;
+    ios::ChromeBrowserState* otr_browser_state =
+        browser_state->GetOffTheRecordChromeBrowserState();
+
+    NSArray<TabModel*>* tab_models =
+        TabModelList::GetTabModelsForChromeBrowserState(otr_browser_state);
+    for (TabModel* tab_model in tab_models)
+      scoped_observer_.Add([tab_model webStateList]);
+  }
+}
+
+IncognitoWebStateObserver::~IncognitoWebStateObserver() {
+  TabModelList::RemoveObserver(this);
+}
+
+void IncognitoWebStateObserver::TabModelRegisteredWithBrowserState(
+    TabModel* tab_model,
+    ios::ChromeBrowserState* browser_state) {
+  if (browser_state->IsOffTheRecord() &&
+      !scoped_observer_.IsObserving([tab_model webStateList])) {
+    scoped_observer_.Add([tab_model webStateList]);
+  }
+}
+
+void IncognitoWebStateObserver::TabModelUnregisteredFromBrowserState(
+    TabModel* tab_model,
+    ios::ChromeBrowserState* browser_state) {
+  if (browser_state->IsOffTheRecord()) {
+    DCHECK(scoped_observer_.IsObserving([tab_model webStateList]));
+    scoped_observer_.Remove([tab_model webStateList]);
+  }
+}
+
+void IncognitoWebStateObserver::WebStateInsertedAt(WebStateList* web_state_list,
+                                                   web::WebState* web_state,
+                                                   int index,
+                                                   bool activating) {
+  OnIncognitoWebStateAdded();
+}
+
+void IncognitoWebStateObserver::WebStateDetachedAt(WebStateList* web_state_list,
+                                                   web::WebState* web_state,
+                                                   int index) {
+  OnIncognitoWebStateRemoved();
+}
+
+void IncognitoWebStateObserver::WebStateReplacedAt(WebStateList* web_state_list,
+                                                   web::WebState* old_web_state,
+                                                   web::WebState* new_web_state,
+                                                   int index) {
+  // This is invoked when a Tab is replaced by another Tab without any visible
+  // UI change. There is nothing to do since the number of Tabs haven't changed.
+}
diff --git a/ios/chrome/browser/metrics/ios_chrome_metrics_service_accessor.cc b/ios/chrome/browser/metrics/ios_chrome_metrics_service_accessor.cc
index e612332..01ec484 100644
--- a/ios/chrome/browser/metrics/ios_chrome_metrics_service_accessor.cc
+++ b/ios/chrome/browser/metrics/ios_chrome_metrics_service_accessor.cc
@@ -9,8 +9,26 @@
 #include "components/prefs/pref_service.h"
 #include "ios/chrome/browser/application_context.h"
 
+namespace {
+
+const bool* g_metrics_consent_for_testing = nullptr;
+
+}  // namespace
+
+// static
+void IOSChromeMetricsServiceAccessor::SetMetricsAndCrashReportingForTesting(
+    const bool* value) {
+  DCHECK_NE(g_metrics_consent_for_testing == nullptr, value == nullptr)
+      << "Unpaired set/reset";
+
+  g_metrics_consent_for_testing = value;
+}
+
 // static
 bool IOSChromeMetricsServiceAccessor::IsMetricsAndCrashReportingEnabled() {
+  if (g_metrics_consent_for_testing)
+    return *g_metrics_consent_for_testing;
+
   return IsMetricsReportingEnabled(GetApplicationContext()->GetLocalState());
 }
 
diff --git a/ios/chrome/browser/metrics/ios_chrome_metrics_service_accessor.h b/ios/chrome/browser/metrics/ios_chrome_metrics_service_accessor.h
index f996378..203f690 100644
--- a/ios/chrome/browser/metrics/ios_chrome_metrics_service_accessor.h
+++ b/ios/chrome/browser/metrics/ios_chrome_metrics_service_accessor.h
@@ -22,6 +22,12 @@
 // Since these methods are private, each user has to be explicitly declared
 // as a 'friend' below.
 class IOSChromeMetricsServiceAccessor : public metrics::MetricsServiceAccessor {
+ public:
+  // If arg is non-null, the value will be returned from future calls to
+  // IsMetricsAndCrashReportingEnabled(). Pointer must be valid until it is
+  // reset to null here.
+  static void SetMetricsAndCrashReportingForTesting(const bool* value);
+
  private:
   friend class IOSChromeMetricsServicesManagerClient;
 
diff --git a/ios/chrome/browser/metrics/ios_chrome_metrics_service_client.h b/ios/chrome/browser/metrics/ios_chrome_metrics_service_client.h
index 5b558cbc..b8701094 100644
--- a/ios/chrome/browser/metrics/ios_chrome_metrics_service_client.h
+++ b/ios/chrome/browser/metrics/ios_chrome_metrics_service_client.h
@@ -20,6 +20,7 @@
 #include "components/omnibox/browser/omnibox_event_global_tracker.h"
 #include "components/ukm/observers/history_delete_observer.h"
 #include "components/ukm/observers/sync_disable_observer.h"
+#import "ios/chrome/browser/metrics/incognito_web_state_observer.h"
 #include "ios/web/public/web_state/global_web_state_observer.h"
 
 class IOSChromeStabilityMetricsProvider;
@@ -40,11 +41,11 @@
 
 // IOSChromeMetricsServiceClient provides an implementation of
 // MetricsServiceClient that depends on //ios/chrome/.
-class IOSChromeMetricsServiceClient
-    : public metrics::MetricsServiceClient,
-      public ukm::HistoryDeleteObserver,
-      public ukm::SyncDisableObserver,
-      public web::GlobalWebStateObserver {
+class IOSChromeMetricsServiceClient : public IncognitoWebStateObserver,
+                                      public metrics::MetricsServiceClient,
+                                      public ukm::HistoryDeleteObserver,
+                                      public ukm::SyncDisableObserver,
+                                      public web::GlobalWebStateObserver {
  public:
   ~IOSChromeMetricsServiceClient() override;
 
@@ -86,6 +87,10 @@
   void WebStateDidStartLoading(web::WebState* web_state) override;
   void WebStateDidStopLoading(web::WebState* web_state) override;
 
+  // IncognitoWebStateObserver:
+  void OnIncognitoWebStateAdded() override;
+  void OnIncognitoWebStateRemoved() override;
+
   metrics::EnableMetricsDefault GetMetricsReportingDefaultState() override;
 
  private:
diff --git a/ios/chrome/browser/metrics/ios_chrome_metrics_service_client.mm b/ios/chrome/browser/metrics/ios_chrome_metrics_service_client.mm
index ea4b847..c141286 100644
--- a/ios/chrome/browser/metrics/ios_chrome_metrics_service_client.mm
+++ b/ios/chrome/browser/metrics/ios_chrome_metrics_service_client.mm
@@ -57,7 +57,7 @@
 #include "ios/chrome/browser/sync/ios_chrome_profile_sync_service_factory.h"
 #include "ios/chrome/browser/sync/ios_chrome_sync_client.h"
 #include "ios/chrome/browser/tab_parenting_global_observer.h"
-#include "ios/chrome/browser/tabs/tab_model_list.h"
+#import "ios/chrome/browser/tabs/tab_model_list.h"
 #include "ios/chrome/browser/translate/translate_ranker_metrics_provider.h"
 #include "ios/chrome/common/channel_info.h"
 #include "ios/web/public/web_thread.h"
@@ -302,6 +302,16 @@
   UpdateRunningServices();
 }
 
+void IOSChromeMetricsServiceClient::OnIncognitoWebStateAdded() {
+  // Signal service manager to enable/disable UKM based on new state.
+  UpdateRunningServices();
+}
+
+void IOSChromeMetricsServiceClient::OnIncognitoWebStateRemoved() {
+  // Signal service manager to enable/disable UKM based on new state.
+  UpdateRunningServices();
+}
+
 bool IOSChromeMetricsServiceClient::IsHistorySyncEnabledOnAllProfiles() {
   return SyncDisableObserver::IsHistorySyncEnabledOnAllProfiles();
 }
diff --git a/ios/chrome/browser/metrics/ios_chrome_metrics_services_manager_client.mm b/ios/chrome/browser/metrics/ios_chrome_metrics_services_manager_client.mm
index 79be995..a16552a 100644
--- a/ios/chrome/browser/metrics/ios_chrome_metrics_services_manager_client.mm
+++ b/ios/chrome/browser/metrics/ios_chrome_metrics_services_manager_client.mm
@@ -116,9 +116,5 @@
 }
 
 bool IOSChromeMetricsServicesManagerClient::IsIncognitoSessionActive() {
-  // return ::IsOffTheRecordSessionActive();
-  // TODO(crbug.com/734091): Conservatively set to true until there is a test
-  // to ensure it gets re-queried when an incognito tab is opened.  This
-  // effectively disables UKM.
-  return true;
+  return TabModelList::IsOffTheRecordSessionActive();
 }
diff --git a/ios/chrome/browser/metrics/ukm_egtest.mm b/ios/chrome/browser/metrics/ukm_egtest.mm
new file mode 100644
index 0000000..9878be9
--- /dev/null
+++ b/ios/chrome/browser/metrics/ukm_egtest.mm
@@ -0,0 +1,332 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <EarlGrey/EarlGrey.h>
+#import <XCTest/XCTest.h>
+
+#include "base/macros.h"
+#include "components/metrics/metrics_service.h"
+#include "components/metrics_services_manager/metrics_services_manager.h"
+#include "components/strings/grit/components_strings.h"
+#include "components/ukm/ukm_service.h"
+#include "ios/chrome/browser/application_context.h"
+#include "ios/chrome/browser/metrics/ios_chrome_metrics_service_accessor.h"
+#import "ios/chrome/browser/ui/authentication/signin_earlgrey_utils.h"
+#import "ios/chrome/browser/ui/authentication/signin_promo_view.h"
+#import "ios/chrome/browser/ui/tab_switcher/tab_switcher_egtest_util.h"
+#include "ios/chrome/browser/ui/ui_util.h"
+#include "ios/chrome/grit/ios_strings.h"
+#import "ios/chrome/test/app/chrome_test_util.h"
+#import "ios/chrome/test/app/sync_test_util.h"
+#import "ios/chrome/test/app/tab_test_util.h"
+#import "ios/chrome/test/earl_grey/chrome_actions.h"
+#import "ios/chrome/test/earl_grey/chrome_earl_grey.h"
+#import "ios/chrome/test/earl_grey/chrome_earl_grey_ui.h"
+#import "ios/chrome/test/earl_grey/chrome_matchers.h"
+#import "ios/chrome/test/earl_grey/chrome_test_case.h"
+#import "ios/public/provider/chrome/browser/signin/fake_chrome_identity.h"
+#import "ios/public/provider/chrome/browser/signin/fake_chrome_identity_service.h"
+#import "ios/testing/wait_util.h"
+#include "services/metrics/public/cpp/ukm_recorder.h"
+#include "ui/base/l10n/l10n_util.h"
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+using chrome_test_util::AccountsSyncButton;
+using chrome_test_util::ButtonWithAccessibilityLabelId;
+using chrome_test_util::GetIncognitoTabCount;
+using chrome_test_util::IsIncognitoMode;
+using chrome_test_util::IsSyncInitialized;
+using chrome_test_util::NavigationBarDoneButton;
+using chrome_test_util::SecondarySignInButton;
+using chrome_test_util::SettingsAccountButton;
+using chrome_test_util::SettingsAccountButton;
+using chrome_test_util::SignOutAccountsButton;
+using chrome_test_util::SyncSwitchCell;
+using chrome_test_util::TabletTabSwitcherCloseButton;
+using chrome_test_util::TabletTabSwitcherOpenTabsPanelButton;
+using chrome_test_util::TurnSyncSwitchOn;
+
+namespace metrics {
+
+// Helper class that provides access to UKM internals.
+class UkmEGTestHelper {
+ public:
+  UkmEGTestHelper() {}
+
+  static bool ukm_enabled() {
+    auto* service = ukm_service();
+    return service ? service->recording_enabled_ : false;
+  }
+
+  static uint64_t client_id() {
+    auto* service = ukm_service();
+    return service ? service->client_id_ : 0;
+  }
+
+ private:
+  static ukm::UkmService* ukm_service() {
+    return GetApplicationContext()
+        ->GetMetricsServicesManager()
+        ->GetUkmService();
+  }
+
+  DISALLOW_COPY_AND_ASSIGN(UkmEGTestHelper);
+};
+
+}  // namespace metrics
+
+namespace {
+
+bool g_metrics_enabled = false;
+
+// Constant for timeout while waiting for asynchronous sync and UKM operations.
+const NSTimeInterval kSyncUKMOperationsTimeout = 10.0;
+
+void AssertSyncInitialized(bool is_initialized) {
+  ConditionBlock condition = ^{
+    return IsSyncInitialized() == is_initialized;
+  };
+  GREYAssert(testing::WaitUntilConditionOrTimeout(kSyncUKMOperationsTimeout,
+                                                  condition),
+             @"Failed to assert whether Sync was initialized or not.");
+}
+
+void AssertUKMEnabled(bool is_enabled) {
+  ConditionBlock condition = ^{
+    return metrics::UkmEGTestHelper::ukm_enabled() == is_enabled;
+  };
+  GREYAssert(testing::WaitUntilConditionOrTimeout(kSyncUKMOperationsTimeout,
+                                                  condition),
+             @"Failed to assert whether UKM was enabled or not.");
+}
+
+void OpenNewIncognitoTab() {
+  NSUInteger incognito_tab_count = GetIncognitoTabCount();
+  chrome_test_util::OpenNewIncognitoTab();
+  [ChromeEarlGrey waitForIncognitoTabCount:(incognito_tab_count + 1)];
+  GREYAssert(IsIncognitoMode(), @"Failed to switch to incognito mode.");
+}
+
+void CloseAllIncognitoTabs() {
+  GREYAssert(chrome_test_util::CloseAllIncognitoTabs(), @"Tabs did not close");
+  [ChromeEarlGrey waitForIncognitoTabCount:0];
+  if (IsIPadIdiom()) {
+    // Switch to the non-incognito panel and leave the tab switcher.
+    [[EarlGrey selectElementWithMatcher:TabletTabSwitcherOpenTabsPanelButton()]
+        performAction:grey_tap()];
+    [[EarlGrey selectElementWithMatcher:TabletTabSwitcherCloseButton()]
+        performAction:grey_tap()];
+  }
+  GREYAssert(!IsIncognitoMode(), @"Failed to switch to normal mode.");
+}
+
+// Signs in to sync.
+void SignIn() {
+  ChromeIdentity* identity = [SigninEarlGreyUtils fakeIdentity1];
+  ios::FakeChromeIdentityService::GetInstanceFromChromeProvider()->AddIdentity(
+      identity);
+
+  [ChromeEarlGreyUI openSettingsMenu];
+  [ChromeEarlGreyUI tapSettingsMenuButton:SecondarySignInButton()];
+  [ChromeEarlGreyUI signInToIdentityByEmail:identity.userEmail];
+  [ChromeEarlGreyUI confirmSigninConfirmationDialog];
+  [[EarlGrey selectElementWithMatcher:NavigationBarDoneButton()]
+      performAction:grey_tap()];
+
+  [SigninEarlGreyUtils assertSignedInWithIdentity:identity];
+}
+
+// Signs in to sync by tapping the sign-in promo view.
+void SignInWithPromo() {
+  [ChromeEarlGreyUI openSettingsMenu];
+  [SigninEarlGreyUtils
+      checkSigninPromoVisibleWithMode:SigninPromoViewModeWarmState];
+  [[EarlGrey selectElementWithMatcher:grey_accessibilityID(
+                                          kSigninPromoPrimaryButtonId)]
+      performAction:grey_tap()];
+  [ChromeEarlGreyUI confirmSigninConfirmationDialog];
+  [[EarlGrey selectElementWithMatcher:NavigationBarDoneButton()]
+      performAction:grey_tap()];
+
+  [SigninEarlGreyUtils
+      assertSignedInWithIdentity:[SigninEarlGreyUtils fakeIdentity1]];
+}
+
+// Signs out of sync.
+void SignOut() {
+  [ChromeEarlGreyUI openSettingsMenu];
+  [[EarlGrey selectElementWithMatcher:SettingsAccountButton()]
+      performAction:grey_tap()];
+  [ChromeEarlGreyUI tapAccountsMenuButton:SignOutAccountsButton()];
+  [[EarlGrey selectElementWithMatcher:
+                 ButtonWithAccessibilityLabelId(
+                     IDS_IOS_DISCONNECT_DIALOG_CONTINUE_BUTTON_MOBILE)]
+      performAction:grey_tap()];
+  [[EarlGrey selectElementWithMatcher:NavigationBarDoneButton()]
+      performAction:grey_tap()];
+
+  [SigninEarlGreyUtils assertSignedOut];
+}
+
+}  // namespace
+
+// UKM tests.
+@interface UKMTestCase : ChromeTestCase
+
+@end
+
+@implementation UKMTestCase
+
++ (void)setUp {
+  [super setUp];
+  if (!base::FeatureList::IsEnabled(ukm::kUkmFeature)) {
+    // ukm::kUkmFeature feature is not enabled. You need to pass
+    // --enable-features=Ukm command line argument in order to run this test.
+    DCHECK(false);
+  }
+}
+
+- (void)setUp {
+  [super setUp];
+
+  AssertSyncInitialized(false);
+  AssertUKMEnabled(false);
+
+  // Enable sync.
+  SignIn();
+  AssertSyncInitialized(true);
+
+  // Grant metrics consent and update MetricsServicesManager.
+  GREYAssert(!g_metrics_enabled, @"Unpaired set/reset of user consent.");
+  g_metrics_enabled = true;
+  IOSChromeMetricsServiceAccessor::SetMetricsAndCrashReportingForTesting(
+      &g_metrics_enabled);
+  GetApplicationContext()->GetMetricsServicesManager()->UpdateUploadPermissions(
+      true);
+  AssertUKMEnabled(true);
+}
+
+- (void)tearDown {
+  AssertSyncInitialized(true);
+  AssertUKMEnabled(true);
+
+  // Revoke metrics consent and update MetricsServicesManager.
+  GREYAssert(g_metrics_enabled, @"Unpaired set/reset of user consent.");
+  g_metrics_enabled = false;
+  GetApplicationContext()->GetMetricsServicesManager()->UpdateUploadPermissions(
+      true);
+  IOSChromeMetricsServiceAccessor::SetMetricsAndCrashReportingForTesting(
+      nullptr);
+  AssertUKMEnabled(false);
+
+  // Disable sync.
+  SignOut();
+  AssertSyncInitialized(false);
+
+  [super tearDown];
+}
+
+// Make sure that UKM is disabled while an incognito tab is open.
+- (void)testIncognito {
+  uint64_t original_client_id = metrics::UkmEGTestHelper::client_id();
+
+  OpenNewIncognitoTab();
+
+  AssertUKMEnabled(false);
+
+  CloseAllIncognitoTabs();
+
+  AssertUKMEnabled(true);
+  // Client ID should not have been reset.
+  GREYAssert(original_client_id == metrics::UkmEGTestHelper::client_id(),
+             @"Client ID was reset.");
+}
+
+// Make sure that UKM is disabled when sync is not enabled.
+- (void)testNoSync {
+  uint64_t original_client_id = metrics::UkmEGTestHelper::client_id();
+
+  SignOut();
+
+  AssertUKMEnabled(false);
+
+  SignInWithPromo();
+
+  AssertUKMEnabled(true);
+  // Client ID should not have been reset.
+  GREYAssert(original_client_id == metrics::UkmEGTestHelper::client_id(),
+             @"Client ID was reset.");
+}
+
+// Make sure that UKM is disabled when sync is disabled.
+- (void)testDisableSync {
+  uint64_t original_client_id = metrics::UkmEGTestHelper::client_id();
+
+  [ChromeEarlGreyUI openSettingsMenu];
+  // Open accounts settings, then sync settings.
+  [[EarlGrey selectElementWithMatcher:SettingsAccountButton()]
+      performAction:grey_tap()];
+  [[EarlGrey selectElementWithMatcher:AccountsSyncButton()]
+      performAction:grey_tap()];
+  // Toggle "Sync Everything" then "History" switches off.
+  [[EarlGrey selectElementWithMatcher:SyncSwitchCell(
+                                          l10n_util::GetNSString(
+                                              IDS_IOS_SYNC_EVERYTHING_TITLE),
+                                          YES)]
+      performAction:TurnSyncSwitchOn(NO)];
+  [[EarlGrey
+      selectElementWithMatcher:SyncSwitchCell(l10n_util::GetNSString(
+                                                  IDS_SYNC_DATATYPE_TYPED_URLS),
+                                              YES)]
+      performAction:TurnSyncSwitchOn(NO)];
+
+  AssertUKMEnabled(false);
+
+  // Toggle "History" then "Sync Everything" switches on.
+  [[EarlGrey
+      selectElementWithMatcher:SyncSwitchCell(l10n_util::GetNSString(
+                                                  IDS_SYNC_DATATYPE_TYPED_URLS),
+                                              NO)]
+      performAction:TurnSyncSwitchOn(YES)];
+  [[EarlGrey selectElementWithMatcher:SyncSwitchCell(
+                                          l10n_util::GetNSString(
+                                              IDS_IOS_SYNC_EVERYTHING_TITLE),
+                                          NO)]
+      performAction:TurnSyncSwitchOn(YES)];
+
+  AssertUKMEnabled(true);
+  // Client ID should have been reset.
+  GREYAssert(original_client_id != metrics::UkmEGTestHelper::client_id(),
+             @"Client ID was not reset.");
+
+  [[EarlGrey selectElementWithMatcher:NavigationBarDoneButton()]
+      performAction:grey_tap()];
+}
+
+// Make sure that UKM is disabled when metrics consent is revoked.
+- (void)testNoConsent {
+  uint64_t original_client_id = metrics::UkmEGTestHelper::client_id();
+
+  // Revoke metrics consent and update MetricsServicesManager.
+  g_metrics_enabled = false;
+  GetApplicationContext()->GetMetricsServicesManager()->UpdateUploadPermissions(
+      true);
+
+  AssertUKMEnabled(false);
+
+  // Grant metrics consent and update MetricsServicesManager.
+  g_metrics_enabled = true;
+  GetApplicationContext()->GetMetricsServicesManager()->UpdateUploadPermissions(
+      true);
+
+  AssertUKMEnabled(true);
+  // Client ID should have been reset.
+  GREYAssert(original_client_id != metrics::UkmEGTestHelper::client_id(),
+             @"Client ID was not reset.");
+}
+
+@end
diff --git a/ios/chrome/test/earl_grey/BUILD.gn b/ios/chrome/test/earl_grey/BUILD.gn
index d9db9136..5cd9f9b 100644
--- a/ios/chrome/test/earl_grey/BUILD.gn
+++ b/ios/chrome/test/earl_grey/BUILD.gn
@@ -208,6 +208,7 @@
     "//ios/chrome/browser/ui/omnibox:omnibox_internal",
     "//ios/chrome/browser/ui/payments:payments_ui",
     "//ios/chrome/browser/ui/settings:settings",
+    "//ios/chrome/browser/ui/settings/cells",
     "//ios/chrome/browser/ui/static_content",
     "//ios/chrome/browser/ui/toolbar/public",
     "//ios/chrome/browser/ui/tools_menu",
diff --git a/ios/chrome/test/earl_grey/chrome_actions.h b/ios/chrome/test/earl_grey/chrome_actions.h
index 53448a5f..726893a 100644
--- a/ios/chrome/test/earl_grey/chrome_actions.h
+++ b/ios/chrome/test/earl_grey/chrome_actions.h
@@ -24,6 +24,9 @@
 // state.
 id<GREYAction> TurnCollectionViewSwitchOn(BOOL on);
 
+// Action to turn the switch of a SyncSwitchCell to the given |on| state.
+id<GREYAction> TurnSyncSwitchOn(BOOL on);
+
 }  // namespace chrome_test_util
 
 #endif  // IOS_CHROME_TEST_EARL_GREY_CHROME_ACTIONS_H_
diff --git a/ios/chrome/test/earl_grey/chrome_actions.mm b/ios/chrome/test/earl_grey/chrome_actions.mm
index 229479a..e0a5bac4 100644
--- a/ios/chrome/test/earl_grey/chrome_actions.mm
+++ b/ios/chrome/test/earl_grey/chrome_actions.mm
@@ -6,6 +6,7 @@
 
 #import "base/mac/foundation_util.h"
 #import "ios/chrome/browser/ui/collection_view/cells/collection_view_switch_item.h"
+#import "ios/chrome/browser/ui/settings/cells/sync_switch_item.h"
 #import "ios/chrome/test/app/chrome_test_util.h"
 #import "ios/web/public/test/earl_grey/web_view_actions.h"
 
@@ -36,7 +37,27 @@
               base::mac::ObjCCastStrict<CollectionViewSwitchCell>(
                   collectionViewCell);
           UISwitch* switchView = switchCell.switchView;
-          if (switchView.on ^ on) {
+          if (switchView.on != on) {
+            id<GREYAction> longPressAction = [GREYActions
+                actionForLongPressWithDuration:kGREYLongPressDefaultDuration];
+            return [longPressAction perform:switchView error:errorOrNil];
+          }
+          return YES;
+        }];
+}
+
+id<GREYAction> TurnSyncSwitchOn(BOOL on) {
+  id<GREYMatcher> constraints = grey_not(grey_systemAlertViewShown());
+  NSString* actionName = [NSString
+      stringWithFormat:@"Turn sync switch to %@ state", on ? @"ON" : @"OFF"];
+  return [GREYActionBlock
+      actionWithName:actionName
+         constraints:constraints
+        performBlock:^BOOL(id syncSwitchCell, __strong NSError** errorOrNil) {
+          SyncSwitchCell* switchCell =
+              base::mac::ObjCCastStrict<SyncSwitchCell>(syncSwitchCell);
+          UISwitch* switchView = switchCell.switchView;
+          if (switchView.on != on) {
             id<GREYAction> longPressAction = [GREYActions
                 actionForLongPressWithDuration:kGREYLongPressDefaultDuration];
             return [longPressAction perform:switchView error:errorOrNil];
diff --git a/ios/chrome/test/earl_grey/chrome_matchers.h b/ios/chrome/test/earl_grey/chrome_matchers.h
index 8a5e022..374c479 100644
--- a/ios/chrome/test/earl_grey/chrome_matchers.h
+++ b/ios/chrome/test/earl_grey/chrome_matchers.h
@@ -74,6 +74,11 @@
 id<GREYMatcher> CollectionViewSwitchCell(NSString* accessibilityIdentifier,
                                          BOOL isOn);
 
+// Matcher for SyncSwitchCell.
+// TODO(crbug.com/684139): Update |is_on| to something more obvious from
+// callsites.
+id<GREYMatcher> SyncSwitchCell(NSString* accessibilityLabel, BOOL is_on);
+
 // Matcher for the Open in New Tab option in the context menu when long pressing
 // a link.
 id<GREYMatcher> OpenLinkInNewTabButton();
diff --git a/ios/chrome/test/earl_grey/chrome_matchers.mm b/ios/chrome/test/earl_grey/chrome_matchers.mm
index 03c76402..984a505e 100644
--- a/ios/chrome/test/earl_grey/chrome_matchers.mm
+++ b/ios/chrome/test/earl_grey/chrome_matchers.mm
@@ -20,6 +20,7 @@
 #import "ios/chrome/browser/ui/payments/payment_request_picker_view_controller.h"
 #import "ios/chrome/browser/ui/payments/payment_request_view_controller.h"
 #import "ios/chrome/browser/ui/settings/accounts_collection_view_controller.h"
+#import "ios/chrome/browser/ui/settings/cells/sync_switch_item.h"
 #import "ios/chrome/browser/ui/settings/clear_browsing_data_collection_view_controller.h"
 #import "ios/chrome/browser/ui/settings/import_data_collection_view_controller.h"
 #import "ios/chrome/browser/ui/settings/settings_collection_view_controller.h"
@@ -170,6 +171,14 @@
                     nil);
 }
 
+id<GREYMatcher> SyncSwitchCell(NSString* accessibilityLabel, BOOL is_on) {
+  return grey_allOf(grey_accessibilityLabel(accessibilityLabel),
+                    grey_accessibilityValue(
+                        is_on ? l10n_util::GetNSString(IDS_IOS_SETTING_ON)
+                              : l10n_util::GetNSString(IDS_IOS_SETTING_OFF)),
+                    grey_sufficientlyVisible(), nil);
+}
+
 id<GREYMatcher> OpenLinkInNewTabButton() {
   return ButtonWithAccessibilityLabelId(IDS_IOS_CONTENT_CONTEXT_OPENLINKNEWTAB);
 }
diff --git a/services/metrics/public/cpp/ukm_recorder.cc b/services/metrics/public/cpp/ukm_recorder.cc
index 58b87eee..70a4c51 100644
--- a/services/metrics/public/cpp/ukm_recorder.cc
+++ b/services/metrics/public/cpp/ukm_recorder.cc
@@ -13,11 +13,7 @@
 
 namespace ukm {
 
-#if defined(OS_IOS)
-const base::Feature kUkmFeature = {"Ukm", base::FEATURE_DISABLED_BY_DEFAULT};
-#else
 const base::Feature kUkmFeature = {"Ukm", base::FEATURE_ENABLED_BY_DEFAULT};
-#endif
 
 UkmRecorder::UkmRecorder() = default;