diff --git a/CHANGELOG.md b/CHANGELOG.md index 885c6a129a..e8c98e1660 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # package:sentry changelog +## 3.0.1 +- Add support for Contexts in Sentry events + ## 3.0.0+1 - `pubspec.yaml` and example code clean-up. diff --git a/lib/src/base.dart b/lib/src/base.dart index 772b5a37b0..3af2883a24 100644 --- a/lib/src/base.dart +++ b/lib/src/base.dart @@ -308,6 +308,7 @@ class Event { this.extra, this.fingerprint, this.userContext, + this.contexts, this.breadcrumbs, }); @@ -370,6 +371,11 @@ class Event { /// set in [SentryClient.userContext] for this logged event. final User userContext; + /// The context interfaces provide additional context data. + /// Typically this is data related to the current user, + /// the current HTTP request. + final Contexts contexts; + /// Used to deduplicate events by grouping ones with the same fingerprint /// together. /// @@ -491,6 +497,11 @@ class Event { json['extra'] = extra; } + Map contextsMap; + if (contexts != null && (contextsMap = contexts.toJson()).isNotEmpty) { + json['contexts'] = contextsMap; + } + Map userContextMap; if (userContext != null && (userContextMap = userContext.toJson()).isNotEmpty) { @@ -511,6 +522,474 @@ class Event { } } +/// The context interfaces provide additional context data. +/// +/// Typically this is data related to the current user, +/// the current HTTP request. +/// +/// See also: https://docs.sentry.io/development/sdk-dev/event-payloads/contexts/. +class Contexts { + /// This describes the device that caused the event. + final Device device; + + /// Describes the operating system on which the event was created. + /// + /// In web contexts, this is the operating system of the browse + /// (normally pulled from the User-Agent string). + final OperatingSystem operatingSystem; + + /// Describes a runtime in more detail. + /// + /// 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). + final List runtimes; + + /// App context describes the application. + /// + /// As opposed to the runtime, this is the actual application that was + /// running and carries metadata about the current session. + final App app; + + /// Carries information about the browser or user agent for web-related + /// errors. + /// + /// This can either be the browser this event ocurred in, or the user + /// agent of a web request that triggered the event. + final Browser browser; + + const Contexts({ + this.device, + this.operatingSystem, + this.runtimes, + this.app, + this.browser, + }); + + /// Produces a [Map] that can be serialized to JSON. + Map toJson() { + final Map json = {}; + + Map deviceMap; + if (device != null && (deviceMap = device.toJson()).isNotEmpty) { + json['device'] = deviceMap; + } + + Map osMap; + if (operatingSystem != null && + (osMap = operatingSystem.toJson()).isNotEmpty) { + json['os'] = osMap; + } + + Map appMap; + if (app != null && (appMap = app.toJson()).isNotEmpty) { + json['app'] = appMap; + } + + Map browserMap; + if (browser != null && (browserMap = browser.toJson()).isNotEmpty) { + json['browser'] = browserMap; + } + + if (runtimes != null) { + if (runtimes.length == 1) { + final Runtime runtime = runtimes[0]; + if (runtime != null) { + final String key = runtime.key ?? 'runtime'; + json[key] = runtime.toJson(); + } + } else if (runtimes.length > 1) { + for (final runtime in runtimes) { + if (runtime != null) { + String key = runtime.key ?? runtime.name.toLowerCase(); + + if (json.containsKey(key)) { + int k = 0; + while (json.containsKey(key)) { + key = '$key$k'; + k++; + } + } + + json[key] = runtime.toJson()..addAll({"type": "runtime"}); + } + } + } + } + + return json; + } +} + +/// This describes the device that caused the event. +class Device { + /// The name of the device. This is typically a hostname. + final String name; + + /// The family of the device. + /// + /// This is normally the common part of model names across generations. + /// For instance `iPhone` would be a reasonable family, + /// so would be `Samsung Galaxy`. + final String family; + + /// The model name. This for instance can be `Samsung Galaxy S3`. + final String model; + + /// An internal hardware revision to identify the device exactly. + final String modelId; + + /// The CPU architecture. + final String arch; + + /// If the device has a battery, this can be an floating point value + /// defining the battery level (in the range 0-100). + final double batteryLevel; + + /// Defines the orientation of a device. + final Orientation orientation; + + /// The manufacturer of the device. + final String manufacturer; + + /// The brand of the device. + final String brand; + + /// The screen resolution. (e.g.: `800x600`, `3040x1444`). + final String screenResolution; + + /// A floating point denoting the screen density. + final String screenDensity; + + /// A decimal value reflecting the DPI (dots-per-inch) density. + final String screenDpi; + + /// Whether the device was online or not. + final bool online; + + /// Whether the device was charging or not. + final bool charging; + + /// Whether the device was low on memory. + final bool lowMemory; + + /// A flag indicating whether this device is a simulator or an actual device. + final bool simulator; + + /// Total system memory available in bytes. + final int memorySize; + + /// Free system memory in bytes. + final int freeMemory; + + /// Memory usable for the app in bytes. + final int usableMemory; + + /// Total device storage in bytes. + final int storageSize; + + /// Free device storage in bytes. + final int freeStorage; + + /// Total size of an attached external storage in bytes + /// (e.g.: android SDK card). + final int externalStorageSize; + + /// Free size of an attached external storage in bytes + /// (e.g.: android SDK card). + final int externalFreeStorage; + + /// When the system was booted + final DateTime bootTime; + + /// The timezone of the device, e.g.: `Europe/Vienna`. + final String timezone; + + const Device({ + this.name, + this.family, + this.model, + this.modelId, + this.arch, + this.batteryLevel, + this.orientation, + this.manufacturer, + this.brand, + this.screenResolution, + this.screenDensity, + this.screenDpi, + this.online, + this.charging, + this.lowMemory, + this.simulator, + this.memorySize, + this.freeMemory, + this.usableMemory, + this.storageSize, + this.freeStorage, + this.externalStorageSize, + this.externalFreeStorage, + this.bootTime, + this.timezone, + }) : assert( + batteryLevel == null || (batteryLevel >= 0 && batteryLevel <= 100)); + + /// Produces a [Map] that can be serialized to JSON. + Map toJson() { + final Map json = {}; + + String orientation; + + switch (this.orientation) { + case Orientation.portrait: + orientation = "portait"; + break; + case Orientation.landscape: + orientation = "landscape"; + break; + } + + if (name != null) json['name'] = name; + + if (family != null) json['family'] = family; + + if (model != null) json['model'] = model; + + if (modelId != null) json['model_id'] = modelId; + + if (arch != null) json['arch'] = arch; + + if (batteryLevel != null) json['battery_level'] = batteryLevel; + + if (orientation != null) json['orientation'] = orientation; + + if (manufacturer != null) json['manufacturer'] = manufacturer; + + if (brand != null) json['brand'] = brand; + + if (screenResolution != null) json['screen_resolution'] = screenResolution; + + if (screenDensity != null) json['screen_density'] = screenDensity; + + if (screenDpi != null) json['screen_dpi'] = screenDpi; + + if (online != null) json['online'] = online; + + if (charging != null) json['charging'] = charging; + + if (lowMemory != null) json['low_memory'] = lowMemory; + + if (simulator != null) json['simulator'] = simulator; + + if (memorySize != null) json['memory_size'] = memorySize; + + if (freeMemory != null) json['free_memory'] = freeMemory; + + if (usableMemory != null) json['usable_memory'] = usableMemory; + + if (storageSize != null) json['storage_size'] = storageSize; + + if (freeStorage != null) json['free_storage'] = freeStorage; + + if (externalStorageSize != null) { + json['external_storage_size'] = externalStorageSize; + } + + if (externalFreeStorage != null) { + json['external_free_storage'] = externalFreeStorage; + } + + if (bootTime != null) json['boot_time'] = bootTime.toIso8601String(); + + if (timezone != null) json['timezone'] = timezone; + + return json; + } +} + +enum Orientation { portrait, landscape } + +/// Describes the operating system on which the event was created. +/// +/// In web contexts, this is the operating system of the browse +/// (normally pulled from the User-Agent string). +class OperatingSystem { + /// The name of the operating system. + final String name; + + /// The version of the operating system. + final String version; + + /// The internal build revision of the operating system. + final String build; + + /// An independent kernel version string. + /// + /// This is typically the entire output of the `uname` syscall. + final String kernelVersion; + + /// A flag indicating whether the OS has been jailbroken or rooted. + final bool rooted; + + /// An unprocessed description string obtained by the operating system. + /// + /// For some well-known runtimes, Sentry will attempt to parse name and + /// version from this string, if they are not explicitly given. + final String rawDescription; + + const OperatingSystem({ + this.name, + this.version, + this.build, + this.kernelVersion, + this.rooted, + this.rawDescription, + }); + + /// Produces a [Map] that can be serialized to JSON. + Map toJson() { + final Map json = {}; + + if (name != null) json['name'] = name; + + if (version != null) json['version'] = version; + + if (build != null) json['build'] = build; + + if (kernelVersion != null) json['kernel_version'] = kernelVersion; + + if (rooted != null) json['rooted'] = rooted; + + if (rawDescription != null) json['raw_description'] = rawDescription; + + return json; + } +} + +/// Describes a runtime in more detail. +/// +/// 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 { + /// 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]. + final String key; + + /// The name of the runtime. + final String name; + + /// The version identifier of the runtime. + final String version; + + /// An unprocessed description string obtained by the runtime. + /// + /// For some well-known runtimes, Sentry will attempt to parse name + /// and version from this string, if they are not explicitly given. + final String rawDescription; + + const Runtime({this.key, this.name, this.version, this.rawDescription}) + : assert(key == null || key.length >= 1); + + /// Produces a [Map] that can be serialized to JSON. + Map toJson() { + final Map json = {}; + + if (name != null) json['name'] = name; + + if (version != null) json['version'] = version; + + if (rawDescription != null) json['raw_description'] = rawDescription; + + return json; + } +} + +/// App context describes the application. +/// +/// As opposed to the runtime, this is the actual application that was +/// running and carries metadata about the current session. +class App { + /// Human readable application name, as it appears on the platform. + final String name; + + /// Human readable application version, as it appears on the platform. + final String version; + + /// Version-independent application identifier, often a dotted bundle ID. + final String identifier; + + /// Internal build identifier, as it appears on the platform. + final String build; + + /// String identifying the kind of build, e.g. `testflight`. + final String buildType; + + /// When the application was started by the user. + final DateTime startTime; + + /// Application specific device identifier. + final String deviceAppHash; + + const App({ + this.name, + this.version, + this.identifier, + this.build, + this.buildType, + this.startTime, + this.deviceAppHash, + }); + + /// Produces a [Map] that can be serialized to JSON. + Map toJson() { + final Map json = {}; + + if (name != null) json['app_name'] = name; + + if (version != null) json['app_version'] = version; + + if (identifier != null) json['app_identifier'] = identifier; + + if (build != null) json['app_build'] = build; + + if (buildType != null) json['build_type'] = buildType; + + if (startTime != null) json['app_start_time'] = startTime.toIso8601String(); + + if (deviceAppHash != null) json['device_app_hash'] = deviceAppHash; + + return json; + } +} + +/// Carries information about the browser or user agent for web-related errors. +/// +/// This can either be the browser this event ocurred in, or the user +/// agent of a web request that triggered the event. +class Browser { + /// Human readable application name, as it appears on the platform. + final String name; + + /// Human readable application version, as it appears on the platform. + final String version; + + const Browser({this.name, this.version}); + + /// Produces a [Map] that can be serialized to JSON. + Map toJson() { + final Map json = {}; + + if (name != null) json['name'] = name; + + if (version != null) json['version'] = version; + + return json; + } +} + /// Describes the current user associated with the application, such as the /// currently signed in user. /// diff --git a/lib/src/version.dart b/lib/src/version.dart index 723d2f223b..1de3a3bfca 100644 --- a/lib/src/version.dart +++ b/lib/src/version.dart @@ -9,7 +9,7 @@ library version; /// The SDK version reported to Sentry.io in the submitted events. -const String sdkVersion = '3.0.0+1'; +const String sdkVersion = '3.0.1'; /// The SDK name reported to Sentry.io in the submitted events. const String sdkName = 'dart'; diff --git a/pubspec.yaml b/pubspec.yaml index 878683a62f..af13b2568d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sentry -version: 3.0.0+1 +version: 3.0.1 description: > A crash reporting library for for Dart that sends crash reports to Sentry.io. This library supports Dart VM, and Flutter for mobile, web, and desktop. diff --git a/test/contexts_test.dart b/test/contexts_test.dart new file mode 100644 index 0000000000..79ffc9f661 --- /dev/null +++ b/test/contexts_test.dart @@ -0,0 +1,98 @@ +// 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/sentry.dart'; +import 'package:test/test.dart'; + +void main() { + group(Contexts, () { + final testBootTime = DateTime.fromMicrosecondsSinceEpoch(0); + test('serializes to JSON', () { + final testDevice = Device( + name: 'testDevice', + family: 'testFamily', + model: 'testModel', + modelId: 'testModelId', + arch: 'testArch', + batteryLevel: 23, + orientation: Orientation.landscape, + manufacturer: 'testOEM', + brand: 'testBrand', + screenResolution: '123x345', + screenDensity: '99', + screenDpi: '100', + online: false, + charging: true, + lowMemory: false, + simulator: true, + memorySize: 1234567, + freeMemory: 12345, + usableMemory: 9876, + storageSize: 1234567, + freeStorage: 1234567, + externalStorageSize: 98765, + externalFreeStorage: 98765, + bootTime: testBootTime, + timezone: 'Australia/Melbourne', + ); + final testOS = OperatingSystem(name: 'testOS'); + final testRuntimes = [ + Runtime(name: 'testRT1', version: '1.0'), + Runtime(name: 'testRT2', version: '2.3.1'), + ]; + final testApp = App(version: '1.2.3'); + final testBrowser = Browser(version: '12.3.4'); + + final contexts = Contexts( + device: testDevice, + operatingSystem: testOS, + runtimes: testRuntimes, + app: testApp, + browser: testBrowser, + ); + + final event = Event(contexts: contexts); + + expect( + event.toJson()['contexts'], + { + 'device': { + 'name': 'testDevice', + 'family': 'testFamily', + 'model': 'testModel', + 'model_id': 'testModelId', + 'arch': 'testArch', + 'battery_level': 23, + 'orientation': 'landscape', + 'manufacturer': 'testOEM', + 'brand': 'testBrand', + 'screen_resolution': '123x345', + 'screen_density': '99', + 'screen_dpi': '100', + 'online': false, + 'charging': true, + 'low_memory': false, + 'simulator': true, + 'memory_size': 1234567, + 'free_memory': 12345, + 'usable_memory': 9876, + 'storage_size': 1234567, + 'free_storage': 1234567, + 'external_storage_size': 98765, + 'external_free_storage': 98765, + 'boot_time': testBootTime.toIso8601String(), + 'timezone': 'Australia/Melbourne', + }, + 'os': { + 'name': 'testOS', + }, + 'testrt1': {'name': 'testRT1', 'type': 'runtime', 'version': '1.0'}, + 'testrt2': {'name': 'testRT2', 'type': 'runtime', 'version': '2.3.1'}, + 'app': {'app_version': '1.2.3'}, + 'browser': {'version': '12.3.4'}, + }, + ); + }); + }); +}