[unified_analytics] Send `enabled_features` as an event parameter rather than a user property (#2007)

Fixes https://github.com/flutter/flutter/issues/147327.

**Summary.** The `Analytics` constructor accepts a list of "enabled features" for the current user of the whatever tool is in use ([src](https://github.com/dart-lang/tools/blob/3934675f1595ed2fee2297d7d1cc96beaaa50c9e/pkgs/unified_analytics/lib/src/analytics.dart#L39-L42)). For example, in the Flutter tool, this will be a comma-delimited list of `flutter config` items set to `true` (explicitly by the user). When any event is sent off to Google Analytics, this list will be included as a _user property_ with the key "enabled_features".

See the parent issue. This list is prone to getting truncated due to the low character limit of user property values. This PR instead sends them as event properties. This has a _much_ higher character limit of 500, which is hopefully sufficient (even if it is not, it would be a huge improvement over the current limit of 36, which makes collecting enabled features near-useless).

However, this creates a pitfall. If any new `Event` decides to include an `enabled_features` event property (and also passes a non-null argument for the `enabledFeatures` parameter in the `Analytics` constructor). There are a few ways handle this:

1. Do nothing. The likelihood of this happening is extremely low (but it will be quite confusing if it does happen).
2. Create an `Event._` constructor that throws an exception if the `eventData` map includes an entry with a key of `enabled_features`. Re-route all of the existing constructors to utilize this one. This working depends on newly added events following the pattern of using `Event._` instead of doing initialization themselves; but I suspect folks adding new events will naturally use the existing ones for inspiration, so this is fine to me.
3. Similar to 2, add this validation logic but instead do so in `generateRequestBody`. This keeps the validation logic much closer to the logic that inserts `enabled_features`, so it will make sense to first-time maintainers slightly more quickly. This also would require the least work to implement. The downside is that no exception would appear until `send` is called, and that exception would be thrown asynchronously.

I suspect 2 is probably the way to go, so I went ahead and implemented that. Let me know if you have a differing opinion. I made this is a separate commit, so it would be easy to revert.

---

<details>
  <summary>Contribution guidelines:</summary><br>

- See our [contributor guide](https://github.com/dart-lang/.github/blob/main/CONTRIBUTING.md) for general expectations for PRs.
- Larger or significant changes should be discussed in an issue before creating a PR.
- Contributions to our repos should follow the [Dart style guide](https://dart.dev/guides/language/effective-dart) and use `dart format`.
- Most changes should add an entry to the changelog and may need to [rev the pubspec package version](https://github.com/dart-lang/sdk/blob/main/docs/External-Package-Maintenance.md#making-a-change).
- Changes to packages require [corresponding tests](https://github.com/dart-lang/.github/blob/main/CONTRIBUTING.md#Testing).

Note that many Dart repos have a weekly cadence for reviewing PRs - please allow for some latency before initial review feedback.
</details>
diff --git a/pkgs/unified_analytics/CHANGELOG.md b/pkgs/unified_analytics/CHANGELOG.md
index 7e42cba..5c7333c 100644
--- a/pkgs/unified_analytics/CHANGELOG.md
+++ b/pkgs/unified_analytics/CHANGELOG.md
@@ -1,3 +1,6 @@
+## 7.0.2
+- Send `enabled_features` as an event parameter in all events rather than as a user property.
+
 ## 7.0.1
 - Fixed `UnsupportedError` thrown when Event.exception is called without providing a value for `args`.
 
diff --git a/pkgs/unified_analytics/lib/src/analytics.dart b/pkgs/unified_analytics/lib/src/analytics.dart
index cc8b4bc..8b9fc11 100644
--- a/pkgs/unified_analytics/lib/src/analytics.dart
+++ b/pkgs/unified_analytics/lib/src/analytics.dart
@@ -339,6 +339,7 @@
   final File _clientIdFile;
   final UserProperty _userProperty;
   final LogHandler _logHandler;
+  final String? _enabledFeatures;
 
   /// Tells the client if they need to show a message to the
   /// user; this will return true if it is the first time the
@@ -406,8 +407,8 @@
               truncateStringToLength(io.Platform.operatingSystemVersion, 36),
           locale: io.Platform.localeName,
           clientIde: clientIde,
-          enabledFeatures: enabledFeatures,
         ),
+        _enabledFeatures = enabledFeatures,
         _configHandler = ConfigHandler(
           homeDirectory: homeDirectory,
           configFile: homeDirectory
@@ -613,6 +614,7 @@
       eventName: event.eventName,
       eventData: event.eventData,
       userProperty: _userProperty,
+      enabledFeatures: _enabledFeatures,
     );
 
     if (_enableAsserts) checkBody(body);
@@ -654,6 +656,7 @@
         eventName: collectionEvent.eventName,
         eventData: collectionEvent.eventData,
         userProperty: _userProperty,
+        enabledFeatures: _enabledFeatures,
       );
 
       _logHandler.save(data: body);
@@ -664,6 +667,7 @@
         clientId: clientId,
         eventName: collectionEvent.eventName,
         eventData: collectionEvent.eventData,
+        enabledFeatures: _enabledFeatures,
         userProperty: _userProperty,
       );
 
@@ -774,6 +778,7 @@
       eventName: event.eventName,
       eventData: event.eventData,
       userProperty: _userProperty,
+      enabledFeatures: _enabledFeatures,
     );
 
     if (_enableAsserts) checkBody(body);
diff --git a/pkgs/unified_analytics/lib/src/constants.dart b/pkgs/unified_analytics/lib/src/constants.dart
index 2be4d0a..c78c6c2 100644
--- a/pkgs/unified_analytics/lib/src/constants.dart
+++ b/pkgs/unified_analytics/lib/src/constants.dart
@@ -87,7 +87,7 @@
 const String kLogFileName = 'dart-flutter-telemetry.log';
 
 /// The current version of the package, should be in line with pubspec version.
-const String kPackageVersion = '7.0.1';
+const String kPackageVersion = '7.0.2';
 
 /// The minimum length for a session.
 const int kSessionDurationMinutes = 30;
diff --git a/pkgs/unified_analytics/lib/src/event.dart b/pkgs/unified_analytics/lib/src/event.dart
index ed30665..158ea97 100644
--- a/pkgs/unified_analytics/lib/src/event.dart
+++ b/pkgs/unified_analytics/lib/src/event.dart
@@ -8,15 +8,27 @@
 
 final class Event {
   final DashEvent eventName;
-  final Map<String, Object?> eventData;
+  late final Map<String, Object?> eventData;
+
+  Event._({required this.eventName, required this.eventData}) {
+    if (eventData.containsKey('enabled_features')) {
+      throw ArgumentError.value(
+        eventData,
+        'eventData',
+        'The enabled_features key is reserved and cannot '
+            "be used as part of an event's data.",
+      );
+    }
+  }
 
   /// Event that is emitted whenever a user has opted in
   /// or out of the analytics collection.
   ///
   /// [status] - boolean value where `true` indicates user is opting in.
   Event.analyticsCollectionEnabled({required bool status})
-      : eventName = DashEvent.analyticsCollectionEnabled,
-        eventData = {'status': status};
+      : this._(
+            eventName: DashEvent.analyticsCollectionEnabled,
+            eventData: {'status': status});
 
   /// Event that is emitted when an error occurs within
   /// `package:unified_analytics`, tools that are using this package
@@ -35,12 +47,14 @@
     required String workflow,
     required String error,
     String? description,
-  })  : eventName = DashEvent.analyticsException,
-        eventData = {
-          'workflow': workflow,
-          'error': error,
-          if (description != null) 'description': description,
-        };
+  }) : this._(
+          eventName: DashEvent.analyticsException,
+          eventData: {
+            'workflow': workflow,
+            'error': error,
+            if (description != null) 'description': description,
+          },
+        );
 
   /// This is for various workflows within the flutter tool related
   /// to iOS and macOS workflows.
