| // 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; | |
| /// Whether the data gathered at the start of a session has been sent. | |
| bool _hasSentStartupData = false; | |
| 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, | |
| ), | |
| ); | |
| } | |
| @visibleForTesting | |
| Future<void> sendPeriodicData() => _sendPeriodicData(); | |
| /// 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 { | |
| await _sendPeriodicData(); | |
| await _sendPluginData(); | |
| 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. | |
| Future<void> _sendPeriodicData() async { | |
| var sessionData = _sessionData; | |
| if (sessionData != null && _contextStructure != null) { | |
| if (!_hasSentStartupData) { | |
| _hasSentStartupData = true; | |
| await _sendSessionData(sessionData); | |
| await _sendAnalysisData(); | |
| } | |
| } | |
| await _sendServerResponseTimes(); | |
| await _sendPluginResponseTimes(); | |
| await _sendNotificationHandlingTimes(); | |
| await _sendLintUsageCounts(); | |
| await _sendSeverityAdjustments(); | |
| await _sendAnalysisStatistics(); | |
| } | |
| /// Sends information about the plugins. | |
| Future<void> _sendPluginData() async { | |
| 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 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 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, | |
| // Send 0 as duration because the session is not ended yet. | |
| duration: 0, | |
| ideName: ideName, | |
| ideVersion: ideVersion, | |
| pluginName: pluginName, | |
| pluginVersion: pluginVersion, | |
| ), | |
| ); | |
| } | |
| /// 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'; | |
| } | |
| } |