// Copyright (c) 2022, 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:async';
import 'dart:convert';

import 'package:analysis_server/lsp_protocol/protocol.dart';
import 'package:analysis_server/protocol/protocol_constants.dart';
import 'package:analysis_server/src/analytics/active_request_data.dart';
import 'package:analysis_server/src/analytics/context_structure.dart';
import 'package:analysis_server/src/analytics/notification_data.dart';
import 'package:analysis_server/src/analytics/percentile_calculator.dart';
import 'package:analysis_server/src/analytics/plugin_data.dart';
import 'package:analysis_server/src/analytics/request_data.dart';
import 'package:analysis_server/src/analytics/session_data.dart';
import 'package:analysis_server/src/lsp/lsp_analysis_server.dart';
import 'package:analysis_server/src/plugin/plugin_manager.dart';
import 'package:analysis_server/src/protocol_server.dart';
import 'package:analysis_server/src/status/pages.dart';
import 'package:analyzer/dart/analysis/analysis_context.dart';
import 'package:analyzer/src/dart/analysis/driver_based_analysis_context.dart';
import 'package:analyzer/src/dart/analysis/status.dart';
import 'package:analyzer/src/util/platform_info.dart';
import 'package:collection/collection.dart';
import 'package:memory_usage/memory_usage.dart';
import 'package:meta/meta.dart';
import 'package:unified_analytics/unified_analytics.dart';

/// An interface for managing and reporting analytics.
///
/// Individual methods can either send an analytics event immediately or can
/// collect and even consolidate information to be reported later. Clients are
/// required to invoke the [shutdown] method before the server shuts down in
/// order to send any cached data.
class AnalyticsManager {
  /// A flag set during development to allow experimental data to be sent to a
  /// development-time analytics account.
  static const bool sendExperimentalData = false;

  static const addedKey = 'added';

  static const removedKey = 'removed';

  static const commandEnumKey = 'command';

  static const openWorkspacePathsKey = 'openWorkspacePaths';

  static const refactoringKindEnumKey = editRequestGetRefactoringKind;

  static const includedKey = analysisRequestSetAnalysisRootsIncluded;

  static const excludedKey = analysisRequestSetAnalysisRootsExcluded;

  static const filesKey = analysisRequestSetPriorityFilesFiles;

  /// The object used to send analytics.
  final Analytics analytics;

  /// Data about the current session, or `null` if the [startUp] method has not
  /// been invoked.
  SessionData? _sessionData;

  final PluginData _pluginData = PluginData();

  /// The data about analysis, or `null` if no analysis has been performed.
  ContextStructure? _contextStructure;

  /// A map from the id of a request to data about the request.
  final Map<String, ActiveRequestData> _activeRequests = {};

  /// A map from the name of a request to data about all such requests that have
  /// been responded to.
  final Map<String, RequestData> _completedRequests = {};

  /// A map from the name of a notification to data about all such notifications
  /// that have been handled.
  final Map<String, NotificationData> _completedNotifications = {};

  /// A map from the name of a lint to the number of options files in which the lint
  /// was enabled.
  final Map<String, int> _lintUsageCounts = {};

  /// A map from the name of a diagnostic to a map whose values are the number
  /// of times that the severity of the diagnostic was changed to the severity
  /// represented by the key.
  final Map<String, Map<String, int>> _severityAdjustments = {};

  /// The start time of the current window during which analysis was working,
  /// or `null` if analysis is currently idle.
  DateTime? _analysisWorkingStart;

  /// Accumulated statistics about analysis working periods.
  AnalyticsAnalysisWorkingStatistics? _analysisWorkingStatistics;

  /// Accumulates durations (in ms) of periods where analysis was working.
  final PercentileCalculator _analysisWorkingDurations = PercentileCalculator();

  /// A periodic timer used to send analytics data. This timer should be
  /// cancelled at shutdown.
  Timer? periodicTimer;

  /// Initialize a newly created analytics manager to report to the [analytics]
  /// service.
  new(this.analytics) {
    if (analytics is! NoOpAnalytics) {
      periodicTimer = Timer.periodic(Duration(minutes: 30), (_) {
        _sendPeriodicData();
      });
    }
  }