@@ -54,12 +68,14 @@
     required String workflow,
     required String parameter,
     String? result,
-  })  : eventName = DashEvent.appleUsageEvent,
-        eventData = {
-          'workflow': workflow,
-          'parameter': parameter,
-          if (result != null) 'result': result,
-        };
+  }) : this._(
+          eventName: DashEvent.appleUsageEvent,
+          eventData: {
+            'workflow': workflow,
+            'parameter': parameter,
+            if (result != null) 'result': result,
+          },
+        );
 
   /// Event that is emitted periodically to report the performance of the
   /// analysis server's handling of a specific kind of notification from the
@@ -78,12 +94,14 @@
     required String duration,
     required String latency,
     required String method,
-  })  : eventName = DashEvent.clientNotification,
-        eventData = {
-          'duration': duration,
-          'latency': latency,
-          'method': method,
-        };
+  }) : this._(
+          eventName: DashEvent.clientNotification,
+          eventData: {
+            'duration': duration,
+            'latency': latency,
+            'method': method,
+          },
+        );
 
   /// Event that is emitted periodically to report the performance of the
   /// analysis server's handling of a specific kind of request from the client.
@@ -138,19 +156,21 @@
     String? included,
     String? openWorkspacePaths,
     String? removed,
-  })  : eventName = DashEvent.clientRequest,
-        eventData = {
-          if (added != null) 'added': added,
-          'duration': duration,
-          if (excluded != null) 'excluded': excluded,
-          if (files != null) 'files': files,
-          if (included != null) 'included': included,
-          'latency': latency,
-          'method': method,
-          if (openWorkspacePaths != null)
-            'openWorkspacePaths': openWorkspacePaths,
-          if (removed != null) 'removed': removed,
-        };
+  }) : this._(
+          eventName: DashEvent.clientRequest,
+          eventData: {
+            if (added != null) 'added': added,
+            'duration': duration,
+            if (excluded != null) 'excluded': excluded,
+            if (files != null) 'files': files,
+            if (included != null) 'included': included,
+            'latency': latency,
+            'method': method,
+            if (openWorkspacePaths != null)
+              'openWorkspacePaths': openWorkspacePaths,
+            if (removed != null) 'removed': removed,
+          },
+        );
 
   /// An event that reports when the code size measurement is run
   /// via `--analyze-size`.
@@ -158,10 +178,12 @@
   /// [platform] - string identifier for which platform was run "ios", "apk",
   ///   "aab", etc.
   Event.codeSizeAnalysis({required String platform})
