-
-
Notifications
You must be signed in to change notification settings - Fork 220
/
sentry.dart
401 lines (328 loc) · 11.9 KB
/
sentry.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/// A pure Dart client for Sentry.io crash reporting.
library sentry;
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart';
import 'package:meta/meta.dart';
import 'package:quiver/time.dart';
import 'package:usage/uuid/uuid.dart';
import 'src/stack_trace.dart';
import 'src/utils.dart';
import 'src/version.dart';
export 'src/version.dart';
/// Logs crash reports and events to the Sentry.io service.
class SentryClient {
/// Sentry.io client identifier for _this_ client.
@visibleForTesting
static const String sentryClient = '$sdkName/$sdkVersion';
/// The default logger name used if no other value is supplied.
static const String defaultLoggerName = 'SentryClient';
/// Instantiates a client using [dns] issued to your project by Sentry.io as
/// the endpoint for submitting events.
///
/// [environmentAttributes] contain event attributes that do not change over
/// the course of a program's lifecycle. These attributes will be added to
/// all events captured via this client. The following attributes often fall
/// under this category: [Event.loggerName], [Event.serverName],
/// [Event.release], [Event.environment].
///
/// If [compressPayload] is `true` the outgoing HTTP payloads are compressed
/// using gzip. Otherwise, the payloads are sent in plain UTF8-encoded JSON
/// text. If not specified, the compression is enabled by default.
///
/// If [httpClient] is provided, it is used instead of the default client to
/// make HTTP calls to Sentry.io. This is useful in tests.
///
/// If [clock] is provided, it is used to get time instead of the system
/// clock. This is useful in tests.
///
/// If [uuidGenerator] is provided, it is used to generate the "event_id"
/// field instead of the built-in random UUID v4 generator. This is useful in
/// tests.
factory SentryClient({
@required String dsn,
Event environmentAttributes,
bool compressPayload,
Client httpClient,
Clock clock,
UuidGenerator uuidGenerator,
}) {
httpClient ??= new Client();
clock ??= const Clock(_getUtcDateTime);
uuidGenerator ??= _generateUuidV4WithoutDashes;
compressPayload ??= true;
final Uri uri = Uri.parse(dsn);
final List<String> userInfo = uri.userInfo.split(':');
assert(() {
if (userInfo.length != 2)
throw new ArgumentError(
'Colon-separated publicKey:secretKey pair not found in the user info field of the DSN URI: $dsn');
if (uri.pathSegments.isEmpty)
throw new ArgumentError(
'Project ID not found in the URI path of the DSN URI: $dsn');
return true;
});
final String publicKey = userInfo.first;
final String secretKey = userInfo.last;
final String projectId = uri.pathSegments.last;
return new SentryClient._(
httpClient: httpClient,
clock: clock,
uuidGenerator: uuidGenerator,
environmentAttributes: environmentAttributes,
dsnUri: uri,
publicKey: publicKey,
secretKey: secretKey,
projectId: projectId,
compressPayload: compressPayload,
);
}
SentryClient._({
@required Client httpClient,
@required Clock clock,
@required UuidGenerator uuidGenerator,
@required this.environmentAttributes,
@required this.dsnUri,
@required this.publicKey,
@required this.secretKey,
@required this.compressPayload,
@required this.projectId,
})
: _httpClient = httpClient,
_clock = clock,
_uuidGenerator = uuidGenerator;
final Client _httpClient;
final Clock _clock;
final UuidGenerator _uuidGenerator;
/// Contains [Event] attributes that are automatically mixed into all events
/// captured through this client.
///
/// This event is designed to contain static values that do not change from
/// event to event, such as local operating system version, the version of
/// Dart/Flutter SDK, etc. These attributes have lower precedence than those
/// supplied in the even passed to [capture].
final Event environmentAttributes;
/// Whether to compress payloads sent to Sentry.io.
final bool compressPayload;
/// The DSN URI.
@visibleForTesting
final Uri dsnUri;
/// The Sentry.io public key for the project.
@visibleForTesting
final String publicKey;
/// The Sentry.io secret key for the project.
@visibleForTesting
final String secretKey;
/// The ID issued by Sentry.io to your project.
///
/// Attached to the event payload.
final String projectId;
@visibleForTesting
String get postUri =>
'${dsnUri.scheme}://${dsnUri.host}/api/$projectId/store/';
/// Reports an [event] to Sentry.io.
Future<SentryResponse> capture({@required Event event}) async {
final DateTime now = _clock.now();
final Map<String, String> headers = <String, String>{
'User-Agent': '$sentryClient',
'Content-Type': 'application/json',
'X-Sentry-Auth': 'Sentry sentry_version=6, '
'sentry_client=$sentryClient, '
'sentry_timestamp=${now.millisecondsSinceEpoch}, '
'sentry_key=$publicKey, '
'sentry_secret=$secretKey',
};
Map<String, dynamic> json = <String, dynamic>{
'project': projectId,
'event_id': _uuidGenerator(),
'timestamp': formatDateAsIso8601WithSecondPrecision(_clock.now()),
'logger': defaultLoggerName,
};
if (environmentAttributes != null)
mergeAttributes(environmentAttributes.toJson(), into: json);
mergeAttributes(event.toJson(), into: json);
List<int> body = UTF8.encode(JSON.encode(json));
if (compressPayload) {
headers['Content-Encoding'] = 'gzip';
body = GZIP.encode(body);
}
final Response response =
await _httpClient.post(postUri, headers: headers, body: body);
if (response.statusCode != 200) {
String errorMessage =
'Sentry.io responded with HTTP ${response.statusCode}';
if (response.headers['x-sentry-error'] != null)
errorMessage += ': ${response.headers['x-sentry-error']}';
return new SentryResponse.failure(errorMessage);
}
final String eventId = JSON.decode(response.body)['id'];
return new SentryResponse.success(eventId: eventId);
}
/// Reports the [exception] and optionally its [stackTrace] to Sentry.io.
Future<SentryResponse> captureException({
@required dynamic exception,
dynamic stackTrace,
}) {
final Event event = new Event(
exception: exception,
stackTrace: stackTrace,
);
return capture(event: event);
}
Future<Null> close() async {
_httpClient.close();
}
@override
String toString() => '$SentryClient("$postUri")';
}
/// A response from Sentry.io.
///
/// If [isSuccessful] the [eventId] field will contain the ID assigned to the
/// captured event by the Sentry.io backend. Otherwise, the [error] field will
/// contain the description of the error.
@immutable
class SentryResponse {
SentryResponse.success({@required eventId})
: isSuccessful = true,
eventId = eventId,
error = null;
SentryResponse.failure(error)
: isSuccessful = false,
eventId = null,
error = error;
/// Whether event was submitted successfully.
final bool isSuccessful;
/// The ID Sentry.io assigned to the submitted event for future reference.
final String eventId;
/// Error message, if the response is not successful.
final String error;
}
typedef UuidGenerator = String Function();
String _generateUuidV4WithoutDashes() {
return new Uuid().generateV4().replaceAll('-', '');
}
/// Severity of the logged [Event].
@immutable
class SeverityLevel {
static const fatal = const SeverityLevel._('fatal');
static const error = const SeverityLevel._('error');
static const warning = const SeverityLevel._('warning');
static const info = const SeverityLevel._('info');
static const debug = const SeverityLevel._('debug');
const SeverityLevel._(this.name);
/// API name of the level as it is encoded in the JSON protocol.
final String name;
}
/// Sentry does not take a timezone and instead expects the date-time to be
/// submitted in UTC timezone.
DateTime _getUtcDateTime() => new DateTime.now().toUtc();
/// An event to be reported to Sentry.io.
@immutable
class Event {
/// Refers to the default fingerprinting algorithm.
///
/// You do not need to specify this value unless you supplement the default
/// fingerprint with custom fingerprints.
static const String defaultFingerprint = '{{ default }}';
/// Creates an event.
Event({
this.loggerName,
this.serverName,
this.release,
this.environment,
this.message,
this.exception,
this.stackTrace,
this.level,
this.culprit,
this.tags,
this.extra,
this.fingerprint,
});
/// The logger that logged the event.
final String loggerName;
/// Identifies the server that logged this event.
final String serverName;
/// The version of the application that logged the event.
final String release;
/// The environment that logged the event, e.g. "production", "staging".
final String environment;
/// Event message.
///
/// Generally an event either contains a [message] or an [exception].
final String message;
/// An object that was thrown.
///
/// It's `runtimeType` and `toString()` are logged. If this behavior is
/// undesirable, consider using a custom formatted [message] instead.
final dynamic exception;
/// The stack trace corresponding to the thrown [exception].
///
/// Can be `null`, a [String], or a [StackTrace].
final dynamic stackTrace;
/// How important this event is.
final SeverityLevel level;
/// What caused this event to be logged.
final String culprit;
/// Name/value pairs that events can be searched by.
final Map<String, String> tags;
/// Arbitrary name/value pairs attached to the event.
///
/// Sentry.io docs do not talk about restrictions on the values, other than
/// they must be JSON-serializable.
final Map<String, dynamic> extra;
/// Used to deduplicate events by grouping ones with the same fingerprint
/// together.
///
/// If not specified a default deduplication fingerprint is used. The default
/// fingerprint may be supplemented by additional fingerprints by specifying
/// multiple values. The default fingerprint can be specified by adding
/// [defaultFingerprint] to the list in addition to your custom values.
///
/// Examples:
///
/// // A completely custom fingerprint:
/// var custom = ['foo', 'bar', 'baz'];
/// // A fingerprint that supplements the default one with value 'foo':
/// var supplemented = [Event.defaultFingerprint, 'foo'];
final List<String> fingerprint;
/// Serializes this event to JSON.
Map<String, dynamic> toJson() {
final Map<String, dynamic> json = <String, dynamic>{
'platform': sdkPlatform,
'sdk': {
'version': sdkVersion,
'name': sdkName,
},
};
if (loggerName != null) json['logger'] = loggerName;
if (serverName != null) json['server_name'] = serverName;
if (release != null) json['release'] = release;
if (environment != null) json['environment'] = environment;
if (message != null) json['message'] = message;
if (exception != null) {
json['exception'] = [
<String, dynamic>{
'type': '${exception.runtimeType}',
'value': '$exception',
}
];
}
if (stackTrace != null) {
json['stacktrace'] = <String, dynamic>{
'frames': encodeStackTrace(stackTrace),
};
}
if (level != null) json['level'] = level.name;
if (culprit != null) json['culprit'] = culprit;
if (tags != null && tags.isNotEmpty) json['tags'] = tags;
if (extra != null && extra.isNotEmpty) json['extra'] = extra;
if (fingerprint != null && fingerprint.isNotEmpty)
json['fingerprint'] = fingerprint;
return json;
}
}