  /// Record information about the number of files and the number of lines of
  /// code in those files, for both immediate files, transitive files, the
  /// number of unique transitive files, and the number and sizes of library
  /// cycles.
  void analysisComplete({
    required int numberOfContexts,
    required int immediateFileCount,
    required int immediateFileLineCount,
    required int transitiveFileCount,
    required int transitiveFileLineCount,
    required int transitiveFileUniqueCount,
    required int transitiveFileUniqueLineCount,
    required List<int> libraryCycleLibraryCounts,
    required List<int> libraryCycleLineCounts,
    required List<int> numberOfPackagesInWorkspace,
    required List<int> contextWorkspaceType,
  }) {
    // This is currently keeping the first report of completed analysis, but we
    // might want to consider alternatives, such as keeping the "largest"
    // analysis or keeping all of the data and sending back percentile.
    _contextStructure ??= ContextStructure(
      numberOfContexts: numberOfContexts,
      immediateFileCount: immediateFileCount,
      immediateFileLineCount: immediateFileLineCount,
      transitiveFileCount: transitiveFileCount,
      transitiveFileLineCount: transitiveFileLineCount,
      transitiveFileUniqueCount: transitiveFileUniqueCount,
      transitiveFileUniqueLineCount: transitiveFileUniqueLineCount,
      libraryCycleLibraryCounts: libraryCycleLibraryCounts,
      libraryCycleLineCounts: libraryCycleLineCounts,
      contextWorkspaceType: contextWorkspaceType,
      numberOfPackagesInWorkspace: numberOfPackagesInWorkspace,
    );
  }

  /// Called when analysis status changes to record analysis statistics.
  ///
  /// When [isWorking] switches to `false`, we (softly) expect [statistics]
  /// to be statistics accumulated over just finished working period.
  void analysisStatusChanged({
    required bool isWorking,
    required AnalysisStatusWorkingStatistics? statistics,
  }) {
    if (isWorking) {
      _analysisWorkingStart ??= DateTime.now();
    } else {
      _finalizeOpenWorkingPeriod();
    }

    if (statistics != null) {
      var accumulated = _analysisWorkingStatistics ??=
          AnalyticsAnalysisWorkingStatistics(
            withFineDependencies: statistics.withFineDependencies,
          );
      accumulated.append(statistics);
    }
  }

  /// Record that the set of plugins known to the [pluginManager] has changed.
  Future<void> changedPlugins(PluginManager pluginManager) async {
    await _pluginData.recordPlugins(pluginManager);
  }

  /// Record the number of [added] folders and [removed] folders.
  void changedWorkspaceFolders({
    required List<String> added,
    required List<String> removed,
  }) {
    var requestData = getRequestData(
      Method.workspace_didChangeWorkspaceFolders.toString(),
    );
    requestData.addValue(addedKey, added.length);
    requestData.addValue(removedKey, removed.length);
  }

  /// Record that the [contexts] have been created.
  void createdAnalysisContexts(List<AnalysisContext> contexts) {
    for (var context in contexts) {
      var allOptions =
          (context as DriverBasedAnalysisContext).allAnalysisOptions;
      for (var analysisOptions in allOptions) {
        for (var rule in analysisOptions.lintRules) {
          var name = rule.name;
          _lintUsageCounts[name] = (_lintUsageCounts[name] ?? 0) + 1;
        }

        for (var processor in analysisOptions.errorProcessors) {
          var severity = processor.severity?.name ?? 'ignore';
          var severityCounts = _severityAdjustments.putIfAbsent(
            processor.code,
            () => {},
          );
          severityCounts[severity] = (severityCounts[severity] ?? 0) + 1;
        }
      }
    }
  }

  /// Record that the given [command] was executed.
  void executedCommand(String command) {
    var requestData = getRequestData(
      Method.workspace_executeCommand.toString(),
    );
    requestData.addEnumValue(commandEnumKey, command);
  }

  /// Return the request data for requests that have the given [method].
  @visibleForTesting
  RequestData getRequestData(String method) {
    return _completedRequests.putIfAbsent(method, () => RequestData(method));
  }