-      : eventName = DashEvent.codeSizeAnalysis,
-        eventData = {
-          'platform': platform,
-        };
+      : this._(
+          eventName: DashEvent.codeSizeAnalysis,
+          eventData: {
+            'platform': platform,
+          },
+        );
 
   /// Event that is emitted periodically to report the number of times a given
   /// command has been executed.
@@ -172,11 +194,13 @@
   Event.commandExecuted({
     required int count,
     required String name,
-  })  : eventName = DashEvent.commandExecuted,
-        eventData = {
-          'count': count,
-          'name': name,
-        };
+  }) : this._(
+          eventName: DashEvent.commandExecuted,
+          eventData: {
+            'count': count,
+            'name': name,
+          },
+        );
 
   /// Event to capture usage values for different flutter commands.
   ///
@@ -228,52 +252,59 @@
     bool? runEnableImpeller,
     String? runIOSInterfaceType,
     bool? runIsTest,
-  })  : eventName = DashEvent.commandUsageValues,
-        eventData = {
-          'workflow': workflow,
-          'commandHasTerminal': commandHasTerminal,
-          if (buildBundleTargetPlatform != null)
-            'buildBundleTargetPlatform': buildBundleTargetPlatform,
-          if (buildBundleIsModule != null)
-            'buildBundleIsModule': buildBundleIsModule,
-          if (buildAarProjectType != null)
-            'buildAarProjectType': buildAarProjectType,
-          if (buildAarTargetPlatform != null)
-            'buildAarTargetPlatform': buildAarTargetPlatform,
-          if (buildApkTargetPlatform != null)
-            'buildApkTargetPlatform': buildApkTargetPlatform,
-          if (buildApkBuildMode != null) 'buildApkBuildMode': buildApkBuildMode,
-          if (buildApkSplitPerAbi != null)
-            'buildApkSplitPerAbi': buildApkSplitPerAbi,
-          if (buildAppBundleTargetPlatform != null)
-            'buildAppBundleTargetPlatform': buildAppBundleTargetPlatform,
-          if (buildAppBundleBuildMode != null)
-            'buildAppBundleBuildMode': buildAppBundleBuildMode,
-          if (createProjectType != null) 'createProjectType': createProjectType,
-          if (createAndroidLanguage != null)
-            'createAndroidLanguage': createAndroidLanguage,
-          if (createIosLanguage != null) 'createIosLanguage': createIosLanguage,
-          if (packagesNumberPlugins != null)
-            'packagesNumberPlugins': packagesNumberPlugins,
-          if (packagesProjectModule != null)
-            'packagesProjectModule': packagesProjectModule,
-          if (packagesAndroidEmbeddingVersion != null)
-            'packagesAndroidEmbeddingVersion': packagesAndroidEmbeddingVersion,
-          if (runIsEmulator != null) 'runIsEmulator': runIsEmulator,
-          if (runTargetName != null) 'runTargetName': runTargetName,
-          if (runTargetOsVersion != null)
-            'runTargetOsVersion': runTargetOsVersion,
-          if (runModeName != null) 'runModeName': runModeName,
-          if (runProjectModule != null) 'runProjectModule': runProjectModule,
-          if (runProjectHostLanguage != null)
-            'runProjectHostLanguage': runProjectHostLanguage,
-          if (runAndroidEmbeddingVersion != null)
-            'runAndroidEmbeddingVersion': runAndroidEmbeddingVersion,
-          if (runEnableImpeller != null) 'runEnableImpeller': runEnableImpeller,
-          if (runIOSInterfaceType != null)
-            'runIOSInterfaceType': runIOSInterfaceType,
-          if (runIsTest != null) 'runIsTest': runIsTest,
-        };
+  }) : this._(
+          eventName: DashEvent.commandUsageValues,
+          eventData: {
+            'workflow': workflow,
+            'commandHasTerminal': commandHasTerminal,
+            if (buildBundleTargetPlatform != null)
+              'buildBundleTargetPlatform': buildBundleTargetPlatform,
+            if (buildBundleIsModule != null)
+              'buildBundleIsModule': buildBundleIsModule,
+            if (buildAarProjectType != null)
+              'buildAarProjectType': buildAarProjectType,
+            if (buildAarTargetPlatform != null)
+              'buildAarTargetPlatform': buildAarTargetPlatform,
+            if (buildApkTargetPlatform != null)
+              'buildApkTargetPlatform': buildApkTargetPlatform,
+            if (buildApkBuildMode != null)
+              'buildApkBuildMode': buildApkBuildMode,
+            if (buildApkSplitPerAbi != null)
+              'buildApkSplitPerAbi': buildApkSplitPerAbi,
+            if (buildAppBundleTargetPlatform != null)
+              'buildAppBundleTargetPlatform': buildAppBundleTargetPlatform,
+            if (buildAppBundleBuildMode != null)
+              'buildAppBundleBuildMode': buildAppBundleBuildMode,
+            if (createProjectType != null)
+              'createProjectType': createProjectType,
+            if (createAndroidLanguage != null)
+              'createAndroidLanguage': createAndroidLanguage,
+            if (createIosLanguage != null)
+              'createIosLanguage': createIosLanguage,
+            if (packagesNumberPlugins != null)
+              'packagesNumberPlugins': packagesNumberPlugins,
+            if (packagesProjectModule != null)
+              'packagesProjectModule': packagesProjectModule,
+            if (packagesAndroidEmbeddingVersion != null)
+              'packagesAndroidEmbeddingVersion':
+                  packagesAndroidEmbeddingVersion,
+            if (runIsEmulator != null) 'runIsEmulator': runIsEmulator,
+            if (runTargetName != null) 'runTargetName': runTargetName,
+            if (runTargetOsVersion != null)
+              'runTargetOsVersion': runTargetOsVersion,
+            if (runModeName != null) 'runModeName': runModeName,
+            if (runProjectModule != null) 'runProjectModule': runProjectModule,
+            if (runProjectHostLanguage != null)
+              'runProjectHostLanguage': runProjectHostLanguage,
+            if (runAndroidEmbeddingVersion != null)
+              'runAndroidEmbeddingVersion': runAndroidEmbeddingVersion,
+            if (runEnableImpeller != null)
+              'runEnableImpeller': runEnableImpeller,
+            if (runIOSInterfaceType != null)
+              'runIOSInterfaceType': runIOSInterfaceType,
+            if (runIsTest != null) 'runIsTest': runIsTest,
+          },
+        );
 
   /// Event that is emitted on shutdown to report the structure of the analysis
   /// contexts created immediately after startup.
