blob: d3907ec4183e7af62e330ea78669773304b8cadf [file] [log] [blame]
// 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:convert';
import 'package:analysis_server/lsp_protocol/protocol.dart';
import 'package:analysis_server/protocol/protocol_constants.dart';
import 'package:analysis_server/src/analytics/analytics_manager.dart';
import 'package:analysis_server/src/analytics/percentile_calculator.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:analyzer/dart/analysis/analysis_context.dart';
import 'package:telemetry/telemetry.dart';
/// An implementation of [AnalyticsManager] that's appropriate to use when
/// analytics have been enabled.
class GoogleAnalyticsManager implements AnalyticsManager {
/// 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();
/// 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 contexts 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 = {};
/// Initialize a newly created analytics manager to report to the [analytics]
/// service.
GoogleAnalyticsManager(this.analytics);
@override
void changedPlugins(PluginManager pluginManager) {
_pluginData.recordPlugins(pluginManager);
}
@override
void changedWorkspaceFolders(
{required List<String> added, required List<String> removed}) {
var requestData =
_getRequestData(Method.workspace_didChangeWorkspaceFolders.toString());
requestData.addValue('added', added.length);
requestData.addValue('removed', removed.length);
}
@override
void createdAnalysisContexts(List<AnalysisContext> contexts) {
for (var context in contexts) {
for (var rule in context.analysisOptions.lintRules) {
var name = rule.name;
_lintUsageCounts[name] = (_lintUsageCounts[name] ?? 0) + 1;
}
for (var processor in context.analysisOptions.errorProcessors) {
var severity = processor.severity?.name ?? 'ignore';
var severityCounts =
_severityAdjustments.putIfAbsent(processor.code, () => {});
severityCounts[severity] = (severityCounts[severity] ?? 0) + 1;
}
}
}
@override
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);
}
@override
void initialize(InitializeParams params) {
var options = LspInitializationOptions(params.initializationOptions);
var paramNames = <String>[
if (options.closingLabels) 'closingLabels',
if (options.flutterOutline) 'flutterOutline',
if (options.onlyAnalyzeProjectsWithOpenFiles)
'onlyAnalyzeProjectsWithOpenFiles',
if (options.outline) 'outline',
if (options.suggestFromUnimportedLibraries)
'suggestFromUnimportedLibraries',
];
_sessionData?.initializeParams = paramNames.join(',');
}
@override
void initialized({required List<String> openWorkspacePaths}) {
var requestData = _getRequestData(Method.initialized.toString());
requestData.addValue('openWorkspacePaths', openWorkspacePaths.length);
}
@override
void sentResponse({required Response response}) {
var sendTime = DateTime.now();
_recordResponseData(response.id, sendTime);
}
@override
void sentResponseMessage({required ResponseMessage response}) {
var sendTime = DateTime.now();
var id = response.id?.asString;
if (id == null) {
return;
}
_recordResponseData(id, sendTime);
}
@override
void shutdown() {
final sessionData = _sessionData;
if (sessionData == null) {
return;
}
_sendSessionData(sessionData);
_sendServerResponseTimes();
_sendPluginResponseTimes();
_sendNotificationHandlingTimes();
_sendLintUsageCounts();
_sendSeverityAdjustments();
analytics.waitForLastPing(timeout: Duration(milliseconds: 200)).then((_) {
analytics.close();
});
}
@override
void startedGetRefactoring(EditGetRefactoringParams params) {
var requestData = _getRequestData(EDIT_REQUEST_GET_REFACTORING);
requestData.addEnumValue(
EDIT_REQUEST_GET_REFACTORING_KIND, params.kind.name);
}
@override
void startedRequest({required Request request, required DateTime startTime}) {
var method = request.method;
_activeRequests[request.id] =
_ActiveRequestData(method, request.clientRequestTime, startTime);
}
@override
void startedRequestMessage(
{required RequestMessage request, required DateTime startTime}) {
_activeRequests[request.id.asString] = _ActiveRequestData(
request.method.toString(), request.clientRequestTime, startTime);
}
@override
void startedSetAnalysisRoots(AnalysisSetAnalysisRootsParams params) {
var requestData = _getRequestData(ANALYSIS_REQUEST_SET_ANALYSIS_ROOTS);
requestData.addValue(
ANALYSIS_REQUEST_SET_ANALYSIS_ROOTS_INCLUDED, params.included.length);
requestData.addValue(
ANALYSIS_REQUEST_SET_ANALYSIS_ROOTS_EXCLUDED, params.excluded.length);
}
@override
void startedSetPriorityFiles(AnalysisSetPriorityFilesParams params) {
var requestData = _getRequestData(ANALYSIS_REQUEST_SET_PRIORITY_FILES);
requestData.addValue(
ANALYSIS_REQUEST_SET_PRIORITY_FILES_FILES, params.files.length);
}
@override
void startUp(
{required DateTime time,
required List<String> arguments,
required String clientId,
required String? clientVersion,
required String sdkVersion}) {
_sessionData = _SessionData(
startTime: time,
commandLineArguments: arguments.join(','),
clientId: clientId,
clientVersion: clientVersion ?? '',
sdkVersion: sdkVersion);
}
/// Return the request data for requests that have the given [method].
_RequestData _getRequestData(String method) {
return _completedRequests.putIfAbsent(method, () => _RequestData(method));
}
/// 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);
}
void _sendLintUsageCounts() {
if (_lintUsageCounts.isNotEmpty) {
analytics.sendEvent('language_server', 'lintUsageCounts', parameters: {
'usageCounts': json.encode(_lintUsageCounts),
});
}
}
/// Send information about the notifications handled by the server.
void _sendNotificationHandlingTimes() {
for (var data in _completedNotifications.values) {
analytics.sendEvent('language_server', 'notification', parameters: {
'latency': data.latencyTimes.toAnalyticsString(),
'method': data.method,
'duration': data.handlingTimes.toAnalyticsString(),
});
}
}
/// Send information about the response times of plugins.
void _sendPluginResponseTimes() {
var responseTimes = PluginManager.pluginResponseTimes;
for (var pluginEntry in responseTimes.entries) {
for (var responseEntry in pluginEntry.value.entries) {
analytics.sendEvent('language_server', 'pluginRequest', parameters: {
'pluginId': pluginEntry.key.pluginId,
'method': responseEntry.key,
'duration': responseEntry.value.toAnalyticsString(),
});
}
}
}
/// Send information about the response times of server.
void _sendServerResponseTimes() {
for (var data in _completedRequests.values) {
analytics.sendEvent('language_server', 'request', parameters: {
'latency': data.latencyTimes.toAnalyticsString(),
'method': data.method,
'duration': data.responseTimes.toAnalyticsString(),
for (var field in data.additionalPercentiles.entries)
field.key: field.value.toAnalyticsString(),
for (var field in data.additionalEnumCounts.entries)
field.key: json.encode(field.value),
});
}
}
/// Send information about the session.
void _sendSessionData(_SessionData sessionData) {
var endTime = DateTime.now().millisecondsSinceEpoch;
var duration = endTime - sessionData.startTime.millisecondsSinceEpoch;
analytics.sendEvent('language_server', 'session', parameters: {
'flags': sessionData.commandLineArguments,
'parameters': sessionData.initializeParams,
'clientId': sessionData.clientId,
'clientVersion': sessionData.clientVersion,
'sdkVersion': sessionData.sdkVersion,
'duration': duration.toString(),
'plugins': _pluginData.usageCountData,
});
}
void _sendSeverityAdjustments() {
if (_severityAdjustments.isNotEmpty) {
analytics
.sendEvent('language_server', 'severityAdjustments', parameters: {
'adjustmentCounts': json.encode(_severityAdjustments),
});
}
}
}
/// Data about a request that was received and is being handled.
class _ActiveRequestData {
/// The name of the request that was received.
final String method;
/// The time at which the client sent the request.
final int? clientRequestTime;
/// The time at which the request was received.
final DateTime startTime;
/// Initialize a newly created data holder.
_ActiveRequestData(this.method, this.clientRequestTime, this.startTime);
}
/// Data about the notifications that have been handled that have the same
/// method.
class _NotificationData {
/// The name of the notifications.
final String method;
/// The percentile calculator for latency times. The _latency time_ is the
/// time from when the client sent the request until the time the server
/// started processing the request.
final PercentileCalculator latencyTimes = PercentileCalculator();
/// The percentile calculator for handling times. The _handling time_ is the
/// time from when the server started processing the notification until the
/// handling was complete.
final PercentileCalculator handlingTimes = PercentileCalculator();
/// Initialize a newly create data holder for notifications with the given
/// [method].
_NotificationData(this.method);
}
/// Data about the plugins associated with the context roots.
class _PluginData {
/// The number of times that plugin information has been recorded.
int recordCount = 0;
/// A table mapping the ids of running plugins to the number of context roots
/// associated with each of the plugins.
Map<String, PercentileCalculator> usageCounts = {};
/// Initialize a newly created holder of plugin data.
_PluginData();
String get usageCountData {
return json.encode({
'recordCount': recordCount,
'rootCounts': _encodeUsageCounts(),
});
}
/// Use the [pluginManager] to record data about the plugins that are
/// currently running.
void recordPlugins(PluginManager pluginManager) {
recordCount++;
var plugins = pluginManager.plugins;
for (var i = 0; i < plugins.length; i++) {
var info = plugins[i];
usageCounts
.putIfAbsent(info.pluginId, () => PercentileCalculator())
.addValue(info.contextRoots.length);
}
}
/// Return an encoding of the [usageCounts].
Map<String, Object> _encodeUsageCounts() {
var encoded = <String, Object>{};
for (var entry in usageCounts.entries) {
encoded[entry.key] = entry.value.toJson();
}
return encoded;
}
}
/// Data about the requests that have been responded to that have the same
/// method.
class _RequestData {
/// The name of the requests.
final String method;
/// The percentile calculator for latency times. The _latency time_ is the
/// time from when the client sent the request until the time the server
/// started processing the request.
final PercentileCalculator latencyTimes = PercentileCalculator();
/// The percentile calculator for response times. The _response time_ is the
/// time from when the server started processing the request until the time
/// the response was sent.
final PercentileCalculator responseTimes = PercentileCalculator();
/// A table mapping the names of fields in a request's parameters to the
/// percentile calculators related to the value of the parameter (such as the
/// length of a list).
final Map<String, PercentileCalculator> additionalPercentiles = {};
/// A table mapping the name of a field in a request's parameters and the name
/// of an enum constant to the number of times that the given constant was
/// used as the value of the field.
final Map<String, Map<String, int>> additionalEnumCounts = {};
/// Initialize a newly create data holder for requests with the given
/// [method].
_RequestData(this.method);
/// Record the occurrence of the enum constant with the given [enumName] for
/// the field with the given [name].
void addEnumValue<E>(String name, String enumName) {
var counts = additionalEnumCounts.putIfAbsent(name, () => {});
counts[enumName] = (counts[enumName] ?? 0) + 1;
}
/// Record a [value] for the field with the given [name].
void addValue(String name, int value) {
additionalPercentiles
.putIfAbsent(name, PercentileCalculator.new)
.addValue(value);
}
}
/// Data about the current session.
class _SessionData {
/// The time at which the current session started.
final DateTime startTime;
/// The command-line arguments passed to the server on startup.
final String commandLineArguments;
/// The parameters passed on initialize.
String initializeParams = '';
/// The name of the client that started the server.
final String clientId;
/// The version of the client that started the server, or an empty string if
/// no version information was provided.
final String clientVersion;
/// The version of the SDK from which the server was started.
final String sdkVersion;
/// Initialize a newly created data holder.
_SessionData(
{required this.startTime,
required this.commandLineArguments,
required this.clientId,
required this.clientVersion,
required this.sdkVersion});
}
extension on Either2<int, String> {
String get asString {
return map((value) => value.toString(), (value) => value);
}
}