blob: bacaa3c8c0faa42c608c0ff290bf354faba2dd1c [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/active_request_data.dart';
import 'package:analysis_server/src/analytics/notification_data.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:collection/collection.dart';
import 'package:telemetry/telemetry.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 {
/// 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.
AnalyticsManager(this.analytics);
/// Record that the set of plugins known to the [pluginManager] has changed.
void changedPlugins(PluginManager pluginManager) {
_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('added', added.length);
requestData.addValue('removed', removed.length);
}
/// Record that the [contexts] have been created.
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;
}
}
}
/// 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.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('openWorkspacePaths', openWorkspacePaths.length);
}
/// 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?.asString;
if (id == null) {
return;
}
_recordResponseData(id, sendTime);
}
/// The server is shutting down. Report any accumulated analytics data.
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();
});
}
/// Record data from the given [params].
void startedGetRefactoring(EditGetRefactoringParams params) {
var requestData = _getRequestData(EDIT_REQUEST_GET_REFACTORING);
requestData.addEnumValue(
EDIT_REQUEST_GET_REFACTORING_KIND, 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.asString] = ActiveRequestData(
request.method.toString(), request.clientRequestTime, startTime);
}
/// Record data from the given [params].
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);
}
/// Record data from the given [params].
void startedSetPriorityFiles(AnalysisSetPriorityFilesParams params) {
var requestData = _getRequestData(ANALYSIS_REQUEST_SET_PRIORITY_FILES);
requestData.addValue(
ANALYSIS_REQUEST_SET_PRIORITY_FILES_FILES, 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], and that it was invoked from an
/// SDK with the given [sdkVersion].
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 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('sdkVersion: ${sessionData.sdkVersion}');
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.pluginId);
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 (_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>');
}
return buffer.toString();
}
/// 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),
});
}
}
}
extension on Either2<int, String> {
String get asString {
return map((value) => value.toString(), (value) => value);
}
}