@@ -322,20 +353,22 @@
     required int transitiveFileLineCount,
     required int transitiveFileUniqueCount,
     required int transitiveFileUniqueLineCount,
-  })  : eventName = DashEvent.contextStructure,
-        eventData = {
-          'contextsFromBothFiles': contextsFromBothFiles,
-          'contextsFromOptionsFiles': contextsFromOptionsFiles,
-          'contextsFromPackagesFiles': contextsFromPackagesFiles,
-          'contextsWithoutFiles': contextsWithoutFiles,
-          'immediateFileCount': immediateFileCount,
-          'immediateFileLineCount': immediateFileLineCount,
-          'numberOfContexts': numberOfContexts,
-          'transitiveFileCount': transitiveFileCount,
-          'transitiveFileLineCount': transitiveFileLineCount,
-          'transitiveFileUniqueCount': transitiveFileUniqueCount,
-          'transitiveFileUniqueLineCount': transitiveFileUniqueLineCount,
-        };
+  }) : this._(
+          eventName: DashEvent.contextStructure,
+          eventData: {
+            'contextsFromBothFiles': contextsFromBothFiles,
+            'contextsFromOptionsFiles': contextsFromOptionsFiles,
+            'contextsFromPackagesFiles': contextsFromPackagesFiles,
+            'contextsWithoutFiles': contextsWithoutFiles,
+            'immediateFileCount': immediateFileCount,
+            'immediateFileLineCount': immediateFileLineCount,
+            'numberOfContexts': numberOfContexts,
+            'transitiveFileCount': transitiveFileCount,
+            'transitiveFileLineCount': transitiveFileLineCount,
+            'transitiveFileUniqueCount': transitiveFileUniqueCount,
+            'transitiveFileUniqueLineCount': transitiveFileUniqueLineCount,
+          },
+        );
 
   /// Event that is emitted when a Dart CLI command has been executed.
   ///
@@ -349,12 +382,14 @@
     required String name,
     required String enabledExperiments,
     int? exitCode,
-  })  : eventName = DashEvent.dartCliCommandExecuted,
-        eventData = {
-          'name': name,
-          'enabledExperiments': enabledExperiments,
-          if (exitCode != null) 'exitCode': exitCode,
-        };
+  }) : this._(
+          eventName: DashEvent.dartCliCommandExecuted,
+          eventData: {
+            'name': name,
+            'enabledExperiments': enabledExperiments,
+            if (exitCode != null) 'exitCode': exitCode,
+          },
+        );
 
   /// Event that is sent from DevTools for various different actions as
   /// indicated by the [eventCategory].
@@ -388,31 +423,33 @@
     String? ideLaunchedFeature,
     String? isWasm,
     CustomMetrics? additionalMetrics,
