Merge pull request #8 from kf6gpe/merge_sentry

Merge sentry
This commit is contained in:
Ray Rischpater, KF6GPE
2018-10-29 08:35:09 -07:00
committed by GitHub
22 changed files with 1435 additions and 4 deletions

9
.gitignore vendored
View File

@ -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
View 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
View 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
View 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>

View 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
View 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>

View 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
View 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
View 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
View File

@ -0,0 +1,60 @@
# Sentry.io client for Dart
[![Build Status](https://travis-ci.org/flutter/sentry.svg?branch=master)](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

View 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');
}

View 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,
};
}
}

View 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}';
}

View 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;
}

View 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';

View 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"

View 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);
}
}

View 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'
},
]);
});
});
}

View 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');
});
});
}

View 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']);
});
});
}

View 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

View 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 ./