  /// Record that the given [notification] was received and has been handled.
  void handledNotificationMessage({
    required NotificationMessage notification,
    required DateTime startTime,
    required DateTime endTime,
  }) {
    var method = notification.method.toString();
    var requestTime = notification.clientRequestTime;
    var start = startTime.millisecondsSinceEpoch;
    var end = endTime.millisecondsSinceEpoch;
    var data = _completedNotifications.putIfAbsent(
      method,
      () => NotificationData(method),
    );
    if (requestTime != null) {
      data.latencyTimes.addValue(start - requestTime);
    }
    data.handlingTimes.addValue(end - start);
  }

  /// Record the parameters passed on initialize.
  void initialize(InitializeParams params) {
    var options = LspInitializationOptions(params.initializationOptions);
    var paramNames = <String>[
      if (options.closingLabels) 'closingLabels',
      if (options.completionBudgetMilliseconds != null)
        'completionBudgetMilliseconds',
      if (options.flutterOutline) 'flutterOutline',
      if (options.onlyAnalyzeProjectsWithOpenFiles)
        'onlyAnalyzeProjectsWithOpenFiles',
      if (options.outline) 'outline',
      if (options.suggestFromUnimportedLibraries)
        'suggestFromUnimportedLibraries',
    ];
    _sessionData?.initializeParams = paramNames.join(',');
  }

  /// Record the number of [openWorkspacePaths].
  void initialized({required List<String> openWorkspacePaths}) {
    var requestData = getRequestData(Method.initialized.toString());
    requestData.addValue(openWorkspacePathsKey, openWorkspacePaths.length);
  }

  bool needsAnslysisCompleteCall() => _contextStructure == null;

  Future<void> sendMemoryUsage(MemoryUsageEvent event) async {
    var delta = event.delta;
    var seconds = event.period?.inSeconds;

    assert((event.delta == null) == (event.period == null));

    if (delta == null || seconds == null) {
      analytics.send(Event.memoryInfo(rss: event.rss));
      return;
    }

    if (seconds == 0) seconds = 1;

    analytics.send(
      Event.memoryInfo(
        rss: event.rss,
        periodSec: seconds,
        mbPerSec: delta / seconds,
      ),
    );
  }

  /// Record that the given [response] was sent to the client.
  void sentResponse({required Response response}) {
    var sendTime = DateTime.now();
    _recordResponseData(response.id, sendTime);
  }

  /// Record that the given [response] was sent to the client.
  void sentResponseMessage({required ResponseMessage response}) {
    var sendTime = DateTime.now();
    var id = response.id?.asLspIdString;
    if (id == null) {
      return;
    }
    _recordResponseData(id, sendTime);
  }

  /// The server is shutting down. Report any accumulated analytics data.
  Future<void> shutdown() async {
    var sessionData = _sessionData;
    if (sessionData == null) {
      return;
    }
    await _sendSessionData(sessionData);
    await _sendPeriodicData();
    await _sendAnalysisData();

    periodicTimer?.cancel();
    periodicTimer = null;
    await analytics.close();
  }

  /// Record data from the given [params].
  void startedGetRefactoring(EditGetRefactoringParams params) {
    var requestData = getRequestData(editRequestGetRefactoring);
    requestData.addEnumValue(refactoringKindEnumKey, params.kind.name);
  }

  /// Record that the server started working on the give [request] at the given
  /// [startTime].
  void startedRequest({required Request request, required DateTime startTime}) {
    var method = request.method;
    _activeRequests[request.id] = ActiveRequestData(
      method,
      request.clientRequestTime,
      startTime,
    );
  }

  /// Record that the server started working on the give [request] at the given
  /// [startTime].
  void startedRequestMessage({
    required RequestMessage request,
    required DateTime startTime,
  }) {
    _activeRequests[request.id.asLspIdString] = ActiveRequestData(
      request.method.toString(),
      request.clientRequestTime,
      startTime,
    );
  }

  /// Record data from the given [params].
  void startedSetAnalysisRoots(AnalysisSetAnalysisRootsParams params) {
    var requestData = getRequestData(analysisRequestSetAnalysisRoots);
    requestData.addValue(includedKey, params.included.length);
    requestData.addValue(excludedKey, params.excluded.length);
  }