-  })  : eventName = DashEvent.devtoolsEvent,
-        eventData = {
-          'screen': screen,
-          'eventCategory': eventCategory,
-          'label': label,
-          'value': value,
+  }) : this._(
+          eventName: DashEvent.devtoolsEvent,
+          eventData: {
+            'screen': screen,
+            'eventCategory': eventCategory,
+            'label': label,
+            'value': value,
 
-          'userInitiatedInteraction': userInitiatedInteraction,
+            'userInitiatedInteraction': userInitiatedInteraction,
 
-          // Optional parameters
-          if (g3Username != null) 'g3Username': g3Username,
-          if (userApp != null) 'userApp': userApp,
-          if (userBuild != null) 'userBuild': userBuild,
-          if (userPlatform != null) 'userPlatform': userPlatform,
-          if (devtoolsPlatform != null) 'devtoolsPlatform': devtoolsPlatform,
-          if (devtoolsChrome != null) 'devtoolsChrome': devtoolsChrome,
-          if (devtoolsVersion != null) 'devtoolsVersion': devtoolsVersion,
-          if (ideLaunched != null) 'ideLaunched': ideLaunched,
-          if (isExternalBuild != null) 'isExternalBuild': isExternalBuild,
-          if (isEmbedded != null) 'isEmbedded': isEmbedded,
-          if (ideLaunchedFeature != null)
-            'ideLaunchedFeature': ideLaunchedFeature,
-          if (isWasm != null) 'isWasm': isWasm,
-          if (additionalMetrics != null) ...additionalMetrics.toMap(),
-        };
+            // Optional parameters
+            if (g3Username != null) 'g3Username': g3Username,
+            if (userApp != null) 'userApp': userApp,
+            if (userBuild != null) 'userBuild': userBuild,
+            if (userPlatform != null) 'userPlatform': userPlatform,
+            if (devtoolsPlatform != null) 'devtoolsPlatform': devtoolsPlatform,
+            if (devtoolsChrome != null) 'devtoolsChrome': devtoolsChrome,
+            if (devtoolsVersion != null) 'devtoolsVersion': devtoolsVersion,
+            if (ideLaunched != null) 'ideLaunched': ideLaunched,
+            if (isExternalBuild != null) 'isExternalBuild': isExternalBuild,
+            if (isEmbedded != null) 'isEmbedded': isEmbedded,
+            if (ideLaunchedFeature != null)
+              'ideLaunchedFeature': ideLaunchedFeature,
+            if (isWasm != null) 'isWasm': isWasm,
+            if (additionalMetrics != null) ...additionalMetrics.toMap(),
+          },
+        );
 
   /// Event that contains the results for a specific doctor validator.
   ///
@@ -435,14 +472,16 @@
     required bool partOfGroupedValidator,
     required int doctorInvocationId,
     String? statusInfo,
-  })  : eventName = DashEvent.doctorValidatorResult,
-        eventData = {
-          'validatorName': validatorName,
-          'result': result,
-          'partOfGroupedValidator': partOfGroupedValidator,
-          'doctorInvocationId': doctorInvocationId,
-          if (statusInfo != null) 'statusInfo': statusInfo,
-        };
+  }) : this._(
+          eventName: DashEvent.doctorValidatorResult,
+          eventData: {
+            'validatorName': validatorName,
+            'result': result,
+            'partOfGroupedValidator': partOfGroupedValidator,
+            'doctorInvocationId': doctorInvocationId,
+            if (statusInfo != null) 'statusInfo': statusInfo,
+          },
+        );
 
   /// Generic event for all dash tools to use when encountering an
   /// exception that we want to log.
@@ -452,11 +491,13 @@
   Event.exception({
     required String exception,
     Map<String, Object?> data = const <String, Object?>{},
-  })  : eventName = DashEvent.exception,
-        eventData = {
-          'exception': exception,
-          ...Map.from(data)..removeWhere((key, value) => value == null),
-        };
+  }) : this._(
+          eventName: DashEvent.exception,
+          eventData: {
+            'exception': exception,
+            ...Map.from(data)..removeWhere((key, value) => value == null),
+          },
+        );
 
   /// Event that is emitted from the flutter tool when a build invocation
   /// has been run by the user.
@@ -479,14 +520,16 @@
     String? command,
     String? settings,
     String? error,
-  })  : eventName = DashEvent.flutterBuildInfo,
-        eventData = {
-          'label': label,
-          'buildType': buildType,
-          if (command != null) 'command': command,
-          if (settings != null) 'settings': settings,
-          if (error != null) 'error': error,
-        };
+  }) : this._(
+          eventName: DashEvent.flutterBuildInfo,
+          eventData: {
+            'label': label,
+            'buildType': buildType,
+            if (command != null) 'command': command,
+            if (settings != null) 'settings': settings,
+            if (error != null) 'error': error,
+          },
+        );
 
   /// Provides information about which flutter command was run
   /// and whether it was successful.
@@ -504,19 +547,23 @@
     required String result,
     required bool commandHasTerminal,
     int? maxRss,
-  })  : eventName = DashEvent.flutterCommandResult,
-        eventData = {
-          'commandPath': commandPath,
-          'result': result,
-          'commandHasTerminal': commandHasTerminal,
-          if (maxRss != null) 'maxRss': maxRss,
-        };
+  }) : this._(
+          eventName: DashEvent.flutterCommandResult,
+          eventData: {
+            'commandPath': commandPath,
+            'result': result,
+            'commandHasTerminal': commandHasTerminal,
+            if (maxRss != null) 'maxRss': maxRss,
+          },
+        );
 
   // TODO: eliasyishak, remove this or replace once we have a generic
   //  timing event that can be used by potentially more than one DashTool
   Event.hotReloadTime({required int timeMs})
