Make sure add-to-app build bundle from outer xcodebuild/gradlew sends analytics (#36122)

diff --git a/dev/devicelab/bin/tasks/module_test.dart b/dev/devicelab/bin/tasks/module_test.dart
index 0eb67c3..1b6e891 100644
--- a/dev/devicelab/bin/tasks/module_test.dart
+++ b/dev/devicelab/bin/tasks/module_test.dart
@@ -154,13 +154,18 @@
         Directory(path.join(hostApp.path, 'gradle', 'wrapper')),
       );
 
+      final File analyticsOutputFile = File(path.join(tempDir.path, 'analytics.log'));
+
       await inDirectory(hostApp, () async {
         if (!Platform.isWindows) {
           await exec('chmod', <String>['+x', 'gradlew']);
         }
         await exec(gradlewExecutable,
           <String>['app:assembleDebug'],
-          environment: <String, String>{ 'JAVA_HOME': javaHome },
+          environment: <String, String>{
+            'JAVA_HOME': javaHome,
+            'FLUTTER_ANALYTICS_LOG_FILE': analyticsOutputFile.path,
+          },
         );
       });
 
@@ -173,10 +178,21 @@
         'debug',
         'app-debug.apk',
       )));
-
       if (!existingAppBuilt) {
         return TaskResult.failure('Failed to build existing app .apk');
       }
+
+      final String analyticsOutput = analyticsOutputFile.readAsStringSync();
+      if (!analyticsOutput.contains('cd24: android-arm64')
+          || !analyticsOutput.contains('cd25: true')
+          || !analyticsOutput.contains('viewName: build/bundle')) {
+        return TaskResult.failure(
+          'Building outer app produced the following analytics: "$analyticsOutput"'
+          'but not the expected strings: "cd24: android-arm64", "cd25: true" and '
+          '"viewName: build/bundle"'
+        );
+      }
+
       return TaskResult.success(null);
     } catch (e) {
       return TaskResult.failure(e.toString());
diff --git a/dev/devicelab/bin/tasks/module_test_ios.dart b/dev/devicelab/bin/tasks/module_test_ios.dart
index a54b555..ec71e03 100644
--- a/dev/devicelab/bin/tasks/module_test_ios.dart
+++ b/dev/devicelab/bin/tasks/module_test_ios.dart
@@ -219,6 +219,8 @@
         hostApp,
       );
 
+      final File analyticsOutputFile = File(path.join(tempDir.path, 'analytics.log'));
+
       await inDirectory(hostApp, () async {
         await exec('pod', <String>['install']);
         await exec(
@@ -236,6 +238,9 @@
             'EXPANDED_CODE_SIGN_IDENTITY=-',
             'CONFIGURATION_BUILD_DIR=${tempDir.path}',
           ],
+          environment: <String, String> {
+            'FLUTTER_ANALYTICS_LOG_FILE': analyticsOutputFile.path,
+          }
         );
       });
 
@@ -244,11 +249,20 @@
         'Host.app',
         'Host',
       )));
-
       if (!existingAppBuilt) {
         return TaskResult.failure('Failed to build existing app .app');
       }
 
+      final String analyticsOutput = analyticsOutputFile.readAsStringSync();
+      if (!analyticsOutput.contains('cd24: ios')
+          || !analyticsOutput.contains('cd25: true')
+          || !analyticsOutput.contains('viewName: build/bundle')) {
+        return TaskResult.failure(
+          'Building outer app produced the following analytics: "$analyticsOutput"'
+          'but not the expected strings: "cd24: ios", "cd25: true", "viewName: build/bundle"'
+        );
+      }
+
       return TaskResult.success(null);
     } catch (e) {
       return TaskResult.failure(e.toString());
diff --git a/packages/flutter_tools/bin/xcode_backend.sh b/packages/flutter_tools/bin/xcode_backend.sh
index 0863b15..cd8299a 100755
--- a/packages/flutter_tools/bin/xcode_backend.sh
+++ b/packages/flutter_tools/bin/xcode_backend.sh
@@ -249,7 +249,7 @@
   fi
 
   StreamOutput " ├─Assembling Flutter resources..."
-  RunCommand "${FLUTTER_ROOT}/bin/flutter"                                  \
+  RunCommand "${FLUTTER_ROOT}/bin/flutter"     \
     ${verbose_flag}                                                         \
     build bundle                                                            \
     --target-platform=ios                                                   \
diff --git a/packages/flutter_tools/gradle/flutter.gradle b/packages/flutter_tools/gradle/flutter.gradle
index 372a1a0..52317dc 100644
--- a/packages/flutter_tools/gradle/flutter.gradle
+++ b/packages/flutter_tools/gradle/flutter.gradle
@@ -223,7 +223,7 @@
                             initWith debug
                         }
                     }