  /// Record data from the given [params].
  void startedSetPriorityFiles(AnalysisSetPriorityFilesParams params) {
    var requestData = getRequestData(analysisRequestSetPriorityFiles);
    requestData.addValue(
      analysisRequestSetPriorityFilesFiles,
      params.files.length,
    );
  }

  /// Record that the server was started at the given [time], that it was passed
  /// the given command-line [arguments], that it was started by the client with
  /// the given [clientId] and [clientVersion].
  void startUp({
    required DateTime time,
    required List<String> arguments,
    required String clientId,
    required String? clientVersion,
  }) {
    _sessionData = SessionData(
      startTime: time,
      commandLineArguments: arguments.join(','),
      clientId: clientId,
      clientVersion: clientVersion ?? '',
    );
  }

  /// Return an HTML representation of the data that has been recorded.
  String? toHtml(StringBuffer buffer) {
    var sessionData = _sessionData;
    if (sessionData == null) {
      return null;
    }

    void h3(String title) {
      buffer.writeln('<h3>${escape(title)}</h3>');
    }

    void h4(String title) {
      buffer.writeln('<h4>${escape(title)}</h4>');
    }

    void h5(String title) {
      buffer.writeln('<h5>${escape(title)}</h5>');
    }

    void li(String item) {
      buffer.writeln('<li>${escape(item)}</li>');
    }

    List<MapEntry<String, V>> sorted<V>(
      Iterable<MapEntry<String, V>> entries,
    ) => entries.sortedBy((entry) => entry.key);

    buffer.writeln('<hr>');

    var endTime = DateTime.now().millisecondsSinceEpoch;
    var duration = endTime - sessionData.startTime.millisecondsSinceEpoch;
    h3('Session data');
    buffer.writeln('<ul>');
    li('clientId: ${sessionData.clientId}');
    li('clientVersion: ${sessionData.clientVersion}');
    li('duration: ${duration.toString()}');
    li('flags: ${sessionData.commandLineArguments}');
    li('parameters: ${sessionData.initializeParams}');
    li('plugins: ${_pluginData.usageCountData}');
    buffer.writeln('</ul>');

    if (_completedRequests.isNotEmpty) {
      h3('Server response times');
      var entries = sorted(_completedRequests.entries);
      for (var entry in entries) {
        var data = entry.value;
        h4(data.method);
        buffer.writeln('<ul>');
        li('latency: ${data.latencyTimes.toAnalyticsString()}');
        li('duration: ${data.responseTimes.toAnalyticsString()}');
        for (var field in data.additionalPercentiles.entries) {
          li('${field.key}: ${field.value.toAnalyticsString()}');
        }
        for (var field in data.additionalEnumCounts.entries) {
          li('${field.key}: ${json.encode(field.value)}');
        }
        buffer.writeln('</ul>');
      }
    }

    var responseTimes = PluginManager.pluginResponseTimes;
    if (responseTimes.isNotEmpty) {
      h3('Plugin response times');
      for (var pluginEntry in responseTimes.entries) {
        h4(pluginEntry.key.safePluginId);
        var entries = sorted(pluginEntry.value.entries);
        for (var responseEntry in entries) {
          h5(responseEntry.key);
          buffer.writeln('<ul>');
          li('duration: ${responseEntry.value.toAnalyticsString()}');
          buffer.writeln('</ul>');
        }
      }
    }

    if (_completedNotifications.isNotEmpty) {
      h3('Notification handling times');
      buffer.writeln('<ul>');
      var entries = sorted(_completedNotifications.entries);
      for (var entry in entries) {
        var data = entry.value;
        li('latency: ${data.latencyTimes.toAnalyticsString()}');
        li('method: ${data.method}');
        li('duration: ${data.handlingTimes.toAnalyticsString()}');
      }
      buffer.writeln('</ul>');
    }

    if (_analysisWorkingDurations.valueCount > 0) {
      h3('Analysis status periods');
      buffer.writeln('<ul>');
      li('workingDurations: ${_analysisWorkingDurations.toAnalyticsString()}');
      buffer.writeln('</ul>');
    }

    if (_lintUsageCounts.isNotEmpty) {
      h3('Lint usage counts');
      buffer.writeln('<ul>');
      li('usageCounts: ${json.encode(_lintUsageCounts)}');
      buffer.writeln('</ul>');
    }

    if (_severityAdjustments.isNotEmpty) {
      h3('Severity adjustments');
      buffer.writeln('<ul>');
      li('adjustmentCounts: ${json.encode(_severityAdjustments)}');
      buffer.writeln('</ul>');
    }

    var analysisData = _contextStructure;
    if (analysisData != null) {
      h3('Analysis data');
      buffer.writeln('<ul>');
      li('numberOfContexts: ${json.encode(analysisData.numberOfContexts)}');
      li(
        'contextWorkspaceType: ${json.encode(analysisData.contextWorkspaceType.toString())}',
      );
      li(
        'numberOfPackagesPerWorkspace: ${json.encode(analysisData.numberOfPackagesInWorkspace.toAnalyticsString())}',
      );
      li('immediateFileCount: ${json.encode(analysisData.immediateFileCount)}');
      li(
        'immediateFileLineCount: ${json.encode(analysisData.immediateFileLineCount)}',
      );
      li(
        'transitiveFileCount: ${json.encode(analysisData.transitiveFileCount)}',
      );
      li(
        'transitiveFileLineCount: ${json.encode(analysisData.transitiveFileLineCount)}',
      );
      li(
        'transitiveFileUniqueCount: ${json.encode(analysisData.transitiveFileUniqueCount)}',
      );
      li(
        'transitiveFileUniqueLineCount: ${json.encode(analysisData.transitiveFileUniqueLineCount)}',
      );
      li(
        'libraryCycleLibraryCounts: ${analysisData.libraryCycleLibraryCounts.toAnalyticsString()}',
      );
      li(
        'libraryCycleLineCounts: ${analysisData.libraryCycleLineCounts.toAnalyticsString()}',
      );
      buffer.writeln('</ul>');
    }

    return buffer.toString();
  }