-      : eventName = DashEvent.hotReloadTime,
-        eventData = {'timeMs': timeMs};
+      : this._(
+          eventName: DashEvent.hotReloadTime,
+          eventData: {'timeMs': timeMs},
+        );
 
   /// Events to be sent for the Flutter Hot Runner.
   Event.hotRunnerInfo({
@@ -539,37 +586,39 @@
     int? scannedSourcesCount,
     int? reassembleTimeInMs,
     int? reloadVMTimeInMs,
-  })  : eventName = DashEvent.hotRunnerInfo,
-        eventData = {
-          'label': label,
-          'targetPlatform': targetPlatform,
-          'sdkName': sdkName,
-          'emulator': emulator,
-          'fullRestart': fullRestart,
-          if (reason != null) 'reason': reason,
-          if (finalLibraryCount != null) 'finalLibraryCount': finalLibraryCount,
-          if (syncedLibraryCount != null)
-            'syncedLibraryCount': syncedLibraryCount,
-          if (syncedClassesCount != null)
-            'syncedClassesCount': syncedClassesCount,
-          if (syncedProceduresCount != null)
-            'syncedProceduresCount': syncedProceduresCount,
-          if (syncedBytes != null) 'syncedBytes': syncedBytes,
-          if (invalidatedSourcesCount != null)
-            'invalidatedSourcesCount': invalidatedSourcesCount,
-          if (transferTimeInMs != null) 'transferTimeInMs': transferTimeInMs,
-          if (overallTimeInMs != null) 'overallTimeInMs': overallTimeInMs,
-          if (compileTimeInMs != null) 'compileTimeInMs': compileTimeInMs,
-          if (findInvalidatedTimeInMs != null)
-            'findInvalidatedTimeInMs': findInvalidatedTimeInMs,
-          if (scannedSourcesCount != null)
-            'scannedSourcesCount': scannedSourcesCount,
-          if (reassembleTimeInMs != null)
-            'reassembleTimeInMs': reassembleTimeInMs,
-          if (reloadVMTimeInMs != null) 'reloadVMTimeInMs': reloadVMTimeInMs,
-        };
+  }) : this._(
+          eventName: DashEvent.hotRunnerInfo,
+          eventData: {
+            'label': label,
+            'targetPlatform': targetPlatform,
+            'sdkName': sdkName,
+            'emulator': emulator,
+            'fullRestart': fullRestart,
+            if (reason != null) 'reason': reason,
+            if (finalLibraryCount != null)
+              'finalLibraryCount': finalLibraryCount,
+            if (syncedLibraryCount != null)
+              'syncedLibraryCount': syncedLibraryCount,
+            if (syncedClassesCount != null)
+              'syncedClassesCount': syncedClassesCount,
+            if (syncedProceduresCount != null)
+              'syncedProceduresCount': syncedProceduresCount,
+            if (syncedBytes != null) 'syncedBytes': syncedBytes,
+            if (invalidatedSourcesCount != null)
+              'invalidatedSourcesCount': invalidatedSourcesCount,
+            if (transferTimeInMs != null) 'transferTimeInMs': transferTimeInMs,
+            if (overallTimeInMs != null) 'overallTimeInMs': overallTimeInMs,
+            if (compileTimeInMs != null) 'compileTimeInMs': compileTimeInMs,
+            if (findInvalidatedTimeInMs != null)
+              'findInvalidatedTimeInMs': findInvalidatedTimeInMs,
+            if (scannedSourcesCount != null)
+              'scannedSourcesCount': scannedSourcesCount,
+            if (reassembleTimeInMs != null)
+              'reassembleTimeInMs': reassembleTimeInMs,
+            if (reloadVMTimeInMs != null) 'reloadVMTimeInMs': reloadVMTimeInMs,
+          },
+        );
 
-  // TODO: eliasyishak, add better dartdocs to explain each param
   /// Event that is emitted periodically to report the number of times each lint
   /// has been enabled.
   ///
@@ -579,11 +628,13 @@
   Event.lintUsageCount({
     required int count,
     required String name,
-  })  : eventName = DashEvent.lintUsageCount,
-        eventData = {
-          'count': count,
-          'name': name,
-        };
+  }) : this._(
+          eventName: DashEvent.lintUsageCount,
+          eventData: {
+            'count': count,
+            'name': name,
+          },
+        );
 
   /// Event that is emitted periodically to report the amount of memory being
   /// used.
@@ -602,12 +653,14 @@
     required int rss,
     int? periodSec,
     double? mbPerSec,
-  })  : eventName = DashEvent.memoryInfo,
-        eventData = {
-          'rss': rss,
-          if (periodSec != null) 'periodSec': periodSec,
-          if (mbPerSec != null) 'mbPerSec': mbPerSec
-        };
+  }) : this._(
+          eventName: DashEvent.memoryInfo,
+          eventData: {
+            'rss': rss,
+            if (periodSec != null) 'periodSec': periodSec,
+            if (mbPerSec != null) 'mbPerSec': mbPerSec
+          },
+        );
 
   /// Event that is emitted periodically to report the performance of plugins
   /// when handling requests.
@@ -623,12 +676,14 @@
     required String duration,
     required String method,
     required String pluginId,
-  })  : eventName = DashEvent.pluginRequest,
-        eventData = {
-          'duration': duration,
-          'method': method,
-          'pluginId': pluginId,
-        };
+  }) : this._(
+          eventName: DashEvent.pluginRequest,
+          eventData: {
+            'duration': duration,
+            'method': method,
+            'pluginId': pluginId,
+          },
+        );
 
   /// Event that is emitted periodically to report the frequency with which a
   /// given plugin has been used.