-                    
+
                     pluginProject.android.buildTypes.each {
                         def buildMode = buildModeFor(it)
                         addFlutterJarCompileOnlyDependency(pluginProject, it.name, project.files( flutterJar ?: baseJar[buildMode] ))
@@ -652,6 +652,7 @@
         project.exec {
             executable flutterExecutable.absolutePath
             workingDir sourceDir
+
             if (localEngine != null) {
                 args "--local-engine", localEngine
                 args "--local-engine-src-path", localEngineSrcPath
diff --git a/packages/flutter_tools/lib/src/base/utils.dart b/packages/flutter_tools/lib/src/base/utils.dart
index ebdc745..2cfbf7a 100644
--- a/packages/flutter_tools/lib/src/base/utils.dart
+++ b/packages/flutter_tools/lib/src/base/utils.dart
@@ -27,6 +27,8 @@
 
         // Set by the IDEs to the IDE name, so a strong signal that this is not a bot.
         || platform.environment.containsKey('FLUTTER_HOST')
+        // When set, GA logs to a local file (normally for tests) so we don't need to filter.
+        || platform.environment.containsKey('FLUTTER_ANALYTICS_LOG_FILE')
     ) {
       return false;
     }
diff --git a/packages/flutter_tools/lib/src/reporting/usage.dart b/packages/flutter_tools/lib/src/reporting/usage.dart
index fbd6604..2cd2138 100644
--- a/packages/flutter_tools/lib/src/reporting/usage.dart
+++ b/packages/flutter_tools/lib/src/reporting/usage.dart
@@ -69,8 +69,18 @@
   Usage({ String settingsName = 'flutter', String versionOverride, String configDirOverride}) {
     final FlutterVersion flutterVersion = FlutterVersion.instance;
     final String version = versionOverride ?? flutterVersion.getVersionString(redactUnknownBranches: true);
-    _analytics = AnalyticsIO(_kFlutterUA, settingsName, version,
-        documentDirectory: configDirOverride != null ? fs.directory(configDirOverride) : null);
+
+    final String logFilePath = platform.environment['FLUTTER_ANALYTICS_LOG_FILE'];
+
+    _analytics = logFilePath == null || logFilePath.isEmpty ?
+        AnalyticsIO(
+          _kFlutterUA,
+          settingsName,
+          version,
+          documentDirectory: configDirOverride != null ? fs.directory(configDirOverride) : null,
+        ) :
+        // Used for testing.
+        LogToFileAnalytics(logFilePath);
 
     // Report a more detailed OS version string than package:usage does by default.
     _analytics.setSessionValue(kSessionHostOsDetails, os.name);
@@ -94,6 +104,7 @@
     _analytics.analyticsOpt = AnalyticsOpt.optOut;
 
     final bool suppressEnvFlag = platform.environment['FLUTTER_SUPPRESS_ANALYTICS'] == 'true';
+    _analytics.sendScreenView('version is $version, is bot $isRunningOnBot, suppressed $suppressEnvFlag');
     // Many CI systems don't do a full git checkout.
     if (version.endsWith('/unknown') || isRunningOnBot || suppressEnvFlag) {
       // If we think we're running on a CI system, suppress sending analytics.
@@ -213,3 +224,22 @@
   ''', emphasis: true);
   }
 }
+
+// An Analytics mock that logs to file. Unimplemented methods goes to stdout.
+// But stdout can't be used for testing since wrapper scripts like
+// xcode_backend.sh etc manipulates them.
+class LogToFileAnalytics extends AnalyticsMock {
+  LogToFileAnalytics(String logFilePath) :
+    logFile = fs.file(logFilePath)..createSync(recursive: true),
+    super(true);
+
+  final File logFile;
+
+  @override
+  Future<void> sendScreenView(String viewName, {Map<String, String> parameters}) {
+    parameters ??= <String, String>{};
+    parameters['viewName'] = viewName;
+    logFile.writeAsStringSync('screenView $parameters\n');
+    return Future<void>.value(null);
+  }
+}
diff --git a/packages/flutter_tools/test/general.shard/base/utils_test.dart b/packages/flutter_tools/test/general.shard/base/utils_test.dart
index 9046c5e..6a98157 100644
--- a/packages/flutter_tools/test/general.shard/base/utils_test.dart
+++ b/packages/flutter_tools/test/general.shard/base/utils_test.dart
@@ -51,6 +51,15 @@
         Stdio: () => mockStdio,
         Platform: () => fakePlatform,
       });
+
+      testUsingContext('can test analytics outputs on bots when outputting to a file', () async {
+        fakePlatform.environment['TRAVIS'] = 'true';
+        fakePlatform.environment['FLUTTER_ANALYTICS_LOG_FILE'] = '/some/file';
+        expect(botDetector.isRunningOnBot, isFalse);
+      }, overrides: <Type, Generator>{
+        Stdio: () => mockStdio,
+        Platform: () => fakePlatform,
+      });
     });
   });
 }