mirror of
https://github.com/flutter/packages.git
synced 2025-06-30 23:03:11 +08:00
9
.gitignore
vendored
9
.gitignore
vendored
@ -1,14 +1,15 @@
|
||||
.DS_Store
|
||||
.atom/
|
||||
.idea
|
||||
.dart_tool/
|
||||
.packages
|
||||
.pub/
|
||||
pubspec.lock
|
||||
|
||||
Podfile.lock
|
||||
Pods/
|
||||
GeneratedPluginRegistrant.h
|
||||
GeneratedPluginRegistrant.m
|
||||
|
||||
GeneratedPluginRegistrant.java
|
||||
|
||||
pubspec.lock
|
||||
packages/sentry/build/
|
||||
packages/sentry/android/
|
||||
packages/sentry/ios/
|
||||
|
8
packages/sentry/.idea/modules.xml
generated
Normal file
8
packages/sentry/.idea/modules.xml
generated
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/sentry.iml" filepath="$PROJECT_DIR$/.idea/sentry.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
18
packages/sentry/.idea/sentry.iml
generated
Normal file
18
packages/sentry/.idea/sentry.iml
generated
Normal file
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.pub" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/bin/packages" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/test/packages" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/tool/packages" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="library" name="Dart SDK" level="project" />
|
||||
<orderEntry type="library" name="Dart Packages" level="project" />
|
||||
</component>
|
||||
</module>
|
6
packages/sentry/.idea/vcs.xml
generated
Normal file
6
packages/sentry/.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
5
packages/sentry/.travis.yml
Normal file
5
packages/sentry/.travis.yml
Normal file
@ -0,0 +1,5 @@
|
||||
language: dart
|
||||
dart:
|
||||
# - stable # there's no Dart 2 on the stable channel yet
|
||||
- dev
|
||||
script: ./tool/presubmit.sh
|
7
packages/sentry/AUTHORS
Normal file
7
packages/sentry/AUTHORS
Normal file
@ -0,0 +1,7 @@
|
||||
# Below is a list of people and organizations that have contributed
|
||||
# to package:sentry. Names should be added to the list like so:
|
||||
#
|
||||
# Name/Organization <email address>
|
||||
|
||||
Google Inc.
|
||||
Simon Lightfoot <simon@devangels.london>
|
58
packages/sentry/CHANGELOG.md
Normal file
58
packages/sentry/CHANGELOG.md
Normal file
@ -0,0 +1,58 @@
|
||||
# package:sentry changelog
|
||||
|
||||
## 2.1.1
|
||||
|
||||
- Defensively copy internal maps event attributes to
|
||||
avoid shared mutable state (https://github.com/flutter/sentry/commit/044e4c1f43c2d199ed206e5529e2a630c90e4434)
|
||||
|
||||
## 2.1.0
|
||||
|
||||
- Support DNS format without secret key.
|
||||
- Remove dependency on `package:quiver`.
|
||||
- The `clock` argument to `SentryClient` constructor _should_ now be
|
||||
`ClockProvider` (but still accepts `Clock` for backwards compatibility).
|
||||
|
||||
## 2.0.2
|
||||
|
||||
- Add support for user context in Sentry events.
|
||||
|
||||
## 2.0.1
|
||||
|
||||
- Invert stack frames to be compatible with Sentry's default culprit detection.
|
||||
|
||||
## 2.0.0
|
||||
|
||||
- Fixed deprecation warnings for Dart 2
|
||||
- Refactored tests to work with Dart 2
|
||||
|
||||
## 1.0.0
|
||||
|
||||
- first and last Dart 1-compatible release (we may fix bugs on a separate branch if there's demand)
|
||||
- fix code for Dart 2
|
||||
|
||||
## 0.0.6
|
||||
|
||||
- use UTC in the `timestamp` field
|
||||
|
||||
## 0.0.5
|
||||
|
||||
- remove sub-seconds from the timestamp
|
||||
|
||||
## 0.0.4
|
||||
|
||||
- parse and report async gaps in stack traces
|
||||
|
||||
## 0.0.3
|
||||
|
||||
- environment attributes
|
||||
- auto-generate event_id and timestamp for events
|
||||
|
||||
## 0.0.2
|
||||
|
||||
- parse and report stack traces
|
||||
- use x-sentry-error HTTP response header
|
||||
- gzip outgoing payloads by default
|
||||
|
||||
## 0.0.1
|
||||
|
||||
- basic ability to send exception reports to Sentry.io
|
27
packages/sentry/LICENSE
Normal file
27
packages/sentry/LICENSE
Normal file
@ -0,0 +1,27 @@
|
||||
// Copyright 2014 The Chromium Authors. All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are
|
||||
// met:
|
||||
//
|
||||
// * Redistributions of source code must retain the above copyright
|
||||
// notice, this list of conditions and the following disclaimer.
|
||||
// * Redistributions in binary form must reproduce the above
|
||||
// copyright notice, this list of conditions and the following disclaimer
|
||||
// in the documentation and/or other materials provided with the
|
||||
// distribution.
|
||||
// * Neither the name of Google Inc. nor the names of its
|
||||
// contributors may be used to endorse or promote products derived from
|
||||
// this software without specific prior written permission.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
17
packages/sentry/PATENTS
Normal file
17
packages/sentry/PATENTS
Normal file
@ -0,0 +1,17 @@
|
||||
Google hereby grants to you a perpetual, worldwide, non-exclusive,
|
||||
no-charge, royalty-free, irrevocable (except as stated in this
|
||||
section) patent license to make, have made, use, offer to sell, sell,
|
||||
import, transfer, and otherwise run, modify and propagate the contents
|
||||
of this implementation, where such license applies only to those
|
||||
patent claims, both currently owned by Google and acquired in the
|
||||
future, licensable by Google that are necessarily infringed by this
|
||||
implementation. This grant does not include claims that would be
|
||||
infringed only as a consequence of further modification of this
|
||||
implementation. If you or your agent or exclusive licensee institute
|
||||
or order or agree to the institution of patent litigation or any other
|
||||
patent enforcement activity against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that this
|
||||
implementation constitutes direct or contributory patent infringement,
|
||||
or inducement of patent infringement, then any patent rights granted
|
||||
to you under this License for this implementation shall terminate as
|
||||
of the date such litigation is filed.
|
60
packages/sentry/README.md
Normal file
60
packages/sentry/README.md
Normal file
@ -0,0 +1,60 @@
|
||||
# Sentry.io client for Dart
|
||||
|
||||
[](https://travis-ci.org/flutter/sentry)
|
||||
|
||||
Use this library in your Dart programs (Flutter, command-line and (TBD) AngularDart) to report errors thrown by your
|
||||
program to https://sentry.io error tracking service.
|
||||
|
||||
## Versions
|
||||
|
||||
`>=0.0.0 <2.0.0` is the range of versions compatible with Dart 1.
|
||||
|
||||
`>=2.0.0 <3.0.0` is the range of versions compatible with Dart 2.
|
||||
|
||||
## Usage
|
||||
|
||||
Sign up for a Sentry.io account and get a DSN at http://sentry.io.
|
||||
|
||||
Add `sentry` dependency to your `pubspec.yaml`:
|
||||
|
||||
```yaml
|
||||
dependencies:
|
||||
sentry: any
|
||||
```
|
||||
|
||||
In your Dart code, import `package:sentry/sentry.dart` and create a `SentryClient` using the DSN issued by Sentry.io:
|
||||
|
||||
```dart
|
||||
import 'package:sentry/sentry.dart';
|
||||
|
||||
final SentryClient sentry = new SentryClient(dsn: YOUR_DSN);
|
||||
```
|
||||
|
||||
In an exception handler, call `captureException()`:
|
||||
|
||||
```dart
|
||||
main() async {
|
||||
try {
|
||||
doSomethingThatMightThrowAnError();
|
||||
} catch(error, stackTrace) {
|
||||
await sentry.captureException(
|
||||
exception: error,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Tips for catching errors
|
||||
|
||||
- use a `try/catch` block
|
||||
- create a `Zone` with an error handler, e.g. using [runZoned][run_zoned]
|
||||
- in Flutter, use [FlutterError.onError][flutter_error]
|
||||
- use `Isolate.current.addErrorListener` to capture uncaught errors in the root zone
|
||||
|
||||
[run_zoned]: https://api.dartlang.org/stable/dart-async/runZoned.html
|
||||
[flutter_error]: https://docs.flutter.io/flutter/foundation/FlutterError/onError.html
|
||||
|
||||
## Found a bug?
|
||||
|
||||
Please file it at https://github.com/flutter/flutter/issues/new
|
51
packages/sentry/bin/test.dart
Normal file
51
packages/sentry/bin/test.dart
Normal file
@ -0,0 +1,51 @@
|
||||
// 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.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:sentry/sentry.dart';
|
||||
|
||||
/// Sends a test exception report to Sentry.io using this Dart client.
|
||||
Future<Null> main(List<String> rawArgs) async {
|
||||
if (rawArgs.length != 1) {
|
||||
stderr.writeln(
|
||||
'Expected exactly one argument, which is the DSN issued by Sentry.io to your project.');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
final String dsn = rawArgs.single;
|
||||
final SentryClient client = new SentryClient(dsn: dsn);
|
||||
|
||||
try {
|
||||
await foo();
|
||||
} catch (error, stackTrace) {
|
||||
print('Reporting the following stack trace: ');
|
||||
print(stackTrace);
|
||||
final SentryResponse response = await client.captureException(
|
||||
exception: error,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
|
||||
if (response.isSuccessful) {
|
||||
print('SUCCESS\nid: ${response.eventId}');
|
||||
} else {
|
||||
print('FAILURE: ${response.error}');
|
||||
}
|
||||
} finally {
|
||||
await client.close();
|
||||
}
|
||||
}
|
||||
|
||||
Future<Null> foo() async {
|
||||
await bar();
|
||||
}
|
||||
|
||||
Future<Null> bar() async {
|
||||
await baz();
|
||||
}
|
||||
|
||||
Future<Null> baz() async {
|
||||
throw new StateError('This is a test error');
|
||||
}
|
489
packages/sentry/lib/sentry.dart
Normal file
489
packages/sentry/lib/sentry.dart
Normal file
@ -0,0 +1,489 @@
|
||||
// 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:usage/uuid/uuid.dart';
|
||||
|
||||
import 'src/stack_trace.dart';
|
||||
import 'src/utils.dart';
|
||||
import 'src/version.dart';
|
||||
|
||||
export 'src/version.dart';
|
||||
|
||||
/// Used to provide timestamp for logging.
|
||||
typedef ClockProvider = DateTime Function();
|
||||
|
||||
/// 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 [dsn] 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. Should be an implementation of [ClockProvider].
|
||||
/// This parameter is dynamic to maintain backwards compatibility with
|
||||
/// previous use of [Clock](https://pub.dartlang.org/documentation/quiver/latest/quiver.time/Clock-class.html)
|
||||
/// from [`package:quiver`](https://pub.dartlang.org/packages/quiver).
|
||||
///
|
||||
/// 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,
|
||||
dynamic clock,
|
||||
UuidGenerator uuidGenerator,
|
||||
}) {
|
||||
httpClient ??= new Client();
|
||||
clock ??= _getUtcDateTime;
|
||||
uuidGenerator ??= _generateUuidV4WithoutDashes;
|
||||
compressPayload ??= true;
|
||||
|
||||
final ClockProvider clockProvider =
|
||||
clock is ClockProvider ? clock : clock.get;
|
||||
|
||||
final Uri uri = Uri.parse(dsn);
|
||||
final List<String> userInfo = uri.userInfo.split(':');
|
||||
|
||||
assert(() {
|
||||
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[0];
|
||||
final String secretKey = userInfo.length >= 2 ? userInfo[1] : null;
|
||||
final String projectId = uri.pathSegments.last;
|
||||
|
||||
return new SentryClient._(
|
||||
httpClient: httpClient,
|
||||
clock: clockProvider,
|
||||
uuidGenerator: uuidGenerator,
|
||||
environmentAttributes: environmentAttributes,
|
||||
dsnUri: uri,
|
||||
publicKey: publicKey,
|
||||
secretKey: secretKey,
|
||||
projectId: projectId,
|
||||
compressPayload: compressPayload,
|
||||
);
|
||||
}
|
||||
|
||||
SentryClient._({
|
||||
@required Client httpClient,
|
||||
@required ClockProvider clock,
|
||||
@required UuidGenerator uuidGenerator,
|
||||
@required this.environmentAttributes,
|
||||
@required this.dsnUri,
|
||||
@required this.publicKey,
|
||||
this.secretKey,
|
||||
@required this.compressPayload,
|
||||
@required this.projectId,
|
||||
}) : _httpClient = httpClient,
|
||||
_clock = clock,
|
||||
_uuidGenerator = uuidGenerator;
|
||||
|
||||
final Client _httpClient;
|
||||
final ClockProvider _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;
|
||||
|
||||
/// Information about the current user.
|
||||
///
|
||||
/// This information is sent with every logged event. If the value
|
||||
/// of this field is updated, all subsequent events will carry the
|
||||
/// new information.
|
||||
///
|
||||
/// [Event.userContext] overrides the [User] context set here.
|
||||
///
|
||||
/// See also:
|
||||
/// * https://docs.sentry.io/learn/context/#capturing-the-user
|
||||
User userContext;
|
||||
|
||||
@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();
|
||||
String authHeader = 'Sentry sentry_version=6, sentry_client=$sentryClient, '
|
||||
'sentry_timestamp=${now.millisecondsSinceEpoch}, sentry_key=$publicKey';
|
||||
if (secretKey != null) {
|
||||
authHeader += ', sentry_secret=$secretKey';
|
||||
}
|
||||
|
||||
final Map<String, String> headers = <String, String>{
|
||||
'User-Agent': '$sentryClient',
|
||||
'Content-Type': 'application/json',
|
||||
'X-Sentry-Auth': authHeader,
|
||||
};
|
||||
|
||||
final Map<String, dynamic> data = <String, dynamic>{
|
||||
'project': projectId,
|
||||
'event_id': _uuidGenerator(),
|
||||
'timestamp': formatDateAsIso8601WithSecondPrecision(now),
|
||||
'logger': defaultLoggerName,
|
||||
};
|
||||
|
||||
if (environmentAttributes != null)
|
||||
mergeAttributes(environmentAttributes.toJson(), into: data);
|
||||
|
||||
// Merge the user context.
|
||||
if (userContext != null) {
|
||||
mergeAttributes({'user': userContext.toJson()}, into: data);
|
||||
}
|
||||
mergeAttributes(event.toJson(), into: data);
|
||||
|
||||
List<int> body = utf8.encode(json.encode(data));
|
||||
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 {
|
||||
const SentryResponse.success({@required this.eventId})
|
||||
: isSuccessful = true,
|
||||
error = null;
|
||||
|
||||
const SentryResponse.failure(this.error)
|
||||
: isSuccessful = false,
|
||||
eventId = null;
|
||||
|
||||
/// 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() =>
|
||||
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.
|
||||
const 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,
|
||||
this.userContext,
|
||||
});
|
||||
|
||||
/// 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;
|
||||
|
||||
/// Information about the current user.
|
||||
///
|
||||
/// The value in this field overrides the user context
|
||||
/// set in [SentryClient.userContext] for this logged event.
|
||||
final User userContext;
|
||||
|
||||
/// 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;
|
||||
|
||||
Map<String, dynamic> userContextMap;
|
||||
if (userContext != null &&
|
||||
(userContextMap = userContext.toJson()).isNotEmpty)
|
||||
json['user'] = userContextMap;
|
||||
|
||||
if (fingerprint != null && fingerprint.isNotEmpty)
|
||||
json['fingerprint'] = fingerprint;
|
||||
|
||||
return json;
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes the current user associated with the application, such as the
|
||||
/// currently signed in user.
|
||||
///
|
||||
/// The user can be specified globally in the [SentryClient.userContext] field,
|
||||
/// or per event in the [Event.userContext] field.
|
||||
///
|
||||
/// You should provide at least either an [id] (a unique identifier for an
|
||||
/// authenticated user) or [ipAddress] (their IP address).
|
||||
///
|
||||
/// Conforms to the User Interface contract for Sentry
|
||||
/// https://docs.sentry.io/clientdev/interfaces/user/.
|
||||
///
|
||||
/// The outgoing JSON representation is:
|
||||
///
|
||||
/// ```
|
||||
/// "user": {
|
||||
/// "id": "unique_id",
|
||||
/// "username": "my_user",
|
||||
/// "email": "foo@example.com",
|
||||
/// "ip_address": "127.0.0.1",
|
||||
/// "subscription": "basic"
|
||||
/// }
|
||||
/// ```
|
||||
class User {
|
||||
/// A unique identifier of the user.
|
||||
final String id;
|
||||
|
||||
/// The username of the user.
|
||||
final String username;
|
||||
|
||||
/// The email address of the user.
|
||||
final String email;
|
||||
|
||||
/// The IP of the user.
|
||||
final String ipAddress;
|
||||
|
||||
/// Any other user context information that may be helpful.
|
||||
///
|
||||
/// These keys are stored as extra information but not specifically processed
|
||||
/// by Sentry.
|
||||
final Map<String, dynamic> extras;
|
||||
|
||||
/// At a minimum you must set an [id] or an [ipAddress].
|
||||
const User({this.id, this.username, this.email, this.ipAddress, this.extras})
|
||||
: assert(id != null || ipAddress != null);
|
||||
|
||||
/// Produces a [Map] that can be serialized to JSON.
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
"id": id,
|
||||
"username": username,
|
||||
"email": email,
|
||||
"ip_address": ipAddress,
|
||||
"extras": extras,
|
||||
};
|
||||
}
|
||||
}
|
57
packages/sentry/lib/src/stack_trace.dart
Normal file
57
packages/sentry/lib/src/stack_trace.dart
Normal file
@ -0,0 +1,57 @@
|
||||
// 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.
|
||||
|
||||
import 'package:stack_trace/stack_trace.dart';
|
||||
|
||||
/// Sentry.io JSON encoding of a stack frame for the asynchronous suspension,
|
||||
/// which is the gap between asynchronous calls.
|
||||
const Map<String, dynamic> asynchronousGapFrameJson = const <String, dynamic>{
|
||||
'abs_path': '<asynchronous suspension>',
|
||||
};
|
||||
|
||||
/// Encodes [stackTrace] as JSON in the Sentry.io format.
|
||||
///
|
||||
/// [stackTrace] must be [String] or [StackTrace].
|
||||
List<Map<String, dynamic>> encodeStackTrace(dynamic stackTrace) {
|
||||
assert(stackTrace is String || stackTrace is StackTrace);
|
||||
final Chain chain = stackTrace is StackTrace
|
||||
? new Chain.forTrace(stackTrace)
|
||||
: new Chain.parse(stackTrace);
|
||||
|
||||
final List<Map<String, dynamic>> frames = <Map<String, dynamic>>[];
|
||||
for (int t = 0; t < chain.traces.length; t += 1) {
|
||||
frames.addAll(chain.traces[t].frames.map(encodeStackTraceFrame));
|
||||
if (t < chain.traces.length - 1) frames.add(asynchronousGapFrameJson);
|
||||
}
|
||||
return frames.reversed.toList();
|
||||
}
|
||||
|
||||
Map<String, dynamic> encodeStackTraceFrame(Frame frame) {
|
||||
final Map<String, dynamic> json = <String, dynamic>{
|
||||
'abs_path': _absolutePathForCrashReport(frame),
|
||||
'function': frame.member,
|
||||
'lineno': frame.line,
|
||||
'in_app': !frame.isCore,
|
||||
};
|
||||
|
||||
if (frame.uri.pathSegments.isNotEmpty)
|
||||
json['filename'] = frame.uri.pathSegments.last;
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
/// A stack frame's code path may be one of "file:", "dart:" and "package:".
|
||||
///
|
||||
/// Absolute file paths may contain personally identifiable information, and
|
||||
/// therefore are stripped to only send the base file name. For example,
|
||||
/// "/foo/bar/baz.dart" is reported as "baz.dart".
|
||||
///
|
||||
/// "dart:" and "package:" imports are always relative and are OK to send in
|
||||
/// full.
|
||||
String _absolutePathForCrashReport(Frame frame) {
|
||||
if (frame.uri.scheme != 'dart' && frame.uri.scheme != 'package')
|
||||
return frame.uri.pathSegments.last;
|
||||
|
||||
return '${frame.uri}';
|
||||
}
|
35
packages/sentry/lib/src/utils.dart
Normal file
35
packages/sentry/lib/src/utils.dart
Normal file
@ -0,0 +1,35 @@
|
||||
// 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.
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
/// Recursively merges [attributes] [into] another map of attributes.
|
||||
///
|
||||
/// [attributes] take precedence over the target map. Recursion takes place
|
||||
/// along [Map] values only. All other types are overwritten entirely.
|
||||
void mergeAttributes(Map<String, dynamic> attributes,
|
||||
{@required Map<String, dynamic> into}) {
|
||||
assert(attributes != null && into != null);
|
||||
attributes.forEach((String name, dynamic value) {
|
||||
dynamic targetValue = into[name];
|
||||
if (value is Map) {
|
||||
if (targetValue is! Map) {
|
||||
// Let mergeAttributes make a deep copy, because assigning a reference
|
||||
// of 'value' will expose 'value' to be mutated by further merges.
|
||||
into[name] = targetValue = <String, dynamic>{};
|
||||
}
|
||||
mergeAttributes(value, into: targetValue);
|
||||
} else {
|
||||
into[name] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
String formatDateAsIso8601WithSecondPrecision(DateTime date) {
|
||||
String iso = date.toIso8601String();
|
||||
final millisecondSeparatorIndex = iso.lastIndexOf('.');
|
||||
if (millisecondSeparatorIndex != -1)
|
||||
iso = iso.substring(0, millisecondSeparatorIndex);
|
||||
return iso;
|
||||
}
|
18
packages/sentry/lib/src/version.dart
Normal file
18
packages/sentry/lib/src/version.dart
Normal file
@ -0,0 +1,18 @@
|
||||
// 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.
|
||||
|
||||
/// Sentry.io has a concept of "SDK", which refers to the client library or
|
||||
/// tool used to submit events to Sentry.io.
|
||||
///
|
||||
/// This library contains Sentry.io SDK constants used by this package.
|
||||
library version;
|
||||
|
||||
/// The SDK version reported to Sentry.io in the submitted events.
|
||||
const String sdkVersion = '2.1.1';
|
||||
|
||||
/// The SDK name reported to Sentry.io in the submitted events.
|
||||
const String sdkName = 'dart';
|
||||
|
||||
/// The name of the SDK platform reported to Sentry.io in the submitted events.
|
||||
const String sdkPlatform = 'dart';
|
20
packages/sentry/pubspec.yaml
Normal file
20
packages/sentry/pubspec.yaml
Normal file
@ -0,0 +1,20 @@
|
||||
name: sentry
|
||||
version: 2.1.1
|
||||
description: A pure Dart Sentry.io client.
|
||||
author: Flutter Authors <flutter-dev@googlegroups.com>
|
||||
homepage: https://github.com/flutter/sentry
|
||||
|
||||
environment:
|
||||
sdk: ">=2.0.0-dev.28.0 <3.0.0"
|
||||
|
||||
dependencies:
|
||||
http: ">=0.11.0 <2.0.0"
|
||||
meta: ">=1.0.0 <2.0.0"
|
||||
stack_trace: ">=1.0.0 <2.0.0"
|
||||
usage: ">=3.0.0 <4.0.0"
|
||||
|
||||
dev_dependencies:
|
||||
args: ">=0.13.0 <2.0.0"
|
||||
test: ">=0.12.0 <2.0.0"
|
||||
yaml: ">=2.1.0 <3.0.0"
|
||||
mockito: ">=2.0.0 <4.0.0"
|
372
packages/sentry/test/sentry_test.dart
Normal file
372
packages/sentry/test/sentry_test.dart
Normal file
@ -0,0 +1,372 @@
|
||||
// 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.
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:http/http.dart';
|
||||
import 'package:sentry/sentry.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
const String _testDsn = 'https://public:secret@sentry.example.com/1';
|
||||
const String _testDsnWithoutSecret = 'https://public@sentry.example.com/1';
|
||||
|
||||
void main() {
|
||||
group('$SentryClient', () {
|
||||
test('can parse DSN', () async {
|
||||
final SentryClient client = new SentryClient(dsn: _testDsn);
|
||||
expect(client.dsnUri, Uri.parse(_testDsn));
|
||||
expect(client.postUri, 'https://sentry.example.com/api/1/store/');
|
||||
expect(client.publicKey, 'public');
|
||||
expect(client.secretKey, 'secret');
|
||||
expect(client.projectId, '1');
|
||||
await client.close();
|
||||
});
|
||||
|
||||
test('can parse DSN without secret', () async {
|
||||
final SentryClient client = new SentryClient(dsn: _testDsnWithoutSecret);
|
||||
expect(client.dsnUri, Uri.parse(_testDsnWithoutSecret));
|
||||
expect(client.postUri, 'https://sentry.example.com/api/1/store/');
|
||||
expect(client.publicKey, 'public');
|
||||
expect(client.secretKey, null);
|
||||
expect(client.projectId, '1');
|
||||
await client.close();
|
||||
});
|
||||
|
||||
test('sends client auth header without secret', () async {
|
||||
final MockClient httpMock = new MockClient();
|
||||
final ClockProvider fakeClockProvider =
|
||||
() => new DateTime.utc(2017, 1, 2);
|
||||
|
||||
Map<String, String> headers;
|
||||
|
||||
httpMock.answerWith((Invocation invocation) async {
|
||||
if (invocation.memberName == #close) {
|
||||
return null;
|
||||
}
|
||||
if (invocation.memberName == #post) {
|
||||
headers = invocation.namedArguments[#headers];
|
||||
return new Response('{"id": "test-event-id"}', 200);
|
||||
}
|
||||
fail('Unexpected invocation of ${invocation.memberName} in HttpMock');
|
||||
});
|
||||
|
||||
final SentryClient client = new SentryClient(
|
||||
dsn: _testDsnWithoutSecret,
|
||||
httpClient: httpMock,
|
||||
clock: fakeClockProvider,
|
||||
compressPayload: false,
|
||||
uuidGenerator: () => 'X' * 32,
|
||||
environmentAttributes: const Event(
|
||||
serverName: 'test.server.com',
|
||||
release: '1.2.3',
|
||||
environment: 'staging',
|
||||
),
|
||||
);
|
||||
|
||||
try {
|
||||
throw new ArgumentError('Test error');
|
||||
} catch (error, stackTrace) {
|
||||
final SentryResponse response = await client.captureException(
|
||||
exception: error, stackTrace: stackTrace);
|
||||
expect(response.isSuccessful, true);
|
||||
expect(response.eventId, 'test-event-id');
|
||||
expect(response.error, null);
|
||||
}
|
||||
|
||||
final Map<String, String> expectedHeaders = <String, String>{
|
||||
'User-Agent': '$sdkName/$sdkVersion',
|
||||
'Content-Type': 'application/json',
|
||||
'X-Sentry-Auth': 'Sentry sentry_version=6, '
|
||||
'sentry_client=${SentryClient.sentryClient}, '
|
||||
'sentry_timestamp=${fakeClockProvider().millisecondsSinceEpoch}, '
|
||||
'sentry_key=public',
|
||||
};
|
||||
|
||||
expect(headers, expectedHeaders);
|
||||
|
||||
await client.close();
|
||||
});
|
||||
|
||||
testCaptureException(bool compressPayload) async {
|
||||
final MockClient httpMock = new MockClient();
|
||||
final ClockProvider fakeClockProvider =
|
||||
() => new DateTime.utc(2017, 1, 2);
|
||||
|
||||
String postUri;
|
||||
Map<String, String> headers;
|
||||
List<int> body;
|
||||
httpMock.answerWith((Invocation invocation) async {
|
||||
if (invocation.memberName == #close) {
|
||||
return null;
|
||||
}
|
||||
if (invocation.memberName == #post) {
|
||||
postUri = invocation.positionalArguments.single;
|
||||
headers = invocation.namedArguments[#headers];
|
||||
body = invocation.namedArguments[#body];
|
||||
return new Response('{"id": "test-event-id"}', 200);
|
||||
}
|
||||
fail('Unexpected invocation of ${invocation.memberName} in HttpMock');
|
||||
});
|
||||
|
||||
final SentryClient client = new SentryClient(
|
||||
dsn: _testDsn,
|
||||
httpClient: httpMock,
|
||||
clock: fakeClockProvider,
|
||||
uuidGenerator: () => 'X' * 32,
|
||||
compressPayload: compressPayload,
|
||||
environmentAttributes: const Event(
|
||||
serverName: 'test.server.com',
|
||||
release: '1.2.3',
|
||||
environment: 'staging',
|
||||
),
|
||||
);
|
||||
|
||||
try {
|
||||
throw new ArgumentError('Test error');
|
||||
} catch (error, stackTrace) {
|
||||
final SentryResponse response = await client.captureException(
|
||||
exception: error, stackTrace: stackTrace);
|
||||
expect(response.isSuccessful, true);
|
||||
expect(response.eventId, 'test-event-id');
|
||||
expect(response.error, null);
|
||||
}
|
||||
|
||||
expect(postUri, client.postUri);
|
||||
|
||||
final Map<String, String> expectedHeaders = <String, String>{
|
||||
'User-Agent': '$sdkName/$sdkVersion',
|
||||
'Content-Type': 'application/json',
|
||||
'X-Sentry-Auth': 'Sentry sentry_version=6, '
|
||||
'sentry_client=${SentryClient.sentryClient}, '
|
||||
'sentry_timestamp=${fakeClockProvider().millisecondsSinceEpoch}, '
|
||||
'sentry_key=public, '
|
||||
'sentry_secret=secret',
|
||||
};
|
||||
|
||||
if (compressPayload) expectedHeaders['Content-Encoding'] = 'gzip';
|
||||
|
||||
expect(headers, expectedHeaders);
|
||||
|
||||
Map<String, dynamic> data;
|
||||
if (compressPayload) {
|
||||
data = json.decode(utf8.decode(GZIP.decode(body)));
|
||||
} else {
|
||||
data = json.decode(utf8.decode(body));
|
||||
}
|
||||
final Map<String, dynamic> stacktrace = data.remove('stacktrace');
|
||||
expect(stacktrace['frames'], const isInstanceOf<List>());
|
||||
expect(stacktrace['frames'], isNotEmpty);
|
||||
|
||||
final Map<String, dynamic> topFrame =
|
||||
(stacktrace['frames'] as Iterable<dynamic>).last;
|
||||
expect(topFrame.keys,
|
||||
<String>['abs_path', 'function', 'lineno', 'in_app', 'filename']);
|
||||
expect(topFrame['abs_path'], 'sentry_test.dart');
|
||||
expect(topFrame['function'], 'main.<fn>.testCaptureException');
|
||||
expect(topFrame['lineno'], greaterThan(0));
|
||||
expect(topFrame['in_app'], true);
|
||||
expect(topFrame['filename'], 'sentry_test.dart');
|
||||
|
||||
expect(data, {
|
||||
'project': '1',
|
||||
'event_id': 'X' * 32,
|
||||
'timestamp': '2017-01-02T00:00:00',
|
||||
'platform': 'dart',
|
||||
'exception': [
|
||||
{'type': 'ArgumentError', 'value': 'Invalid argument(s): Test error'}
|
||||
],
|
||||
'sdk': {'version': sdkVersion, 'name': 'dart'},
|
||||
'logger': SentryClient.defaultLoggerName,
|
||||
'server_name': 'test.server.com',
|
||||
'release': '1.2.3',
|
||||
'environment': 'staging',
|
||||
});
|
||||
|
||||
await client.close();
|
||||
}
|
||||
|
||||
test('sends an exception report (compressed)', () async {
|
||||
await testCaptureException(true);
|
||||
});
|
||||
|
||||
test('sends an exception report (uncompressed)', () async {
|
||||
await testCaptureException(false);
|
||||
});
|
||||
|
||||
test('reads error message from the x-sentry-error header', () async {
|
||||
final MockClient httpMock = new MockClient();
|
||||
final ClockProvider fakeClockProvider =
|
||||
() => new DateTime.utc(2017, 1, 2);
|
||||
|
||||
httpMock.answerWith((Invocation invocation) async {
|
||||
if (invocation.memberName == #close) {
|
||||
return null;
|
||||
}
|
||||
if (invocation.memberName == #post) {
|
||||
return new Response('', 401, headers: <String, String>{
|
||||
'x-sentry-error': 'Invalid api key',
|
||||
});
|
||||
}
|
||||
fail('Unexpected invocation of ${invocation.memberName} in HttpMock');
|
||||
});
|
||||
|
||||
final SentryClient client = new SentryClient(
|
||||
dsn: _testDsn,
|
||||
httpClient: httpMock,
|
||||
clock: fakeClockProvider,
|
||||
uuidGenerator: () => 'X' * 32,
|
||||
compressPayload: false,
|
||||
environmentAttributes: const Event(
|
||||
serverName: 'test.server.com',
|
||||
release: '1.2.3',
|
||||
environment: 'staging',
|
||||
),
|
||||
);
|
||||
|
||||
try {
|
||||
throw new ArgumentError('Test error');
|
||||
} catch (error, stackTrace) {
|
||||
final SentryResponse response = await client.captureException(
|
||||
exception: error, stackTrace: stackTrace);
|
||||
expect(response.isSuccessful, false);
|
||||
expect(response.eventId, null);
|
||||
expect(response.error,
|
||||
'Sentry.io responded with HTTP 401: Invalid api key');
|
||||
}
|
||||
|
||||
await client.close();
|
||||
});
|
||||
|
||||
test('$Event userContext overrides client', () async {
|
||||
final MockClient httpMock = new MockClient();
|
||||
final ClockProvider fakeClockProvider =
|
||||
() => new DateTime.utc(2017, 1, 2);
|
||||
|
||||
String loggedUserId; // used to find out what user context was sent
|
||||
httpMock.answerWith((Invocation invocation) async {
|
||||
if (invocation.memberName == #close) {
|
||||
return null;
|
||||
}
|
||||
if (invocation.memberName == #post) {
|
||||
// parse the body and detect which user context was sent
|
||||
var bodyData = invocation.namedArguments[new Symbol("body")];
|
||||
var decoded = new Utf8Codec().decode(bodyData);
|
||||
var decodedJson = new JsonDecoder().convert(decoded);
|
||||
loggedUserId = decodedJson['user']['id'];
|
||||
return new Response('', 401, headers: <String, String>{
|
||||
'x-sentry-error': 'Invalid api key',
|
||||
});
|
||||
}
|
||||
fail('Unexpected invocation of ${invocation.memberName} in HttpMock');
|
||||
});
|
||||
|
||||
final clientUserContext = new User(
|
||||
id: "client_user",
|
||||
username: "username",
|
||||
email: "email@email.com",
|
||||
ipAddress: "127.0.0.1");
|
||||
final eventUserContext = new User(
|
||||
id: "event_user",
|
||||
username: "username",
|
||||
email: "email@email.com",
|
||||
ipAddress: "127.0.0.1",
|
||||
extras: {"foo": "bar"});
|
||||
|
||||
final SentryClient client = new SentryClient(
|
||||
dsn: _testDsn,
|
||||
httpClient: httpMock,
|
||||
clock: fakeClockProvider,
|
||||
uuidGenerator: () => 'X' * 32,
|
||||
compressPayload: false,
|
||||
environmentAttributes: const Event(
|
||||
serverName: 'test.server.com',
|
||||
release: '1.2.3',
|
||||
environment: 'staging',
|
||||
),
|
||||
);
|
||||
client.userContext = clientUserContext;
|
||||
|
||||
try {
|
||||
throw new ArgumentError('Test error');
|
||||
} catch (error, stackTrace) {
|
||||
final eventWithoutContext =
|
||||
new Event(exception: error, stackTrace: stackTrace);
|
||||
final eventWithContext = new Event(
|
||||
exception: error,
|
||||
stackTrace: stackTrace,
|
||||
userContext: eventUserContext);
|
||||
await client.capture(event: eventWithoutContext);
|
||||
expect(loggedUserId, clientUserContext.id);
|
||||
await client.capture(event: eventWithContext);
|
||||
expect(loggedUserId, eventUserContext.id);
|
||||
}
|
||||
|
||||
await client.close();
|
||||
});
|
||||
});
|
||||
|
||||
group('$Event', () {
|
||||
test('serializes to JSON', () {
|
||||
final user = new User(
|
||||
id: "user_id",
|
||||
username: "username",
|
||||
email: "email@email.com",
|
||||
ipAddress: "127.0.0.1",
|
||||
extras: {"foo": "bar"});
|
||||
expect(
|
||||
new Event(
|
||||
message: 'test-message',
|
||||
exception: new StateError('test-error'),
|
||||
level: SeverityLevel.debug,
|
||||
culprit: 'Professor Moriarty',
|
||||
tags: <String, String>{
|
||||
'a': 'b',
|
||||
'c': 'd',
|
||||
},
|
||||
extra: <String, dynamic>{
|
||||
'e': 'f',
|
||||
'g': 2,
|
||||
},
|
||||
fingerprint: <String>[Event.defaultFingerprint, 'foo'],
|
||||
userContext: user,
|
||||
).toJson(),
|
||||
<String, dynamic>{
|
||||
'platform': 'dart',
|
||||
'sdk': {'version': sdkVersion, 'name': 'dart'},
|
||||
'message': 'test-message',
|
||||
'exception': [
|
||||
{'type': 'StateError', 'value': 'Bad state: test-error'}
|
||||
],
|
||||
'level': 'debug',
|
||||
'culprit': 'Professor Moriarty',
|
||||
'tags': {'a': 'b', 'c': 'd'},
|
||||
'extra': {'e': 'f', 'g': 2},
|
||||
'fingerprint': ['{{ default }}', 'foo'],
|
||||
'user': {
|
||||
'id': 'user_id',
|
||||
'username': 'username',
|
||||
'email': 'email@email.com',
|
||||
'ip_address': '127.0.0.1',
|
||||
'extras': {'foo': 'bar'}
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
typedef Answer = dynamic Function(Invocation invocation);
|
||||
|
||||
class MockClient implements Client {
|
||||
Answer _answer;
|
||||
|
||||
void answerWith(Answer answer) {
|
||||
_answer = answer;
|
||||
}
|
||||
|
||||
noSuchMethod(Invocation invocation) {
|
||||
return _answer(invocation);
|
||||
}
|
||||
}
|
78
packages/sentry/test/stack_trace_test.dart
Normal file
78
packages/sentry/test/stack_trace_test.dart
Normal file
@ -0,0 +1,78 @@
|
||||
// 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.
|
||||
|
||||
import 'package:sentry/src/stack_trace.dart';
|
||||
import 'package:stack_trace/stack_trace.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
group('encodeStackTraceFrame', () {
|
||||
test('marks dart: frames as not app frames', () {
|
||||
final Frame frame = new Frame(Uri.parse('dart:core'), 1, 2, 'buzz');
|
||||
expect(encodeStackTraceFrame(frame), {
|
||||
'abs_path': 'dart:core',
|
||||
'function': 'buzz',
|
||||
'lineno': 1,
|
||||
'in_app': false,
|
||||
'filename': 'core'
|
||||
});
|
||||
});
|
||||
|
||||
test('cleanses absolute paths', () {
|
||||
final Frame frame =
|
||||
new Frame(Uri.parse('file://foo/bar/baz.dart'), 1, 2, 'buzz');
|
||||
expect(encodeStackTraceFrame(frame)['abs_path'], 'baz.dart');
|
||||
});
|
||||
});
|
||||
|
||||
group('encodeStackTrace', () {
|
||||
test('encodes a simple stack trace', () {
|
||||
expect(encodeStackTrace('''
|
||||
#0 baz (file:///pathto/test.dart:50:3)
|
||||
#1 bar (file:///pathto/test.dart:46:9)
|
||||
'''), [
|
||||
{
|
||||
'abs_path': 'test.dart',
|
||||
'function': 'bar',
|
||||
'lineno': 46,
|
||||
'in_app': true,
|
||||
'filename': 'test.dart'
|
||||
},
|
||||
{
|
||||
'abs_path': 'test.dart',
|
||||
'function': 'baz',
|
||||
'lineno': 50,
|
||||
'in_app': true,
|
||||
'filename': 'test.dart'
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('encodes an asynchronous stack trace', () {
|
||||
expect(encodeStackTrace('''
|
||||
#0 baz (file:///pathto/test.dart:50:3)
|
||||
<asynchronous suspension>
|
||||
#1 bar (file:///pathto/test.dart:46:9)
|
||||
'''), [
|
||||
{
|
||||
'abs_path': 'test.dart',
|
||||
'function': 'bar',
|
||||
'lineno': 46,
|
||||
'in_app': true,
|
||||
'filename': 'test.dart'
|
||||
},
|
||||
{
|
||||
'abs_path': '<asynchronous suspension>',
|
||||
},
|
||||
{
|
||||
'abs_path': 'test.dart',
|
||||
'function': 'baz',
|
||||
'lineno': 50,
|
||||
'in_app': true,
|
||||
'filename': 'test.dart'
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
}
|
68
packages/sentry/test/utils_test.dart
Normal file
68
packages/sentry/test/utils_test.dart
Normal file
@ -0,0 +1,68 @@
|
||||
// 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.
|
||||
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'package:sentry/src/utils.dart';
|
||||
|
||||
void main() {
|
||||
group('mergeAttributes', () {
|
||||
test('merges attributes', () {
|
||||
final Map<String, dynamic> target = <String, dynamic>{
|
||||
'overwritten': 1,
|
||||
'unchanged': 2,
|
||||
'recursed': <String, dynamic>{
|
||||
'overwritten_child': [1, 2, 3],
|
||||
'unchanged_child': 'qwerty',
|
||||
},
|
||||
};
|
||||
|
||||
final Map<String, dynamic> attributes = <String, dynamic>{
|
||||
'overwritten': 2,
|
||||
'recursed': <String, dynamic>{
|
||||
'overwritten_child': [4, 5, 6],
|
||||
},
|
||||
};
|
||||
|
||||
mergeAttributes(attributes, into: target);
|
||||
expect(target, <String, dynamic>{
|
||||
'overwritten': 2,
|
||||
'unchanged': 2,
|
||||
'recursed': <String, dynamic>{
|
||||
'overwritten_child': [4, 5, 6],
|
||||
'unchanged_child': 'qwerty',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('does not allow overriding original maps', () {
|
||||
final environment = <String, dynamic>{
|
||||
'extra': {
|
||||
'device': 'Pixel 2',
|
||||
},
|
||||
};
|
||||
|
||||
final event = <String, dynamic>{
|
||||
'extra': {
|
||||
'widget': 'Scaffold',
|
||||
},
|
||||
};
|
||||
|
||||
final target = <String, dynamic>{};
|
||||
mergeAttributes(environment, into: target);
|
||||
mergeAttributes(event, into: target);
|
||||
expect(environment['extra'], {'device': 'Pixel 2'});
|
||||
});
|
||||
});
|
||||
|
||||
group('formatDateAsIso8601WithSecondPrecision', () {
|
||||
test('strips sub-millisecond parts', () {
|
||||
final DateTime testDate =
|
||||
new DateTime.fromMillisecondsSinceEpoch(1502467721598, isUtc: true);
|
||||
expect(testDate.toIso8601String(), '2017-08-11T16:08:41.598Z');
|
||||
expect(formatDateAsIso8601WithSecondPrecision(testDate),
|
||||
'2017-08-11T16:08:41');
|
||||
});
|
||||
});
|
||||
}
|
19
packages/sentry/test/version_test.dart
Normal file
19
packages/sentry/test/version_test.dart
Normal file
@ -0,0 +1,19 @@
|
||||
// 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.
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:sentry/sentry.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:yaml/yaml.dart' as yaml;
|
||||
|
||||
void main() {
|
||||
group('sdkVersion', () {
|
||||
test('matches that of pubspec.yaml', () {
|
||||
final dynamic pubspec =
|
||||
yaml.loadYaml(new File('pubspec.yaml').readAsStringSync());
|
||||
expect(sdkVersion, pubspec['version']);
|
||||
});
|
||||
});
|
||||
}
|
7
packages/sentry/tool/dart2_test.sh
Executable file
7
packages/sentry/tool/dart2_test.sh
Executable file
@ -0,0 +1,7 @@
|
||||
#!/bin/sh
|
||||
# Temporary workaround until Pub supports --preview-dart-2 flag
|
||||
set -e
|
||||
set -x
|
||||
for filename in test/*_test.dart; do
|
||||
dart --preview-dart-2 --enable_asserts "$filename"
|
||||
done
|
10
packages/sentry/tool/presubmit.sh
Executable file
10
packages/sentry/tool/presubmit.sh
Executable file
@ -0,0 +1,10 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
set -x
|
||||
|
||||
pub get
|
||||
dartanalyzer --strong --fatal-warnings ./
|
||||
pub run test --platform vm
|
||||
./tool/dart2_test.sh
|
||||
dartfmt -n --set-exit-if-changed ./
|
Reference in New Issue
Block a user