  void _finalizeOpenWorkingPeriod() {
    if (_analysisWorkingStart case var workingStart?) {
      var durationMilliseconds =
          DateTime.now().millisecondsSinceEpoch -
          workingStart.millisecondsSinceEpoch;
      _analysisWorkingDurations.addValue(durationMilliseconds);
      _analysisWorkingStart = null;
    }
  }

  /// Record that the request with the given [id] was responded to at the given
  /// [sendTime].
  void _recordResponseData(String id, DateTime sendTime) {
    var data = _activeRequests.remove(id);
    if (data == null) {
      return;
    }

    var method = data.method;
    var clientRequestTime = data.clientRequestTime;
    var startTime = data.startTime.millisecondsSinceEpoch;

    var requestData = getRequestData(method);

    if (clientRequestTime != null) {
      var latencyTime = startTime - clientRequestTime;
      requestData.latencyTimes.addValue(latencyTime);
    }

    var responseTime = sendTime.millisecondsSinceEpoch - startTime;
    requestData.responseTimes.addValue(responseTime);
  }

  /// Send information about the number of files and the number of lines of code
  /// in those files.
  Future<void> _sendAnalysisData() async {
    var contextStructure = _contextStructure;
    if (contextStructure != null) {
      analytics.send(
        Event.contextStructure(
          numberOfContexts: contextStructure.numberOfContexts,
          immediateFileCount: contextStructure.immediateFileCount,
          immediateFileLineCount: contextStructure.immediateFileLineCount,
          transitiveFileCount: contextStructure.transitiveFileCount,
          transitiveFileLineCount: contextStructure.transitiveFileLineCount,
          transitiveFileUniqueCount: contextStructure.transitiveFileUniqueCount,
          transitiveFileUniqueLineCount:
              contextStructure.transitiveFileUniqueLineCount,
          libraryCycleLibraryCounts: contextStructure.libraryCycleLibraryCounts
              .toAnalyticsString(),
          libraryCycleLineCounts: contextStructure.libraryCycleLineCounts
              .toAnalyticsString(),
          contextWorkspaceType: contextStructure.contextWorkspaceType
              .toString(),
          numberOfPackagesInWorkspace: contextStructure
              .numberOfPackagesInWorkspace
              .toAnalyticsString(),
        ),
      );
    }
  }

