blob: 63ca84b7581852a6148894159098a2bdee8642b5 [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 'package:analysis_server/lsp_protocol/protocol.dart';
import 'package:analysis_server/protocol/protocol.dart';
import 'package:analysis_server/src/analytics/analytics_manager.dart';
import 'package:analysis_server/src/analytics/percentile_calculator.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;
/// A map from the id of a request to data about the request.
Map<String, _ActiveRequestData> activeRequests = {};
/// A map from the name of a request to data about all such requests that have
/// been responded to.
Map<String, _RequestData> completedRequests = {};
/// Initialize a newly created analytics manager to report to the [analytics]
/// service.
GoogleAnalyticsManager(this.analytics);
@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 = this.sessionData;
if (sessionData == null) {
return;
}
// Send session data.
var endTime = DateTime.now().millisecondsSinceEpoch;
var duration = endTime - sessionData.startTime.millisecondsSinceEpoch;
analytics.sendEvent('language_server', 'session', parameters: {
'flags': sessionData.commandLineArguments,
'clientId': sessionData.clientId,
'sdkVersion': sessionData.sdkVersion,
'duration': duration.toString(),
// TODO(brianwilkerson) Report a list of the names of the plugins that
// were loaded, or possibly a map from plugin names to the number of
// analysis roots in which the plugins were loaded.
'plugins': '',
});
// Send response data.
for (var data in completedRequests.values) {
analytics.sendEvent('language_server', 'request', parameters: {
'latency': data.latencyTimes.toAnalyticsString(),
'name': data.method,
'duration': data.responseTimes.toAnalyticsString(),
// TODO(brianwilkerson) Report the latencies for each of the plugins,
// probably as a map from plugin name to latency information.
'plugins': '',
});
}
analytics.waitForLastPing(timeout: Duration(milliseconds: 200)).then((_) {
analytics.close();
});
}
@override
void startedRequest({required Request request, required DateTime startTime}) {
activeRequests[request.id] = _ActiveRequestData(
request.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 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);
}
/// 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 requestName = data.requestName;
var clientRequestTime = data.clientRequestTime;
var startTime = data.startTime.millisecondsSinceEpoch;
var requestData = completedRequests.putIfAbsent(
requestName, () => _RequestData(requestName));
if (clientRequestTime != null) {
var latencyTime = startTime - clientRequestTime;
requestData.latencyTimes.addValue(latencyTime);
}
var responseTime = sendTime.millisecondsSinceEpoch - startTime;
requestData.responseTimes.addValue(responseTime);
}
}
/// Data about a request that was received and is being handled.
class _ActiveRequestData {
/// The name of the request that was received.
final String requestName;
/// 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.requestName, this.clientRequestTime, this.startTime);
}
/// Data about the requests that have been responded to that have the same name.
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();
/// Initialize a newly create data holder for requests with the given
/// [method].
_RequestData(this.method);
}
/// 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 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);
}
}