@@ -644,12 +699,14 @@
     required int count,
     required String enabled,
     required String pluginId,
-  })  : eventName = DashEvent.pluginUse,
-        eventData = {
-          'count': count,
-          'enabled': enabled,
-          'pluginId': pluginId,
-        };
+  }) : this._(
+          eventName: DashEvent.pluginUse,
+          eventData: {
+            'count': count,
+            'enabled': enabled,
+            'pluginId': pluginId,
+          },
+        );
 
   /// Event that is emitted when `pub get` is run.
   ///
@@ -663,12 +720,14 @@
     required String packageName,
     required String version,
     required String dependencyType,
-  })  : eventName = DashEvent.pubGet,
-        eventData = {
-          'packageName': packageName,
-          'version': version,
-          'dependencyType': dependencyType,
-        };
+  }) : this._(
+          eventName: DashEvent.pubGet,
+          eventData: {
+            'packageName': packageName,
+            'version': version,
+            'dependencyType': dependencyType,
+          },
+        );
 
   /// Event that is emitted on shutdown to report information about the whole
   /// session for which the analysis server was running.
@@ -692,14 +751,16 @@
     required int duration,
     required String flags,
     required String parameters,
-  })  : eventName = DashEvent.serverSession,
-        eventData = {
-          'clientId': clientId,
-          'clientVersion': clientVersion,
-          'duration': duration,
-          'flags': flags,
-          'parameters': parameters,
-        };
+  }) : this._(
+          eventName: DashEvent.serverSession,
+          eventData: {
+            'clientId': clientId,
+            'clientVersion': clientVersion,
+            'duration': duration,
+            'flags': flags,
+            'parameters': parameters,
+          },
+        );
 
   /// Event that is emitted periodically to report the number of times the
   /// severity of a diagnostic was changed in the analysis options file.
@@ -711,11 +772,13 @@
   Event.severityAdjustment({
     required String diagnostic,
     required String adjustments,
-  })  : eventName = DashEvent.severityAdjustment,
-        eventData = {
-          'diagnostic': diagnostic,
-          'adjustments': adjustments,
-        };
+  }) : this._(
+          eventName: DashEvent.severityAdjustment,
+          eventData: {
+            'diagnostic': diagnostic,
+            'adjustments': adjustments,
+          },
+        );
 
   /// Event that is emitted by `package:unified_analytics` when
   /// the user takes action when prompted with a survey.
@@ -727,11 +790,13 @@
   Event.surveyAction({
     required String surveyId,
     required String status,
-  })  : eventName = DashEvent.surveyAction,
-        eventData = {
-          'surveyId': surveyId,
-          'status': status,
-        };
+  }) : this._(
+          eventName: DashEvent.surveyAction,
+          eventData: {
+            'surveyId': surveyId,
+            'status': status,
+          },
+        );
 
   /// Event that is emitted by `package:unified_analytics` when the
   /// user has been shown a survey.
@@ -739,10 +804,12 @@
   /// [surveyId] - the unique id for a given survey.
   Event.surveyShown({
     required String surveyId,
-  })  : eventName = DashEvent.surveyShown,
-        eventData = {
-          'surveyId': surveyId,
-        };
+  }) : this._(
+          eventName: DashEvent.surveyShown,
+          eventData: {
+            'surveyId': surveyId,
+          },
+        );
 
   /// Event that records how long a given process takes to complete.
   ///
@@ -763,17 +830,15 @@
     required String variableName,
     required int elapsedMilliseconds,
     String? label,
-  })  : eventName = DashEvent.timing,
-        eventData = {
-          'workflow': workflow,
-          'variableName': variableName,
-          'elapsedMilliseconds': elapsedMilliseconds,
-          if (label != null) 'label': label,
-        };
-
-  /// Private constructor to be used when deserializing JSON into an instance
-  /// of [Event].
-  Event._({required this.eventName, required this.eventData});
+  }) : this._(
+          eventName: DashEvent.timing,
+          eventData: {
+            'workflow': workflow,
+            'variableName': variableName,
+            'elapsedMilliseconds': elapsedMilliseconds,
+            if (label != null) 'label': label,
+          },
+        );
 
   @override
   int get hashCode => Object.hash(eventName, jsonEncode(eventData));
diff --git a/pkgs/unified_analytics/lib/src/user_property.dart b/pkgs/unified_analytics/lib/src/user_property.dart
index f0e177d..a607b07e 100644
--- a/pkgs/unified_analytics/lib/src/user_property.dart
+++ b/pkgs/unified_analytics/lib/src/user_property.dart
@@ -21,7 +21,6 @@
   final String hostOsVersion;
   final String locale;
   final String? clientIde;
-  final String? enabledFeatures;
 
   final File sessionFile;
 
@@ -43,7 +42,6 @@
     required this.hostOsVersion,
     required this.locale,
     required this.clientIde,
-    required this.enabledFeatures,
     required this.sessionFile,
   });
 
@@ -157,6 +155,5 @@
         'host_os_version': hostOsVersion,
         'locale': locale,
         'client_ide': clientIde,
-        'enabled_features': enabledFeatures,
       };
 }