  /// Send information about analysis statistics.
  Future<void> _sendAnalysisStatistics() async {
    var statistics = _analysisWorkingStatistics;
    if (statistics == null) {
      return;
    }

    analytics.send(
      Event.analysisStatistics(
        workingDuration: _analysisWorkingDurations.toAnalyticsString(),
        withFineDependencies: statistics.withFineDependencies,
        changedFileUniqueCount: statistics.uniqueChangedFiles.length,
        removedFileUniqueCount: statistics.uniqueRemovedFiles.length,
        changedFileEventCount: statistics.changeFileEventCount,
        removedFileEventCount: statistics.removeFileEventCount,
        immediateFileCountPercentiles: statistics.immediateFileCountPercentiles
            .toAnalyticsString(),
        immediateFileLineCountPercentiles: statistics
            .immediateFileLineCountPercentiles
            .toAnalyticsString(),
        transitiveFileCountPercentiles: statistics
            .transitiveFileCountPercentiles
            .toAnalyticsString(),
        transitiveFileLineCountPercentiles: statistics
            .transitiveFileLineCountPercentiles
            .toAnalyticsString(),
        produceErrorsPotentialFileCount:
            statistics.produceErrorsPotentialFileCount,
        produceErrorsPotentialFileLineCount:
            statistics.produceErrorsPotentialFileLineCount,
        produceErrorsActualFileCount: statistics.produceErrorsActualFileCount,
        produceErrorsActualFileLineCount:
            statistics.produceErrorsActualFileLineCount,
        produceErrorsDurationMs: statistics.produceErrorsMs,
        produceErrorsElementsDurationMs: statistics.produceErrorsElementsMs,
        libraryDiagnosticsBundleFailures: statistics
            .libraryDiagnosticsBundleRequirementsFailures
            .entries
            .map((e) => '${e.key.id}:${e.value}')
            .join(','),
      ),
    );

    _analysisWorkingStatistics = null;
    _analysisWorkingDurations.clear();
  }

  /// Send information about the number of times each lint is enabled in an
  /// analysis options file.
  Future<void> _sendLintUsageCounts() async {
    if (_lintUsageCounts.isNotEmpty) {
      var entries = _lintUsageCounts.entries.toList();
      _lintUsageCounts.clear();
      for (var entry in entries) {
        analytics.send(
          Event.lintUsageCount(count: entry.value, name: entry.key),
        );
      }
    }
  }

  /// Send information about the notifications handled by the server.
  Future<void> _sendNotificationHandlingTimes() async {
    if (_completedNotifications.isNotEmpty) {
      var completedNotifications = _completedNotifications.values.toList();
      _completedNotifications.clear();
      for (var data in completedNotifications) {
        analytics.send(
          Event.clientNotification(
            latency: data.latencyTimes.toAnalyticsString(),
            method: data.method,
            duration: data.handlingTimes.toAnalyticsString(),
          ),
        );
      }
    }
  }

  /// Send the information that is sent periodically, which is everything other
  /// than the session data.
  Future<void> _sendPeriodicData() async {
    await _sendServerResponseTimes();
    await _sendPluginResponseTimes();
    await _sendNotificationHandlingTimes();
    await _sendLintUsageCounts();
    await _sendSeverityAdjustments();
    await _sendAnalysisStatistics();
  }

  /// Send information about the response times of plugins.
  Future<void> _sendPluginResponseTimes() async {
    var responseTimes = PluginManager.pluginResponseTimes;
    if (responseTimes.isNotEmpty) {
      var entries = responseTimes.entries.toList();
      responseTimes.clear();
      for (var pluginEntry in entries) {
        for (var responseEntry in pluginEntry.value.entries) {
          analytics.send(
            Event.pluginRequest(
              pluginId: pluginEntry.key.safePluginId,
              method: responseEntry.key,
              duration: responseEntry.value.toAnalyticsString(),
            ),
          );
        }
      }
    }
  }

