| // Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file | |
| // for details. All rights reserved. Use of this source code is governed by a | |
| // BSD-style license that can be found in the LICENSE file. | |
| import 'dart:convert'; | |
| import 'package:meta/meta.dart'; | |
| import 'enums.dart'; | |
| final class Event { | |
| final DashEvent eventName; | |
| 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}) | |
| : 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 | |
| /// should not use this event constructor. | |
| /// | |
| /// Tools using this package should instead use the more generic | |
| /// [Event.exception] constructor. | |
| /// | |
| /// * [workflow] - refers to what process caused the error, such as | |
| /// "LogHandler.logFileStats". | |
| /// | |
| /// * [error] - the name of the error, such as "FormatException". | |
| /// | |
| /// * [description] - the description of the error being caught. | |
| Event.analyticsException({ | |
| required String workflow, | |
| required String error, | |
| String? description, | |
| }) : this._( | |
| eventName: DashEvent.analyticsException, | |
| eventData: { | |
| 'workflow': workflow, | |
| 'error': error, | |
| 'description': ?description, | |
| }, | |
| ); | |
| /// This is for various workflows within the flutter tool related | |
| /// to iOS and macOS workflows. | |
| /// | |
| /// * [workflow] - which workflow is running, such as "assemble". | |
| /// | |
| /// * [parameter] - subcategory of the workflow, such as "ios-archive". | |
| /// | |
| /// * [result] - usually to indicate success or failure of the workflow. | |
| Event.appleUsageEvent({ | |
| required String workflow, | |
| required String parameter, | |
| String? result, | |
| }) : this._( | |
| eventName: DashEvent.appleUsageEvent, | |
| eventData: { | |
| 'workflow': workflow, | |
| 'parameter': parameter, | |
| '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 | |
| /// client. | |
| /// | |
| /// * [duration] - JSON-encoded percentile values indicating how long it took | |
| /// from the time the server started handling the notification until the | |
| /// server had finished handling the notification. | |
| /// | |
| /// * [latency] - JSON-encoded percentile values indicating how long it took | |
| /// from the time the notification was sent until the server started | |
| /// handling it. | |
| /// | |
| /// * [method] - the name of the notification method that was sent. | |
| Event.clientNotification({ | |
| required String duration, | |
| required String latency, | |
| required String method, | |
| }) : this._( | |
| eventName: DashEvent.clientNotification, | |
| eventData: { | |
| 'duration': duration, | |
| 'latency': latency, | |
| 'method': method, | |
| }, | |
| ); | |
| /// Event that is emitted periodically to report the performance of the | |
| /// analyzer. | |
| /// | |
| /// * [workingDuration] - JSON-encoded percentile values indicating how long | |
| /// the analysis status was "working". | |
| /// * [withFineDependencies] - whether the fine-grained feature is enabled. | |
| /// | |
| /// Then there are three groups of measurements: | |
| /// * file modifications | |
| /// * workspace shape | |
| /// * background analysis | |
| /// | |
| /// The file modifications group includes: | |
| /// * [changedFileEventCount] - the number of file change events received. | |
| /// * [removedFileEventCount] - the number of file removal events received. | |
| /// * [changedFileUniqueCount] - the number of unique files that were changed. | |
| /// * [removedFileUniqueCount] - the number of unique files that were removed. | |
| /// | |
| /// The workspace shape group includes: | |
| /// * [immediateFileCountPercentiles] - JSON-encoded percentile values for the | |
| /// number of files in the immediate workspace. | |
| /// * [immediateFileLineCountPercentiles] - JSON-encoded percentile values for | |
| /// the number of lines in the immediate workspace files. | |
| /// * [transitiveFileCountPercentiles] - JSON-encoded percentile values for | |
| /// the number of files in the transitive workspace. | |
| /// * [transitiveFileLineCountPercentiles] - JSON-encoded percentile values | |
| /// for the number of lines in the transitive workspace files. | |
| /// | |
| /// This allows us to understand how big is the workspace, and how it changed | |
| /// over the reported period. | |
| /// | |
| /// The background analysis group includes: | |
| /// * [produceErrorsPotentialFileCount] - the total number of files for which | |
| /// the reported diagnostics could potentially change. | |
| /// * [produceErrorsPotentialFileLineCount] - the total number of lines in | |
| /// files for which the reported diagnostics could potentially change. | |
| /// * [produceErrorsActualFileCount] - the total number of files that actually | |
| /// were analyzed during background analysis. | |
| /// * [produceErrorsActualFileLineCount] - the total number of lines in files | |
| /// that actually were analyzed during background analysis. | |
| /// * [produceErrorsDurationMs] - the total duration in milliseconds for | |
| /// producing diagnostics. | |
| /// * [produceErrorsElementsDurationMs] - the total duration in milliseconds | |
| /// for preparing elements before analysis. | |
| /// * [libraryDiagnosticsBundleFailures] - the counts of requirement failures | |
| /// for library diagnostics bundles. The key is the `kindId` of the | |
| /// `RequirementFailure`. | |
| /// | |
| /// This allows us to understand how many files were scheduled for analysis, | |
| /// and how many of these files are served from the cache, because we | |
| /// determined that the change that caused them to be scheduled for analysis | |
| /// actually does not affect resolution and the set of diagnostics, e.g. | |
| /// because the change was in a method body, or to an API that does not | |
| /// affect this specific file (if fine-grained dependencies enabled). | |
| Event.analysisStatistics({ | |
| required String workingDuration, | |
| required bool withFineDependencies, | |
| required int changedFileEventCount, | |
| required int removedFileEventCount, | |
| required int changedFileUniqueCount, | |
| required int removedFileUniqueCount, | |
| required String immediateFileCountPercentiles, | |
| required String immediateFileLineCountPercentiles, | |
| required String transitiveFileCountPercentiles, | |
| required String transitiveFileLineCountPercentiles, | |
| required int produceErrorsPotentialFileCount, | |
| required int produceErrorsPotentialFileLineCount, | |
| required int produceErrorsActualFileCount, | |
| required int produceErrorsActualFileLineCount, | |
| required int produceErrorsDurationMs, | |
| required int produceErrorsElementsDurationMs, | |
| required String libraryDiagnosticsBundleFailures, | |
| }) : this._( | |
| eventName: DashEvent.analysisStatistics, | |
| eventData: { | |
| 'workingDuration': workingDuration, | |
| 'withFineDependencies': withFineDependencies, | |
| 'changedFileUniqueCount': changedFileUniqueCount, | |
| 'removedFileUniqueCount': removedFileUniqueCount, | |
| 'changedFileEventCount': changedFileEventCount, | |
| 'removedFileEventCount': removedFileEventCount, | |
| 'immediateFileCountPercentiles': immediateFileCountPercentiles, | |
| 'immediateFileLineCountPercentiles': | |
| immediateFileLineCountPercentiles, | |
| 'transitiveFileCountPercentiles': transitiveFileCountPercentiles, | |
| 'transitiveFileLineCountPercentiles': | |
| transitiveFileLineCountPercentiles, | |
| 'produceErrorsPotentialFileCount': produceErrorsPotentialFileCount, | |
| 'produceErrorsPotentialFileLineCount': | |
| produceErrorsPotentialFileLineCount, | |
| 'produceErrorsActualFileCount': produceErrorsActualFileCount, | |
| 'produceErrorsActualFileLineCount': produceErrorsActualFileLineCount, | |
| 'produceErrorsDurationMs': produceErrorsDurationMs, | |
| 'produceErrorsElementsDurationMs': produceErrorsElementsDurationMs, | |
| 'libraryDiagnosticsBundleFailures': libraryDiagnosticsBundleFailures, | |
| }, | |
| ); | |
| /// Event that is emitted periodically to report the performance of the | |
| /// analysis server's handling of a specific kind of request from the client. | |
| /// | |
| /// * [duration] - JSON-encoded percentile values indicating how long it took | |
| /// from the time the server started handling the request until the server | |
| /// had send a response. | |
| /// | |
| /// * [latency] - JSON-encoded percentile values indicating how long it took | |
| /// from the time the request was sent until the server started handling it. | |
| /// | |
| /// * [method] - the name of the request method that was sent. | |
| /// | |
| /// If the method is `workspace/didChangeWorkspaceFolders`, then the following | |
| /// parameters should be included: | |
| /// | |
| /// * [added] - JSON-encoded percentile values indicating the number of | |
| /// folders that were added. | |
| /// | |
| /// * [removed] - JSON-encoded percentile values indicating the number of | |
| /// folders that were removed. | |
| /// | |
| /// If the method is `initialized`, then the following parameters should be | |
| /// included: | |
| /// | |
| /// * [openWorkspacePaths] - JSON-encoded percentile values indicating the | |
| /// number of workspace paths that were opened. | |
| /// | |
| /// If the method is `analysis.setAnalysisRoots`, then the following | |
| /// parameters should be included: | |
| /// | |
| /// * [included] - JSON-encoded percentile values indicating the number of | |
| /// analysis roots in the included list. | |
| /// | |
| /// * [excluded] - JSON-encoded percentile values indicating the number of | |
| /// analysis roots in the excluded list. | |
| /// | |
| /// If the method is `analysis.setPriorityFiles`, then the following | |
| /// parameters should be included: | |
| /// | |
| /// * [files] - JSON-encoded percentile values indicating the number of | |
| /// priority files. | |
| Event.clientRequest({ | |
| required String duration, | |
| required String latency, | |
| required String method, | |
| String? added, | |
| String? excluded, | |
| String? files, | |
| String? included, | |
| String? openWorkspacePaths, | |
| String? removed, | |
| }) : this._( | |
| eventName: DashEvent.clientRequest, | |
| eventData: { | |
| 'added': ?added, | |
| 'duration': duration, | |
| 'excluded': ?excluded, | |
| 'files': ?files, | |
| 'included': ?included, | |
| 'latency': latency, | |
| 'method': method, | |
| 'openWorkspacePaths': ?openWorkspacePaths, | |
| 'removed': ?removed, | |
| }, | |
| ); | |
| /// An event that reports when the code size measurement is run | |
| /// via `--analyze-size`. | |
| /// | |
| /// * [platform] - string identifier for which platform was run "ios", "apk", | |
| /// "aab", etc. | |
| Event.codeSizeAnalysis({required String 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. | |
| /// | |
| /// * [count] - the number of times the command was executed. | |
| /// | |
| /// * [name] - the name of the command that was executed. | |
| Event.commandExecuted({required int count, required String name}) | |
| : this._( | |
| eventName: DashEvent.commandExecuted, | |
| eventData: {'count': count, 'name': name}, | |
| ); | |
| /// Event to capture usage values for different Flutter commands. | |
| /// | |
| /// There are several implementations of the `FlutterCommand` class within the | |
| /// Flutter tool that pass information based on the [workflow] being ran. An | |
| /// example of a [workflow] can be "create". The optional parameters for this | |
| /// constructor are a superset of all the implementations of `FlutterCommand`. | |
| /// There should never be a time where all of the parameters are passed to | |
| /// this constructor. | |
| Event.commandUsageValues({ | |
| required String workflow, | |
| required bool commandHasTerminal, | |
| // Assemble && build bundle implementation parameters | |
| String? buildBundleTargetPlatform, | |
| bool? buildBundleIsModule, | |
| bool? buildBundleEnableHcpp, | |
| // Build aar implementation parameters | |
| String? buildAarProjectType, | |
| String? buildAarTargetPlatform, | |
| bool? buildAarEnableHcpp, | |
| // Build apk implementation parameters | |
| String? buildApkTargetPlatform, | |
| String? buildApkBuildMode, | |
| bool? buildApkSplitPerAbi, | |
| bool? buildApkEnableHcpp, | |
| // Build app bundle implementation parameters | |
| String? buildAppBundleTargetPlatform, | |
| String? buildAppBundleBuildMode, | |
| // Create implementation parameters | |
| String? createProjectType, | |
| String? createAndroidLanguage, | |
| String? createIosLanguage, | |
| // Packages implementation parameters | |
| int? packagesNumberPlugins, | |
| bool? packagesProjectModule, | |
| String? packagesAndroidEmbeddingVersion, | |
| // Run implementation parameters | |
| bool? runIsEmulator, | |
| String? runTargetName, | |
| String? runTargetOsVersion, | |
| String? runModeName, | |
| bool? runProjectModule, | |
| String? runProjectHostLanguage, | |
| String? runAndroidEmbeddingVersion, | |
| bool? runEnableImpeller, | |
| String? runIOSInterfaceType, | |
| bool? runIsTest, | |
| bool? runEnableHcpp, | |
| }) : this._( | |
| eventName: DashEvent.commandUsageValues, | |
| eventData: { | |
| 'workflow': workflow, | |
| 'commandHasTerminal': commandHasTerminal, | |
| 'buildBundleTargetPlatform': ?buildBundleTargetPlatform, | |
| 'buildBundleIsModule': ?buildBundleIsModule, | |
| 'buildBundleEnableHcpp': ?buildBundleEnableHcpp, | |
| 'buildAarProjectType': ?buildAarProjectType, | |
| 'buildAarTargetPlatform': ?buildAarTargetPlatform, | |
| 'buildAarEnableHcpp': ?buildAarEnableHcpp, | |
| 'buildApkTargetPlatform': ?buildApkTargetPlatform, | |
| 'buildApkBuildMode': ?buildApkBuildMode, | |
| 'buildApkSplitPerAbi': ?buildApkSplitPerAbi, | |
| 'buildApkEnableHcpp': ?buildApkEnableHcpp, | |
| 'buildAppBundleTargetPlatform': ?buildAppBundleTargetPlatform, | |
| 'buildAppBundleBuildMode': ?buildAppBundleBuildMode, | |
| 'createProjectType': ?createProjectType, | |
| 'createAndroidLanguage': ?createAndroidLanguage, | |
| 'createIosLanguage': ?createIosLanguage, | |
| 'packagesNumberPlugins': ?packagesNumberPlugins, | |
| 'packagesProjectModule': ?packagesProjectModule, | |
| 'packagesAndroidEmbeddingVersion': ?packagesAndroidEmbeddingVersion, | |
| 'runIsEmulator': ?runIsEmulator, | |
| 'runTargetName': ?runTargetName, | |
| 'runTargetOsVersion': ?runTargetOsVersion, | |
| 'runModeName': ?runModeName, | |
| 'runProjectModule': ?runProjectModule, | |
| 'runProjectHostLanguage': ?runProjectHostLanguage, | |
| 'runAndroidEmbeddingVersion': ?runAndroidEmbeddingVersion, | |
| 'runEnableImpeller': ?runEnableImpeller, | |
| 'runIOSInterfaceType': ?runIOSInterfaceType, | |
| 'runIsTest': ?runIsTest, | |
| 'runEnableHcpp': ?runEnableHcpp, | |
| }, | |
| ); | |
| /// Event that is emitted on shutdown to report the structure of the analysis | |
| /// contexts created immediately after startup. | |
| /// | |
| /// * [immediateFileCount] - the number of files in one of the analysis | |
| /// contexts. | |
| /// | |
| /// * [immediateFileLineCount] - the number of lines in the immediate files. | |
| /// | |
| /// * [numberOfContexts] - the number of analysis context created. | |
| /// | |
| /// * [transitiveFileCount] - the number of files reachable from the files in | |
| /// each analysis context, where files can be counted multiple times if they | |
| /// are reachable from multiple contexts. | |
| /// | |
| /// * [transitiveFileLineCount] - the number of lines in the transitive files, | |
| /// where files can be counted multiple times if they are reachable from | |
| /// multiple contexts. | |
| /// | |
| /// * [transitiveFileUniqueCount] - the number of unique files reachable from | |
| /// the files in each analysis context. | |
| /// | |
| /// * [transitiveFileUniqueLineCount] - the number of lines in the unique | |
| /// transitive files. | |
| /// | |
| /// * [libraryCycleLibraryCounts] - JSON-encoded percentile values indicating | |
| /// the number of libraries in a single library cycle. | |
| /// | |
| /// * [libraryCycleLineCounts] - JSON-encoded percentile values indicating the | |
| /// number of lines of code in all of the files in a single library cycle. | |
| /// | |
| /// * [contextWorkspaceType] - JSON-encoded list with the total number of | |
| /// workspaces of each type for all of the contexts: | |
| /// * index 0: Blaze, GN or other workspace count | |
| /// * index 1: Package workspace count | |
| /// * index 2: Pub workspace count | |
| /// | |
| /// * [numberOfPackagesInWorkspace] - JSON-encoded percentile values for the | |
| /// number of packages in the Pub workspaces. | |
| Event.contextStructure({ | |
| required int immediateFileCount, | |
| required int immediateFileLineCount, | |
| required int numberOfContexts, | |
| required int transitiveFileCount, | |
| required int transitiveFileLineCount, | |
| required int transitiveFileUniqueCount, | |
| required int transitiveFileUniqueLineCount, | |
| String libraryCycleLibraryCounts = '', | |
| String libraryCycleLineCounts = '', | |
| String contextWorkspaceType = '', | |
| String numberOfPackagesInWorkspace = '', | |
| }) : this._( | |
| eventName: DashEvent.contextStructure, | |
| eventData: { | |
| 'immediateFileCount': immediateFileCount, | |
| 'immediateFileLineCount': immediateFileLineCount, | |
| 'numberOfContexts': numberOfContexts, | |
| 'transitiveFileCount': transitiveFileCount, | |
| 'transitiveFileLineCount': transitiveFileLineCount, | |
| 'transitiveFileUniqueCount': transitiveFileUniqueCount, | |
| 'transitiveFileUniqueLineCount': transitiveFileUniqueLineCount, | |
| 'libraryCycleLibraryCounts': libraryCycleLibraryCounts, | |
| 'libraryCycleLineCounts': libraryCycleLineCounts, | |
| 'contextWorkspaceType': contextWorkspaceType, | |
| 'numberOfPackagesInWorkspace': numberOfPackagesInWorkspace, | |
| }, | |
| ); | |
| /// Event that is emitted when a Dart CLI command has been executed. | |
| /// | |
| /// * [name] - the name of the command that was executed. | |
| /// | |
| /// * [enabledExperiments] - a set of Dart language experiments enabled when | |
| /// running the command. | |
| /// | |
| /// * [exitCode] - the process exit code set as a result of running the | |
| /// command. | |
| /// | |
| /// * [pubspecHasFlutterSdk] - whether the project's pubspec explicitly | |
| /// declares a dependency on the Flutter SDK (e.g. `sdk: flutter` or | |
| /// `environment.flutter`). | |
| /// | |
| /// * [pubspecDependencies] - a set of public package names depended upon by | |
| /// the project. Callers MUST verify packages against | |
| /// `.dart_tool/package_config.json` to ensure only public hosted packages | |
| /// (published to pub.dev or public registries) are passed. Non-hosted | |
| /// packages (path, git, un-versioned workspace members) must be excluded | |
| /// to prevent leaking proprietary or internal package names. | |
| /// | |
| /// * [pubspecEnvironmentSdk] - the Dart SDK version constraint declared in | |
| /// the project's pubspec (e.g. `^3.5.0` from `environment.sdk:`). | |
| /// | |
| /// NOTE FOR MAINTAINERS: Google Analytics 4 limits each event to at most 25 | |
| /// custom parameters. With standard metadata fields taking up 4–5 slots and | |
| /// dependency chunking consuming up to 20 parameters (`pubspec_dep_0` through | |
| /// `pubspec_dep_19`), adding any new parameters to this constructor requires | |
| /// decreasing `maxChunks` in [chunkDependencies] to prevent dropping event | |
| /// parameters. | |
| Event.dartCliCommandExecuted({ | |
| required String name, | |
| required String enabledExperiments, | |
| int? exitCode, | |
| bool? pubspecHasFlutterSdk, | |
| Set<String>? pubspecDependencies, | |
| String? pubspecEnvironmentSdk, | |
| }) : this._( | |
| eventName: DashEvent.dartCliCommandExecuted, | |
| eventData: { | |
| 'name': name, | |
| 'enabledExperiments': enabledExperiments, | |
| 'exitCode': ?exitCode, | |
| 'pubspec_has_flutter_sdk': ?pubspecHasFlutterSdk, | |
| 'pubspec_environment_sdk': ?pubspecEnvironmentSdk, | |
| if (pubspecDependencies != null) | |
| ...chunkDependencies(pubspecDependencies), | |
| }, | |
| ); | |
| /// Event that is sent from DevTools for various different actions as | |
| /// indicated by the [eventCategory]. | |
| /// | |
| /// The optional parameters in the parameter list contain metadata that is | |
| /// sent with each event, when available. | |
| /// | |
| /// [additionalMetrics] may contain any additional data for the event being | |
| /// sent. This often looks like metrics that are unique to the event or to a | |
| /// specific screen. | |
| Event.devtoolsEvent({ | |
| required String screen, | |
| required String eventCategory, | |
| required String label, | |
| required int value, | |
| // Defaulted values | |
| bool userInitiatedInteraction = true, | |
| // Optional parameters | |
| String? g3Username, | |
| String? userApp, | |
| String? userBuild, | |
| String? userPlatform, | |
| String? devtoolsPlatform, | |
| String? devtoolsChrome, | |
| String? devtoolsVersion, | |
| String? ideLaunched, | |
| String? isExternalBuild, | |
| String? isEmbedded, | |
| String? ideLaunchedFeature, | |
| String? isWasm, | |
| CustomMetrics? additionalMetrics, | |
| }) : this._( | |
| eventName: DashEvent.devtoolsEvent, | |
| eventData: { | |
| 'screen': screen, | |
| 'eventCategory': eventCategory, | |
| 'label': label, | |
| 'value': value, | |
| 'userInitiatedInteraction': userInitiatedInteraction, | |
| // Optional parameters | |
| 'g3Username': ?g3Username, | |
| 'userApp': ?userApp, | |
| 'userBuild': ?userBuild, | |
| 'userPlatform': ?userPlatform, | |
| 'devtoolsPlatform': ?devtoolsPlatform, | |
| 'devtoolsChrome': ?devtoolsChrome, | |
| 'devtoolsVersion': ?devtoolsVersion, | |
| 'ideLaunched': ?ideLaunched, | |
| 'isExternalBuild': ?isExternalBuild, | |
| 'isEmbedded': ?isEmbedded, | |
| 'ideLaunchedFeature': ?ideLaunchedFeature, | |
| 'isWasm': ?isWasm, | |
| if (additionalMetrics != null) ...additionalMetrics.toMap(), | |
| }, | |
| ); | |
| /// Event that contains the results for a specific doctor validator. | |
| /// | |
| /// * [validatorName] - the name for the doctor validator. | |
| /// | |
| /// * [result] - the final result for a specific doctor validator. | |
| /// | |
| /// * [partOfGroupedValidator] - `true` indicates that this validator belongs | |
| /// to a grouped validator. | |
| /// | |
| /// * [doctorInvocationId] - epoch formatted timestamp that can be used in | |
| /// combination with the client ID in GA4 to group the validators that | |
| /// ran in one doctor invocation. | |
| /// | |
| /// * [statusInfo] - optional description of the result from the doctor | |
| /// validator. | |
| Event.doctorValidatorResult({ | |
| required String validatorName, | |
| required String result, | |
| required bool partOfGroupedValidator, | |
| required int doctorInvocationId, | |
| String? statusInfo, | |
| }) : this._( | |
| eventName: DashEvent.doctorValidatorResult, | |
| eventData: { | |
| 'validatorName': validatorName, | |
| 'result': result, | |
| 'partOfGroupedValidator': partOfGroupedValidator, | |
| 'doctorInvocationId': doctorInvocationId, | |
| 'statusInfo': ?statusInfo, | |
| }, | |
| ); | |
| /// Generic event for all dash tools to use when encountering an | |
| /// exception that we want to log. | |
| /// | |
| /// * [exception] - string representation of the exception that occured. | |
| /// * [data] - optional structured data to include with the exception event. | |
| Event.exception({ | |
| required String exception, | |
| Map<String, Object?> data = const <String, Object?>{}, | |
| }) : 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. | |
| /// | |
| /// * [label] - the identifier for that build event. | |
| /// | |
| /// * [buildType] - the identifier for which platform the build event was for, | |
| /// examples include "ios", "gradle", and "web". | |
| /// | |
| /// * [command] - the command that was ran to kick off the build event. | |
| /// | |
| /// * [settings] - the settings used for the build event related to | |
| /// configuration and other relevant build information. | |
| /// | |
| /// * [error] - short identifier used to explain the cause of the build error, | |
| /// stacktraces should not be passed to this parameter. | |
| Event.flutterBuildInfo({ | |
| required String label, | |
| required String buildType, | |
| String? command, | |
| String? settings, | |
| String? error, | |
| }) : this._( | |
| eventName: DashEvent.flutterBuildInfo, | |
| eventData: { | |
| 'label': label, | |
| 'buildType': buildType, | |
| 'command': ?command, | |
| 'settings': ?settings, | |
| 'error': ?error, | |
| }, | |
| ); | |
| /// Provides information about which flutter command was run | |
| /// and whether it was successful. | |
| /// | |
| /// * [commandPath] - information about the flutter command, such as | |
| /// "build/apk". | |
| /// | |
| /// * [result] - if the command failed or succeeded. | |
| /// | |
| /// * [commandHasTerminal] - Boolean indicating if the flutter command ran | |
| /// with a terminal. | |
| /// | |
| /// * [hostArch] - the host CPU architecture executing the command (e.g., | |
| /// "x64", "arm64", "arm", "ia32", "riscv64"). | |
| /// | |
| /// * [maxRss] - maximum resident size for a given flutter command. | |
| Event.flutterCommandResult({ | |
| required String commandPath, | |
| required String result, | |
| required bool commandHasTerminal, | |
| String? hostArch, | |
| int? maxRss, | |
| }) : this._( | |
| eventName: DashEvent.flutterCommandResult, | |
| eventData: { | |
| 'commandPath': commandPath, | |
| 'result': result, | |
| 'commandHasTerminal': commandHasTerminal, | |
| 'hostArch': ?hostArch, | |
| 'maxRss': ?maxRss, | |
| }, | |
| ); | |
| @Deprecated( | |
| "Use 'flutterWasmDryRunPackage' instead. This will be removed in " | |
| 'a future version.', | |
| ) | |
| Event.flutterWasmDryRun({ | |
| required String result, | |
| required int exitCode, | |
| String? findingsSummary, | |
| }) : this._( | |
| eventName: DashEvent.flutterWasmDryRun, | |
| eventData: { | |
| 'result': result, | |
| 'exitCode': exitCode, | |
| 'findings': ?findingsSummary, | |
| }, | |
| ); | |
| /// Provides information about the results of a WASM dry run including public | |
| /// package names and versions. | |
| /// | |
| /// * [result] - dry run result summary. | |
| /// | |
| /// * [exitCode] - the exit code of the dry run. | |
| /// | |
| /// * [findingsInfo] - findings for the dry run, keyed by finding index. | |
| /// The value is a comma-separated string containing flags and package | |
| /// information in `name:version` format, e.g., `'-ph,pkg1:1.2.3'`. | |
| Event.flutterWasmDryRunPackage({ | |
| required String result, | |
| required int exitCode, | |
| required Map<String, String> findingsInfo, | |
| }) : this._( | |
| eventName: DashEvent.flutterWasmDryRunPackage, | |
| eventData: {'result': result, 'exitCode': exitCode, ...findingsInfo}, | |
| ); | |
| /// Provides information about the plugins injected into an iOS or macOS | |
| /// project. | |
| /// | |
| /// This event is not sent if a project has no plugins. | |
| /// | |
| /// * [platform] - The project's platform. Either 'ios' or 'macos'. | |
| /// | |
| /// * [isModule] - whether the project is an add-to-app Flutter module. | |
| /// | |
| /// * [swiftPackageManagerUsable] - if `true`, Swift Package Manager can be | |
| /// used for the project's plugins if any are Swift Package Manager | |
| /// compatible. | |
| /// | |
| /// * [swiftPackageManagerFeatureEnabled] - if the Swift Package Manager | |
| /// feature flag is on. If false, Swift Package Manager is off for all | |
| /// projects on the development machine. | |
| /// | |
| /// * [projectDisabledSwiftPackageManager] - if the project's .pubspec has | |
| /// `disable-swift-package-manager: true`. This turns off Swift Package | |
| /// Manager for a single project. | |
| /// | |
| /// * [projectHasSwiftPackageManagerIntegration] - if the Xcode project has | |
| /// Swift Package Manager integration. If `false`, the project needs to be | |
| /// migrated. | |
| /// | |
| /// * [pluginCount] - the total number of plugins for this project. A plugin | |
| /// can be compatible with both Swift Package Manager and CocoaPods. Plugins | |
| /// compatible with both will be counted in both [swiftPackageCount] and | |
| /// [podCount]. Swift Package Manager was used to inject all plugins if | |
| /// [pluginCount] is equal to [swiftPackageCount]. | |
| /// | |
| /// * [swiftPackageCount] - the number of plugins compatible with Swift | |
| /// Package Manager. This is less than or equal to [pluginCount]. If | |
| /// [swiftPackageCount] is less than [pluginCount], the project uses | |
| /// CocoaPods to inject plugins. | |
| /// | |
| /// * [podCount] - the number of plugins compatible with CocoaPods. This is | |
| /// less than or equal to [pluginCount]. | |
| Event.flutterInjectDarwinPlugins({ | |
| required String platform, | |
| required bool isModule, | |
| required bool swiftPackageManagerUsable, | |
| required bool swiftPackageManagerFeatureEnabled, | |
| required bool projectDisabledSwiftPackageManager, | |
| required bool projectHasSwiftPackageManagerIntegration, | |
| required int pluginCount, | |
| required int swiftPackageCount, | |
| required int podCount, | |
| }) : this._( | |
| eventName: DashEvent.flutterInjectDarwinPlugins, | |
| eventData: { | |
| 'platform': platform, | |
| 'isModule': isModule, | |
| 'swiftPackageManagerUsable': swiftPackageManagerUsable, | |
| 'swiftPackageManagerFeatureEnabled': | |
| swiftPackageManagerFeatureEnabled, | |
| 'projectDisabledSwiftPackageManager': | |
| projectDisabledSwiftPackageManager, | |
| 'projectHasSwiftPackageManagerIntegration': | |
| projectHasSwiftPackageManagerIntegration, | |
| 'pluginCount': pluginCount, | |
| 'swiftPackageCount': swiftPackageCount, | |
| 'podCount': podCount, | |
| }, | |
| ); | |
| /// Provides information about the android dependencies of a project. | |
| /// | |
| /// * [success] - whether the build succeded. | |
| /// | |
| /// * [label] - the error label if the build fails. | |
| /// | |
| /// * [isModule] - whether the project is an add-to-app Flutter module. | |
| /// | |
| /// * [agpVersion] - the Android Gradle plugin of the build target. | |
| /// | |
| /// * [minSDK] - the minimum SDK version supported by the build target. | |
| /// | |
| /// * [targetSDK] - the target SDK version the build target is built against. | |
| /// | |
| /// * [compileSDK] - the compile SDK version the build target is built | |
| /// against. | |
| /// | |
| /// * [jdkVersion] - the JDK version used by the build target. | |
| /// | |
| /// * [ndkVersion] - the NDK version used by the build target. | |
| /// | |
| /// * [gradleVersion] - the Gradle version used by the build target. | |
| Event.flutterTrackAndroidDependencies({ | |
| required bool success, | |
| String? label, | |
| bool? isModule, | |
| String? agpVersion, | |
| int? minSDK, | |
| int? targetSDK, | |
| int? compileSDK, | |
| int? jdkVersion, | |
| String? ndkVersion, | |
| String? gradleVersion, | |
| }) : this._( | |
| eventName: DashEvent.flutterTrackAndroidDependencies, | |
| eventData: { | |
| 'success': success, | |
| 'label': ?label, | |
| 'isModule': ?isModule, | |
| 'agpVersion': ?agpVersion, | |
| 'minSDK': ?minSDK, | |
| 'targetSDK': ?targetSDK, | |
| 'compileSDK': ?compileSDK, | |
| 'jdkVersion': ?jdkVersion, | |
| 'ndkVersion': ?ndkVersion, | |
| 'gradleVersion': ?gradleVersion, | |
| }, | |
| ); | |
| // 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}) | |
| : this._(eventName: DashEvent.hotReloadTime, eventData: {'timeMs': timeMs}); | |
| /// Events to be sent for the Flutter Hot Runner. | |
| Event.hotRunnerInfo({ | |
| required String label, | |
| required String targetPlatform, | |
| required String sdkName, | |
| required bool emulator, | |
| required bool fullRestart, | |
| String? reason, | |
| int? finalLibraryCount, | |
| int? syncedLibraryCount, | |
| int? syncedClassesCount, | |
| int? syncedProceduresCount, | |
| int? syncedBytes, | |
| int? invalidatedSourcesCount, | |
| int? transferTimeInMs, | |
| int? overallTimeInMs, | |
| int? compileTimeInMs, | |
| int? findInvalidatedTimeInMs, | |
| int? scannedSourcesCount, | |
| int? reassembleTimeInMs, | |
| int? reloadVMTimeInMs, | |
| }) : this._( | |
| eventName: DashEvent.hotRunnerInfo, | |
| eventData: { | |
| 'label': label, | |
| 'targetPlatform': targetPlatform, | |
| 'sdkName': sdkName, | |
| 'emulator': emulator, | |
| 'fullRestart': fullRestart, | |
| 'reason': ?reason, | |
| 'finalLibraryCount': ?finalLibraryCount, | |
| 'syncedLibraryCount': ?syncedLibraryCount, | |
| 'syncedClassesCount': ?syncedClassesCount, | |
| 'syncedProceduresCount': ?syncedProceduresCount, | |
| 'syncedBytes': ?syncedBytes, | |
| 'invalidatedSourcesCount': ?invalidatedSourcesCount, | |
| 'transferTimeInMs': ?transferTimeInMs, | |
| 'overallTimeInMs': ?overallTimeInMs, | |
| 'compileTimeInMs': ?compileTimeInMs, | |
| 'findInvalidatedTimeInMs': ?findInvalidatedTimeInMs, | |
| 'scannedSourcesCount': ?scannedSourcesCount, | |
| 'reassembleTimeInMs': ?reassembleTimeInMs, | |
| 'reloadVMTimeInMs': ?reloadVMTimeInMs, | |
| }, | |
| ); | |
| /// Event that is emitted periodically to report the number of times each lint | |
| /// has been enabled. | |
| /// | |
| /// * [count] - the number of options files in which the lint was enabled. | |
| /// | |
| /// * [name] - the name of the lint. | |
| Event.lintUsageCount({required int count, required String name}) | |
| : this._( | |
| eventName: DashEvent.lintUsageCount, | |
| eventData: {'count': count, 'name': name}, | |
| ); | |
| /// Event that is emitted periodically to report the amount of memory being | |
| /// used. | |
| /// | |
| /// * [rss] - the resident set size in megabytes. | |
| /// | |
| /// If this is not the first time memory has been reported for this session, | |
| /// then the following parameters should be included: | |
| /// | |
| /// * [periodSec] - the number of seconds since the last memory usage data was | |
| /// gathered. | |
| /// | |
| /// * [mbPerSec] - the number of megabytes of memory that were added or | |
| /// subtracted per second since the last report. | |
| Event.memoryInfo({required int rss, int? periodSec, double? mbPerSec}) | |
| : this._( | |
| eventName: DashEvent.memoryInfo, | |
| eventData: {'rss': rss, 'periodSec': ?periodSec, 'mbPerSec': ?mbPerSec}, | |
| ); | |
| /// Event that is emitted periodically to report the performance of analyzer | |
| /// plugins when handling requests. | |
| /// | |
| /// * [duration] - JSON-encoded percentile values indicating how long it took | |
| /// from the time the request was sent to the plugin until the response | |
| /// was processed by the server. | |
| /// | |
| /// * [method] - the name of the request sent to the plugin. | |
| /// | |
| /// * [pluginId] - the ID of the plugin whose performance is being reported. | |
| Event.pluginRequest({ | |
| required String duration, | |
| required String method, | |
| required String pluginId, | |
| }) : this._( | |
| eventName: DashEvent.pluginRequest, | |
| eventData: { | |
| 'duration': duration, | |
| 'method': method, | |
| 'pluginId': pluginId, | |
| }, | |
| ); | |
| /// Event that is emitted periodically to report information about how _new_ | |
| /// analyzer plugins are used. | |
| /// | |
| /// - [count] - the number of new plugins that were configured. | |
| /// - [lintRuleCounts] - JSON-encoded percentile values indicating the number | |
| /// of enabled lint rules for each plugin. | |
| /// - [warningRuleCounts] - JSON-encoded percentile values indicating the | |
| /// number of enabled warning rules for each plugin. | |
| /// - [fixCounts] - JSON-encoded percentile values indicating the number of | |
| /// fixes provided by each plugin. | |
| /// - [assistCounts] - JSON-encoded percentile values indicating the number of | |
| /// assists provided by each plugin. | |
| Event.plugins({ | |
| required int count, | |
| required String lintRuleCounts, | |
| required String warningRuleCounts, | |
| required String fixCounts, | |
| required String assistCounts, | |
| }) : this._( | |
| eventName: DashEvent.plugins, | |
| eventData: { | |
| 'count': count, | |
| 'lintRuleCounts': lintRuleCounts, | |
| 'warningRuleCounts': warningRuleCounts, | |
| 'fixCounts': fixCounts, | |
| 'assistCounts': assistCounts, | |
| }, | |
| ); | |
| /// Event that is emitted periodically to report the frequency with which a | |
| /// given _legacy_ analyzer plugin has been used. | |
| /// | |
| /// * [count] - the number of times plugins usage was changed, which will | |
| /// always be at least one. | |
| /// | |
| /// * [enabled] - JSON-encoded percentile values indicating the number of | |
| /// contexts for which the plugin was enabled. | |
| /// | |
| /// * [pluginId] - the ID of the plugin associated with the data. | |
| Event.pluginUse({ | |
| required int count, | |
| required String enabled, | |
| required String pluginId, | |
| }) : this._( | |
| eventName: DashEvent.pluginUse, | |
| eventData: {'count': count, 'enabled': enabled, 'pluginId': pluginId}, | |
| ); | |
| /// Event that is emitted when `pub get` is run. | |
| /// | |
| /// * [packageName] - the name of the package that was resolved. | |
| /// | |
| /// * [version] - the resolved, canonicalized package version. | |
| /// | |
| /// * [dependencyType] - the kind of dependency that resulted in this package | |
| /// being resolved (e.g., direct, transitive, or dev dependencies). | |
| Event.pubGet({ | |
| required String packageName, | |
| required String version, | |
| required String 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. | |
| /// | |
| /// * [clientId] - the id of the client that started the server. | |
| /// | |
| /// * [clientVersion] - the version of the client that started the server. | |
| /// | |
| /// * [duration] - the number of milliseconds for which the server was | |
| /// running. | |
| /// | |
| /// * [flags] - the flags passed to the analysis server on startup, without | |
| /// any argument values for flags that take values, or an empty string if | |
| /// there were no arguments. | |
| /// | |
| /// * [parameters] - the names of the parameters passed to the `initialize` | |
| /// request, or an empty string if the `initialize` request was not sent | |
| /// or if there were no parameters given. | |
| /// | |
| /// * [ideName] - the name of the IDE, e.g. IntelliJ IDEA. | |
| /// | |
| /// * [ideVersion] - the version of the IDE platform. | |
| /// | |
| /// * [pluginName] - the name of the plugin, e.g. Dart. | |
| /// | |
| /// * [pluginVersion] - the version of the plugin. | |
| Event.serverSession({ | |
| required String clientId, | |
| required String clientVersion, | |
| required int duration, | |
| required String flags, | |
| required String parameters, | |
| String ideName = '', | |
| String ideVersion = '', | |
| String pluginName = '', | |
| String pluginVersion = '', | |
| }) : this._( | |
| eventName: DashEvent.serverSession, | |
| eventData: { | |
| 'clientId': clientId, | |
| 'clientVersion': clientVersion, | |
| 'duration': duration, | |
| 'flags': flags, | |
| 'ideName': ideName, | |
| 'ideVersion': ideVersion, | |
| 'pluginName': pluginName, | |
| 'pluginVersion': pluginVersion, | |
| '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. | |
| /// | |
| /// * [diagnostic] - the name of the diagnostic whose severity was changed. | |
| /// | |
| /// * [adjustments] - JSON-encoded map of severities to the number of times | |
| /// the diagnostic's severity was changed to the key. | |
| Event.severityAdjustment({ | |
| required String diagnostic, | |
| required String 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. | |
| /// | |
| /// * [surveyId] - the unique id for a given survey. | |
| /// | |
| /// * [status] - the string identifier for a given `SurveyButton` under the | |
| /// `action` field. | |
| Event.surveyAction({required String surveyId, required String 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. | |
| /// | |
| /// * [surveyId] - the unique ID for a given survey. | |
| Event.surveyShown({required String surveyId}) | |
| : this._( | |
| eventName: DashEvent.surveyShown, | |
| eventData: {'surveyId': surveyId}, | |
| ); | |
| /// Event that records how long a given process takes to complete. | |
| /// | |
| /// * [workflow] - the overall process or command being run, for example | |
| /// "build" is a possible value for the flutter tool. | |
| /// | |
| /// * [variableName] - the specific variable being measured, for example | |
| /// "gradle" would indicate how long it took for a gradle build under the | |
| /// "build" [workflow]. | |
| /// | |
| /// * [elapsedMilliseconds] - how long the process took in milliseconds. | |
| /// | |
| /// * [label] - an optional field that can be used for further filtering, for | |
| /// example, "success" can indicate how long a successful build in gradle | |
| /// takes to complete. | |
| Event.timing({ | |
| required String workflow, | |
| required String variableName, | |
| required int elapsedMilliseconds, | |
| String? label, | |
| }) : this._( | |
| eventName: DashEvent.timing, | |
| eventData: { | |
| 'workflow': workflow, | |
| 'variableName': variableName, | |
| 'elapsedMilliseconds': elapsedMilliseconds, | |
| 'label': ?label, | |
| }, | |
| ); | |
| /// An event that is sent from the Dart MCP server. | |
| /// | |
| /// The [client] is the name of the client, as given when it connected to the | |
| /// MCP server, and [clientVersion] is the version of the client. | |
| /// | |
| /// The [serverVersion] is the version of the Dart MCP server. | |
| /// | |
| /// The [type] identifies the kind of event this is, and [additionalData] is | |
| /// the actual data for the event. | |
| /// | |
| /// The [agentPlugin] is optional and is the name of the agent plugin that is | |
| /// using the MCP server. | |
| Event.dartMCPEvent({ | |
| required String client, | |
| required String clientVersion, | |
| required String serverVersion, | |
| required String type, | |
| String? agentPlugin, | |
| CustomMetrics? additionalData, | |
| }) : this._( | |
| eventName: DashEvent.dartMCPEvent, | |
| eventData: { | |
| 'client': client, | |
| 'clientVersion': clientVersion, | |
| 'serverVersion': serverVersion, | |
| 'type': type, | |
| 'agentPlugin': ?agentPlugin, | |
| ...?additionalData?.toMap(), | |
| }, | |
| ); | |
| /// An event that is sent from package:skills. | |
| /// | |
| /// The [version] is the version of the skills package. | |
| /// | |
| /// The [type] identifies the kind of event this is, and [additionalData] is | |
| /// the actual data for the event. | |
| /// | |
| /// See https://github.com/dart-lang/ai/tree/main/pkgs/skills for the actual | |
| /// events sent by this package. | |
| Event.packageSkillsEvent({ | |
| required String version, | |
| required String type, | |
| CustomMetrics? additionalData, | |
| }) : this._( | |
| eventName: DashEvent.packageSkillsEvent, | |
| eventData: { | |
| 'version': version, | |
| 'type': type, | |
| ...?additionalData?.toMap(), | |
| }, | |
| ); | |
| @override | |
| int get hashCode => Object.hash(eventName, jsonEncode(eventData)); | |
| @override | |
| bool operator ==(Object other) => | |
| other is Event && | |
| other.runtimeType == runtimeType && | |
| other.eventName == eventName && | |
| _compareEventData(other.eventData, eventData); | |
| /// Converts an instance of [Event] to JSON. | |
| String toJson() => | |
| jsonEncode({'eventName': eventName.label, 'eventData': eventData}); | |
| @override | |
| String toString() => toJson(); | |
| /// Utility function to take in two maps [a] and [b] and compares them | |
| /// to ensure that they have the same keys and values | |
| bool _compareEventData(Map<String, Object?> a, Map<String, Object?> b) { | |
| final keySetA = a.keys.toSet(); | |
| final keySetB = b.keys.toSet(); | |
| final intersection = keySetA.intersection(keySetB); | |
| // Ensure that the keys are the same for each object. | |
| if (intersection.length != keySetA.length || | |
| intersection.length != keySetB.length) { | |
| return false; | |
| } | |
| // Ensure that each of the key's values are the same. | |
| for (final key in a.keys) { | |
| if (a[key] != b[key]) return false; | |
| } | |
| return true; | |
| } | |
| /// Returns a valid instance of [Event] if [json] follows the correct schema. | |
| /// | |
| /// Common use case for this static method involves clients of this package | |
| /// that have a client-server setup where the server sends events that the | |
| /// client creates. | |
| static Event? fromJson(String json) { | |
| try { | |
| final jsonMap = jsonDecode(json) as Map<String, Object?>; | |
| // Ensure that `eventName` is a string and a valid label and `eventData` | |
| // is a nested object. | |
| if (jsonMap case { | |
| 'eventName': final String eventName, | |
| 'eventData': final Map<String, Object?> eventData, | |
| }) { | |
| final dashEvent = DashEvent.fromLabel(eventName); | |
| if (dashEvent == null) return null; | |
| return Event._(eventName: dashEvent, eventData: eventData); | |
| } | |
| return null; | |
| } on FormatException { | |
| return null; | |
| } | |
| } | |
| } | |
| /// A base class for custom metrics that will be defined by a unified_analytics | |
| /// client. | |
| /// | |
| /// This base type can be used as a parameter in any event constructor that | |
| /// allows custom metrics to be added by a unified_analytics client. | |
| abstract base class CustomMetrics { | |
| /// Converts the custom metrics data to a [Map] object. | |
| /// | |
| /// This must be a JSON-encodable [Map]. | |
| Map<String, Object> toMap(); | |
| } | |
| /// Public helper in unified_analytics to sort and chunk dependencies. | |
| /// | |
| /// Respects GA4's 100-character limit per parameter and 25-parameter limit | |
| /// per event. | |
| /// | |
| /// To eliminate alphabetical bias (e.g., always omitting packages starting | |
| /// with 'z' when exceeding the 20-chunk cap) while preventing ecosystem-wide | |
| /// starvation of specific package names across projects, dependencies are | |
| /// sorted deterministically using a 32-bit FNV-1a hash salted with the | |
| /// project's canonical dependency set. | |
| /// | |
| /// This ensures: | |
| /// 1. **Cross-Machine Stability**: Identical dependency sets always produce the | |
| /// exact same deterministic chunk payload across developers and CI runs. | |
| /// 2. **Cross-Project Diversity**: Different projects produce different hash | |
| /// permutations, ensuring no package is globally starved across the | |
| /// ecosystem under truncation. | |
| /// 3. **Zero PII**: Salt is derived strictly from public package names without | |
| /// relying on local paths, usernames, or private project identifiers. | |
| @visibleForTesting | |
| Map<String, String> chunkDependencies(Set<String> deps) { | |
| if (deps.isEmpty) return const {}; | |
| // Compute a deterministic project-level salt from the canonical | |
| // (alphabetically sorted) dependency set. | |
| final sortedBase = deps.toList()..sort(); | |
| var setSalt = 2166136261; | |
| for (final dep in sortedBase) { | |
| setSalt = _fnv1a(dep, setSalt); | |
| } | |
| // Sort deterministically by salted FNV-1a hash value. Fall back to | |
| // alphabetical comparison if there is a hash collision to guarantee absolute | |
| // determinism. | |
| final sortedDeps = deps.toList() | |
| ..sort((a, b) { | |
| final hashA = _fnv1a(a, setSalt); | |
| final hashB = _fnv1a(b, setSalt); | |
| if (hashA != hashB) { | |
| return hashA.compareTo(hashB); | |
| } | |
| return a.compareTo(b); | |
| }); | |
| final chunks = <String, String>{}; | |
| var currentChunk = <String>[]; | |
| var currentLength = 0; | |
| var chunkIndex = 0; | |
| // We have a maximum of 25 parameters per event in GA4. Standard event | |
| // parameters (name, enabledExperiments, exitCode, pubspec_has_flutter_sdk) | |
| // take up to 4 slots, leaving 21 slots. Capping at 20 chunks guarantees | |
| // safety. | |
| const maxChunks = 20; | |
| for (final dep in sortedDeps) { | |
| // Guard: Skip package names that are somehow longer than 100 characters | |
| // to prevent violating GA4's value length limit. | |
| if (dep.length > 100) continue; | |
| final lengthToAdd = dep.length + (currentChunk.isEmpty ? 0 : 1); | |
| if (currentLength + lengthToAdd > 100) { | |
| chunks['pubspec_dep_$chunkIndex'] = currentChunk.join(','); | |
| chunkIndex++; | |
| // Stop adding chunks if we reach the GA4 parameter count safety limit | |
| if (chunkIndex >= maxChunks) { | |
| currentChunk = const []; | |
| break; | |
| } | |
| currentChunk = [dep]; | |
| currentLength = dep.length; | |
| } else { | |
| currentChunk.add(dep); | |
| currentLength += lengthToAdd; | |
| } | |
| } | |
| if (currentChunk.isNotEmpty && chunkIndex < maxChunks) { | |
| chunks['pubspec_dep_$chunkIndex'] = currentChunk.join(','); | |
| } | |
| return chunks; | |
| } | |
| /// Computes a deterministic 32-bit FNV-1a hash of a string with an optional | |
| /// seed. | |
| /// | |
| /// This is used to shuffle dependency names in a stable, pseudo-random | |
| /// way to eliminate alphabetical and global package bias when sampling packages | |
| /// for telemetry while maintaining 100% determinism across runs. | |
| int _fnv1a(String s, [int seed = 2166136261]) { | |
| var hash = seed; | |
| for (var i = 0; i < s.length; i++) { | |
| hash ^= s.codeUnitAt(i); | |
| // Split the multiplication to prevent exceeding the 53-bit safe integer | |
| // limit on the web. | |
| hash = (((hash & 0xff) << 24) + (hash * 403)) & 0xffffffff; | |
| } | |
| return hash; | |
| } |