mirror of
https://github.com/flutter/packages.git
synced 2025-07-02 16:39:13 +08:00
9
.gitignore
vendored
9
.gitignore
vendored
@ -1,14 +1,15 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
.atom/
|
.atom/
|
||||||
.idea
|
.idea
|
||||||
|
.dart_tool/
|
||||||
.packages
|
.packages
|
||||||
.pub/
|
.pub/
|
||||||
pubspec.lock
|
|
||||||
|
|
||||||
Podfile.lock
|
Podfile.lock
|
||||||
Pods/
|
Pods/
|
||||||
GeneratedPluginRegistrant.h
|
GeneratedPluginRegistrant.h
|
||||||
GeneratedPluginRegistrant.m
|
GeneratedPluginRegistrant.m
|
||||||
|
|
||||||
GeneratedPluginRegistrant.java
|
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