  /// Send information about the response times of server.
  Future<void> _sendServerResponseTimes() async {
    if (_completedRequests.isNotEmpty) {
      var completedRequests = _completedRequests.values.toList();
      _completedRequests.clear();
      for (var data in completedRequests) {
        analytics.send(
          Event.clientRequest(
            latency: data.latencyTimes.toAnalyticsString(),
            method: data.method,
            duration: data.responseTimes.toAnalyticsString(),
            added: data.additionalPercentiles[addedKey]?.toAnalyticsString(),
            excluded: data.additionalPercentiles[excludedKey]
                ?.toAnalyticsString(),
            files: data.additionalPercentiles[filesKey]?.toAnalyticsString(),
            included: data.additionalPercentiles[includedKey]
                ?.toAnalyticsString(),
            openWorkspacePaths: data
                .additionalPercentiles[openWorkspacePathsKey]
                ?.toAnalyticsString(),
            removed: data.additionalPercentiles[removedKey]
                ?.toAnalyticsString(),
          ),
        );
        var commandMap = data.additionalEnumCounts[commandEnumKey];
        if (commandMap != null) {
          for (var entry in commandMap.entries) {
            analytics.send(
              Event.commandExecuted(count: entry.value, name: entry.key),
            );
          }
        }
        // TODO(brianwilkerson): We don't appear to have an event defined that we
        //  can use to send analytics about how often old-style refactorings are
        //  being invoked.
        // var refactoringMap = data.additionalEnumCounts[refactoringKindEnumKey];
      }
    }
  }

  /// Sends information about the session, after the session has ended.
  Future<void> _sendSessionData(SessionData sessionData) async {
    var endTime = DateTime.now().millisecondsSinceEpoch;
    var duration = endTime - sessionData.startTime.millisecondsSinceEpoch;

    // Record the start of the session.
    var ideName = platform.environment['DASH__IDE_NAME'] ?? '';
    var ideVersion = platform.environment['DASH__IDE_VERSION'] ?? '';
    var pluginName = platform.environment['DASH__PLUGIN_NAME'] ?? '';
    var pluginVersion = platform.environment['DASH__PLUGIN_VERSION'] ?? '';

    analytics.send(
      Event.serverSession(
        flags: sessionData.commandLineArguments,
        parameters: sessionData.initializeParams,
        clientId: sessionData.clientId,
        clientVersion: sessionData.clientVersion,
        duration: duration,
        ideName: ideName,
        ideVersion: ideVersion,
        pluginName: pluginName,
        pluginVersion: pluginVersion,
      ),
    );
    for (var MapEntry(key: pluginId, value: percentileCalculator)
        in _pluginData.usageCounts.entries) {
      analytics.send(
        Event.pluginUse(
          count: _pluginData.recordCount,
          enabled: percentileCalculator.toAnalyticsString(),
          pluginId: pluginId,
        ),
      );
    }
    for (var counts in _pluginData.counts.values) {
      analytics.send(
        Event.plugins(
          count: counts.pluginCount,
          lintRuleCounts: counts.lintRuleCounts.toAnalyticsString(),
          warningRuleCounts: counts.warningRuleCounts.toAnalyticsString(),
          fixCounts: counts.fixCounts.toAnalyticsString(),
          assistCounts: counts.assistCounts.toAnalyticsString(),
        ),
      );
    }
  }

  /// Send information about the number of times that the severity of a
  /// diagnostic is changed in an analysis options file.
  Future<void> _sendSeverityAdjustments() async {
    if (_severityAdjustments.isNotEmpty) {
      var entries = _severityAdjustments.entries.toList();
      _severityAdjustments.clear();
      for (var entry in entries) {
        analytics.send(
          Event.severityAdjustment(
            adjustments: json.encode(entry.value),
            diagnostic: entry.key,
          ),
        );
      }
    }
  }
}

extension on Either2<int, String> {
  /// Returns a String ID for this LSP request ID.
  ///
  /// Prefixes with "LSP:" to avoid collisions with legacy IDs when both kinds
  /// of requests are being used (and may have independent/overlapping IDs).
  String get asLspIdString {
    var idString = map((value) => value.toString(), (value) => value);
    return 'LSP:$idString';
  }
}
