diff --git a/CHANGELOG.md b/CHANGELOG.md index c0e64f6c78..03f2000ebb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ## 4.0.0-alpha.2 - feat: add contexts to scope +- feat: add missing protocol classes - fix: logger method and refactoring little things - fix: sentry protocol is v7 diff --git a/dart/example/event_example.dart b/dart/example/event_example.dart index 3ccc3752a3..f088d98e72 100644 --- a/dart/example/event_example.dart +++ b/dart/example/event_example.dart @@ -35,7 +35,7 @@ final event = SentryEvent( 'Linux version 3.4.39-5726670 (dpi@SWHC3807) (gcc version 4.8 (GCC) ) #1 SMP PREEMPT Thu Dec 1 19:42:39 KST 2016', rooted: false, ), - runtimes: [const Runtime(name: 'ART', version: '5')], + runtimes: [const SentryRuntime(name: 'ART', version: '5')], app: App( name: 'Example Dart App', version: '1.42.0', diff --git a/dart/example_web/web/event.dart b/dart/example_web/web/event.dart index 3ccc3752a3..f088d98e72 100644 --- a/dart/example_web/web/event.dart +++ b/dart/example_web/web/event.dart @@ -35,7 +35,7 @@ final event = SentryEvent( 'Linux version 3.4.39-5726670 (dpi@SWHC3807) (gcc version 4.8 (GCC) ) #1 SMP PREEMPT Thu Dec 1 19:42:39 KST 2016', rooted: false, ), - runtimes: [const Runtime(name: 'ART', version: '5')], + runtimes: [const SentryRuntime(name: 'ART', version: '5')], app: App( name: 'Example Dart App', version: '1.42.0', diff --git a/dart/lib/src/hub.dart b/dart/lib/src/hub.dart index 8e810ed936..9933eba304 100644 --- a/dart/lib/src/hub.dart +++ b/dart/lib/src/hub.dart @@ -35,11 +35,11 @@ class Hub { static void _validateOptions(SentryOptions options) { if (options == null) { - throw ArgumentError.notNull('SentryOptions is required.'); + throw ArgumentError('SentryOptions is required.'); } if (options.dsn?.isNotEmpty != true) { - throw ArgumentError.notNull('DSN is required.'); + throw ArgumentError('DSN is required.'); } } @@ -96,7 +96,7 @@ class Hub { /// Captures the exception Future captureException( - dynamic exception, { + dynamic throwable, { dynamic stackTrace, dynamic hint, }) async { @@ -107,7 +107,7 @@ class Hub { SentryLevel.warning, "Instance is disabled and this 'captureException' call is a no-op.", ); - } else if (exception == null) { + } else if (throwable == null) { _options.logger( SentryLevel.warning, 'captureException called with null parameter.', @@ -117,7 +117,7 @@ class Hub { if (item != null) { try { sentryId = await item.client.captureException( - exception, + throwable, stackTrace: stackTrace, scope: item.scope, hint: hint, @@ -125,7 +125,7 @@ class Hub { } catch (err) { _options.logger( SentryLevel.error, - 'Error while capturing exception : ${exception}', + 'Error while capturing exception : ${throwable}', ); } finally { _lastEventId = sentryId; diff --git a/dart/lib/src/hub_adapter.dart b/dart/lib/src/hub_adapter.dart index 36d0e85af6..64b37daba9 100644 --- a/dart/lib/src/hub_adapter.dart +++ b/dart/lib/src/hub_adapter.dart @@ -28,12 +28,12 @@ class HubAdapter implements Hub { @override Future captureException( - dynamic exception, { + dynamic throwable, { dynamic stackTrace, dynamic hint, }) => Sentry.captureException( - exception, + throwable, stackTrace: stackTrace, hint: hint, ); diff --git a/dart/lib/src/noop_hub.dart b/dart/lib/src/noop_hub.dart index caebabe214..36a93bcb8c 100644 --- a/dart/lib/src/noop_hub.dart +++ b/dart/lib/src/noop_hub.dart @@ -1,9 +1,6 @@ import 'dart:async'; import 'hub.dart'; -import 'protocol/sentry_event.dart'; -import 'protocol/sentry_id.dart'; -import 'protocol/sentry_level.dart'; import 'protocol.dart'; import 'sentry_client.dart'; @@ -25,7 +22,7 @@ class NoOpHub implements Hub { @override Future captureException( - dynamic exception, { + dynamic throwable, { dynamic stackTrace, dynamic hint, }) => diff --git a/dart/lib/src/protocol.dart b/dart/lib/src/protocol.dart index 8b6e8680bd..826c336266 100644 --- a/dart/lib/src/protocol.dart +++ b/dart/lib/src/protocol.dart @@ -2,15 +2,23 @@ export 'protocol/app.dart'; export 'protocol/breadcrumb.dart'; export 'protocol/browser.dart'; export 'protocol/contexts.dart'; +export 'protocol/debug_image.dart'; +export 'protocol/debug_meta.dart'; export 'protocol/device.dart'; export 'protocol/dsn.dart'; export 'protocol/gpu.dart'; +export 'protocol/mechanism.dart'; export 'protocol/message.dart'; -export 'protocol/package.dart'; -export 'protocol/runtime.dart'; -export 'protocol/sdk.dart'; +export 'protocol/operating_system.dart'; +export 'protocol/request.dart'; +export 'protocol/sdk_info.dart'; +export 'protocol/sdk_version.dart'; export 'protocol/sentry_event.dart'; +export 'protocol/sentry_exception.dart'; export 'protocol/sentry_id.dart'; export 'protocol/sentry_level.dart'; -export 'protocol/system.dart'; +export 'protocol/sentry_package.dart'; +export 'protocol/sentry_runtime.dart'; +export 'protocol/sentry_stack_frame.dart'; +export 'protocol/sentry_stack_trace.dart'; export 'protocol/user.dart'; diff --git a/dart/lib/src/protocol/contexts.dart b/dart/lib/src/protocol/contexts.dart index bb95a53d09..0a26b62498 100644 --- a/dart/lib/src/protocol/contexts.dart +++ b/dart/lib/src/protocol/contexts.dart @@ -1,11 +1,6 @@ import 'dart:collection'; import '../protocol.dart'; -import 'app.dart'; -import 'browser.dart'; -import 'device.dart'; -import 'gpu.dart'; -import 'runtime.dart'; /// The context interfaces provide additional context data. /// @@ -17,14 +12,14 @@ class Contexts extends MapView { Contexts({ Device device, OperatingSystem operatingSystem, - List runtimes, + List runtimes, App app, Browser browser, Gpu gpu, }) : super({ Device.type: device, OperatingSystem.type: operatingSystem, - Runtime.listType: runtimes ?? [], + SentryRuntime.listType: runtimes ?? [], App.type: app, Browser.type: browser, Gpu.type: gpu, @@ -47,11 +42,14 @@ class Contexts extends MapView { /// Describes a list of runtimes in more detail /// (for instance if you have a Flutter application running /// on top of Android). - List get runtimes => List.unmodifiable(this[Runtime.listType]); + List get runtimes => + List.unmodifiable(this[SentryRuntime.listType]); - void addRuntime(Runtime runtime) => this[Runtime.listType].add(runtime); + void addRuntime(SentryRuntime runtime) => + this[SentryRuntime.listType].add(runtime); - void removeRuntime(Runtime runtime) => this[Runtime.listType].remove(runtime); + void removeRuntime(SentryRuntime runtime) => + this[SentryRuntime.listType].remove(runtime); /// App context describes the application. /// @@ -116,12 +114,12 @@ class Contexts extends MapView { } break; - case Runtime.listType: + case SentryRuntime.listType: if (runtimes != null) { if (runtimes.length == 1) { final runtime = runtimes[0]; if (runtime != null) { - final key = runtime.key ?? Runtime.type; + final key = runtime.key ?? SentryRuntime.type; json[key] = runtime.toJson(); } } else if (runtimes.length > 1) { @@ -138,7 +136,7 @@ class Contexts extends MapView { } json[key] = runtime.toJson() - ..addAll({'type': Runtime.type}); + ..addAll({'type': SentryRuntime.type}); } } } @@ -173,8 +171,8 @@ class Contexts extends MapView { App.type, Device.type, OperatingSystem.type, - Runtime.listType, - Runtime.type, + SentryRuntime.listType, + SentryRuntime.type, Gpu.type, Browser.type, ]; diff --git a/dart/lib/src/protocol/debug_image.dart b/dart/lib/src/protocol/debug_image.dart new file mode 100644 index 0000000000..eac551f101 --- /dev/null +++ b/dart/lib/src/protocol/debug_image.dart @@ -0,0 +1,113 @@ +/// The list of debug images contains all dynamic libraries loaded into +/// the process and their memory addresses. +/// Instruction addresses in the Stack Trace are mapped into the list of debug +/// images in order to retrieve debug files for symbolication. +/// There are two kinds of debug images: +// +/// Native debug images with types macho, elf, and pe +/// Android debug images with type proguard +/// more details : https://develop.sentry.dev/sdk/event-payloads/debugmeta/ +class DebugImage { + final String uuid; + + /// Required. Type of the debug image. Must be "macho". + final String type; + + /// Required. Identifier of the dynamic library or executable. It is the value of the LC_UUID load command in the Mach header, formatted as UUID. + final String debugId; + + /// Required. Memory address, at which the image is mounted in the virtual address space of the process. + /// Should be a string in hex representation prefixed with "0x". + final String imageAddr; + + /// Required. The size of the image in virtual memory. If missing, Sentry will assume that the image spans up to the next image, which might lead to invalid stack traces. + final int imageSize; + + /// OptionalName or absolute path to the dSYM file containing debug information for this image. This value might be required to retrieve debug files from certain symbol servers. + final String debugFile; + + /// Optional. The absolute path to the dynamic library or executable. This helps to locate the file if it is missing on Sentry. + final String codeFile; + + /// Optional Architecture of the module. If missing, this will be backfilled by Sentry. + final String arch; + + /// Optional. Identifier of the dynamic library or executable. It is the value of the LC_UUID load command in the Mach header, formatted as UUID. Can be empty for Mach images, as it is equivalent to the debug identifier. + final String codeId; + + DebugImage({ + this.type, + this.imageAddr, + this.debugId, + this.debugFile, + this.imageSize, + this.uuid, + this.codeFile, + this.arch, + this.codeId, + }); + + Map toJson() { + final json = {}; + + if (uuid != null) { + json['uuid'] = uuid; + } + + if (type != null) { + json['type'] = type; + } + + if (debugId != null) { + json['debug_id'] = debugId; + } + + if (debugFile != null) { + json['debug_file'] = debugFile; + } + + if (codeFile != null) { + json['code_file'] = codeFile; + } + + if (imageAddr != null) { + json['image_addr'] = imageAddr; + } + + if (imageSize != null) { + json['image_size'] = imageSize; + } + + if (arch != null) { + json['arch'] = arch; + } + + if (codeId != null) { + json['code_id'] = codeId; + } + + return json; + } + + DebugImage copyWith({ + String uuid, + String type, + String debugId, + String debugFile, + String codeFile, + String imageAddr, + int imageSize, + String arch, + String codeId, + }) => + DebugImage( + uuid: uuid ?? this.uuid, + type: type ?? this.type, + debugId: debugId ?? this.debugId, + codeFile: codeFile ?? this.codeFile, + imageAddr: imageAddr ?? this.imageAddr, + imageSize: imageSize ?? this.imageSize, + arch: arch ?? this.arch, + codeId: codeId ?? this.codeId, + ); +} diff --git a/dart/lib/src/protocol/debug_meta.dart b/dart/lib/src/protocol/debug_meta.dart new file mode 100644 index 0000000000..1b184119c9 --- /dev/null +++ b/dart/lib/src/protocol/debug_meta.dart @@ -0,0 +1,31 @@ +import '../protocol.dart'; + +/// The debug meta interface carries debug information for processing errors and crash reports. +class DebugMeta { + /// An object describing the system SDK. + final SdkInfo sdk; + + final List _images; + + /// An immutable list of dynamic libraries loaded into the process (see below). + List get images => List.unmodifiable(_images); + + DebugMeta({this.sdk, List images}) : _images = images; + + Map toJson() { + final json = {}; + + if (sdk != null) { + json['sdk_info'] = sdk.toJson(); + } + + if (_images != null && _images.isNotEmpty) { + json['images'] = _images.map((e) => e.toJson()); + } + + return json; + } + + DebugMeta copyWith({SdkVersion sdk, List images}) => + DebugMeta(sdk: sdk ?? this.sdk, images: images ?? _images); +} diff --git a/dart/lib/src/protocol/mechanism.dart b/dart/lib/src/protocol/mechanism.dart new file mode 100644 index 0000000000..0030d82377 --- /dev/null +++ b/dart/lib/src/protocol/mechanism.dart @@ -0,0 +1,109 @@ +import 'package:meta/meta.dart'; + +/// Sentry Exception Mechanism +/// The exception mechanism is an optional field residing +/// in the Exception Interface. It carries additional information about +/// the way the exception was created on the target system. +/// This includes general exception values obtained from operating system or +/// runtime APIs, as well as mechanism-specific values. +class Mechanism { + /// Required unique identifier of this mechanism determining rendering and processing of the mechanism data + /// The type attribute is required to send any exception mechanism attribute, + /// even if the SDK cannot determine the specific mechanism. + /// In this case, set the type to "generic". See below for an example. + final String type; + + /// Optional human readable description of the error mechanism and a possible hint on how to solve this error + final String description; + + /// Optional fully qualified URL to an online help resource, possible interpolated with error parameters + final String helpLink; + + /// Optional flag indicating whether the exception has been handled by the user (e.g. via try..catch) + final bool handled; + + final Map _meta; + + /// Optional information from the operating system or runtime on the exception mechanism + /// The mechanism meta data usually carries error codes reported by + /// the runtime or operating system, along with a platform dependent + /// interpretation of these codes. SDKs can safely omit code names and + /// descriptions for well known error codes, as it will be filled out by + /// Sentry. For proprietary or vendor-specific error codes, + /// adding these values will give additional information to the user. + Map get meta => Map.unmodifiable(_meta); + + final Map _data; + + /// Arbitrary extra data that might help the user understand the error thrown by this mechanism + Map get data => Map.unmodifiable(_data); + + /// An optional flag indicating that this error is synthetic. + /// Synthetic errors are errors that carry little meaning by themselves. + /// This may be because they are created at a central place (like a crash handler), and are all called the same: Error, Segfault etc. When the flag is set, Sentry will then try to use other information (top in-app frame function) rather than exception type and value in the UI for the primary event display. This flag should be set for all "segfaults" for instance as every single error group would look very similar otherwise. + final bool synthetic; + + const Mechanism({ + @required this.type, + this.description, + this.helpLink, + this.handled, + this.synthetic, + Map meta, + Map data, + }) : _meta = meta, + _data = data; + + Mechanism copyWith({ + String type, + String description, + String helpLink, + bool handled, + Map meta, + Map data, + bool synthetic, + }) => + Mechanism( + type: type ?? this.type, + description: description ?? this.description, + helpLink: helpLink ?? this.helpLink, + handled: handled ?? this.handled, + meta: meta ?? this.meta, + data: data ?? this.data, + synthetic: synthetic ?? this.synthetic, + ); + + Map toJson() { + final json = {}; + + if (type != null) { + json['type'] = type; + } + + if (description != null) { + json['description'] = description; + } + + if (helpLink != null) { + json['help_link'] = helpLink; + } + + if (handled != null) { + json['handled'] = handled; + } + + if (_meta != null && _meta.isNotEmpty) { + json['meta'] = _meta; + } + + if (_data != null && _data.isNotEmpty) { + json['data'] = _data; + } + + if (synthetic != null) { + json['synthetic'] = synthetic; + } + + return json; + } +} diff --git a/dart/lib/src/protocol/noop_origin.dart b/dart/lib/src/protocol/noop_origin.dart new file mode 100644 index 0000000000..0d02495ae3 --- /dev/null +++ b/dart/lib/src/protocol/noop_origin.dart @@ -0,0 +1,2 @@ +/// request origin, empty out of browser context +String eventOrigin = ''; diff --git a/dart/lib/src/protocol/system.dart b/dart/lib/src/protocol/operating_system.dart similarity index 100% rename from dart/lib/src/protocol/system.dart rename to dart/lib/src/protocol/operating_system.dart diff --git a/dart/lib/src/transport/origin.dart b/dart/lib/src/protocol/origin.dart similarity index 61% rename from dart/lib/src/transport/origin.dart rename to dart/lib/src/protocol/origin.dart index 347709496c..ed1e066c5e 100644 --- a/dart/lib/src/transport/origin.dart +++ b/dart/lib/src/protocol/origin.dart @@ -1,3 +1,4 @@ import 'dart:html'; +/// request origin, used for browser stacktrace String get eventOrigin => '${window.location.origin}/'; diff --git a/dart/lib/src/protocol/request.dart b/dart/lib/src/protocol/request.dart new file mode 100644 index 0000000000..876d3f959d --- /dev/null +++ b/dart/lib/src/protocol/request.dart @@ -0,0 +1,129 @@ +/// The Request interface contains information on a HTTP request related to the event. +/// In client SDKs, this can be an outgoing request, or the request that rendered the current web page. +/// On server SDKs, this could be the incoming web request that is being handled. +class Request { + ///The URL of the request if available. + ///The query string can be declared either as part of the url, + ///or separately in queryString. + final String url; + + ///The HTTP method of the request. + final String method; + + /// The query string component of the URL. + /// + /// If the query string is not declared and part of the url parameter, + /// Sentry moves it to the query string. + final String queryString; + + /// The cookie values as string. + final String cookies; + + final dynamic _data; + + /// Submitted data in a format that makes the most sense. + /// SDKs should discard large bodies by default. + /// Can be given as string or structural data of any format. + dynamic get data { + if (_data is List) { + return List.unmodifiable(_data); + } else if (_data is Map) { + return Map.unmodifiable(_data); + } + + return _data; + } + + final Map _headers; + + /// A dictionary of submitted headers. + /// If a header appears multiple times it, + /// needs to be merged according to the HTTP standard for header merging. + /// Header names are treated case-insensitively by Sentry. + Map get headers => + _headers != null ? Map.unmodifiable(_headers) : null; + + final Map _env; + + /// A dictionary containing environment information passed from the server. + /// This is where information such as CGI/WSGI/Rack keys go that are not HTTP headers. + Map get env => _env != null ? Map.unmodifiable(_env) : null; + + final Map _other; + + Map get other => + _other != null ? Map.unmodifiable(_other) : null; + + const Request({ + this.url, + this.method, + this.queryString, + this.cookies, + dynamic data, + Map headers, + Map env, + Map other, + }) : _data = data, + _headers = headers, + _env = env, + _other = other; + + Map toJson() { + final json = {}; + + if (url != null) { + json['url'] = url; + } + + if (method != null) { + json['method'] = method; + } + + if (queryString != null) { + json['query_string'] = queryString; + } + + if (_data != null) { + json['data'] = _data; + } + + if (cookies != null) { + json['cookies'] = cookies; + } + + if (headers != null && headers.isNotEmpty) { + json['headers'] = headers; + } + + if (env != null && env.isNotEmpty) { + json['env'] = env; + } + + if (other != null && other.isNotEmpty) { + json['other'] = other; + } + + return json; + } + + Request copyWith({ + String url, + String method, + String queryString, + String cookies, + dynamic data, + Map headers, + Map env, + Map other, + }) => + Request( + url: url ?? this.url, + method: method ?? this.method, + queryString: queryString ?? this.queryString, + cookies: cookies ?? this.cookies, + data: data ?? _data, + headers: headers ?? _headers, + env: env ?? _env, + other: other ?? _other, + ); +} diff --git a/dart/lib/src/protocol/sdk_info.dart b/dart/lib/src/protocol/sdk_info.dart new file mode 100644 index 0000000000..944ae4e095 --- /dev/null +++ b/dart/lib/src/protocol/sdk_info.dart @@ -0,0 +1,48 @@ +/// An object describing the system SDK. +class SdkInfo { + final String sdkName; + final int versionMajor; + final int versionMinor; + final int versionPatchlevel; + + SdkInfo({ + this.sdkName, + this.versionMajor, + this.versionMinor, + this.versionPatchlevel, + }); + + Map toJson() { + final json = {}; + if (sdkName != null) { + json['sdk_name'] = sdkName; + } + + if (versionMajor != null) { + json['version_major'] = versionMajor; + } + + if (versionMinor != null) { + json['version_minor'] = versionMinor; + } + + if (versionPatchlevel != null) { + json['version_patchlevel'] = versionPatchlevel; + } + + return json; + } + + SdkInfo copyWith({ + String sdkName, + int versionMajor, + int versionMinor, + int versionPatchlevel, + }) => + SdkInfo( + sdkName: sdkName ?? this.sdkName, + versionMajor: versionMajor ?? this.versionMajor, + versionMinor: versionMinor ?? this.versionMinor, + versionPatchlevel: versionPatchlevel ?? this.versionPatchlevel, + ); +} diff --git a/dart/lib/src/protocol/sdk.dart b/dart/lib/src/protocol/sdk_version.dart similarity index 90% rename from dart/lib/src/protocol/sdk.dart rename to dart/lib/src/protocol/sdk_version.dart index a632eb42a7..a481773de7 100644 --- a/dart/lib/src/protocol/sdk.dart +++ b/dart/lib/src/protocol/sdk_version.dart @@ -1,6 +1,6 @@ import 'package:meta/meta.dart'; -import 'package.dart'; +import 'sentry_package.dart'; /// Describes the SDK that is submitting events to Sentry. /// @@ -33,9 +33,9 @@ import 'package.dart'; /// } /// ``` @immutable -class Sdk { - /// Creates an [Sdk] object which represents the SDK that created an [Event]. - const Sdk({ +class SdkVersion { + /// Creates an [SdkVersion] object which represents the SDK that created an [Event]. + const SdkVersion({ @required this.name, @required this.version, this.integrations, @@ -52,7 +52,7 @@ class Sdk { final List integrations; /// A list of packages that compose this SDK. - final List packages; + final List packages; String get identifier => '${name}/${version}'; diff --git a/dart/lib/src/protocol/sentry_event.dart b/dart/lib/src/protocol/sentry_event.dart index a5cb832f0a..3501dfeb2b 100644 --- a/dart/lib/src/protocol/sentry_event.dart +++ b/dart/lib/src/protocol/sentry_event.dart @@ -1,7 +1,6 @@ import 'package:meta/meta.dart'; import '../protocol.dart'; -import '../stack_trace.dart'; import '../utils.dart'; import '../version.dart'; @@ -22,8 +21,9 @@ class SentryEvent { this.modules, this.message, this.transaction, - this.exception, + this.throwable, this.stackTrace, + this.exception, this.level, this.culprit, this.tags, @@ -32,6 +32,8 @@ class SentryEvent { this.user, Contexts contexts, this.breadcrumbs, + this.request, + this.debugMeta, }) : eventId = eventId ?? SentryId.newId(), timestamp = timestamp ?? getUtcDateTime(), contexts = contexts ?? Contexts(); @@ -76,15 +78,20 @@ class SentryEvent { /// 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; + /// It's `runtimeType` and `toString()` are logged. + /// If it's an Error, with a stackTrace, the stackTrace is logged. + /// If this behavior is undesirable, consider using a custom formatted [message] instead. + final dynamic throwable; /// The stack trace corresponding to the thrown [exception]. /// /// Can be `null`, a [String], or a [StackTrace]. final dynamic stackTrace; + /// an exception or error that occurred in a program + /// TODO more doc + final SentryException exception; + /// The name of the transaction which generated this event, /// for example, the route name: `"/users//"`. final String transaction; @@ -138,7 +145,15 @@ class SentryEvent { final List fingerprint; /// The SDK Interface describes the Sentry SDK and its configuration used to capture and transmit an event. - final Sdk sdk; + final SdkVersion sdk; + + /// contains information on a HTTP request related to the event. + /// In client, this can be an outgoing request, or the request that rendered the current web page. + /// On server, this could be the incoming web request that is being handled + final Request request; + + /// The debug meta interface carries debug information for processing errors and crash reports. + final DebugMeta debugMeta; SentryEvent copyWith({ SentryId eventId, @@ -152,7 +167,8 @@ class SentryEvent { Map modules, Message message, String transaction, - dynamic exception, + dynamic throwable, + SentryException exception, dynamic stackTrace, SentryLevel level, String culprit, @@ -162,7 +178,9 @@ class SentryEvent { User user, Contexts contexts, List breadcrumbs, - Sdk sdk, + SdkVersion sdk, + Request request, + DebugMeta debugMeta, }) => SentryEvent( eventId: eventId ?? this.eventId, @@ -176,6 +194,7 @@ class SentryEvent { modules: modules ?? this.modules, message: message ?? this.message, transaction: transaction ?? this.transaction, + throwable: throwable ?? this.throwable, exception: exception ?? this.exception, stackTrace: stackTrace ?? this.stackTrace, level: level ?? this.level, @@ -187,10 +206,12 @@ class SentryEvent { contexts: contexts ?? this.contexts, breadcrumbs: breadcrumbs ?? this.breadcrumbs, sdk: sdk ?? this.sdk, + request: request ?? this.request, + debugMeta: debugMeta ?? this.debugMeta, ); /// Serializes this event to JSON. - Map toJson({String origin}) { + Map toJson() { final json = {}; if (eventId != null) { @@ -238,28 +259,8 @@ class SentryEvent { } if (exception != null) { - json['exception'] = [ - { - 'type': '${exception.runtimeType}', - 'value': '$exception', - } - ]; - if (exception is Error && exception.stackTrace != null) { - json['stacktrace'] = { - 'frames': encodeStackTrace( - exception.stackTrace, - origin: origin, - ), - }; - } - } - - if (stackTrace != null) { - json['stacktrace'] = { - 'frames': encodeStackTrace( - stackTrace, - origin: origin, - ), + json['exception'] = { + 'values': [exception.toJson()].toList(growable: false) }; } @@ -305,6 +306,14 @@ class SentryEvent { 'version': sdkVersion, }; + if (request != null) { + json['request'] = request.toJson(); + } + + if (debugMeta != null) { + json['debug_meta'] = debugMeta.toJson(); + } + return json; } } diff --git a/dart/lib/src/protocol/sentry_exception.dart b/dart/lib/src/protocol/sentry_exception.dart new file mode 100644 index 0000000000..065bf2a2b1 --- /dev/null +++ b/dart/lib/src/protocol/sentry_exception.dart @@ -0,0 +1,63 @@ +import 'package:meta/meta.dart'; + +import '../protocol.dart'; + +/// The Exception Interface specifies an exception or error that occurred in a program. +class SentryException { + /// Required. The type of exception + final String type; + + /// Required. The value of the exception + final String value; + + /// The optional module, or package which the exception type lives in. + final String module; + + /// An optional stack trace object + final SentryStackTrace stacktrace; + + /// An optional object describing the [Mechanism] that created this exception + final Mechanism mechanism; + + /// Represents a thread id. not available in Dart + final int threadId; + + const SentryException({ + @required this.type, + @required this.value, + this.module, + this.stacktrace, + this.mechanism, + this.threadId, + }); + + Map toJson() { + final json = {}; + + if (type != null) { + json['type'] = type; + } + + if (value != null) { + json['value'] = value; + } + + if (module != null) { + json['module'] = module; + } + + if (stacktrace != null) { + json['stacktrace'] = stacktrace.toJson(); + } + + if (mechanism != null) { + json['mechanism'] = mechanism.toJson(); + } + + if (threadId != null) { + json['thread_id'] = threadId; + } + + return json; + } +} diff --git a/dart/lib/src/protocol/package.dart b/dart/lib/src/protocol/sentry_package.dart similarity index 67% rename from dart/lib/src/protocol/package.dart rename to dart/lib/src/protocol/sentry_package.dart index bd0a1f1e1a..f446ae6eb7 100644 --- a/dart/lib/src/protocol/package.dart +++ b/dart/lib/src/protocol/sentry_package.dart @@ -1,10 +1,10 @@ import 'package:meta/meta.dart'; -/// A [Package] part of the [Sdk]. +/// A [SentryPackage] part of the [Sdk]. @immutable -class Package { - /// Creates an [Package] object that is part of the [Sdk]. - const Package(this.name, this.version) +class SentryPackage { + /// Creates an [SentryPackage] object that is part of the [Sdk]. + const SentryPackage(this.name, this.version) : assert(name != null && version != null); /// The name of the SDK. diff --git a/dart/lib/src/protocol/runtime.dart b/dart/lib/src/protocol/sentry_runtime.dart similarity index 86% rename from dart/lib/src/protocol/runtime.dart rename to dart/lib/src/protocol/sentry_runtime.dart index 5a4696661a..f50d672d95 100644 --- a/dart/lib/src/protocol/runtime.dart +++ b/dart/lib/src/protocol/sentry_runtime.dart @@ -3,17 +3,17 @@ /// Typically this context is used multiple times if multiple runtimes /// are involved (for instance if you have a JavaScript application running /// on top of JVM). -class Runtime { +class SentryRuntime { static const listType = 'runtimes'; static const type = 'runtime'; - const Runtime({this.key, this.name, this.version, this.rawDescription}) + const SentryRuntime({this.key, this.name, this.version, this.rawDescription}) : assert(key == null || key.length >= 1); /// Key used in the JSON and which will be displayed /// in the Sentry UI. Defaults to lower case version of [name]. /// - /// Unused if only one [Runtime] is provided in [Contexts]. + /// Unused if only one [SentryRuntime] is provided in [Contexts]. final String key; /// The name of the runtime. @@ -47,7 +47,7 @@ class Runtime { return json; } - Runtime clone() => Runtime( + SentryRuntime clone() => SentryRuntime( key: key, name: name, version: version, diff --git a/dart/lib/src/protocol/sentry_stack_frame.dart b/dart/lib/src/protocol/sentry_stack_frame.dart new file mode 100644 index 0000000000..3ed9bc56cf --- /dev/null +++ b/dart/lib/src/protocol/sentry_stack_frame.dart @@ -0,0 +1,233 @@ +class SentryStackFrame { + static final SentryStackFrame asynchronousGapFrameJson = + SentryStackFrame(absPath: ''); + + SentryStackFrame({ + this.absPath, + this.fileName, + this.function, + this.module, + this.lineNo, + this.colNo, + this.contextLine, + this.inApp, + this.package, + this.native, + this.platform, + this.imageAddr, + this.symbolAddr, + this.instructionAddr, + this.rawFunction, + List framesOmitted, + List preContext, + List postContext, + Map vars, + }) : _framesOmitted = framesOmitted, + _preContext = preContext, + _postContext = postContext, + _vars = vars; + + /// The absolute path to filename. + final String absPath; + + final List _preContext; + + /// An immutable list of source code lines before context_line (in order) – usually [lineno - 5:lineno]. + List get preContext => List.unmodifiable(_preContext); + + final List _postContext; + + /// An immutable list of source code lines after context_line (in order) – usually [lineno + 1:lineno + 5]. + List get postContext => List.unmodifiable(_postContext); + + final Map _vars; + + /// An immutable mapping of variables which were available within this frame (usually context-locals). + Map get vars => Map.unmodifiable(_vars); + + final List _framesOmitted; + + /// Which frames were omitted, if any. + /// + /// If the list of frames is large, you can explicitly tell the system + /// that you’ve omitted a range of frames. + /// The frames_omitted must be a single tuple two values: start and end. + // + /// Example : If you only removed the 8th frame, the value would be (8, 9), + /// meaning it started at the 8th frame, and went untilthe 9th (the number of frames omitted is end-start). + /// The values should be based on a one-index. + List get framesOmitted => List.unmodifiable(_framesOmitted); + + /// The relative file path to the call. + final String fileName; + + /// The name of the function being called. + final String function; + + /// Platform-specific module path. + final String module; + + /// The column number of the call + final int lineNo; + + /// The column number of the call + final int colNo; + + /// Source code in filename at line number. + final String contextLine; + + /// Signifies whether this frame is related to the execution of the relevant code in this stacktrace. + /// + /// For example, the frames that might power the framework’s web server of your app are probably not relevant, however calls to the framework’s library once you start handling code likely are. + final bool inApp; + + /// The "package" the frame was contained in. + final String package; + + final bool native; + + /// This can override the platform for a single frame. Otherwise, the platform of the event is assumed. This can be used for multi-platform stack traces + final String platform; + + /// Optionally an address of the debug image to reference. + final String imageAddr; + + /// An optional address that points to a symbol. We use the instruction address for symbolication, but this can be used to calculate an instruction offset automatically. + final String symbolAddr; + + /// The instruction address + /// The official docs refer to it as 'The difference between instruction address and symbol address in bytes.' + final String instructionAddr; + + /// The original function name, if the function name is shortened or demangled. Sentry shows the raw function when clicking on the shortened one in the UI. + final String rawFunction; + + Map toJson() { + final json = {}; + + if (_preContext != null && _preContext.isNotEmpty) { + json['pre_context'] = _preContext; + } + + if (_postContext != null && _postContext.isNotEmpty) { + json['post_context'] = _postContext; + } + + if (_vars != null && _vars.isNotEmpty) { + json['vars'] = _vars; + } + + if (_framesOmitted != null && _framesOmitted.isNotEmpty) { + json['frames_omitted'] = _framesOmitted; + } + + if (fileName != null) { + json['filename'] = fileName; + } + + if (package != null) { + json['package'] = package; + } + + if (function != null) { + json['function'] = function; + } + + if (module != null) { + json['module'] = module; + } + + if (lineNo != null) { + json['lineno'] = lineNo; + } + + if (colNo != null) { + json['colno'] = colNo; + } + + if (absPath != null) { + json['abs_path'] = absPath; + } + + if (contextLine != null) { + json['context_line'] = contextLine; + } + + if (inApp != null) { + json['in_app'] = inApp; + } + + if (package != null) { + json['package'] = package; + } + + if (native != null) { + json['native'] = native; + } + + if (platform != null) { + json['platform'] = platform; + } + + if (imageAddr != null) { + json['image_addr'] = imageAddr; + } + + if (symbolAddr != null) { + json['symbol_addr'] = symbolAddr; + } + + if (instructionAddr != null) { + json['instruction_addr'] = instructionAddr; + } + + if (rawFunction != null) { + json['raw_function'] = rawFunction; + } + + return json; + } + + SentryStackFrame copyWith({ + String absPath, + String fileName, + String function, + String module, + int lineNo, + int colNo, + String contextLine, + bool inApp, + String package, + bool native, + String platform, + String imageAddr, + String symbolAddr, + String instructionAddr, + String rawFunction, + List framesOmitted, + List preContext, + List postContext, + Map vars, + }) => + SentryStackFrame( + absPath: absPath ?? this.absPath, + fileName: fileName ?? this.fileName, + function: function ?? this.function, + module: module ?? this.module, + lineNo: lineNo ?? this.lineNo, + colNo: colNo ?? this.colNo, + contextLine: contextLine ?? this.contextLine, + inApp: inApp ?? this.inApp, + package: package ?? this.package, + native: native ?? this.native, + platform: platform ?? this.platform, + imageAddr: imageAddr ?? this.imageAddr, + symbolAddr: symbolAddr ?? this.symbolAddr, + instructionAddr: instructionAddr ?? this.instructionAddr, + rawFunction: rawFunction ?? this.rawFunction, + framesOmitted: framesOmitted ?? _framesOmitted, + preContext: preContext ?? _preContext, + postContext: postContext ?? _postContext, + vars: vars ?? _vars, + ); +} diff --git a/dart/lib/src/protocol/sentry_stack_trace.dart b/dart/lib/src/protocol/sentry_stack_trace.dart new file mode 100644 index 0000000000..4de2e432a3 --- /dev/null +++ b/dart/lib/src/protocol/sentry_stack_trace.dart @@ -0,0 +1,49 @@ +import 'package:meta/meta.dart'; + +import 'sentry_stack_frame.dart'; + +/// Stacktrace holds information about the frames of the stack. +class SentryStackTrace { + const SentryStackTrace({ + @required List frames, + Map registers, + }) : _frames = frames, + _registers = registers; + + final List _frames; + + /// Required. A non-empty list of stack frames (see below). + /// The list is ordered from caller to callee, or oldest to youngest. + /// The last frame is the one creating the exception. + List get frames => List.unmodifiable(_frames); + + final Map _registers; + + /// Optional. A map of register names and their values. + /// The values should contain the actual register values of the thread, + /// thus mapping to the last frame in the list. + Map get registers => Map.unmodifiable(_registers); + + Map toJson() { + final json = {}; + + if (_frames != null && _frames.isNotEmpty) { + json['frames'] = _frames.map((frame) => frame.toJson()).toList(); + } + + if (_registers != null && _registers.isNotEmpty ?? false) { + json['registers'] = _registers; + } + + return json; + } + + SentryStackTrace copyWith({ + List frames, + Map registers, + }) => + SentryStackTrace( + frames: frames ?? this.frames, + registers: registers ?? this.registers, + ); +} diff --git a/dart/lib/src/scope.dart b/dart/lib/src/scope.dart index 4d7db35334..716b2f7070 100644 --- a/dart/lib/src/scope.dart +++ b/dart/lib/src/scope.dart @@ -81,7 +81,11 @@ class Scope { final SentryOptions _options; - Scope(this._options) : assert(_options != null, 'SentryOptions is required'); + Scope(this._options) { + if (_options == null) { + throw ArgumentError('SentryOptions is required'); + } + } /// Adds a breadcrumb to the breadcrumbs queue void addBreadcrumb(Breadcrumb breadcrumb, {dynamic hint}) { @@ -173,9 +177,9 @@ class Scope { _contexts.clone().forEach((key, value) { // add the contexts runtime list to the event.contexts.runtimes - if (key == Runtime.listType && value is List && value.isNotEmpty) { + if (key == SentryRuntime.listType && value is List && value.isNotEmpty) { _mergeEventContextsRuntimes(value, event); - } else if (key != Runtime.listType && + } else if (key != SentryRuntime.listType && (!event.contexts.containsKey(key) || event.contexts[key] == null) && value != null) { event.contexts[key] = value; diff --git a/dart/lib/src/sentry.dart b/dart/lib/src/sentry.dart index 585bcd7710..8da191834d 100644 --- a/dart/lib/src/sentry.dart +++ b/dart/lib/src/sentry.dart @@ -24,6 +24,11 @@ class Sentry { static void init(OptionsConfiguration optionsConfiguration) { final options = SentryOptions(); optionsConfiguration(options); + + if (options == null) { + throw ArgumentError('SentryOptions is required.'); + } + _init(options); } @@ -58,14 +63,14 @@ class Sentry { }) async => currentHub.captureEvent(event, hint: hint); - /// Reports the [exception] and optionally its [stackTrace] to Sentry.io. + /// Reports the [throwable] and optionally its [stackTrace] to Sentry.io. static Future captureException( - dynamic exception, { + dynamic throwable, { dynamic stackTrace, dynamic hint, }) async => currentHub.captureException( - exception, + throwable, stackTrace: stackTrace, hint: hint, ); @@ -115,8 +120,9 @@ class Sentry { static bool _setDefaultConfiguration(SentryOptions options) { // if DSN is null, let's crash the App. if (options.dsn == null) { - throw ArgumentError.notNull( - 'DSN is required. Use empty string to disable SDK.'); + throw ArgumentError( + 'DSN is required. Use empty string to disable SDK.', + ); } // if the DSN is empty, let's disable the SDK if (options.dsn.isEmpty) { diff --git a/dart/lib/src/sentry_client.dart b/dart/lib/src/sentry_client.dart index ec34e60342..344c7cd718 100644 --- a/dart/lib/src/sentry_client.dart +++ b/dart/lib/src/sentry_client.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'protocol.dart'; import 'scope.dart'; +import 'sentry_exception_factory.dart'; import 'sentry_options.dart'; import 'transport/http_transport.dart'; import 'transport/noop_transport.dart'; @@ -12,6 +13,10 @@ import 'version.dart'; class SentryClient { /// Instantiates a client using [SentryOptions] factory SentryClient(SentryOptions options) { + if (options == null) { + throw ArgumentError('SentryOptions is required.'); + } + if (options.transport is NoOpTransport) { options.transport = HttpTransport(options); } @@ -22,11 +27,15 @@ class SentryClient { final Random _random; + final SentryExceptionFactory _exceptionFactory; + static final _sentryId = Future.value(SentryId.empty()); /// Instantiates a client using [SentryOptions] - SentryClient._(this._options) - : _random = _options.sampleRate == null ? null : Random(); + SentryClient._(this._options, {SentryExceptionFactory exceptionFactory}) + : _exceptionFactory = + exceptionFactory ?? SentryExceptionFactory(options: _options), + _random = _options.sampleRate == null ? null : Random(); /// Reports an [event] to Sentry.io. Future captureEvent( @@ -72,25 +81,36 @@ class SentryClient { return _options.transport.send(event); } - SentryEvent _prepareEvent(SentryEvent event) => event.copyWith( - serverName: event.serverName ?? _options.serverName, - dist: event.dist ?? _options.dist, - environment: - event.environment ?? _options.environment ?? defaultEnvironment, - release: event.release ?? _options.release, - sdk: event.sdk ?? _options.sdk, - platform: event.platform ?? sdkPlatform, - ); + SentryEvent _prepareEvent(SentryEvent event) { + event = event.copyWith( + serverName: event.serverName ?? _options.serverName, + dist: event.dist ?? _options.dist, + environment: + event.environment ?? _options.environment ?? defaultEnvironment, + release: event.release ?? _options.release, + sdk: event.sdk ?? _options.sdk, + platform: event.platform ?? sdkPlatform, + ); + + if (event.throwable != null && event.exception == null) { + final sentryException = _exceptionFactory + .getSentryException(event.throwable, stackTrace: event.stackTrace); + + event = event.copyWith(exception: sentryException); + } + + return event; + } - /// Reports the [exception] and optionally its [stackTrace] to Sentry.io. + /// Reports the [throwable] and optionally its [stackTrace] to Sentry.io. Future captureException( - dynamic exception, { + dynamic throwable, { dynamic stackTrace, Scope scope, dynamic hint, }) { final event = SentryEvent( - exception: exception, + throwable: throwable, stackTrace: stackTrace, timestamp: _options.clock(), ); diff --git a/dart/lib/src/sentry_exception_factory.dart b/dart/lib/src/sentry_exception_factory.dart new file mode 100644 index 0000000000..c7560cce82 --- /dev/null +++ b/dart/lib/src/sentry_exception_factory.dart @@ -0,0 +1,46 @@ +import 'package:meta/meta.dart'; + +import 'protocol.dart'; +import 'sentry_options.dart'; +import 'sentry_stack_trace_factory.dart'; + +/// class to convert Dart Error and exception to SentryException +class SentryExceptionFactory { + SentryStackTraceFactory _stacktraceFactory; + + SentryExceptionFactory({ + SentryStackTraceFactory stacktraceFactory, + @required SentryOptions options, + }) { + if (options == null) { + throw ArgumentError('SentryOptions is required.'); + } + + _stacktraceFactory = stacktraceFactory ?? SentryStackTraceFactory(options); + } + + SentryException getSentryException( + dynamic exception, { + dynamic stackTrace, + Mechanism mechanism, + }) { + if (exception is Error) { + stackTrace ??= exception.stackTrace; + } else { + stackTrace ??= StackTrace.current; + } + + final sentryStackTrace = SentryStackTrace( + frames: _stacktraceFactory.getStackFrames(stackTrace), + ); + + final sentryException = SentryException( + type: '${exception.runtimeType}', + value: '$exception', + mechanism: mechanism, + stacktrace: sentryStackTrace, + ); + + return sentryException; + } +} diff --git a/dart/lib/src/sentry_options.dart b/dart/lib/src/sentry_options.dart index d5d46dfb4c..1a4736f8b4 100644 --- a/dart/lib/src/sentry_options.dart +++ b/dart/lib/src/sentry_options.dart @@ -128,15 +128,17 @@ class SentryOptions { final List _inAppExcludes = []; - /// A list of string prefixes of module names that do not belong to the app, but rather third-party - /// packages. Modules considered not to be part of the app will be hidden from stack traces by + /// A list of string prefixes of packages names that do not belong to the app, but rather third-party + /// packages. Packages considered not to be part of the app will be hidden from stack traces by /// default. + /// example : ['sentry'] will exclude exception from 'package:sentry/sentry.dart' List get inAppExcludes => List.unmodifiable(_inAppExcludes); final List _inAppIncludes = []; - /// A list of string prefixes of module names that belong to the app. This option takes precedence + /// A list of string prefixes of packages names that belong to the app. This option takes precedence /// over inAppExcludes. + /// example : ['sentry'] will include exception from 'package:sentry/sentry.dart' List get inAppIncludes => List.unmodifiable(_inAppIncludes); Transport _transport = NoOpTransport(); @@ -151,12 +153,12 @@ class SentryOptions { /// The server name used in the Sentry messages. String serverName; - Sdk _sdk = Sdk(name: sdkName, version: sdkVersion); + SdkVersion _sdk = SdkVersion(name: sdkName, version: sdkVersion); /// Sdk object that contains the Sentry Client Name and its version - Sdk get sdk => _sdk; + SdkVersion get sdk => _sdk; - set sdk(Sdk sdk) { + set sdk(SdkVersion sdk) { _sdk = sdk ?? _sdk; } diff --git a/dart/lib/src/sentry_stack_trace_factory.dart b/dart/lib/src/sentry_stack_trace_factory.dart new file mode 100644 index 0000000000..3c6076aa18 --- /dev/null +++ b/dart/lib/src/sentry_stack_trace_factory.dart @@ -0,0 +1,121 @@ +import 'package:meta/meta.dart'; +import 'package:stack_trace/stack_trace.dart'; + +import 'protocol/noop_origin.dart' + if (dart.library.html) 'protocol/origin.dart'; +import 'protocol.dart'; +import 'sentry_options.dart'; + +/// converts [StackTrace] to [SentryStackFrames] +class SentryStackTraceFactory { + /// A list of string prefixes of module names that do not belong to the app, but rather third-party + /// packages. Modules considered not to be part of the app will be hidden from stack traces by + /// default. + List _inAppExcludes; + + /// A list of string prefixes of module names that belong to the app. This option takes precedence + /// over inAppExcludes. + List _inAppIncludes; + + SentryStackTraceFactory(SentryOptions options) { + if (options == null) { + throw ArgumentError('SentryOptions is required.'); + } + + _inAppExcludes = options.inAppExcludes; + _inAppIncludes = options.inAppIncludes; + } + + /// returns the [SentryStackFrame] list from a stackTrace ([StackTrace] or [String]) + List getStackFrames(dynamic stackTrace) { + if (stackTrace == null) return null; + + final chain = stackTrace is StackTrace + ? Chain.forTrace(stackTrace) + : Chain.parse(stackTrace as String); + + final frames = []; + for (var t = 0; t < chain.traces.length; t += 1) { + final encodedFrames = + chain.traces[t].frames.map((f) => encodeStackTraceFrame(f)); + + frames.addAll(encodedFrames); + + if (t < chain.traces.length - 1) { + frames.add(SentryStackFrame.asynchronousGapFrameJson); + } + } + + return frames.reversed.toList(); + } + + /// converts [Frame] to [SentryStackFrame] + @visibleForTesting + SentryStackFrame encodeStackTraceFrame(Frame frame) { + final fileName = + frame.uri.pathSegments.isNotEmpty ? frame.uri.pathSegments.last : null; + + var sentryStackFrame = SentryStackFrame( + absPath: '$eventOrigin${_absolutePathForCrashReport(frame)}', + function: frame.member, + // https://docs.sentry.io/development/sdk-dev/features/#in-app-frames + inApp: isInApp(frame), + fileName: fileName, + package: frame.package, + ); + + if (frame.line != null && frame.line >= 0) { + sentryStackFrame = sentryStackFrame.copyWith(lineNo: frame.line); + } + + if (frame.column != null && frame.column >= 0) { + sentryStackFrame = sentryStackFrame.copyWith(colNo: frame.column); + } + + return sentryStackFrame; + } + + /// 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}'; + } + + /// whether this frame comes from the app and not from Dart core or 3rd party librairies + bool isInApp(Frame frame) { + final scheme = frame.uri.scheme; + + if (scheme == null || scheme.isEmpty) { + return true; + } + + if (_inAppIncludes != null) { + for (final include in _inAppIncludes) { + if (frame.package != null && frame.package.startsWith(include)) { + return true; + } + } + } + if (_inAppExcludes != null) { + for (final exclude in _inAppExcludes) { + if (frame.package != null && frame.package.startsWith(exclude)) { + return false; + } + } + } + + if (frame.isCore || frame.uri.scheme == 'package') return false; + + return true; + } +} diff --git a/dart/lib/src/stack_trace.dart b/dart/lib/src/stack_trace.dart deleted file mode 100644 index 299c9bb10d..0000000000 --- a/dart/lib/src/stack_trace.dart +++ /dev/null @@ -1,74 +0,0 @@ -// 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 asynchronousGapFrameJson = { - 'abs_path': '', -}; - -/// Encodes [stackTrace] as JSON in the Sentry.io format. -/// -/// [stackTrace] must be [String] or [StackTrace]. -List> encodeStackTrace( - dynamic stackTrace, { - String origin, -}) { - assert(stackTrace is String || stackTrace is StackTrace); - origin ??= ''; - - final chain = stackTrace is StackTrace - ? Chain.forTrace(stackTrace) - : Chain.parse(stackTrace as String); - - final frames = >[]; - for (var t = 0; t < chain.traces.length; t += 1) { - final encodedFrames = chain.traces[t].frames - .map((f) => encodeStackTraceFrame(f, origin: origin)); - - frames.addAll(encodedFrames); - - if (t < chain.traces.length - 1) { - frames.add(asynchronousGapFrameJson); - } - } - - return frames.reversed.toList(); -} - -Map encodeStackTraceFrame(Frame frame, {String origin}) { - origin ??= ''; - - final json = { - 'abs_path': '$origin${_absolutePathForCrashReport(frame)}', - 'function': frame.member, - 'lineno': frame.line, - 'colno': frame.column, - '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}'; -} diff --git a/dart/lib/src/transport/http_transport.dart b/dart/lib/src/transport/http_transport.dart index 864b3cb22b..de4de0f712 100644 --- a/dart/lib/src/transport/http_transport.dart +++ b/dart/lib/src/transport/http_transport.dart @@ -8,7 +8,6 @@ import '../protocol.dart'; import '../sentry_options.dart'; import '../utils.dart'; import 'noop_encode.dart' if (dart.library.io) 'encode.dart'; -import 'noop_origin.dart' if (dart.library.html) 'origin.dart'; import 'transport.dart'; /// A transport is in charge of sending the event to the Sentry server. @@ -22,6 +21,10 @@ class HttpTransport implements Transport { final Map _headers; factory HttpTransport(SentryOptions options) { + if (options == null) { + throw ArgumentError('SentryOptions is required.'); + } + if (options.httpClient is NoOpClient) { options.httpClient = Client(); } @@ -41,7 +44,7 @@ class HttpTransport implements Transport { @override Future send(SentryEvent event) async { - final data = event.toJson(origin: eventOrigin); + final data = event.toJson(); final body = _bodyEncoder( data, diff --git a/dart/lib/src/transport/noop_origin.dart b/dart/lib/src/transport/noop_origin.dart deleted file mode 100644 index 6cecb4be62..0000000000 --- a/dart/lib/src/transport/noop_origin.dart +++ /dev/null @@ -1 +0,0 @@ -String eventOrigin; diff --git a/dart/test/contexts_test.dart b/dart/test/contexts_test.dart index b2df80ad17..9643587329 100644 --- a/dart/test/contexts_test.dart +++ b/dart/test/contexts_test.dart @@ -40,8 +40,8 @@ void main() { ); const testOS = OperatingSystem(name: 'testOS'); final testRuntimes = [ - const Runtime(name: 'testRT1', version: '1.0'), - const Runtime(name: 'testRT2', version: '2.3.1'), + const SentryRuntime(name: 'testRT1', version: '1.0'), + const SentryRuntime(name: 'testRT2', version: '2.3.1'), ]; const testApp = App(version: '1.2.3'); const testBrowser = Browser(version: '12.3.4'); diff --git a/dart/test/exception_factory_test.dart b/dart/test/exception_factory_test.dart new file mode 100644 index 0000000000..fa46dc3327 --- /dev/null +++ b/dart/test/exception_factory_test.dart @@ -0,0 +1,59 @@ +import 'package:sentry/sentry.dart'; +import 'package:sentry/src/sentry_exception_factory.dart'; +import 'package:test/test.dart'; + +void main() { + group('Exception factory', () { + final exceptionFactory = SentryExceptionFactory(options: SentryOptions()); + + test('exceptionFactory.getSentryException', () { + SentryException sentryException; + try { + throw StateError('a state error'); + } catch (err, stacktrace) { + final mechanism = Mechanism( + type: 'example', + description: 'a mechanism', + ); + sentryException = exceptionFactory.getSentryException( + err, + mechanism: mechanism, + stackTrace: stacktrace, + ); + } + + expect(sentryException.type, 'StateError'); + expect(sentryException.stacktrace.frames, isNotEmpty); + }); + + test('should not override event.stacktrace', () { + SentryException sentryException; + try { + throw StateError('a state error'); + } catch (err) { + final mechanism = Mechanism( + type: 'example', + description: 'a mechanism', + ); + sentryException = exceptionFactory.getSentryException( + err, + mechanism: mechanism, + stackTrace: ''' +#0 baz (file:///pathto/test.dart:50:3) + +#1 bar (file:///pathto/test.dart:46:9) + ''', + ); + } + + expect(sentryException.type, 'StateError'); + expect(sentryException.stacktrace.frames.first.lineNo, 46); + expect(sentryException.stacktrace.frames.first.colNo, 9); + expect(sentryException.stacktrace.frames.first.fileName, 'test.dart'); + }); + }); + + test("options can't be null", () { + expect(() => SentryExceptionFactory(options: null), throwsArgumentError); + }); +} diff --git a/dart/test/http_transport_test.dart b/dart/test/http_transport_test.dart new file mode 100644 index 0000000000..1cb3d6b75c --- /dev/null +++ b/dart/test/http_transport_test.dart @@ -0,0 +1,8 @@ +import 'package:sentry/src/transport/http_transport.dart'; +import 'package:test/test.dart'; + +void main() { + test("options can't be null", () { + expect(() => HttpTransport(null), throwsArgumentError); + }); +} diff --git a/dart/test/mocks.dart b/dart/test/mocks.dart index c1ee58059a..852d19113b 100644 --- a/dart/test/mocks.dart +++ b/dart/test/mocks.dart @@ -48,7 +48,7 @@ final fakeEvent = SentryEvent( kernelVersion: 'Linux version 3.4.39-5726670 (dpi@SWHC3807) (gcc version 4.8 (GCC) ) #1 SMP PREEMPT Thu Dec 1 19:42:39 KST 2016', rooted: false), - runtimes: [const Runtime(name: 'ART', version: '5')], + runtimes: [const SentryRuntime(name: 'ART', version: '5')], app: App( name: 'Example Dart App', version: '1.42.0', diff --git a/dart/test/protocol/sentry_exception_test.dart b/dart/test/protocol/sentry_exception_test.dart new file mode 100644 index 0000000000..b55a8ad4aa --- /dev/null +++ b/dart/test/protocol/sentry_exception_test.dart @@ -0,0 +1,85 @@ +import 'package:sentry/sentry.dart'; +import 'package:test/test.dart'; + +void main() { + test('should serialize stacktrace', () { + final mechanism = Mechanism( + type: 'mechanism-example', + description: 'a mechanism', + handled: true, + synthetic: false, + helpLink: 'https://help.com', + data: {'polyfill': 'bluebird'}, + meta: { + 'signal': { + 'number': 10, + 'code': 0, + 'name': 'SIGBUS', + 'code_name': 'BUS_NOOP' + } + }, + ); + final stacktrace = SentryStackTrace(frames: [ + SentryStackFrame( + absPath: 'frame-path', + fileName: 'example.dart', + function: 'parse', + module: 'example-module', + lineNo: 1, + colNo: 2, + contextLine: 'context-line example', + inApp: true, + package: 'example-package', + native: false, + platform: 'dart', + rawFunction: 'example-rawFunction', + framesOmitted: [1, 2, 3], + ), + ]); + + final sentryException = SentryException( + type: 'StateError', + value: 'Bad state: error', + module: 'example.module', + stacktrace: stacktrace, + mechanism: mechanism, + threadId: 123456, + ); + + final serialized = sentryException.toJson(); + + expect(serialized['type'], 'StateError'); + expect(serialized['value'], 'Bad state: error'); + expect(serialized['module'], 'example.module'); + expect(serialized['thread_id'], 123456); + expect(serialized['mechanism']['type'], 'mechanism-example'); + expect(serialized['mechanism']['description'], 'a mechanism'); + expect(serialized['mechanism']['handled'], true); + expect(serialized['mechanism']['synthetic'], false); + expect(serialized['mechanism']['help_link'], 'https://help.com'); + expect(serialized['mechanism']['data'], {'polyfill': 'bluebird'}); + expect(serialized['mechanism']['meta'], { + 'signal': { + 'number': 10, + 'code': 0, + 'name': 'SIGBUS', + 'code_name': 'BUS_NOOP' + } + }); + + final serializedFrame = serialized['stacktrace']['frames'].first; + expect(serializedFrame['abs_path'], 'frame-path'); + expect(serializedFrame['filename'], 'example.dart'); + expect(serializedFrame['function'], 'parse'); + expect(serializedFrame['module'], 'example-module'); + expect(serializedFrame['lineno'], 1); + expect(serializedFrame['colno'], 2); + expect(serializedFrame['context_line'], 'context-line example'); + expect(serializedFrame['in_app'], true); + expect(serializedFrame['package'], 'example-package'); + expect(serializedFrame['native'], false); + expect(serializedFrame['platform'], 'dart'); + expect(serializedFrame['raw_function'], 'example-rawFunction'); + expect(serializedFrame['frames_omitted'], [1, 2, 3]); + }); +} diff --git a/dart/test/scope_test.dart b/dart/test/scope_test.dart index 1ebe15ecb5..ed2650c903 100644 --- a/dart/test/scope_test.dart +++ b/dart/test/scope_test.dart @@ -312,7 +312,7 @@ void main() { device: Device(name: 'event-device'), app: App(name: 'event-app'), gpu: Gpu(name: 'event-gpu'), - runtimes: [Runtime(name: 'event-runtime')], + runtimes: [SentryRuntime(name: 'event-runtime')], browser: Browser(name: 'event-browser'), operatingSystem: OperatingSystem(name: 'event-os'), ), @@ -331,8 +331,8 @@ void main() { Gpu(name: 'context-gpu'), ) ..setContexts( - Runtime.listType, - [Runtime(name: 'context-runtime')], + SentryRuntime.listType, + [SentryRuntime(name: 'context-runtime')], ) ..setContexts( Browser.type, @@ -348,8 +348,8 @@ void main() { expect(updatedEvent.contexts[Device.type].name, 'event-device'); expect(updatedEvent.contexts[App.type].name, 'event-app'); expect(updatedEvent.contexts[Gpu.type].name, 'event-gpu'); - expect( - updatedEvent.contexts[Runtime.listType].first.name, 'event-runtime'); + expect(updatedEvent.contexts[SentryRuntime.listType].first.name, + 'event-runtime'); expect(updatedEvent.contexts[Browser.type].name, 'event-browser'); expect(updatedEvent.contexts[OperatingSystem.type].name, 'event-os'); }); @@ -360,7 +360,8 @@ void main() { ..setContexts(Device.type, Device(name: 'context-device')) ..setContexts(App.type, App(name: 'context-app')) ..setContexts(Gpu.type, Gpu(name: 'context-gpu')) - ..setContexts(Runtime.listType, [Runtime(name: 'context-runtime')]) + ..setContexts( + SentryRuntime.listType, [SentryRuntime(name: 'context-runtime')]) ..setContexts(Browser.type, Browser(name: 'context-browser')) ..setContexts(OperatingSystem.type, OperatingSystem(name: 'context-os')) ..setContexts('theme', 'material') @@ -373,7 +374,7 @@ void main() { expect(updatedEvent.contexts[App.type].name, 'context-app'); expect(updatedEvent.contexts[Gpu.type].name, 'context-gpu'); expect( - updatedEvent.contexts[Runtime.listType].first.name, + updatedEvent.contexts[SentryRuntime.listType].first.name, 'context-runtime', ); expect(updatedEvent.contexts[Browser.type].name, 'context-browser'); @@ -392,6 +393,10 @@ void main() { expect(updatedEvent.level, SentryLevel.error); }); }); + + test("options can't be null", () { + expect(() => Scope(null), throwsArgumentError); + }); } class Fixture { diff --git a/dart/test/sentry_client_test.dart b/dart/test/sentry_client_test.dart index b3d0b3c551..768a4089a7 100644 --- a/dart/test/sentry_client_test.dart +++ b/dart/test/sentry_client_test.dart @@ -43,7 +43,7 @@ void main() { options.transport = MockTransport(); }); - test('should capture exception', () async { + test('should capture error', () async { try { throw StateError('Error'); } on Error catch (err, stack) { @@ -58,8 +58,88 @@ void main() { options.transport.send(captureAny), ).captured.first) as SentryEvent; - expect(capturedEvent.exception, error); - expect(capturedEvent.stackTrace, stackTrace); + expect(capturedEvent.throwable, error); + expect(capturedEvent.exception is SentryException, true); + expect(capturedEvent.exception.stacktrace, isNotNull); + }); + }); + + group('SentryClient captures exception and stacktrace', () { + SentryOptions options; + + Error error; + + final stacktrace = ''' +#0 baz (file:///pathto/test.dart:50:3) + +#1 bar (file:///pathto/test.dart:46:9) + '''; + + setUp(() { + options = SentryOptions(dsn: fakeDsn); + options.transport = MockTransport(); + }); + + test('should capture error', () async { + try { + throw StateError('Error'); + } on Error catch (err) { + error = err; + } + + final client = SentryClient(options); + await client.captureException(error, stackTrace: stacktrace); + + final capturedEvent = (verify( + options.transport.send(captureAny), + ).captured.first) as SentryEvent; + + expect(capturedEvent.throwable, error); + expect(capturedEvent.exception is SentryException, true); + expect(capturedEvent.exception.stacktrace, isNotNull); + expect(capturedEvent.exception.stacktrace.frames.first.fileName, + 'test.dart'); + expect(capturedEvent.exception.stacktrace.frames.first.lineNo, 46); + expect(capturedEvent.exception.stacktrace.frames.first.colNo, 9); + }); + }); + + group('SentryClient captures exception and stacktrace', () { + SentryOptions options; + + Exception exception; + + setUp(() { + options = SentryOptions(dsn: fakeDsn); + options.transport = MockTransport(); + }); + + test('should capture exception', () async { + try { + throw Exception('Error'); + } catch (err) { + exception = err; + } + + final stacktrace = ''' +#0 baz (file:///pathto/test.dart:50:3) + +#1 bar (file:///pathto/test.dart:46:9) + '''; + + final client = SentryClient(options); + await client.captureException(exception, stackTrace: stacktrace); + + final capturedEvent = (verify( + options.transport.send(captureAny), + ).captured.first) as SentryEvent; + + expect(capturedEvent.throwable, exception); + expect(capturedEvent.exception is SentryException, true); + expect(capturedEvent.exception.stacktrace.frames.first.fileName, + 'test.dart'); + expect(capturedEvent.exception.stacktrace.frames.first.lineNo, 46); + expect(capturedEvent.exception.stacktrace.frames.first.colNo, 9); }); }); @@ -229,6 +309,10 @@ void main() { verify(options.transport.send(any)).called(1); }); }); + + test("options can't be null", () { + expect(() => SentryClient(null), throwsArgumentError); + }); } SentryEvent beforeSendCallbackDropEvent(SentryEvent event, dynamic hint) => diff --git a/dart/test/sentry_event_test.dart b/dart/test/sentry_event_test.dart index 7f254b0c10..314aac18e3 100644 --- a/dart/test/sentry_event_test.dart +++ b/dart/test/sentry_event_test.dart @@ -3,7 +3,7 @@ // found in the LICENSE file. import 'package:sentry/sentry.dart'; -import 'package:sentry/src/stack_trace.dart'; +import 'package:sentry/src/protocol/request.dart'; import 'package:sentry/src/utils.dart'; import 'package:test/test.dart'; @@ -25,17 +25,17 @@ void main() { }, ); }); - test('$Sdk serializes', () { + test('$SdkVersion serializes', () { final event = SentryEvent( eventId: SentryId.empty(), timestamp: DateTime.utc(2019), platform: sdkPlatform, - sdk: Sdk( + sdk: SdkVersion( name: 'sentry.dart.flutter', version: '4.3.2', integrations: ['integration'], - packages: [ - Package('npm:@sentry/javascript', '1.3.4'), + packages: [ + SentryPackage('npm:@sentry/javascript', '1.3.4'), ], ), ); @@ -70,7 +70,7 @@ void main() { category: 'test'), ]; - final error = StateError('test-error'); + final request = Request(url: 'https://api.com/users', method: 'GET'); expect( SentryEvent( @@ -83,7 +83,6 @@ void main() { params: ['1', '2'], ), transaction: '/test/1', - exception: error, level: SentryLevel.debug, culprit: 'Professor Moriarty', tags: const { @@ -97,6 +96,27 @@ void main() { fingerprint: const [SentryEvent.defaultFingerprint, 'foo'], user: user, breadcrumbs: breadcrumbs, + request: request, + debugMeta: DebugMeta( + sdk: SdkInfo( + sdkName: 'sentry.dart', + versionMajor: 4, + versionMinor: 1, + versionPatchlevel: 2, + ), + images: [ + DebugImage( + type: 'macho', + debugId: '84a04d24-0e60-3810-a8c0-90a65e2df61a', + debugFile: 'libDiagnosticMessagesClient.dylib', + codeFile: '/usr/lib/libDiagnosticMessagesClient.dylib', + imageAddr: '0x7fffe668e000', + imageSize: 8192, + arch: 'x86_64', + codeId: '123', + ) + ], + ), ).toJson(), { 'platform': isWeb ? 'javascript' : 'dart', @@ -109,9 +129,6 @@ void main() { 'params': ['1', '2'] }, 'transaction': '/test/1', - 'exception': [ - {'type': 'StateError', 'value': 'Bad state: test-error'} - ], 'level': 'debug', 'culprit': 'Professor Moriarty', 'tags': {'a': 'b', 'c': 'd'}, @@ -134,18 +151,79 @@ void main() { }, ] }, - }..addAll( - error.stackTrace == null - ? {} - : { - 'stacktrace': { - 'frames': encodeStackTrace( - error.stackTrace, - origin: null, - ) - } - }, + 'request': { + 'url': request.url, + 'method': request.method, + }, + 'debug_meta': { + 'sdk_info': { + 'sdk_name': 'sentry.dart', + 'version_major': 4, + 'version_minor': 1, + 'version_patchlevel': 2 + }, + 'images': [ + { + 'type': 'macho', + 'debug_id': '84a04d24-0e60-3810-a8c0-90a65e2df61a', + 'debug_file': 'libDiagnosticMessagesClient.dylib', + 'code_file': '/usr/lib/libDiagnosticMessagesClient.dylib', + 'image_addr': '0x7fffe668e000', + 'image_size': 8192, + 'arch': 'x86_64', + 'code_id': '123', + }, + ] + } + }, + ); + }); + + test('should not serialize throwable', () { + final error = StateError('test-error'); + + final serialized = SentryEvent(throwable: error).toJson(); + expect(serialized['throwable'], null); + expect(serialized['stacktrace'], null); + expect(serialized['exception'], null); + }); + + test('serializes to JSON with sentryException', () { + var sentryException; + try { + throw StateError('an error'); + } catch (err) { + sentryException = SentryException( + type: '${err.runtimeType}', + value: '$err', + mechanism: Mechanism( + type: 'mech-type', + description: 'a description', + helpLink: 'https://help.com', + synthetic: false, + handled: true, + meta: {}, + data: {}, ), + ); + } + + final serialized = SentryEvent(exception: sentryException).toJson(); + + expect(serialized['exception']['values'].first['type'], 'StateError'); + expect( + serialized['exception']['values'].first['value'], + 'Bad state: an error', + ); + expect( + serialized['exception']['values'].first['mechanism'], + { + 'type': 'mech-type', + 'description': 'a description', + 'help_link': 'https://help.com', + 'synthetic': false, + 'handled': true, + }, ); }); }); diff --git a/dart/test/sentry_test.dart b/dart/test/sentry_test.dart index 690baaffd3..592be78526 100644 --- a/dart/test/sentry_test.dart +++ b/dart/test/sentry_test.dart @@ -101,4 +101,8 @@ void main() { expect(called, true); }); }); + + test("options can't be null", () { + expect(() => Sentry.init((options) => options = null), throwsArgumentError); + }); } diff --git a/dart/test/stack_trace_test.dart b/dart/test/stack_trace_test.dart index ed7e01508a..4bb45e9496 100644 --- a/dart/test/stack_trace_test.dart +++ b/dart/test/stack_trace_test.dart @@ -2,7 +2,10 @@ // 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:sentry/sentry.dart'; +import 'package:sentry/src/protocol/noop_origin.dart' + if (dart.library.html) 'package:sentry/src/protocol/origin.dart'; +import 'package:sentry/src/sentry_stack_trace_factory.dart'; import 'package:stack_trace/stack_trace.dart'; import 'package:test/test.dart'; @@ -10,30 +13,78 @@ void main() { group('encodeStackTraceFrame', () { test('marks dart: frames as not app frames', () { final frame = Frame(Uri.parse('dart:core'), 1, 2, 'buzz'); - expect(encodeStackTraceFrame(frame), { - 'abs_path': 'dart:core', - 'function': 'buzz', - 'lineno': 1, - 'colno': 2, - 'in_app': false, - 'filename': 'core' - }); + + expect( + SentryStackTraceFactory(SentryOptions()) + .encodeStackTraceFrame(frame) + .toJson(), + { + 'abs_path': '${eventOrigin}dart:core', + 'function': 'buzz', + 'lineno': 1, + 'colno': 2, + 'in_app': false, + 'filename': 'core' + }, + ); }); test('cleanses absolute paths', () { final frame = Frame(Uri.parse('file://foo/bar/baz.dart'), 1, 2, 'buzz'); - expect(encodeStackTraceFrame(frame)['abs_path'], 'baz.dart'); + expect( + SentryStackTraceFactory(SentryOptions()) + .encodeStackTraceFrame(frame) + .toJson()['abs_path'], + '${eventOrigin}baz.dart', + ); + }); + + test('send exception package', () { + final frame = Frame(Uri.parse('package:toolkit/baz.dart'), 1, 2, 'buzz'); + final serializedFrame = + SentryStackTraceFactory(SentryOptions()..addInAppExclude('toolkit')) + .encodeStackTraceFrame(frame) + .toJson(); + expect(serializedFrame['package'], 'toolkit'); + }); + + test('apply inAppExcludes', () { + final frame = Frame(Uri.parse('package:toolkit/baz.dart'), 1, 2, 'buzz'); + final serializedFrame = + SentryStackTraceFactory(SentryOptions()..addInAppExclude('toolkit')) + .encodeStackTraceFrame(frame) + .toJson(); + expect(serializedFrame['in_app'], false); + }); + + test('apply inAppIncludes', () { + final frame = Frame(Uri.parse('package:toolkit/baz.dart'), 1, 2, 'buzz'); + final serializedFrame = + SentryStackTraceFactory(SentryOptions()..addInAppInclude('toolkit')) + .encodeStackTraceFrame(frame) + .toJson(); + expect(serializedFrame['in_app'], true); + }); + + test('apply inAppIncludes with precedence', () { + final frame = Frame(Uri.parse('package:toolkit/baz.dart'), 1, 2, 'buzz'); + final serializedFrame = SentryStackTraceFactory(SentryOptions() + ..addInAppInclude('toolkit') + ..addInAppExclude('toolkit')) + .encodeStackTraceFrame(frame) + .toJson(); + expect(serializedFrame['in_app'], true); }); }); group('encodeStackTrace', () { test('encodes a simple stack trace', () { - expect(encodeStackTrace(''' + expect(SentryStackTraceFactory(SentryOptions()).getStackFrames(''' #0 baz (file:///pathto/test.dart:50:3) #1 bar (file:///pathto/test.dart:46:9) - '''), [ + ''').map((frame) => frame.toJson()), [ { - 'abs_path': 'test.dart', + 'abs_path': '${eventOrigin}test.dart', 'function': 'bar', 'lineno': 46, 'colno': 9, @@ -41,7 +92,7 @@ void main() { 'filename': 'test.dart' }, { - 'abs_path': 'test.dart', + 'abs_path': '${eventOrigin}test.dart', 'function': 'baz', 'lineno': 50, 'colno': 3, @@ -52,13 +103,13 @@ void main() { }); test('encodes an asynchronous stack trace', () { - expect(encodeStackTrace(''' + expect(SentryStackTraceFactory(SentryOptions()).getStackFrames(''' #0 baz (file:///pathto/test.dart:50:3) #1 bar (file:///pathto/test.dart:46:9) - '''), [ + ''').map((frame) => frame.toJson()), [ { - 'abs_path': 'test.dart', + 'abs_path': '${eventOrigin}test.dart', 'function': 'bar', 'lineno': 46, 'colno': 9, @@ -69,7 +120,7 @@ void main() { 'abs_path': '', }, { - 'abs_path': 'test.dart', + 'abs_path': '${eventOrigin}test.dart', 'function': 'baz', 'lineno': 50, 'colno': 3, diff --git a/dart/test/test_utils.dart b/dart/test/test_utils.dart index fc0558c6bf..70f6fbeb26 100644 --- a/dart/test/test_utils.dart +++ b/dart/test/test_utils.dart @@ -5,7 +5,7 @@ import 'dart:async'; import 'dart:convert'; -import 'package:http/http.dart'; +import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:sentry/sentry.dart'; import 'package:test/test.dart'; @@ -60,12 +60,12 @@ Future testCaptureException( String postUri; Map headers; List body; - final httpMock = MockClient((Request request) async { + final httpMock = MockClient((http.Request request) async { if (request.method == 'POST') { postUri = request.url.toString(); headers = request.headers; body = request.bodyBytes; - return Response('{"id": "test-event-id"}', 200); + return http.Response('{"id": "test-event-id"}', 200); } fail('Unexpected request on ${request.method} ${request.url} in HttpMock'); }); @@ -109,21 +109,17 @@ Future testCaptureException( // so we assert the generated and returned id data['event_id'] = sentryId.toString(); - final stacktrace = data.remove('stacktrace') as Map; + final stacktrace = data['exception']['values'].first['stacktrace']; expect(stacktrace['frames'], const TypeMatcher()); expect(stacktrace['frames'], isNotEmpty); final topFrame = (stacktrace['frames'] as Iterable).last as Map; - expect(topFrame.keys, [ - 'abs_path', - 'function', - 'lineno', - 'colno', - 'in_app', - 'filename', - ]); + expect( + topFrame.keys, + ['filename', 'function', 'lineno', 'colno', 'abs_path', 'in_app'], + ); if (isWeb) { // can't test the full url @@ -138,35 +134,32 @@ Future testCaptureException( ); expect(topFrame['function'], 'Object.wrapException'); - expect(data, { - 'event_id': sentryId.toString(), - 'timestamp': '2017-01-02T00:00:00', - 'platform': 'javascript', - 'sdk': {'version': sdkVersion, 'name': sdkName}, - 'server_name': 'test.server.com', - 'release': '1.2.3', - 'environment': 'staging', - 'exception': [ - {'type': 'ArgumentError', 'value': 'Invalid argument(s): Test error'} - ], - }); + expect(data['event_id'], sentryId.toString()); + expect(data['timestamp'], '2017-01-02T00:00:00'); + expect(data['platform'], 'javascript'); + expect(data['sdk'], {'version': sdkVersion, 'name': sdkName}); + expect(data['server_name'], 'test.server.com'); + expect(data['release'], '1.2.3'); + expect(data['environment'], 'staging'); + + expect(data['exception']['values'].first['type'], 'ArgumentError'); + expect(data['exception']['values'].first['value'], + 'Invalid argument(s): Test error'); } else { expect(topFrame['abs_path'], 'test_utils.dart'); expect(topFrame['filename'], 'test_utils.dart'); expect(topFrame['function'], 'testCaptureException'); - expect(data, { - 'event_id': sentryId.toString(), - 'timestamp': '2017-01-02T00:00:00', - 'platform': 'dart', - 'exception': [ - {'type': 'ArgumentError', 'value': 'Invalid argument(s): Test error'} - ], - 'sdk': {'version': sdkVersion, 'name': 'sentry.dart'}, - 'server_name': 'test.server.com', - 'release': '1.2.3', - 'environment': 'staging', - }); + expect(data['event_id'], sentryId.toString()); + expect(data['timestamp'], '2017-01-02T00:00:00'); + expect(data['platform'], 'dart'); + expect(data['sdk'], {'version': sdkVersion, 'name': 'sentry.dart'}); + expect(data['server_name'], 'test.server.com'); + expect(data['release'], '1.2.3'); + expect(data['environment'], 'staging'); + expect(data['exception']['values'].first['type'], 'ArgumentError'); + expect(data['exception']['values'].first['value'], + 'Invalid argument(s): Test error'); } expect(topFrame['lineno'], greaterThan(0)); @@ -247,10 +240,10 @@ void runTest({Codec, List> gzip, bool isWeb = false}) { Map headers; - final httpMock = MockClient((Request request) async { + final httpMock = MockClient((http.Request request) async { if (request.method == 'POST') { headers = request.headers; - return Response('{"id": "testeventid"}', 200); + return http.Response('{"id": "testeventid"}', 200); } fail( 'Unexpected request on ${request.method} ${request.url} in HttpMock'); @@ -299,9 +292,9 @@ void runTest({Codec, List> gzip, bool isWeb = false}) { test('reads error message from the x-sentry-error header', () async { final fakeClockProvider = () => DateTime.utc(2017, 1, 2); - final httpMock = MockClient((Request request) async { + final httpMock = MockClient((http.Request request) async { if (request.method == 'POST') { - return Response('', 401, headers: { + return http.Response('', 401, headers: { 'x-sentry-error': 'Invalid api key', }); } @@ -336,13 +329,13 @@ void runTest({Codec, List> gzip, bool isWeb = false}) { final fakeClockProvider = () => DateTime.utc(2017, 1, 2); String loggedUserId; // used to find out what user context was sent - final httpMock = MockClient((Request request) async { + final httpMock = MockClient((http.Request request) async { if (request.method == 'POST') { final bodyData = request.bodyBytes; final decoded = const Utf8Codec().decode(bodyData); final dynamic decodedJson = const JsonDecoder().convert(decoded); loggedUserId = decodedJson['user']['id'] as String; - return Response('', 401, headers: { + return http.Response('', 401, headers: { 'x-sentry-error': 'Invalid api key', }); } @@ -378,21 +371,22 @@ void runTest({Codec, List> gzip, bool isWeb = false}) { try { throw ArgumentError('Test error'); - } catch (error, stackTrace) { + } catch (error) { final eventWithoutContext = SentryEvent( eventId: SentryId.empty(), - exception: error, - stackTrace: stackTrace, + throwable: error, ); final eventWithContext = SentryEvent( eventId: SentryId.empty(), - exception: error, - stackTrace: stackTrace, + throwable: error, user: eventUser, ); - await client.captureEvent(eventWithoutContext, - scope: Scope(options)..user = clientUser); + await client.captureEvent( + eventWithoutContext, + scope: Scope(options)..user = clientUser, + ); expect(loggedUserId, clientUser.id); + await client.captureEvent( eventWithContext, scope: Scope(options)..user = clientUser, diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index c0048ba2bf..52b8fdcad9 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -37,7 +37,7 @@ Future main() async { }, (error, stackTrace) async { print('Capture from runZonedGuarded $error'); final event = SentryEvent( - exception: error, + throwable: error, stackTrace: stackTrace, // release is required on Web to match the source maps release: _release,