diff --git a/pkgs/unified_analytics/lib/src/utils.dart b/pkgs/unified_analytics/lib/src/utils.dart
index 41848a2..182facb 100644
--- a/pkgs/unified_analytics/lib/src/utils.dart
+++ b/pkgs/unified_analytics/lib/src/utils.dart
@@ -76,13 +76,17 @@
   required DashEvent eventName,
   required Map<String, Object?> eventData,
   required UserProperty userProperty,
+  required String? enabledFeatures,
 }) =>
     <String, Object?>{
       'client_id': clientId,
       'events': <Map<String, Object?>>[
         <String, Object?>{
           'name': eventName.label,
-          'params': eventData,
+          'params': {
+            ...eventData,
+            if (enabledFeatures != null) 'enabled_features': enabledFeatures,
+          },
         }
       ],
       'user_properties': userProperty.preparePayload()
diff --git a/pkgs/unified_analytics/pubspec.yaml b/pkgs/unified_analytics/pubspec.yaml
index f0a99f1..6ef6475 100644
--- a/pkgs/unified_analytics/pubspec.yaml
+++ b/pkgs/unified_analytics/pubspec.yaml
@@ -5,7 +5,7 @@
 # LINT.IfChange
 # When updating this, keep the version consistent with the changelog and the
 # value in lib/src/constants.dart.
-version: 7.0.1
+version: 7.0.2
 # LINT.ThenChange(lib/src/constants.dart)
 repository: https://github.com/dart-lang/tools/tree/main/pkgs/unified_analytics
 issue_tracker: https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Aunified_analytics
diff --git a/pkgs/unified_analytics/test/unified_analytics_test.dart b/pkgs/unified_analytics/test/unified_analytics_test.dart
index 2e78df4..cd5b942 100644
--- a/pkgs/unified_analytics/test/unified_analytics_test.dart
+++ b/pkgs/unified_analytics/test/unified_analytics_test.dart
@@ -498,7 +498,7 @@
 # All other lines are configuration lines. They have
 # the form "name=value". If multiple lines contain
 # the same configuration name with different values,
-# the parser will default to a conservative value. 
+# the parser will default to a conservative value.
 
 # DISABLING TELEMETRY REPORTING
 #
@@ -548,7 +548,7 @@
 # All other lines are configuration lines. They have
 # the form "name=value". If multiple lines contain
 # the same configuration name with different values,
-# the parser will default to a conservative value. 
+# the parser will default to a conservative value.
 
 # DISABLING TELEMETRY REPORTING
 #
@@ -632,7 +632,7 @@
     );
   });
 
-  test('Check that UserProperty class has all the necessary keys', () {
+  test('The UserProperty class has all the necessary keys', () {
     const userPropertyKeys = <String>[
       'session_id',
       'flutter_channel',
@@ -645,7 +645,6 @@
       'host_os_version',
       'locale',
       'client_ide',
-      'enabled_features',
     ];
     expect(analytics.userPropertyMap.keys.length, userPropertyKeys.length,
         reason: 'There should only be ${userPropertyKeys.length} keys');
@@ -826,6 +825,7 @@
       eventName: DashEvent.hotReloadTime,
       eventData: eventData,
       userProperty: analytics.userProperty,
+      enabledFeatures: 'enable-native-assets',
     );
 
     // Checks for the top level keys
@@ -833,8 +833,6 @@
         reason: '"client_id" is required at the top level');
     expect(body.containsKey('events'), true,
         reason: '"events" is required at the top level');
-    expect(body.containsKey('user_properties'), true,
-        reason: '"user_properties" is required at the top level');
 
     // Regex for the client id
     final clientIdPattern = RegExp(
@@ -846,14 +844,36 @@
     expect(clientIdPattern.hasMatch(body['client_id'] as String), true,
         reason: 'The client id is not properly formatted, ie '
             '46cc0ba6-f604-4fd9-aa2f-8a20beb24cd4');
-    expect(
-        (body['events'][0] as Map<String, dynamic>).containsKey('name'), true,
+    expect(body['events'][0] as Map<String, Object?>, contains('name'),
         reason: 'Each event in the events array needs a name');
-    expect(
-        (body['events'][0] as Map<String, dynamic>).containsKey('params'), true,
+    expect(body['events'][0] as Map<String, Object?>, contains('params'),
         reason: 'Each event in the events array needs a params key');
   });
 
+  test(
+      'The list of enabled features is included as an event parameter in every sent event',
+      () {
+    final eventData = <String, dynamic>{
+      'time': 5,
+      'command': 'run',
+    };
+
+    final Map<String, dynamic> body = generateRequestBody(
+      clientId: Uuid().generateV4(),
+      eventName: DashEvent.hotReloadTime,
+      eventData: eventData,
+      userProperty: analytics.userProperty,
+      enabledFeatures: 'enable-native-assets',
+    );
+
+    expect((body['events'][0] as Map<String, Object?>)['params'],
+        contains('enabled_features'));
+    expect(
+        (body['events'][0] as Map<String, dynamic>)['params']
+            ['enabled_features'],
+        'enable-native-assets');
+  });
+
   test('Check that log file is correctly persisting events sent', () {
     final int numberOfEvents = max((kLogFileLength * 0.1).floor(), 5);